爬虫-DrissionPage
这篇讲什么
- DrissionPage 把 HTTP 请求与浏览器控制放进同一个库:
SessionPage不执行 JavaScript,浏览器标签页读取渲染后的 DOM,WebPage/MixTab能切换两种模式。两条路线都可以结构化解析 HTML。 - 本篇以 DrissionPage 4.1.1.4、Python 3.14.10、Chrome 156 为实测环境,从官方入口讲到定位、元素、等待、配置、标签页、监听、cookie、上传下载、iframe 和异常,附完整代码与实跑输出。
- 重点区分「页面返回」与「业务数据出现」、「返回 False」与「抛异常」,并复现 CSS 裸标签多命中、
NoneElement和浏览器开关差异。在线数据、布局和浏览器版本会变化,数字与输出不保证永久不变。
安装与第一例
官方安装页 推荐:
1 | python -m pip install DrissionPage |
本篇安装版本为 4.1.1.4。电脑仍需 Chrome、Edge 等 Chromium 内核浏览器,但不用另外配置 chromedriver。CDP 是 Chrome DevTools Protocol,即浏览器提供的调试控制协议,不是另一款浏览器。
先按官方的「浏览器 → 标签页」入口抓一页:
1 | # -*- coding: utf-8 -*- |
1 | 标签对象: MixTab |
ChromiumOptions配置启动方式,headless(True)不显示窗口,auto_port()为本例分配独立端口和环境;Chromium(options)获取浏览器,latest_tab取得标签页。get()返回布尔值,不返回页面对象。SPA 还需等接口完成,wait.eles_loaded()等业务元素,eles()返回元素列表,逐个.text取文本。finally只关闭本例新建的浏览器。- ssr1 本次复核出现 502/503;spa1 首页接口返回了含空字段的记录,前端报
reading 'join'后没有电影卡片。本例使用已验证的/page/2,片名定位为t:h2。该页没有.name类,不能直接套用 ssr1 的选择器;等待超时也不等于等待 API 失效。
对象与包面
ChromiumPage 直接给一个绑定标签页的页面对象;Chromium 先取得浏览器,再选标签。下面打印本机版本的公开名与签名。列出名字并不等于执行过全部方法,行为以对应章节的调用为准。
1 | # -*- coding: utf-8 -*- |
1 | DrissionPage version: 4.1.1.4 |
- 顶层有 8 个公开名:
Chromium、ChromiumOptions、ChromiumPage、SessionOptions、SessionPage、WebPage、errors、version。By、Keys、Settings从DrissionPage.common导入。 - MRO 是 Python 查找父类成员的顺序。
ChromiumPage与SessionPage有共同的解析器基类,但不互为父子类;共享定位语法不代表搜索实现与等待能力完全相同。 WebPage同时继承两种页面类。构造参数mode='d'表示浏览器模式,mode='s'表示 HTTP 模式;运行中用change_mode()切换,不能给只读属性mode赋值。errors的 20 个公开异常类包含基类BaseError自身。除库异常外,还会遇到 Python 的TypeError、ValueError等。
同一页两种路线
对同一个 SPA,分别读取 HTTP 源码、浏览器 DOM 和公开接口。是否执行 JavaScript 与 是否能结构化解析 HTML 是两件事:
1 | # -*- coding: utf-8 -*- |
1 | HTTP 状态: 200 |
SessionPage内部使用 requests 会话,也能ele()/eles(),但只解析收到的源码。spa1 第二页源码有脚本而没有电影标题,因此t:h2为 0;不是解析器不能用。make_session_ele()对静态 HTML 生成SessionElement,同样能读.text;ChromiumElement则连接浏览器的活 DOM,还能点击、等待、执行 JS。- 浏览器等待后取得 10 项。不要用“DOM 更长”判断 JS 是否执行:序列化可能改变空白与属性,业务元素出现才是本例的判据。
- 知道接口地址时,用
SessionPage直接读 JSON 不必启动浏览器。接口片名没有 DOM 的英文副标题,因此比较包含关系;先检查非空,不让两个空列表制造“相同”的假象。
属性与方法
page.title、ele.text 是属性,page.get()、ele.attr() 是调用。下例用 inspect.getattr_static() 标注性质,并执行错误写法:
1 | # -*- coding: utf-8 -*- |
1 | == ChromiumPage 全部 69 个公开成员的性质 == |
- 本版本
ChromiumPage类有 69 个公开成员:32 个 property、37 个方法。实例上可能多出成员,不能混用两种统计。 title()是先拿到字符串,再试图调用它,因而报'str' object is not callable;tabs_count()则报整数不可调用。这种错误也可能来自同名变量遮住函数,不只发生在 property 上。tab_count、tabs、页面上的set_paths、get_page_info在本版本未提供。数量读tabs_count、列表用get_tabs()、路径配置用ChromiumOptions.set_paths();不据此编造旧版本迁移历史。ele.click虽是 property,却返回可调用的Clicker,所以ele.click()正确。是否加括号还要看返回对象。ele.states各状态直接读取。普通链接的is_checked/is_selected是None,表示本例不适用,不能误当“未选中”。page.address包含主机和调试端口;示例为了避免随机端口干扰只打印主机部分。get()的布尔返回值不能当页面使用。
定位语法
ele(locator, index=1, timeout=None) 第一个业务参数是定位符;eles(locator, timeout=None) 取全部。下例先等 spa1 第二页的 t:h2 出现,再比较命中数量与首个元素:
1 | # -*- coding: utf-8 -*- |
1 | 页面: https://spa1.scrape.center/page/2 | title: Scrape | Movie |
- 标签用
tag:div/t:div,属性用@id=app。=完整相等、:包含、^开头、$结尾,例如@href^/detail。@href^=https多出的等号也进入匹配内容,不要把 CSS 属性选择器的等号原样搬过来。 #app简写 id,.cover简写完整 class 匹配;text=Scrape/tx=Scrape匹配完整文本,text:Scra匹配包含。裸字符串也是文本包含;没有目标标签限制时可能命中标题或容器,需查看返回的 tag 与文本。.item不等同于 CSS 类词匹配。卡片实际 class 为el-card item m-t is-hover-shadow,@class=item/.item为 0;tag:div@class:item按包含匹配,css:div.item才是 CSS 形式。XPath//div[@class="item"]同样要求属性完整相等,本例也是 0。@@是与、@|是或、@!是否定。@@class:item连评分图标一起命中,加tag:div才缩小到卡片;有命中不等于选对。- XPath 前缀是
xpath:/x:,CSS 是css:/c:。css:>div本例为 0,不作为通用页面根节点写法;纯标签 CSS 的额外命中见下一节。 page('t:a')简写单元素定位,元素自身也能继续定位。本例卡片内.//a与//a均为两个链接,建议显式.//a表达子树,不把观察推及所有解析器。- XPath 取属性会返回字符串:
box.ele('xpath:.//a/@href')得到'/detail/10',box.eles(...)取字符串列表,不是元素列表。
未识别的前缀不一定报错,可能只是合法查询没有命中。这里把转换结果单独打印:
1 | # -*- coding: utf-8 -*- |
1 | id:app -> by=xpath value='//*/text()[contains(., "id:app")]/..' |
id:app、class:name等被当作包含整串文字的文本查询,不会自动变成 id/class 条件;css:div.item:simple是无效 CSS;tag:div@@class=summary则是合法 XPath,只是页面没有匹配项。三种零命中的原因不同。get_loc()是用于核对本版解析规则的底层工具,业务定位仍用公开接口。
找不到时的处理:
ele()默认返回NoneElement,bool()为假、== None为真,但不是 Python 的None。先判断再访问;直接.text、.tag、.click()会抛ElementNotFoundError。eles()未命中是空列表,可以遍历;业务要求至少一条数据时仍需检查非空。page.set.NoneElement_value("取不到", True)配置缺失值,本例.text和.attr直接变成字符串,不能再调用.attr()。恢复用page.set.NoneElement_value(None, False)。Settings.raise_when_ele_not_found = True让定位阶段抛错,示例保存旧值并复位;ele(show=True)则因不支持参数而报TypeError。
CSS 搜索边界
页面级 css:p 不总等于原生 querySelectorAll('p')。在最小页面中把定位转换、CDP 搜索和元素结果分别对照:
1 | # -*- coding: utf-8 -*- |
1 | 页面里每种标签各 1 个,共 8 个元素 |
- 页面只有一个
<p>,原生 CSS 返回一个,page.eles('css:p')却返回p、span、script;css:b也有额外命中。 - 页面入口使用
DOM.performSearch,不是强制 CSS → XPath。 本版本页面查找实现调用默认get_loc(locator)后取查询串,css:p最终传入的是p。CDP 此入口兼有纯文本、CSS、XPath 搜索语义,并不等价于querySelectorAll。 - 原始
DOM.performSearch本例返回 4 个节点,库再过滤后得到 3 个元素;//p则只命中一个。不要要求原始节点计数与最终元素计数始终相同。 get_loc(..., translate_css=True)确实生成 XPath,但属于另一个显式分支,不能拿它证明页面级调用走了转换。- 只按标签查找时,用已验证的
t:p/xpath://p。带 id/class 的几个例子正确,不代表所有 CSS 都没有边界问题;By.CSS_SELECTOR同样要对照原生命中集合。 - 本页没有
p的子元素b,css:p > b为 0 是合理结果,不是转换失败。私有函数只用来检查本版本实现,不作为业务依赖。
元素对象能取什么
拿到一个元素之后能干的事,比「取文本」多得多。这段用一个片名 <a> 做样本,介绍文本、属性、rect、states、关系定位、滚动和 JS。窗口、缩放、浏览器、站点和加载时机都会影响页面数据;学习时应关注 API 的用途,不把某次文本或坐标当成固定答案。
1 | # -*- coding: utf-8 -*- |
1 | 元素: ChromiumElement | tag: a | text: 狮子王 - The Lion King |
ele.text是处理后的文本,ele.raw_text是原始文本;示例打印两者之前都调用了brief()折叠空白,因此相同输出不能说明原始空白相同。texts()返回文本片段列表,texts(True)只取直接文本节点:文字若放在子元素<h2>内,就不属于<a>的直接文本。取完整片名通常用.text即可。ele.html含自身标签,ele.inner_html只含标签内部内容。可用它们检查链接、子节点和嵌套结构,不必依赖 HTML 的固定长度。ele.css_path和ele.xpath可以辅助确认取到了哪个节点;含位置序号的路径会随 DOM 结构变化,不宜当作长期稳定的选择器。- 对链接元素,
ele.attr('href')和ele.link会将链接解析为绝对 URL,不能把前者当作读取源码相对值的接口;查看标签中的写法可用ele.html。不存在的attr('data-x')返回None,与定位不到元素时的NoneElement不同。 ele.attrs提供属性字典,sorted(ele.attrs)可查看属性名。优先选择语义稳定的属性定位,不依赖页面构建工具生成的属性名。ele.style('color')读取计算样式,不是 CSS 源码的原始写法;ele.style('display', ':before')读取伪元素样式。ele.rect提供元素的几何信息:size是宽高,location、midpoint分别表示位置和中点,click_point是点击点,corners是四角。浏览器布局与取整会影响数值,不应要求不同字段的计算结果始终精确相等。viewport_location使用视口坐标,screen_location使用屏幕坐标;阅读坐标前先分清参照系。滚动会改变元素相对于视口的位置,点击点也不必等于几何中心。page.rect是页面级几何信息;size、viewport_size、window_size分别描述页面、视口与浏览器窗口。结合scroll_position判断滚动状态,不把这些尺寸混为一谈。- 本版
ElementStates有 10 个公开状态属性:is_selected、is_checked、is_displayed、is_enabled、is_alive、is_in_viewport、is_whole_in_viewport、is_covered、is_clickable、has_rect。它们分别回答选中、勾选、显示、可用、存活、视口范围、遮挡、可点击及矩形状态等问题,不应互相替代。 is_covered在点击点调用DOM.getNodeForLocation;命中节点与自身不同就返回其backendNodeId,否则返回False(捕获CDPError时也返回False)。真值不一定表示外部遮挡层,命中子节点也可能不同。has_rect有矩形时返回角点列表,否则为False;普通链接的is_checked/is_selected可为None。- 本版本未提供
sibling()/siblings()。parent()向上、children()/child()向下;next()/nexts()和prev()/prevs()找兄弟节点,before()/befores()、after()/afters()则按文档顺序查找,不限于兄弟。后者使用 XPath 的preceding/following轴,也不等同于查找祖先或后代。 - 在卡片元素内调用
card.ele(...)、card.eles(...)可缩小查找范围,card('t:h2')是单元素查找的简写。图片可读取attr('src')或link,不要将某个站点的图片地址当成固定结果。 - 页面滚动用
page.scroll,元素滚动用ele.scroll;如page.scroll.to_bottom()、page.scroll.to_top()、ele.scroll.to_see()和ele.scroll.to_center()。is_in_viewport为真只说明在视口内,不能单凭它证明精确居中。 ele.run_js('return this.tagName')中的this是该元素;page.run_js('return document.title')是页面级调用。page.run_async_js()用于异步执行,不应把它的即时返回值当成脚本的计算结果。
等待与超时
浏览器页面可能出现「取早了」。这里区分定位调用的 timeout、页面级的 base/page_load/script 三档,以及 page.wait / ele.wait 的具名等待方法。本机靶页在点击按钮后设置 800ms 定时器,再注入 10 个 <li>;脚本还包含在线 SPA 的对照。
1 | # -*- coding: utf-8 -*- |
1 | == dir(page.wait) 真实成员 == |
page.wait列出 14 个具名方法:alert_closed、all_downloads_done、doc_loaded、download_begin、downloads_done、ele_deleted、ele_displayed、ele_hidden、eles_loaded、load_start、new_tab、title_change、upload_paths_inputted、url_change。这是成员枚举,不代表逐个运行过;本版本未提供wait.ready()、wait.to_stop(),调用会抛AttributeError。ele.wait列出 11 个:clickable、covered、deleted、disabled、disabled_or_deleted、displayed、enabled、has_rect、hidden、not_covered、stop_moving。应按元素需要满足的条件选择方法。page.wait本身可调用:page.wait(0.2)是固定时长等待,返回ChromiumPage;固定等待不等于目标元素已就绪。- 三档超时分别用于基础等待、页面加载和脚本执行。本机示例的初值是
{'base': 10, 'page_load': 30, 'script': 30}。示例使用ChromiumOptions(),没有关闭配置文件读取,不能将这组值当作所有环境的保证。 - 配置对象的方法名是
set_timeouts(base=None, page_load=None, script=None);页面运行中则用page.set.timeouts(base=3, page_load=7)。未传的script保持原值,可读page.timeouts确认当前设置。 - 本版本未提供
page.set_timeouts()、page.set.auto_port()。set_timeouts()、auto_port()提供在ChromiumOptions上,注意区分配置对象与页面对象。 - 本机按钮示例中,点击前列表为空,点击后由定时器注入列表项。
timeout=0表示不等待,timeout=0.3或timeout=5则给定位设置等待上限,并非总要等满该时长。短超时可能来不及找到元素;两次点击的定时器没有取消,不能把它当作精确计时实验。 page.wait.eles_loaded('t:h9', timeout=0.3)演示等待失败,示例设置下返回False,加raise_err=True抛WaitTimeoutError;不要推广为所有等待 API 的统一返回规则。page.wait.ele_displayed('#go', timeout=0.3)成功时返回元素对象,而非True;示例中查不存在的t:h9返回False。使用返回值时要区分元素与失败值。- 对在线 SPA,先确认
get()成功,再比较eles(..., timeout=0)与允许等待的定位调用。两次取值之间 DOM 可能变化,不是同一时刻的 DOM 对照,也不保证固定的数量变化;请求失败、接口异常或定位串失效都不能直接归因于等待机制。
page.wait.doc_loaded() 面向文档状态,不保证之后的定时器或异步任务已经改完 DOM;本块的 eles_loaded、ele_displayed 示例则以目标元素为等待条件。
配置对象
ChromiumOptions / SessionOptions 是配置入口,不启动浏览器也能检查成员和读取设置项。下面枚举 ChromiumOptions 的 57 个公开成员,打印可取得的签名,再回显部分设置;签名存在或选项写入不等于浏览器行为已验证。本节使用 DrissionPage 4.1.1.4。
1 | # -*- coding: utf-8 -*- |
1 | == ChromiumOptions 公开成员 == |
- 06 用的是
ChromiumOptions(read_file=False)。设置前回显address='127.0.0.1:9222',is_headless、is_auto_port、is_existing_only均为False。SessionOptions(read_file=False)的记录为timeout=10、proxies={'http': '', 'https': ''}、verify=None。 load_mode='eager'来自set_load_mode('eager'),retry_times=3、retry_interval=0.5来自set_retry(times=3, interval=0.5),不是初始默认值;base=5同样是显式设置。本版源码在不读文件时初始化为load_mode='normal'、retry_interval=2,要分清初始化值与主动写入的配置。headless(True)后is_headless=True,参数列表出现--headless=new。set_user_agent('DP-Probe/9.9')后出现--user-agent=DP-Probe/9.9,但ChromiumOptions本版本未提供user_agent读取属性;这不等于页面对象没有该属性。set_local_port(8053)后地址为127.0.0.1:8053;随后auto_port()回显address=''、is_auto_port=(9600, 59600)。这是自动选端口的配置状态,本段未启动浏览器,不能据此保证并发实例永不冲突。arguments的 10 项由内置参数和脚本新加参数共同组成。--window-size=1200,900是脚本显式set_argument()写入的,不是默认窗口尺寸,而且这里没有启动窗口验证最终大小。其余内置项包括--disable-infobars、--disable-popup-blocking、--no-first-run、--no-default-browser-check、--disable-features=PrivacySandboxSettings4、--disable-suggestions-ui、--hide-crash-restore-bubble。set_pref()后检查preferences是否含指定键,记录为True,不代表这个属性只能做成员判断。本版本的ChromiumOptions未提供no_infobar、user_agent、mobile_emulation、local_port、prefs、encoding、set_encoding这些所测名称;不推断它们是否来自其他版本或框架。set_paths()在本版本提供。签名有browser_path、local_port、address、download_path、user_data_path、cache_path六个可选参数,不是六个文件路径。示例只设置浏览器路径和端口,并读回这两项。read_file=False不读取 ini,此时ini_path=None不是「默认文件位置」。未传该参数的ChromiumOptions()会走配置文件读取逻辑;排查配置差异时,应确认是否读取了配置文件及其内容,不要与本节的无文件初始化混为一谈。SessionOptions的 40 个成员包含headers、cookies、proxies、auth、cert、verify、timeout等会话设置。添加适配器、设置超时、接收已有会话的成员与签名,另见后面的补充代码,避免只凭名称猜参数。用于接收会话的from_session在这里仅枚举签名,没有实际调用,不据此声称 requests 用法都能直接平移。
运行模式与接管
配置开关写入后是否产生预期行为,还要看页面结果。本节用自控页面回显请求头、执行 JS、加载图片;HTTP 服务地址与浏览器 CDP 调试地址用途不同。下述行为限于本机 Chrome 156(156.0.8078.80)和示例配置,不推广到其他版本或所有站点。
1 | # -*- coding: utf-8 -*- |
1 | == 1. headless(True) == |
- 无头模式下,页面属性、服务端请求头、页面 JS 三处 UA 记录一致,包含
HeadlessChrome/156.0.0.0;有头模式记录了页面属性和 JS 两处,包含Chrome/156.0.0.0,没有打印有头模式的服务端 UA 对照。这只是一项可观察差异,不是两种模式的全部差异。 set_user_agent('DP-Probe/9.9 (custom)')后,上述三处记录同为该字符串;结论限于本机靶页的这三处观测,不声称所有客户端特征均已改变。navigator.webdriver只在无头模式那组被读取,记录为False(JS 的false);脚本没有读取有头模式的此属性。单个本机结果既不是跨环境保证,也不表示站点无法检测自动化。- 在本机 Chrome 156 的记录中,
no_js(True)写入了--disable-javascript,但#js仍为'JS已执行'。07 实际使用的是page.run_cdp('Emulation.setScriptExecutionDisabled', value=True),随后page.refresh(),记录#js变空,再用value=False恢复。不要把这个命令写成其他 CDP 域的方法。 - 同样只限本机 Chrome 156:
no_imgs(True)后图片naturalWidth仍为 1;脚本调用page.set.blocked_urls('/px.png')再加载,记录变为 0。这是本地单张图片的对照,不证明跨浏览器版本效果或总体流量节省。 set_load_mode()的normal、eager、none三组在两次取值时均数到 3 个<p>,非法值抛ValueError。第一次是timeout=0,第二次是eles(..., timeout=5),表示最多等待 5 秒,不是固定暂停。这些<p>本来就在 HTML 中,不能用这组数量判断三种加载模式的耗时差异。incognito(True)那组启动后,page.set.local_storage('dp', 'v1')写入,再读回'v1'。这只验证当前无痕会话内可读写,不代表数据持久化;没有关闭并重开后的存储对照,不据此推断重启、落盘或清除行为。- 固定端口那组先用
ChromiumOptions().headless(True).set_local_port(8071)创建页面,再在同一 Python 进程内调用Chromium('127.0.0.1:8071');读到 2 个本机标签,标题均为ua-probe。该调用可能复用进程内已有对象,不能作为另一个进程独立接管成功的证据。 existing_only(True)那组在关闭浏览器后尝试连接 8071,得到BrowserConnectError。这个选项表示「只连接已有浏览器」;示例展示无可连接实例时的失败路径,不涉及已登录浏览器或跨进程接管。
标签页与多实例
先分清浏览器对象、绑定某个标签的页面对象,以及单独的标签对象。本段从 ChromiumPage 开始,打开多个页面,演示查找、激活、新建和关闭标签;多标签不等于多浏览器实例,这里也没有多进程并发实验。涉及在线页面的标题或 URL 过滤时,先确认导航成功。
1 | # -*- coding: utf-8 -*- |
1 | 站点 A=spa1,B=quotes/js,C=quotes 根;不用 ssr1 做标题靶站。 |
MixTab继承SessionPage和ChromiumTab,比ChromiumTab多出change_mode、mode、post、raw_data、response、cookies_to_browser、cookies_to_session、encoding这 8 个公开名称。操作前要分清取得的标签类型,不因一次latest_tab的类型就推广到所有获取方式。ChromiumPage.new_tab()返回ChromiumTab,它有close(),本版本未提供quit();标签由tab_id标识。不要把这个返回类型推广到所有对象的同名方法:本版Chromium.new_tab()默认请求的是MixTab。get_tab(0)与get_tab(1)都取当前tab_ids的首项,正数从 1 起,负数从末尾倒数。源码用id_or_num - 1 if id_or_num > 0 else id_or_num索引列表。索引指向当前列表位置,不是创建时的固定编号,也不绑定某个固定 URL。tab_id和对象引用适合保留对指定标签的引用;url=/title=可用于过滤。多个标签可能有相同标题或 URL,不能把过滤条件当作唯一身份标识;导航失败导致标题不匹配,也不等于标签 API 出错。- 标签数量不足时
get_tab(9)会抛IndexError;没有匹配标题时,get_tab(title='zzz')抛RuntimeError: 没有找到指定标签页。按位置查找与按标题过滤是两种不同的查找方式。 - 启用单例的示例中,
page.get_tab(1)两次is比较为True,按tab_c.tab_id取回的对象也与tab_c相同。这说明同一标签对象可以复用,不是每次获取都会新建对象。 activate_tab()只激活标签,不改变page绑定的标签。比较激活前后的page.tab_id是否相等,可以确认绑定不变;之后page.get()仍作用于绑定标签。要导航另一个标签,应取得对应的tab对象再调用tab.get(...)。- MixTab 冲突的方向是:先由
page.new_tab()建立ChromiumTab对象,再由浏览器对象请求同标签的MixTab,于是出现「已有非MixTab版本」错误。示例的page.close_tabs(t_new)和传其 ID 的调用也遇到该错误,改用t_new.close()可以关闭。错误提示提到的Settings.set_singleton_tab_obj(False)不属于这里已验证的解决步骤。 new_window=True、background=True、new_context=True分别用于请求新窗口、后台标签和新上下文。标签对象未提供window_id不等于库没有窗口概念;绑定的page.url不变也不能证明前台焦点。判断可见性可结合页面状态,判断上下文隔离则应比较实际存储数据。latest_tab不是前台标签查询接口。本版取当前tab_ids[0];完整列表的排序不能仅靠几次 URL 回显推断。多个标签可访问同一 URL,要确认是否为同一标签,应比较tab_id。page.url始终描述绑定标签,不代表前台。需要继续操作已取得的标签时,使用对应对象或 ID,不把latest_tab当成前台状态。- 只关闭非绑定标签,可遍历标签并比较
tab_id,对其他标签调用close()。close_tabs([], others=True)不保留任何标签,不是「保留当前,只关多余标签」。关闭调用返回None也不是浏览器进程已退出的证明,应根据需要确认关闭后的状态。
网络监听抓接口
page.listen 可以获取浏览器发出的请求和收到的响应。监听时仍启动浏览器、页面仍会渲染;直接读接口数据省掉的是 DOM 解析,不是浏览器渲染。下面用 spa1 演示。
1 | # -*- coding: utf-8 -*- |
1 | == Listener 对象公开成员 == |
- 这次
dir()列出Listener的 12 个公开成员:clear、listening、pause、resume、set_targets、start、steps、stop、tab_id、targets、wait、wait_silent。没有listen.once;只取一个包可用wait(count=1)。 - 先
start(目标串)再导航:page.listen.start("api/movie")返回None,targets变成{'api/movie'},然后访问页面并wait()。记录中未启动监听就调用wait()抛RuntimeError。 - 成功取得的
item是DataPacket,可读target、url、method、resourceType、is_failed。本例为 GET/XHR;item.request.params中limit=10、offset=10对应第二页,值是字符串。请求另有postData、headers;响应可读status、statusText、mimeType、headers、body、raw_body。 - 本次 JSON 响应的
item.response.body是 dict,确认results非空后可取body["results"][0]["name"];raw_body是原始字符串。本例访问item.response.json得到None,调用item.frames()抛AttributeError,不要据此把所有响应体都当 dict。 - 本次
statusText为'OK',status_text为None,应使用实际字段名statusText。 - 连调
start("api/movie")、start("static"),目标从前者变成后者,并非累加;多个目标可一次传给set_targets(["api", "scrape.center"])。 - 等到指定数量时,
wait(count=2)返回列表,wait(count=1)返回单个包。已有输出中的两个包为 CSS 和 JS,类型是Stylesheet/Script;请求顺序与缓存状态可能影响实际结果。 steps()提供迭代方式,例如代码中的steps(count=1, timeout=20)循环。- 目标串默认按包含关系匹配;本例
start(r"api/\w+", is_regex=True)使用正则并命中接口。 method='POST'按请求方法过滤;本例 GET 接口未命中,wait(timeout=3)返回False。这不代表等待一定不抛异常:源码中raise_err=True或相应全局等待设置可令超时抛WaitTimeoutError。pause()后listening为False,resume()后为True;clear()清空已缓存包。当前版本没有公开的cached_count属性,存在性检查为False,不要把它当缓存数量。- 同次导航中,接口与 DOM 都先检查非空,再比较首条片名。接口直接提供评分、时长、地区等字段,DOM 路线需要定位和提取。ssr1 的文档请求实测出现 502/503,因而跳过接口边界结论;错误页没有匹配包不能证明正常页面永远没有接口。
cookie 与 storage
浏览器里的 cookie 可以交给 SessionPage 复用。下面的本机靶站会设置 cookie,并按收到的 cookie 返回 COOKIE-HIT 或 COOKIE-MISS,便于观察搬运结果;这只是 cookie 检查示例,不是完整登录流程。
1 | # -*- coding: utf-8 -*- |
1 | 页面标题: cookie-probe |
cookies是方法,用page.cookies()或sess.cookies()读取。本次浏览器侧返回的条目默认有domain、name、value;page.cookies(all_info=True)还显示过期时间、路径等信息,按需要选择。- 浏览器写 cookie 用
page.set.cookies(...),本例试了 dict 和'k=v'字符串;SessionPage 也试了字典列表。传整数的错误信息列出允许类型为Cookie, CookieJar, list, tuple, str, dict,不表示本例逐一验证了所有类型。 - 本例用
page.run_cdp('Network.clearBrowserCookies')清空浏览器 cookie,随后读到空列表;重新访问靶站后,服务端又设置了 cookie。这是清空示例,不是删除单条 cookie 的测试。 - 不给 cookie 时靶站回
COOKIE-MISS,但sess.get()仍返回True:请求成功不等于登录态满足。执行sess.set.cookies(page.cookies())后,同一地址回COOKIE-HIT;手工构造匹配的 cookie 也得到COOKIE-HIT。本例的ChromiumPage没有cookies_to_session成员,采用上述显式搬运即可。 WebPage提供cookies_to_session()和cookies_to_browser()两个搬运入口。已有输出中,前者调用后切到s模式能读到dp_probe=browser-set,后者返回None;脚本未另造差异 cookie 核验回传后的浏览器状态。这是两侧 cookie 的复制,不是持续共享同一份 cookie。- storage 的读写入口不同:
page.local_storage()读全部,page.local_storage('dp_key')读单项;写入用page.set.local_storage('dp_key', 'dp_val')。session_storage对应使用page.session_storage(...)与page.set.session_storage(key, value)。 - 删除单项用
page.set.local_storage('dp_key', False):示例实际执行后,page.local_storage()得到空字典{},page.local_storage('dp_key')得到None。本版两个 storage setter 都以value is False作为删除条件;读取缺失项返回None,不表示写入None就是删除。 - 清空全部 storage 可执行
page.run_js('localStorage.clear(); sessionStorage.clear();'),示例中两者随后均为空字典。page.local_storage.remove会抛AttributeError,不要把读取方法当成带remove的对象。
SessionPage 与边界
SessionPage 用 requests 发请求,不启动浏览器,也不执行页面 JavaScript。下面对照静态 HTML 与前端渲染页面,再演示请求头、POST、本机代理和 WebPage 模式切换。
1 | # -*- coding: utf-8 -*- |
1 | == SessionPage 成员 == |
- 本次实例的
dir()列出 25 个公开成员:close、cookies、download、download_path、ele、eles、encoding、find、get、html、json、post、raw_data、response、retry_interval、retry_times、s_ele、s_eles、session、set、timeout、title、url、url_available、user_agent。 - 本例
hasattr(sess, 'headers')为False;sess.session是内部持有的requests.Session。构造签名为SessionPage(session_or_options=None, timeout=None)。 sess.status抛AttributeError,HTTP 状态码要看sess.response.status_code,原因短语看reason。初始sess.response为None;请求后也应先确认响应存在,再访问这些字段。get()返回布尔值;HTTP 状态和业务数据还需单独核对。sess.url、sess.title、sess.html用属性读取;元素可用ele()/eles()定位。ssr1 本轮返回 502/503,示例跳过该页取数,继续其他实验,不把失败页面上的空结果当有效数据。sess.json按 JSON 解析响应,解析失败可返回None。本例接口为 dict,仍检查results是非空列表;不要从 HTTP 200 直接推断字段齐全。- 同一定位串
css:div.item,spa1 第二页的静态源码与浏览器 DOM 分别为 0 和 10;quotes/js 的.quote同样为 0 和 10。t:blockquote在该浏览器页也为 0,因此零结果还可能是选择器不符合页面结构,不能全归因于缺少 JS 执行。 - 直接请求 spa1 的接口时,已有输出中的
sess.json是 dict,results有 10 条。知道可用接口及所需请求条件时,可考虑这条不启动浏览器的路线。 sess.set.headers已生效,但读取底层会话头中的X-Probe仍为None,靶站收到的却是dp-session。源码中 setter 修改的是 SessionPage 自持的_headers,发请求时再合并为请求头;它与 requests 底层Session.headers分开,不能只读后者判断设置是否生效,也不建议直接操作私有字段。- 本例
sess.set.a_header(...)和sess.clear_headers()均抛AttributeError。已验证的整组配置方式是sess.set.headers({...}),或先SessionOptions.set_headers({...})再构造页面;后者的靶站回显为from-options。 post()签名为(self, url, show_errmsg=False, retry=None, interval=None, **kwargs)。本例实际测试了data={'a': '1' * 100},靶站收到 102 字节;不能仅因有**kwargs就宣称 requests 的所有参数组合均已验证。sess.set.proxies('http://127.0.0.1:8053')后,本例请求得到假代理的PROXY-HIT,sess.url仍为给定地址。set.proxies(12345)调用未报错,底层代理配置中出现整数,但这不说明该配置有效。浏览器代理部分只检查了ChromiumOptions.set_proxy()生成的启动参数,未验证浏览器流量。set.verify(False)/set.verify(True)改变底层会话设置。示例先清除非法代理,再独立测试请求级verify=123,本次得到False和无响应;verify=False请求得到 200。这不等于验证了自签证书场景,也不建议为了通网长期关闭证书校验;show_errmsg=True可让请求错误抛出。- 模式切换用
web.change_mode('s')/web.change_mode('d'),不是给web.mode赋值。 本例先展示 SPA 在两模式下的区别,再用本机静态页确认两侧均取到 2 个元素且文本一致。go=False暂不自动访问,随后显式get()便于检查结果;copy_cookies=True控制切换时的复制,不是永久共享。 SessionPage.s_ele(locator=None, index=1)、s_eles(locator)是可用解析入口,但没有timeout参数。本版WebPage在s模式的同名转发仍传入timeout,即使调用者没写也会出现所示TypeError;不能因此说SessionPage.s_ele()本身不存在。示例分别标明当前模式。
弹窗上传下载与截图
这一组演示浏览器侧的 JavaScript 弹窗、文本输入、文件上传、下载、iframe 和截图。全部使用同一个本机靶站,不把这些交互与单纯发送 HTTP 请求混为一谈。
1 | # -*- coding: utf-8 -*- |
1 | == 1. 弹窗 handle_alert == |
- 本例
page.handle_alert()返回弹窗文本'DP-ALERT-TEXT';handle_alert(accept=False)取消确认框,页面显示'已取消'。没有弹窗时这次handle_alert(timeout=0.5)返回False,不据此推断所有调用都不会抛异常。page.get_alert()在本版抛AttributeError。 Settings.set_auto_handle_alert(True)设置全局自动接受;False是自动取消,None才是不启用全局自动处理。示例恢复None后手动处理后续确认框,再读取元素属性,避免未处理弹窗阻塞后面的操作。- 默认
input()不清空,在当前光标位置插入,不是保证在末尾追加:本例依次得到第一段原始、第一段追加原始。clear=True先清空再输入;by_js=True的值写入也由按钮读回验证。脚本没有记录键盘事件或input/change事件,不能据此判断哪些事件触发、哪些不会触发。 ele.click是可调用的Clicker对象,其__call__签名为(self, by_js=False, timeout=1.5, wait_stop=True)。本例click()与click(by_js=True)均返回ChromiumElement,读回 id 为confirm-btn;这是本次成功点击的结果,不保证所有点击都返回元素。ele.click(actions=True)抛TypeError,本例元素没有actions,页面有page.actions。按已有输出列表实际计数为 26 个公开成员:click、curr_x、curr_y、down、drag_in、hold、input、key_down、key_up、left、m_click、m_hold、m_release、modifier、move、move_to、owner、r_click、r_hold、r_release、release、right、scroll、type、up、wait;列出成员不等于逐一测试。- 本例
ele.hover()后 class 从None变为hovered,对应靶站的鼠标移入处理。 - 对
<input type=file>调用ele.input(文件路径),本例返回元素,files.length为 1、文件名正确,不需要操作系统文件对话框。另一段只验证了page.set.upload_files(sample)后page.upload_list的条数为 1,并未完整验证该入口触发选择框后的上传流程。 - 两张表单使用同一文件:默认编码的提交未收到文件内容标记,
enctype="multipart/form-data"的提交收到文件和内容标记。文件选中与文件内容被提交是两件事;这里是浏览器原生表单提交,不能据此概括所有 JavaScript 上传方案。 - 设置
page.set.download_path(目录)再点下载链接;已有记录中wait.download_begin()返回DownloadMission,wait.downloads_done()返回页面对象,page.download.wait()返回None。仍需确认等待结果与落盘文件,不能把本次返回类型当作失败时的保证。 - 本次落盘文件为
dp-probe.txt,224 字节,打印的 SHA-256 前 12 位为8e7913f2ff10。脚本打印了大小与摘要,并未做完整内容相等断言。 - 本例
page.download类型为DrissionGet,列出了任务管理成员;page.download_dir不存在。未显式设下载路径的错误在 14 脚本中触发,为RuntimeError,提示可以通过 setter、配置对象或 ini 配置。 - 本例只有一个 iframe,
page.get_frame('#fr')返回ChromiumFrame;可读frame.url、frame.title,用frame.ele('#fp').text取文本、frame.ele('#fbtn').click()点击。数字索引get_frame(0)抛ElementNotFoundError,get_frame(1)成功;不能说所有数字索引都不可用。调用doc()抛AttributeError。 page.ele('#fp', timeout=0.3)在本机同源 iframe 示例中直接取得内部元素,返回ChromiumElement。此结果不证明任意跨域、嵌套 iframe 都能用同样方式定位。- 页面和元素截图在本例返回路径字符串,
as_bytes='png'返回非空bytes,两个文件的 PNG 魔数检查通过。full_page=True那行只比较「长度是否更大」,结果为False,连长度相等都不能由此确定。字节长度不能证明全页截图是否有效;需另用长页面检查图像尺寸及底部内容覆盖。
异常体系
下面列出 errors 中的异常类,再记录若干错误调用的实际表现。部分探针只打印异常类名;「没有抛异常」也不等于操作成功,需要结合返回值和页面状态判断。
1 | # -*- coding: utf-8 -*- |
1 | == errors 模块的异常类与继承 == |
- 已有输出列出 20 个异常类,其中包含
BaseError自身;其余 19 个继承BaseError,而BaseError继承Exception。可按需要分别捕获ElementNotFoundError、WaitTimeoutError,或用BaseError捕获这一组库异常。 - 本例默认设置下,
page.ele('t:zzz', timeout=0.3)返回NoneElement,访问.text才抛ElementNotFoundError;开启Settings.set_raise_when_ele_not_found(True)后,查找调用本身就抛。错误信息包含方法、定位参数和版本,有助于复现。 - 本例
wait.eles_loaded(..., raise_err=True)超时抛WaitTimeoutError;没有配置下载路径时,wait.download_begin()抛的却是 Python 的RuntimeError。except (BaseError, RuntimeError)只覆盖这两类及其子类,不是全兜底,也接不住这里的ValueError、KeyError、TypeError、AttributeError;应按业务恢复方式选择捕获范围。 LocatorError的三个已测输入是混用@@与@|的字符串、未知方式元组('wrong_by', 'x')、长度错误元组('id',);page.ele(123)则抛AttributeError。即使都属于“定位写错”,异常类型也不同。- 点击
display:none元素和读取其rect.location,本例均抛NoRectError。禁用输入框那行给input传了timeout=0.5,先因不支持timeout抛TypeError。它只测到了错误参数,没有测到使用正确签名向 disabled 元素输入的结果。 - 在本例未处理 alert 时,再定位并点击另一元素抛
ElementNotFoundError,取page.html抛AlertExistsError;处理弹窗后能正常读标题。弹窗是排查方向之一,不是这两类错误的唯一原因。 get('不是url')抛IncorrectURLError。get('ftp://127.0.0.1/y')那行只记录了没有抛异常,没有打印返回值,不能推出访问成功或库的协议校验策略。run_cdp('Not_a_Command')抛RuntimeError,信息包含浏览器版本和命令名;run_js主动抛 JS 异常或包含语法错误时,记录中均为JavaScriptError。- cookie 缺少
value字段时,本例抛KeyError: 'value';传不支持的类型则抛ValueError,并非所有 cookie 错误都属于BaseError。 - 本机元素执行
this.remove()后再读取旧引用的.text,本次没有抛异常。从 DOM 移除不等于所有远端引用立即失效,不能写成已经触发ElementLostError。关闭标签后读取对象、quit()后再访问页面则都触发PageDisconnectedError。 existing_only(True)连接无浏览器的端口,以及连接本机非 CDP 靶站端口,本例都抛BrowserConnectError。可先核对浏览器是否启动、地址端口是否正确以及远程调试是否开启。- 配置参数错误的补充检查如下;
headless接收字符串未报错,不等于建议传字符串。
1 | # -*- coding: utf-8 -*- |
1 | SessionOptions.add_adapter: (self, url, adapter) |
show_errmsg=True时不可达请求抛出 requests 的ConnectTimeout;默认返回False的路径见前面的会话示例。ChromiumPage(123)的已有输出显示整数被当作端口号,地址为127.0.0.1:123,随后执行了quit();这是本次环境的结果,不保证该端口在其他环境也能使用。
指纹与脚本注入
下面在自控页面验证浏览器属性、文档初始化脚本和运行时 UA,并检查 Selenium 迁移时的成员名。这里只记录本机观察,不承诺能通过任何站点的检测。
1 | # -*- coding: utf-8 -*- |
1 | == 1. navigator.webdriver 的默认值(不加任何处理) == |
- 本机无头与有头的
navigator.webdriver都是false,但单个属性不足以判断浏览器是否可被识别。CDP 与 WebDriver 是不同控制接口,不代表“使用 CDP 就不会被检测”。 add_init_js(js)返回脚本 id,后续新文档自动执行;例中第二次导航仍读到标记。传入脚本 id 调用remove_init_js撤销一条,不传 id 撤销全部。把webdriver改成undefined这里只演示注入,不判断它比false更好或更差。page.set.user_agent()后,页面属性、JS 读取、服务端请求头三处变成新 UA。但 UA 不等于完整浏览器指纹,也不等于 TLS 指纹。- 两个
auto_port()实例的地址、browser.id、process_id均不同,说明浏览器进程独立;不等于已经测过多进程调度与性能。示例没有操作用户日常浏览器。 page.session可取得requests.Session,直接.get()返回Response;不要据此假设 cookie 已自动同步,浏览器与会话的搬运见前面的 cookie 节。mobile_emulation、use_cooperative_mode在本例检查的类上未提供。不要依据名字相似就套用其他库的参数。
迁移时的对应关系:
find_element→ele,find_elements→eles,save_screenshot→get_screenshot,execute_script→run_js;窗口切换用标签对象,不存在 Selenium 的switch_to/window_handles接口。By元组可用于定位,例如把By.ID和元素 id 组成元组。不认识的 by 抛LocatorError;没有命中的eles()返回空列表。CSS 裸标签多命中的原因见「CSS 搜索边界」,不是 Selenium 的选择器语义保证。page.driver是 DrissionPage 自己的Driver,不是 Selenium WebDriver。日常用公开接口;需底层能力时可以run_cdp(),本例取得Page.getResourceTree的frameTree。common里除了By/Keys/Settings,还有make_session_ele、wait_until、tree、from_selenium、from_playwright等工具。本节对转换工具仅检查存在性与签名,未执行跨库对象转换;不要把清单当作迁移成功的测试。
怎么选
根据需要选最轻的一条路线:
| 需求 | 建议入口 | 边界 |
|---|---|---|
| 响应源码已有数据 | SessionPage |
不执行页面 JS,也能用定位语法解析 |
| 已知 JSON 接口 | SessionPage.get() + .json |
核对 HTTP 状态、返回类型与业务字段 |
| 必须点击或等 JS | Chromium 的标签页 / ChromiumPage |
要浏览器及显式的业务等待条件 |
| 观察页面接口响应 | page.listen |
仍启动浏览器,只免去 DOM 二次拆字段 |
| HTTP 与浏览器交替 | WebPage / MixTab |
用 change_mode(),理解 cookie 搬运时点 |
| 队列与持久化调度 | Scrapy / Feapder / Crawlee 等框架 | DrissionPage 不提供这些框架式组件 |
| 正文 Markdown | Crawl4AI | DrissionPage 不以内置 Markdown 管线为主 |
- DrissionPage 适合单机交互脚本、浏览器与 HTTP 混合采集;它有请求重试参数,不能笼统说“没有重试”。持久化队列、任务去重、失败入库等调度策略则需要另做。
- 接管已有浏览器之前,确认目标确实是自己的调试实例;演示的
quit()会关闭所连接浏览器,不应照搬到有未保存工作的日常浏览器。无头模式不需要可见窗口,有头模式需要桌面会话。 - 本篇异常包括返回
False、NoneElement、库异常和 Python 内置异常,不能归纳成一种固定模式。对关键配置读回确认,对响应和业务元素分别校验。 - 输出中的版本、签名、API 成员属于本次安装;布局、数量、UA 和部分字段属于本次页面。升级后复核相关小节,不因一次失败就把网络错误解释成 API 删除。