💥 4.0 功能介绍
3.x 是自主研发底层的初步尝试,很多地方摸着石头过河,存在一些不太成熟的地方。
经过一段时间的使用,积累一些经验之后,4.0 在 3.x 的基础上对底层进行了大幅重构,新增大量功能,改善运行效率和稳定性,优化项目结构,解决很多存在的问题。对比旧版本有质的提高。
但同时不少 api 发生了变化,不能完全兼容旧版本。
api 的变化有些是功能优化必需的改变,有些则是本人对命名简洁的执念,趁着大版本的更新顺便把长期不太满意的命名给改了。
给使用者造成一定不便感到抱歉,但长痛不如短痛,趁着项目用的人不多,干脆舍弃历史包袱果断改掉。
有些原来的写法在 4.0.0 中还能正常使用,但 IDE 会提示无效,将在以后的版本中完全删除。推荐尽快更新为新写法。
本节仅简述功能变化,具体使用方法详见各对应章节。
✅️ 新的抓包功能
3.2 中,抓包功能主要由 FlowViewer 和wait.data_packets()
提供。
FlowViewer 是本人的一个练手作品,写得比较随意,技术也还没到家。存在漏抓、信息不全、api 不够合理的问题。
4.0 中,每个页面对象都内置了监听器,能力全面升级,api 也更合理。
📌 旧 api 变化
- 弃用 FlowViewer,以后也不会再升级
- 删除
wait.set_targets()
- 删除
wait.stop_listening()
方法 - 删除
wait.data_packets()
方法 DrissionPage.common
路径删除FlowViewer
📌 新 api
- 每个标签页对象(包括
ChromiumFrame
)新增listen
属性,内置监听功能 - 用
listen.set_start()
和listen.stop()
启动和停止监听 - 用
listen.wait()
阻塞等待数据包 - 用
listen.steps()
同步获取监听结果 - 增加
listen.wait_silent()
等待所有请求完成(包含 target 以外的) - 监听结果结构优化,request 和 response 数据分开存放
📌 示例
下面示例可直接运行查看结果。这个示例会计时,用于与下个示例对比。
from DrissionPage import ChromiumPage
from TimePinner import Pinner
from pprint import pprint
page = ChromiumPage()
page.listen.start('api/getkeydata') # 指定监听目标并启动监听
pinner = Pinner(True, False)
page.get('http://www.hao123.com/') # 访问网站
packet = page.listen.wait() # 等待数据包
pprint(packet.response.body) # 打印数据包正文
pinner.pin('用时', True)
输出:
{'hao123.new.shishi.bangdan.recom': [{'index': '1',
'pure_title': '以色列和哈马斯移交首批被扣押人员'},
{'index': '2',
'pure_title': '听到免签政策法国外长笑了'},
......
用时:3.3114853000151925
✅️ 新的页面访问逻辑
3.x 中存连接存在以下主要问题:
- 浏览器页面对象
get()
方法的timeout
参数只对加载阶段生效,无法覆盖连接阶段; - 加载策略
none
模式没有实际用处。
这两个问题都在 4.0 中解决,且能够让用户自主控制终止连接的时机。 另外还对连接逻辑进行了优化,避免卡死情况出现。
📌 api 变化
- 页面对象
page_load_strategy
属性改名为load_mode
set.load_strategy
改为set.load_mode
📌 行为变化
get()
方法的timeout
参数现在可覆盖整个过程timeout
参数对非get()
方法触发的加载(如点击链接)也能生效SessionPage
和WebPage
的 s 模式,如收到空数据,也会重试SessionPage
的get()
方法可以指向本地文件
📌 新的none
加载模式
旧版中,none
加载策略是当页面连接成功就立刻停止加载,这在实际使用时没有什么意义。
新版中,这个模式改成:除非加载完成,否则程序不会主动将其停止(即使已超时),同时连接状态不再阻塞程序,而允许用户进行状态判断,主动停止加载。
这样提供给用户非常大的自由度,可等到关键数据包或元素出现就主动停止页面加载,大幅提升执行效率。
📌 示例
我们继续使用上一个示例的代码,但把加载模式设为none
,且获取到数据时主动停止加载。
from DrissionPage import ChromiumPage
from TimePinner import Pinner
from pprint import pprint
page = ChromiumPage()
page.set.load_mode.none() # 设置加载模式为none
page.listen.start('api/getkeydata') # 指定监听目标并启动监听
pinner = Pinner(True, False)
page.get('http://www.hao123.com/') # 访问网站
packet = page.listen.wait() # 等待数据包
page.stop_loading() # 主动停止加载
pprint(packet.response.body) # 打印数据包正文
pinner.pin('用时', True)
输出:
{'hao123.new.shishi.bangdan.recom': [{'index': '1',
'pure_title': '以色列和哈马斯移交首批被扣押人员'},
{'index': '2',
'pure_title': '听到免签政策法国外长笑了'},
......
用时:1.2575092000188306
可见节省了2秒时间。 当网站要访问一些不稳定资源时,节省的时间相当客观,也能提高程序的稳定性。
✅️ 新的下载管理功能
在旧版中,下载管理功能存在以下问题:
- 浏览器下载管理和内置下载器
DownloadKit
的配置都使用download_set
属性进行设置,容易造成混淆。 - 浏览器下载任务不能在下载前指定文件名
- 该功能有随着浏览器版本更新失效的风险
4.0 对浏览器下载管理功能进行了完完全全的重构,结构更为合理,功能更多。 同时,内置下载器的设置和浏览器下载任务设置进行了分离。
📌 api 变化
- 页面对象删除
download_set
属性 - 增加
set.download_path()
方法 - 增加
set.download_file_name()
方法
📌 新增功能
- Tab 对象和 Frame 对象也支持
download()
方法 - 每个 Tab 对象可单独设置下载路径和重命名文件名
- 可拦截浏览器下载任务并获取其信息
- 可取消浏览器下载任务、获取下载进度、等待任务完成
- 可设置遇到文件夹已存在时的处理方式
📌 行为变化
4.0 中默认不启用浏览器下载任务管理,只有在启动参数中设置了下载路径,或调用set.download_path()
方法时才会启动。
未启动任务管理功能时,下载行为和普通使用一样。
📌 示例
以下示例可直接运行。
from DrissionPage import ChromiumPage
page = ChromiumPage()
page.get('https://office.qq.com/download.html')
page.set.download_path('tmp') # 设置文件保存路径
page.set.download_file_name('qq') # 设置文件名
page('#downloadWin').click() # 点击触发下载
mission = page.wait.download_begin() # 等待下载开始并获取任务对象
mission.wait() # 等下下载任务完成
输出:
url:https://dldir1.qq.com/qqfile/qq/TIM3.4.8/TIM3.4.8.22124.exe
文件名:qq.exe
目标路径:D:\coding\projects\DrissionPage\tmp
100.0% 下载完成 D:\coding\projects\DrissionPage\tmp\qq.exe
✅️ 页面对象
这里说的页面对象包括 Page 对象(ChromiumPage
、WebPage
)、Tab 对象(ChromiumTab
、WebPageTab
)、ChromiumFrame
对象。
📌 启动参数变化
4.0 中,创建WebPage
和ChromiumPage
对象时,不再接收ChromiumDriver
对象。
意思是不再支持传递控制权的方式创建页面对象。
因为本身支持多个页面对象控制同一个标签页,如果需要多页面对象协同,只要get_tab()
创建一个新对象就行,可和原有对象并行使用。
而且,传递控制权本身有稳定性方面隐患,因此新版中将其删除。
相应地,启动参数的名称也发生了变化:
WebPage
对象的driver_or_options
参数改名为chromium_options
ChromiumPage
对象的addr_driver_opts
参数改名为addr_or_opts
另外,ChromiumPage
的启动参数addr_or_opts
现在可以接收int
数据,直接传入端口号。
📌 内置动作链
4.0 中,每个页面对象内置actions
属性,即动作链。
内置的动作链与直接创建的动作链对象有一个不同点,每次操作会等待页面加载完成再执行。
示例:
📌 状态信息
旧版中,页面对象拥有ready_state
、is_loading
、is_alive
属性,现在都合并到states
属性中。
📌 其它
- 页面对象增加
raw_data
参数,s 模式下返回原始数据 - 所有页面对象增加
close()
方法,SessionPage
用于关闭连接,浏览器页面对象用于关闭标签页 - 浏览器页面对象增加
wait()
方法,用于等待若干秒 - 浏览器页面对象增加
wait.ele_load()
方法,等待元素加载到DOM - 浏览器页面对象增加
set.cookie()
方法,可设置单个 cookie - 浏览器页面对象增加
wait.title_change()
和wait.url_change()
方法,用于等待 title 和 url 变化 quit()
方法增加force
参数,可强制关闭浏览器进程ChromiumFrame
增加ract
属性ChromiumFrame
的frame_size
属性改为rect.size
- 优化
SessionPage
和WebPage
s 模式访问速度
✅️ 标签页管理
📌 不再支持to_tab()
功能
to_tab()
的设计源自于 selenium,用于在多个标签页间切换程序焦点。
selenium 没有 tab 对象,driver 每次只能操作一个 tab。多 tab 使用时需在不同的 tab 间来回切换,且切换的时候会丢失之前获取过的元素,效率低,使用不便。
DrissionPage 3.x 开始就支持多 tab 对象共存,对象之间互不影响,而且标签页无需激活即可操作。因此不再需要切换标签页。
而且焦点切换时如果页面正在加载,实现逻辑较为复杂,也会有稳定性问题。
基于此,决定删除to_tab()
方法,使用get_tab()
取代之。
涉及的 api修改:
- 删除
to_tab()
方法 - 删除
to_main_tab()
、set.main_tab()
方法 - 删除
main_tab
属性 new_tab()
方法删除switch_to
参数
新建标签页并切换到新标签页:
操作另一个标签页
# ------ 旧版代码 ------
page.to_tab(page.tabs[1])
# ------ 新版代码 ------
tab = page.get_tab(1) # 创建一个可与page同时使用的tab对象
📌 new_tab()
的新功能
Page 对象的new_tab()
方法增加了 3 个参数:
new_window
:是否创建新的窗口,新窗口与旧标签页同属一个浏览器,只是独立窗口background
:新建的标签页是否为不激活状态(即使不激活也可以操作)new_context
:是否创建独立的隐身窗口,该窗口与旧标签页 cookies 相互独立
现在new_tab()
返回新建的标签页对象,而非其 tab_id。
示例:
📌 标签页的位置与大小
在旧版中,只有ChromiumPage
或WebPage
对象可以设置窗口位置、大小、状态。
Tab 对象(ChromiumTan
、WebPageTab
)虽然能获取窗口窗口上述信息,但只是获取 Page 所控制的标签页信息。
在 4.0 中,独立窗口的标签页,也能设置和获取上面这些属性。
以下属性和方法名称进行了修改:
rect.borwser_size
改为rect.window_size
rect.borwser_location
改为rect.window_location
set.window.maximized()
改为set.window.max()
set.window.minimized()
改为set.window.mini()
set.window.fullscreen()
改为set.window.full()
示例:
tab = page.get_tab(1)
print(tab.rect.window_state) # 获取窗口状态
print(tab.rect.window_location) # 获取窗口位置
print(tab.rect.window_size) # 获取窗口大小
tab.set.window.size(500, 500) # 设置窗口大小
tab.set.window.location(500, 500) # 设置窗口位置
tab.set.window.max() # 窗口最大化
# 更多详见相关文档……
📌 标签页的行为
在旧版中,标签页的开关、激活由 Page 对象管理。JS 弹窗也只有 Page 对象能处理。
在 4.0 中,标签页对象可以激活、关闭自己,也可以处理自己的弹出对话框。
tab.set.activate()
:标签页对象激活自己tab.close()
:标签页对象关闭自己tab.handle_alert()
:标签页对象处理自己的弹出tab.states.has_alert
:标签页增加此属性表示是否有弹窗存在
📌 get_tab()
参数变化
现在get_tab()
方法可接收标签页序号(序号从 0 开始)。tab_id
参数改为id_or_num
。
但需要注意,序号与标签页视觉排序不一定一致,而是按照激活顺序排列。
示例:
✅️ 元素相关
📌 定位语法变化
旧版中,定位语法中使用@@-
或@|-
表示否定,但视觉效果不明显,意义也不太明确。
因此改用@!
替代,使其更能体现否定的含义。
@!
可与@@
或@|
混用,与还是或关系视@@
还是@|
而定。
也可以单独使用,否定某个单独属性。
示例:
# ------ 旧版语法 ------
page.ele('@@arg1=abc@@-arg2=def')
# ------ 新版语法 ------
page.ele('@@arg1=abc@!arg2=def')
# ------ 旧版语法 ------
page.ele('t:div@|arg1=abc@|-arg2=def')
# ------ 新版语法 ------
page.ele('t:div@|arg1=abc@!arg2=def')
# ------ 旧版语法 ------
page.ele('@@-arg1=abc')
# ------ 新版语法 ------
page.ele('@!arg1=abc')
📌 相对定位参数优化
旧版本中,相对定位如果想用序号获取元素,需要写成ele.next(index=1)
。
但是我想把这个语句变得更简化一点,写成ele.next(1)
即可表示获取下一个兄弟元素。
4.0 中支持这种写法,当filter_loc
参数接收int
类型参数,即可当作index
参数使用。
parent()
方法增加index
参数,当level_or_loc
传入定位符,使用此参数选择第几个结果。
📌 位置和大小
旧版中,元素大小和位置信息由location
、locations
、size
几个属性提供。
这些属性都与形状相关,为使逻辑更清晰,与页面对象逻辑一致,将它们统一归纳到rect
属性中。
- 删除
size
、location
、locations
属性,新增rect
属性 - 旧版中
loactions.xxxx
的属性改为rect.xxxx
- 大小和位置信息,从
int
类型改为float
类型 - 增加
states.has_rect
属性,返回元素是否拥有大小和位置 - 增加
states.is_whole_in_viewport
属性,返回元素是否整个都在视口内
# ------ 旧版代码 ------
ele.size
ele.location
ele.locations.midpoint
# ------ 新版代码 ------
ele.rect.size
ele.rect.location
ele.rect.midpoint
📌 点击
click()
增加wait_stop
参数,默认等待元素运动停止再点击click()
的by_js
参数默认改为None
,即点击失败时自动改用 js 方式点击click()
默认等待元素运动停止再执行点击click.twice()
改为click.multiple()
📌 查找元素失败显示细节
旧版链式查找元素时,遇到其中一个查找失败,不能很直观地显示哪个语句失败。
在 4.0 中,可以把查找失败的语句和定位语句显示在报错信息中。
示例
from DrissionPage import ChromiumPage
page = ChromiumPage(timeout=1)
page.get('https://baidu.com')
print(page('#wrapper')('#s_tab')('#abcd').text) # ('#abcd')这个元素不存在
输出:
📌 设置查找失败元素返回默认值
如果查找元素后要获取一个属性,但这个元素不一定存在,或者链式查找其中一个节点找不到,可以设置查找失败时返回的值,而不是抛出异常。
这样可以简化一些采集逻辑。
示例
比如说,遍历页面上一个列表中多个对象,但其中有些元素可能确实某个子元素,旧版中要这样写:
from DrissionPage import ChromiumPage
page = ChromiumPage()
for li in page.eles('t:li'):
ele = li('.name')
name = ele.text if ele else None
ele = li('.age')
age = ele.text if ele else None
ele = li('.phone')
phone = ele.text if ele else None
在新版中,可以这样写:
from DrissionPage import ChromiumPage
page = ChromiumPage()
page.set.NoneElement_value('没找到')
for li in page.eles('t:li'):
name = li('.name').text
age = li('.age').text
phone = li('.phone').text
这样,假如某个子元素不存在,不会抛出异常,而是返回'没找到'
这个字符串。
📌 更多
- 增加
wait.stop_moving()
方法,可等待移动结束 - 增加
wait()
方法,等待若干秒 - 滚动添加
to_center()
方式,可滚动到视口中央 - 增加
select.by_option()
和select.cancel_by_option()
方法,可选取列表项元素 - 增加
states.has_rect
属性 - 添加
states.is_whole_in_viewport
属性,判断是否整个都在视口中 input()
方法增加by_js
参数save()
的rename
参数改为name
get_src()
支持 blob 类型css_path
获取的路径更精确- 相对定位的
timeout
参数默认改为None
✅️ 启动配置
📌 删除 easy_set 方法
easy_set 原本设计目的是为了方便修改 ini 文件设置。
本想着设置一次,之后无需再调用。但可能由于文档描述不清晰,很多人将其写到了正式的代码中。
因为 ini 文件的修改会影响其它项目,这是作者不推荐的用法。
而且,实际使用中发现 easy_set 并没有变得更方便,功能可用ChromiumOptions
对象的save()
方法代替。
因此决定将其删除,也可以免去多维护一份代码。
📌 支持设置实验项
4.0 新增支持启动浏览器时设置实验项,即'chrome://flags'
中的项目。
使用新增的set_flag()
方法设置。使用clear_flags_in_file()
清空配置文件中已设置的项。
有哪些实验项,具体在'chrome://flags'
查看。
示例:
from DrissionPage import ChromiumOptions
co = ChromiumOptions()
co.set_flag('temporary-unexpire-flags-m118', '1')
co.set_flag('disable-accelerated-2d-canvas')
📌 ini 文件变化
chrome_options
类改为chromium_options
binary_location
项改为browser_path
page_load_strategy
项改为load_mode
debugger_address
项改为address
arguments
项删除'--remote-allow-origins=*'
参数arguments
项增加'--no-default-browser-check'
、'--disable-suggestions-ui'
参数- 删除
experimental_options
项 chrome_options
类增加prefs
、flags
、existing_only
项- 增加
others
类,包含retry_times
和retry_interval
项
📌 ChromiumOptions
修改
- 增加
set_flag()
和clear_flags_in_file()
,用于设置实验项 - 增加
existing_only()
方法和is_existing_only
属性,可指定只接管浏览器而不自动启动新的 - 增加
ignore_certificate_errors()
方法,可忽略证书报错 - 增加
retry_times
、retry_interval
属性和set_retry()
方法,可设置重试参数 - 增加
incognito()
方法,可设置无痕模式 set_paths()
方法拆分成set_browser_path()
、set_local_port()
、set_address()
、set_download_path()
、set_user_data_path()
、set_cache_path()
方法set_page_load_strategy()
改成set_load_mode()
set_headless()
改成headless()
set_no_imgs()
改成no_imgs()
set_no_js()
改成no_js()
set_mute()
改成mute()
debugger_address
改成address
📌 SessionOptions
修改
SessionOptions
的set_paths()
方法改为set_download_path()
- 增加
retry_times
、retry_interval
属性和set_retry()
方法,可设置重试参数
📌 其它
- 启动或接管浏览器时,可自动关闭弹出的隐私声明
- 在无界面系统启动浏览器时,自动使用无头,可用
set_headless(False)
禁用 - 当
set_headless(False)
但接管了无头浏览器,将关闭并启动新的有头浏览器 auto_port()
方法支持多线程使用
✅️ 其它
📌 删除 2.x 代码
MixPage
是 WebPage
的前身,基于 selenium。
随着 3.x 版本迭代,自研的底层日渐成熟并完全超越旧版。旧版已到了退休的时候。。
为对项目进行精简,避免新代码受旧功能制约,因此将旧代码删除。
删除以下类:MixPage
、DriverPage
、DriverOptions
、Drission
。
旧版对作者的成长有过巨大贡献,因此将其独立成一个库,以此来继续它的生命,和纪念它做出过的成果。
可用以下命令安装旧版尝旧:
📌 异常变化
CallMethodError
改为CDPError
ElementLossError
改为ElementLostError
ContextLossError
改为ContextLostError
TabClosedError
改为PageClosedError
- 增加
WaitTimeoutError
- 增加
GetDocumentError
- 增加
WrongURLError
- 增加
StorageError
- 增加
CookieFormatError
📌 Settings 变化
raise_ele_not_found
改为raise_when_ele_not_found
raise_click_failed
改为raise_when_click_failed
📌 更多
handle_alert()
方法增加next_one
参数,可处理下一个出现的弹窗- 浏览器页面对象增加
set.auto_handle_alert()
方法,可设置自动处理弹窗 SessionPage
增加set.encoding()
方法和encoding
属性run_js()
、run_js_loaded()
、run_async_js()
方法增加timeout
参数timeouts
的implicit
改成base
ActionChains
改成Actions
- 动作链的移动方法增加
duration
参数 - 动作链增加
input()
方法 - 动作链
key_down()
和key_up()
方法可接收按键名称文本 - 动作链
type()
方法text
参数改为keys
get_screenshot()
方法增加name
属性,可指定文件名- 元素的
get_screenshot()
方法增加scroll_to_center
参数,截图前先滚动到页面正中 wait.new_tab()
方法成功时返回新标签页 idtabs
不包含 F12 的窗口DrissionPage.common
路径增加wait_until()
方法,支持自定义组合等待条件
✅️ 问题修复
- 修复网络连接极不稳定时获取文档失败问题
- 修复相对定位
timeout
失效问题 - 修复 shadow root 内定位元素可能偏差问题
- 修复异域
ChromiumFrame
内部元素无法获取屏幕坐标的问题 - 修复相对路径插件加载失败的问题
- 所有循环增加超时设置,避免出现卡死
- 修复元素截图时窗口外部分空白问题
- 修复 Tab 没有继承 Page 下载路径的问题
- 修复
<iframe>
内元素获取 href 属性错误问题 - 修复 cookie 设置 expires 时的问题
✅️ 下一步计划
- 支持跨标签页监听数据包
- 模拟手机环境,支持触摸操作
- 支持随时修改浏览器代理
- 研究远程控制浏览器方案
- 支持 websocket 抓包
- 支持操作插件小窗
- 移动和点击时显示鼠标位置
✅️ 加快开发进度
开源项目的推进有赖你的支持。欢迎投喂加速。