diff --git a/DrissionPage/chromium_element.py b/DrissionPage/chromium_element.py index 2a01094..3757abe 100644 --- a/DrissionPage/chromium_element.py +++ b/DrissionPage/chromium_element.py @@ -1234,7 +1234,7 @@ def _run_script(page_or_ele, script: str, as_expr: bool = False, timeout: float obj_id = page_or_ele.obj_id else: page = page_or_ele - obj_id = page_or_ele.root.obj_id + obj_id = page_or_ele._root_id if as_expr: res = page.run_cdp('Runtime.evaluate', diff --git a/DrissionPage/chromium_page.py b/DrissionPage/chromium_page.py index a5d4652..65984ae 100644 --- a/DrissionPage/chromium_page.py +++ b/DrissionPage/chromium_page.py @@ -31,6 +31,8 @@ class ChromiumPage(BasePage): :param timeout: 超时时间 """ super().__init__(timeout) + self._is_loading = None + self._root_id = None self._connect_browser(Tab_or_Options, tab_id) def _connect_browser(self, Tab_or_Options: Union[Tab, DriverOptions] = None, tab_id: str = None) -> None: @@ -39,6 +41,8 @@ class ChromiumPage(BasePage): :param tab_id: 要控制的标签页id,不指定默认为激活的 :return: None """ + self._is_loading = False + self._root_id = None self.timeouts = Timeout(self) self._page_load_strategy = 'normal' if isinstance(Tab_or_Options, Tab): @@ -66,12 +70,28 @@ class ChromiumPage(BasePage): self._driver.start() self._driver.DOM.enable() self._driver.Page.enable() - root = self._driver.DOM.getDocument() - self.root = ChromiumElement(self, node_id=root['root']['nodeId']) + root_id = self._driver.DOM.getDocument()['root']['nodeId'] + self._root_id = self._driver.DOM.resolveNode(nodeId=root_id)['object']['objectId'] self._alert = Alert() - self.driver.Page.javascriptDialogOpening = self._on_alert_open - self.driver.Page.javascriptDialogClosed = self._on_alert_close + self._driver.Page.javascriptDialogOpening = self._on_alert_open + self._driver.Page.javascriptDialogClosed = self._on_alert_close + + self._driver.Page.frameNavigated = self.onFrameNavigated + self._driver.Page.loadEventFired = self.onLoadEventFired + + def onLoadEventFired(self, **kwargs): + """在页面刷新、变化后重新读取页面内容""" + self._is_loading = True + self._driver.DOM.enable() + self._driver.Page.enable() + root_id = self._driver.DOM.getDocument()['root']['nodeId'] + self._root_id = self._driver.DOM.resolveNode(nodeId=root_id)['object']['objectId'] + self._is_loading = False + + def onFrameNavigated(self, **kwargs): + if not kwargs['frame'].get('parentId', None): + self._is_loading = True def __call__(self, loc_or_str: Union[Tuple[str, str], str, 'ChromiumElement'], timeout: float = None) -> Union['ChromiumElement', None]: @@ -86,6 +106,8 @@ class ChromiumPage(BasePage): @property def driver(self) -> Tab: """返回用于控制浏览器的Tab对象""" + while self._is_loading: + sleep(.1) return self._driver @property @@ -236,7 +258,6 @@ class ChromiumPage(BasePage): interval=interval, show_errmsg=show_errmsg, timeout=timeout) - self.driver.DOM.getDocument() return self._url_available def get_cookies(self, as_dict: bool = False) -> Union[list, dict]: @@ -412,7 +433,10 @@ class ChromiumPage(BasePage): :return: None """ node_id = self.ele(loc_or_ele).node_id - self.driver.DOM.scrollIntoViewIfNeeded(nodeId=node_id) + try: + self.driver.DOM.scrollIntoViewIfNeeded(nodeId=node_id) + except Exception: + self.ele(loc_or_ele).run_script("this.scrollIntoView();") def refresh(self, ignore_cache: bool = False) -> None: """刷新当前页面 \n @@ -590,6 +614,7 @@ class ChromiumPage(BasePage): timeout = timeout or self.timeout end_time = perf_counter() + timeout while not self._alert.activated and perf_counter() < end_time: + print('vvv') sleep(.1) if not self._alert.activated: return None diff --git a/DrissionPage/common.py b/DrissionPage/common.py index bede592..4350da3 100644 --- a/DrissionPage/common.py +++ b/DrissionPage/common.py @@ -618,7 +618,7 @@ def _get_running_args(opt: DriverOptions) -> list: return result from json import load, dump - with open(prefs_file, "r") as f: + with open(prefs_file, "r", encoding='utf-8') as f: j = load(f) for pref in prefs: @@ -627,7 +627,7 @@ def _get_running_args(opt: DriverOptions) -> list: _make_leave_in_dict(j, pref, 0, len(pref)) _set_value_to_dict(j, pref, value) - with open(prefs_file, 'w') as f: + with open(prefs_file, 'w', encoding='utf-8') as f: dump(j, f) return result @@ -668,8 +668,7 @@ def _location_in_viewport(page, loc_x: int, loc_y: int) -> bool: :param loc_y: 页面绝对坐标y :return: """ - js = f''' - function(){{var x = {loc_x};var y = {loc_y}; + js = f'''function(){{var x = {loc_x};var y = {loc_y}; const vWidth = window.innerWidth || document.documentElement.clientWidth const vHeight = window.innerHeight || document.documentElement.clientHeight if (x< document.documentElement.scrollLeft || y < document.documentElement.scrollTop diff --git a/DrissionPage/easy_set.py b/DrissionPage/easy_set.py index 92244a8..b4a421b 100644 --- a/DrissionPage/easy_set.py +++ b/DrissionPage/easy_set.py @@ -38,7 +38,7 @@ def set_paths(driver_path: str = None, user_data_path: str = None, cache_path: str = None, ini_path: str = None, - check_version: bool = True) -> None: + check_version: bool = False) -> None: """快捷的路径设置函数 \n :param driver_path: chromedriver.exe路径 :param chrome_path: chrome.exe路径 diff --git a/DrissionPage/web_page.py b/DrissionPage/web_page.py index 847321b..47eaa9b 100644 --- a/DrissionPage/web_page.py +++ b/DrissionPage/web_page.py @@ -123,7 +123,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): if self._driver is None: self._connect_browser(self._driver_options, self._setting_tab_id) - return self._driver + return super().driver @property def _session_url(self) -> str: diff --git a/README.md b/README.md index 7ff0dae..3f34256 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ DrissionPage,即 driver 和 session 组合而成的 page。 - 可以对整个网页截图,包括视口外的部分(90以上版本浏览器支持) +- 对 Linux 提供良好支持 + 新版是自己实现的功能,开发不会受太多限制,以后将主要对`WebPage`进行更新。 3.0 版已经发布,目前正在测试,欢迎试用并提出意见,让我做得更好。 diff --git a/docs/README.md b/docs/README.md index da00c16..ebc8355 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,10 +40,14 @@ DrissionPage,即 driver 和 session 组合而成的 page。 - 可以对整个网页截图,包括视口外的部分(90以上版本浏览器支持) +- 对 Linux 提供良好支持 + 新版是自己实现的功能,开发不会受太多限制,以后将主要对`WebPage`进行更新。 3.0 版已经发布,目前正在测试,欢迎试用并提出意见,让我做得更好。 +文档正在更新,目前还是旧版,以后实例主要使用`WebPage`,`MixPage`与其不一致的地方才会说明。 + # 💡 特性和亮点 作者踩过无数坑,总结出的经验全写到这个库里了。内置了 N 多实用功能,对常用功能作了整合和优化。 @@ -51,7 +55,7 @@ DrissionPage,即 driver 和 session 组合而成的 page。 ## 🎉 特性 - 代码高度集成,以简洁的代码为第一追求。 -- 页面对象可在 selenium 和 requests 模式间任意切换,保留登录状态。 +- 页面对象可在浏览器和 requests 模式间任意切换,保留登录状态。 - 极简单但强大的元素定位语法,支持链式操作,代码极其简洁。 - 两种模式提供一致的 API,使用体验一致。 - 人性化设计,集成众多实用功能,大大降低开发工作量。 diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 6d9625c..ebb14d7 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -1,8 +1,9 @@ # DrissionPage -以页面为单位整合 selenium 和 requests,封装了常用操作。 +DrissionPage 是一个 Web 自动化框架。 +以页面为单位整合浏览器和 requests,封装了常用操作。 极大地简化了代码,易于使用,并可实现两种模式的无缝切换。 -可兼顾 selenium 的易用性和 requests 的高性能。 +可兼顾浏览器的易用性和 requests 的高性能。 如果本库对你有所帮助,请给予支持!持续维护中。 diff --git a/docs/入门指南/基本概念.md b/docs/入门指南/基本概念.md index d6fbbf3..5f9d9e0 100644 --- a/docs/入门指南/基本概念.md +++ b/docs/入门指南/基本概念.md @@ -1,5 +1,5 @@ 本节讲解 DrissionPage 的一些基本概念。了解它大概的构成和运作原理。 -默认读者对网络协议、html、selenium、requests 等有基本认识。 +默认读者对网络协议、html、网页自动化、requests 等有基本认识。 # 网页自动化 @@ -12,8 +12,10 @@ 后者使用浏览器模拟人的行为,如 selenium。写起来简单得多,免去复杂的分析过程,但速度非常慢,占用资源巨大。 -鉴于此,DrissionPage 以页面为单位将两者整合,对 selenium 和 requests 进行了重新封装,实现两种模式的互通,并加入常用的页面和元素控制功能,可大幅降低开发难度和代码量。 -selenium 用于操作浏览器的对象叫 Driver,requests 用于管理连接的对象叫 Session,Drission 就是它们两者的合体。 +鉴于此,DrissionPage 以页面为单位将两者整合,对 chromium 协议 和 requests 进行了重新封装,实现两种模式的互通,并加入常用的页面和元素控制功能,可大幅降低开发难度和代码量。 +用于操作浏览器的对象叫 Driver,requests 用于管理连接的对象叫 Session,Drission 就是它们两者的合体。 +在旧版本,本库是通过对 selenium 和 requests 的重新封装实现的。 +从 3.0 版开始,作者另起炉灶,用 chromium 协议自行实现了 selenium 全部功能,从而摆脱了对 selenium 的依赖,功能更多更强,运行效率更高,开发更灵活。 # 工作模式 @@ -58,13 +60,14 @@ rows = ele.eles('tag:tr') # 主要对象 -## MixPage +## WebPage -顾名思义,`MixPage`对象是整合了两种模式的页面对象,所有页面的跳转、读取、操作、标签页控制都由该对象进行。 +是从 3.0 版开始推出的页面页面控制对象,整合了两种模式,d 模式所有页面的跳转、读取、操作、标签页控,s 模式发送和解析数据都由该对象进行。 +该对象是作者用 chromium devtools protocol 开发而成,不依赖 selenium 对浏览器进行控制。 ```python # 创建页面对象 -page = MixPage('d') +page = WebPage('d') # 访问百度 page.get('http://www.baidu.com') # 定位输入框并输入关键字 @@ -75,9 +78,9 @@ page.ele('@value=百度一下').click() 详细使用方法见“创建页面对象”和“操作页面”章节。 -## DriverElement +## ChromiumElement -`DriverElemnet`对象是 d 模式所产生的页面元素对象,用于可对其进行点击、文本输入、拖拽、运行 js 脚本等操作,也可以基于这个元素查找其下级或周围的元素。 +`ChromiumElemnet`对象是 `WebPage`在 d 模式所产生的页面元素对象,用于可对其进行点击、文本输入、拖拽、运行 js 脚本等操作,也可以基于这个元素查找其下级或周围的元素。 ```python # 点击一个元素 @@ -97,7 +100,7 @@ links = ele1.eles('tag:a') ## SessionElement `SessionElement`对象是 s 模式所产生的页面元素对象,可以读取元素信息,或基于它进行下级元素查找、相对定位其它元素,但不能执行点击等操作。 -这种对象解析效率很高,当 d 模式页面太复杂时,可把 d 模式的元素转换为`SessionElement`进行解析,提高速度。转换的同时可以执行下级元素的查找。 +这种对象解析效率非常高,当 d 模式页面太复杂时,可把 d 模式的元素转换为`SessionElement`进行解析,提高速度。转换的同时可以执行下级元素的查找。 ```python # 获取元素 tag 属性 @@ -105,13 +108,51 @@ tag = ele1.tag # 在元素下查找第一个 name 为 name1 的子元素 ele2 = ele1.ele('@name=name1') -# 假设 d_ele 是一个 DriverElement,使用其 SessionElement 版本进行子元素查找 +# 假设 d_ele 是一个 ChromiumElement,使用其 SessionElement 版本进行子元素查找 ele2 = d_ele.s_ele('@name=name1') ``` +## MixPage + +`MixPage`对象使用方法与`WebPage`大致相同,不同的是它控制浏览器方面的功能是通过封装 selenium 实现的。作者以后主要对`WebPage`进行更新,`MixPage`会一直维持当前状态。 + +```python +# 创建页面对象 +page = MixPage('d') +# 访问百度 +page.get('http://www.baidu.com') +# 定位输入框并输入关键字 +page.ele('#kw').input('python') +# 点击“百度一下”按钮 +page.ele('@value=百度一下').click() +``` + +详细使用方法见“创建页面对象”和“操作页面”章节。 + +## DriverElement + +`DriverElemnet`对象与`ChromiumElement`大致相同。区别是它是由`MiPage`所产生。 + +```python +# 点击一个元素 +ele1.click() +# 输入文本 +ele1.input('some text') +# 获取 class 属性 +attr = ele1.attr('class') +# 设置 style 属性 +ele1.set_attr('style', 'display:none;') +# 获取其子元素中所有 a 元素 +links = ele1.eles('tag:a') +``` + +详细使用方法见“获取页面元素”、“获取元素信息”和“操作页面元素”章节。 + ## Drission -`Drission`对象用于管理与网页通讯的`WebDriver`对象和`Session`对象,相当于驱动器的角色。能实现这两个对象间的登录状态传递等。但它提供的功能往往通过`MixPage`调用,所以出场几率并不高。 +`Drission`对象用于管理`MixPage`中与网页通讯的`WebDriver`对象和`Session`对象,相当于驱动器的角色。能实现这两个对象间的登录状态传递等。但它提供的功能往往通过`MixPage` +调用,所以出场几率并不高。 +`WebPage`自行实现了这些功能,所以`WebPage`不需要`Drission`对象。 ```python from DrissionPage import Drission @@ -133,6 +174,12 @@ page = MixPage() # 结构图 +## `WebPage`结构图 + +待更新。 + +## `MixPage`结构图 + 如图所示,`Drission`对象负责链接的创建、共享登录状态等工作,类似 selenium 中 driver 的概念。 `MixPage`对象负责对获取到的页面进行解析、操作。 `DriverElement`和`SessionElement`则是从页面对象中获取到的元素对象。负责对元素进行解析和操作。 @@ -141,9 +188,9 @@ page = MixPage() # 配置管理 -无论 requests 还是 selenium,都通常须要一些配置信息才能正常工作,如长长的`user_agent`、driver 路径、浏览器配置等。这些代码往往是繁琐而重复的,不利于代码的简洁。 +无论 requests 还是浏览器,都通常须要一些配置信息才能正常工作,如长长的`user_agent`、driver 路径、浏览器配置等。这些代码往往是繁琐而重复的,不利于代码的简洁。 因此,DrissionPage 使用配置文件记录常用配置信息,程序会自动读取默认配置文件里的内容。所以,在示例中,通常看不见配置信息的代码。 这个功能支持用户保存不同的配置文件,按情况调研,也可以支持直接把配置写在代码里面,屏蔽读取配置文件。 -?> **Tips:**
当须要打包程序时,必须把配置写到代码里,否则会报错。 \ No newline at end of file +?> **Tips:**
当须要打包程序时,必须把配置写到代码里,或打包后手动复制配置文件到运行路径,否则会报错。 \ No newline at end of file diff --git a/docs/入门指南/快速上手.md b/docs/入门指南/快速上手.md index e10b716..cee2138 100644 --- a/docs/入门指南/快速上手.md +++ b/docs/入门指南/快速上手.md @@ -13,32 +13,20 @@ pip install DrissionPage --upgrade # 导入 ```python -from DrissionPage import MixPage +from DrissionPage import WebPage ``` # 初始化 -s 模式是无须初始化的,导入后就可以直接使用。 -d 模式因为使用浏览器,须要配置浏览器和对应的 driver。 +如果只使用 s 模式是无须初始化的,导入后就可以直接使用。 +如果使用 d 模式,`WebPage`须指定浏览器执行文件路径。`MixPage`还须下载与浏览器版本匹配的 driver 文件(后面的章节介绍)。 -!> **注意:**
这里介绍的方法只适用于 Windows 系统和 Chrome 浏览器,使用其它系统或浏览器请查看 [使用其它系统或浏览器](使用方法\使用其它系统或浏览器.md) 章节。
如果您有已打开的 Chrome -浏览器,请先关闭,否则会造成冲突。后面在 [创建页面对象](使用方法\使创建页面对象.md) 章节再介绍多 Chrome 浏览器共存的方法。 +默认情况下,如果系统内安装了 Chrome 浏览器,或在系统路径中指定了 chrome 执行文件,DrissionPage 就可以使用了。 -## 自动配置方式 +当然也可指定其它浏览器或启动路径: -身为自动化工具,DrissionPage 拥有自动识别电脑安装的 Chrome 版本并自动下载对应 chromedriver 功能。这时只要直接创建页面对象即可正常使用。 -第一次运行后,程序会记录 Chrome 和 driver 路径到配置文件,以后直接调用。 +!> **注意:**
这段代码只用于设置配置文件中的路径信息,**运行一次即可**,勿写到正式程序里 。 -```python -page = MixPage() -page.get('https://www.baidu.com') -``` - -## 手动配置方式 - -!> **注意:**
这段代码只用于设置配置文件中的路径信息,**运行一次即可**,勿写到正式程序里 。 - -有些版本的 Chrome 程序无法获取正确的 driver,就须要手动配置路径。 新建一个**临时文件** ,修改并运行以下代码,可手动指定 Chrome 和 driver 路径,记录到配置文件,以后程序会自动读取其中的配置,无须再写。 如果想把配置信息写在代码里,请查阅“使用方法”里相关章节。 @@ -46,78 +34,21 @@ page.get('https://www.baidu.com') ```python from DrissionPage.easy_set import set_paths -# 请将以下路径修改为本机实际路径,chrome_path多数情况下可省略 -set_paths(driver_path=r"D:\chrome\chromedriver.exe", - chrome_path=r"D:\chrome\chrome.exe") +# 请将以下路径修改为本机实际路径 +set_paths(chrome_path=r"D:\chrome\chrome.exe", # 浏览器执行文件路径 + user_data_path=r"D:\chrome\userData", # 用户数据保存路径 + local_port=9999) # 设置浏览器端口,默认9222 ``` -当出现以下提示,说明设置成功: - -```shell -正在检测可用性... -版本匹配,可正常使用。 -``` - -各版本 driver 下载地址: - -- 国外:https://chromedriver.chromium.org/downloads -- 国内:http://npm.taobao.org/mirrors/chromedriver/ - # 上手示例 现在,我们通过一些例子,来直观感受一下 DrissionPage 的工作方式。 -## 爬取新冠排行榜 +!> **注意:**
如果您有已打开的 Chrome +浏览器,请先关闭,否则会造成冲突。后面在 [创建页面对象](%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%5C%E4%BD%BF%E5%88%9B%E5%BB%BA%E9%A1%B5%E9%9D%A2%E5%AF%B9%E8%B1%A1.md) +章节再介绍多 Chrome 浏览器共存的方法。 -网址:https://www.outbreak.my/zh/world -此示例爬取全球新冠情况排行榜。该网站是纯 html 页面,特别适合 s 模式爬取和解析。 - -该网址为外网网址,连接可能稍慢。 - -![](https://gitee.com/g1879/DrissionPage-demos/raw/master/pics/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20211231225026.jpg) - -```python -from DrissionPage import MixPage - -# 用 s 模式创建页面对象 -page = MixPage('s') -# 访问数据网页 -page.get('https://www.outbreak.my/zh/world') - -# 获取表头元素 -thead = page('tag:thead') -# 获取表头列,跳过其中的隐藏的列 -title = thead.eles('tag:th@@-style:display: none;') -data = [th.text for th in title] - -print(data) # 打印表头 - -# 获取内容表格元素 -tbody = page('tag:tbody') -# 获取表格所有行 -rows = tbody.eles('tag:tr') - -for row in rows: - # 获取当前行所有列 - cols = row.eles('tag:td') - # 生成当前行数据列表(跳过其中没用的几列) - data = [td.text for k, td in enumerate(cols) if k not in (2, 4, 6)] - - print(data) # 打印行数据 -``` - -输出: - -```shell -['总 (205)', '累积确诊', '死亡', '治愈', '现有确诊', '死亡率', '恢复率'] -['美国', '55252823', '845745', '41467660', '12,939,418', '1.53%', '75.05%'] -['印度', '34838804', '481080', '34266363', '91,361', '1.38%', '98.36%'] -['巴西', '22277239', '619024', '21567845', '90,370', '2.78%', '96.82%'] -['英国', '12748050', '148421', '10271706', '2,327,923', '1.16%', '80.57%'] -['俄罗斯', '10499982', '308860', '9463919', '727,203', '2.94%', '90.13%'] -['法国', '9740600', '123552', '8037752', '1,579,296', '1.27%', '82.52%'] -...... -``` +## ## 登录 gitee 网站 @@ -125,10 +56,10 @@ for row in rows: 此示例演示使用控制浏览器的方式自动登录 gitee 网站。 ```python -from DrissionPage import MixPage +from DrissionPage import WebPage # 用 d 模式创建页面对象(默认模式) -page = MixPage() +page = WebPage() # 跳转到登录页面 page.get('https://gitee.com/login') @@ -142,7 +73,6 @@ page.ele('@value=登 录').click() # 说明 -无论电脑安装的 Chrome 能否正常使用,我们都建议使用绿色版 Chrome。因为 driver 的更新速度往往跟不上浏览器的更新速度,一旦浏览器自动升级而 driver -未出新版,就会出现无法启动的情况。所以建议用关闭升级功能的绿色版浏览器,使程序运行更稳定。 +无论电脑安装的 Chrome 能否正常使用,都建议使用绿色版 Chrome,并且设置`user_data_path`。 如果计划程序打包成 exe 文件,就不能使用默认配置文件记录配置,具体方法请查看“使用方法->打包程序”章节。 diff --git a/setup.py b/setup.py index 27e4693..b385362 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r", encoding='utf-8') as fh: setup( name="DrissionPage", - version="3.0.0", + version="3.0.2", author="g1879", author_email="g1879@qq.com", description="A module that integrates selenium and requests session, encapsulates common page operations.", @@ -23,7 +23,8 @@ setup( "tldextract", "requests", "DownloadKit", - "FlowViewer" + "FlowViewer", + "pychrome" ], classifiers=[ "Programming Language :: Python :: 3.6",