使用 MutationObserver 和 Playwright 捕获动态 DOM 变化
Web Data Collection Specialist
TL;DR:
- 动态页面在初始 HTML 加载后可以更改 DOM。 抓取程序必须等待可用状态,而不是将第一次响应视为最终文档。
- MutationObserver 记录 DOM 如何变化。 当您需要添加节点、删除节点、文本更新或属性过渡时,请在触发操作之前安装观察者。
- Playwright 定位器在所需状态可用时等待确认。 使用观察者来获取变化的来源,并使用定位器来确保最终状态的准备就绪。
- 限制观察者应仅监视相关的子树和变更类型。 序列化小记录,过滤嘈杂属性,并在预期状态出现后断开连接。
- 当结构化响应数据是真正目标时,网络拦截通常更好。 当 DOM 过渡本身很重要时,MutationObserver 最为有用。
- 相同的观察工作流程可以在本地和通过 Scrapeless 运行。 转向 Scrapeless 抓取浏览器更改浏览器创建,而不是页面端观察者。
- 免费开始。 新注册的 Scrapeless 账户包括免费的抓取浏览器运行时间—请在 app.scrapeless.com 注册。
介绍:初始 HTML 只是起始状态
动态页面可以在其初始 HTML 加载后插入、显示、替换或删除数据。本指南在触发公共 Selenium 设备之前安装一个限制的 MutationObserver,捕获精确的 DOM 更改,并在将相同的工作流程移动到 Scrapeless 抓取浏览器之前,比较事件驱动的观察与 Playwright 的定位器等待。
MutationObserver 为抓取添加了什么
MutationObserver 报告对 DOM 子树的更改。它可以监视子项的插入/删除、属性和文本更改,而无需反复扫描页面。MDN 的 MutationObserver 参考 定义了回调和观察选项。
观察者不会自行决定数据何时准备就绪。它记录发生了什么变化。Playwright 定位器仍然是等待特定可用元素的更清晰方式,而观察者记录解释了该状态如何以及何时出现。
为什么要将 Playwright 与 Scrapeless 一起使用
Playwright 可以在触发点击之前在页面中安装观察者,执行操作,等待最终 UI 状态,并返回一个紧凑的事件日志。Scrapeless 抓取浏览器 提供了一个托管的 Chromium 会话,用于相同的页面端脚本。
云浏览器使用 connect_over_cdp。只有浏览器创建发生变化。
先决条件
- Python 3.10 或更高版本。
playwright1.59.0 或当前兼容版本。- 本地 Chrome/Chromium 用于完整的实时示例。
- 一个资金充足的 Scrapeless 账户和
SCRAPELESS_API_KEY用于云连接。验证账户返回代码 14500,因此该连接是一个标记的前提条件缺口。
安装
bash
python -m pip install "playwright==1.59.0"
连接到 Scrapeless 抓取浏览器
注意:该块需要资金充足的抓取浏览器余额。最终的验证账户返回
余额不足,请先充值; 以下完整观察者工作流程在本地 Chrome 中运行。
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
params = urlencode({
"token": os.environ["SCRAPELESS_API_KEY"],
"sessionTTL": 120,
"proxyCountry": "US",
})
cdp_url = f"wss://browser.scrapeless.com/api/v2/browser?{params}"
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(cdp_url)
page = browser.contexts[0].pages[0]
# 在下面安装并使用观察者。
browser.close()
使用 Scrapeless 开发者文档 获取当前连接参数。
第一步 — 检查页面的初始状态
Selenium 的公共设备暴露了两个按钮。一个在一秒后插入 #box0;另一个在一秒后显示隐藏的 #revealed 输入:
python
TARGET_URL = "https://www.selenium.dev/selenium/web/dynamic.html"
page.goto(TARGET_URL, wait_until="domcontentloaded")
print("box before:", page.locator("#box0").count())
print("input visible before:", page.locator("#revealed").is_visible())
text
box before: 0
input visible before: False
设备的初始 HTML 包含隐藏输入,但不包含框。这种差异就是为什么观察者必须监视子列表和属性变更的原因。
第二步 — 在操作之前安装观察者
存储纯字典而不是 DOM 节点。MutationRecord 公开了变更类型、目标、已更改的属性以及添加/移除的节点。
python
page.evaluate("""
window.capturedMutations = [];
window.scrapeObserver = new MutationObserver(records => {
for (const record of records) {
for (const node of record.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
capturedMutations.push({
type: 'added',
id: node.id,
display: getComputedStyle(node).display,
});
}
}
if (record.type === 'attributes') {
capturedMutations.push({
type: 'attribute',
id: record.target.id,
name: record.attributeName,
display: getComputedStyle(record.target).display,
});
}
}
});
scrapeObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style'],
});
""")
attributeFilter 保持无关的类、ARIA 或框架属性不被捕获。
第 3 步 — 触发更改并等待可用状态
python
page.click("#adder")
page.click("#reveal")
page.wait_for_selector("#box0")
page.locator("#revealed").wait_for(state="visible")
Playwright 的 自动等待指导 解释了定位器为何是正确的准备检查。观察者日志是补充证据,而不是具体最终状态断言的替代。
第 4 步 — 断开观察者并读取记录
python
changes = page.evaluate("""() => {
scrapeObserver.disconnect();
return capturedMutations;
}""")
print("changes:", changes)
print("change count:", len(changes))
print("box matches:", page.locator("#box0").count())
print("input visible:", page.locator("#revealed").is_visible())
text
changes: [{'type': 'added', 'id': 'box0', 'display': 'block'}, {'type': 'attribute', 'id': 'revealed', 'name': 'style', 'display': 'inline-block'}]
change count: 2
box matches: 1
input visible: True
断开连接可以防止后续无关页面更改扩展事件列表。长时间存在的观察者应该始终有一个明确的停止条件。
准备好观察托管的浏览器会话了吗?创建一个免费的 Scrapeless 账户并仅替换浏览器创建。
完整的可运行捕获
python
from playwright.sync_api import sync_playwright
TARGET_URL = "https://www.selenium.dev/selenium/web/dynamic.html"
with sync_playwright() as p:
browser = p.chromium.launch(
executable_path="/usr/bin/google-chrome",
headless=True,
)
page = browser.new_page()
page.goto(TARGET_URL, wait_until="domcontentloaded")
box_before = page.locator("#box0").count()
input_before = page.locator("#revealed").is_visible()
page.evaluate("""
window.capturedMutations = [];
window.scrapeObserver = new MutationObserver(records => {
for (const record of records) {
for (const node of record.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
capturedMutations.push({
type: 'added',
id: node.id,
display: getComputedStyle(node).display,
});
}
}
if (record.type === 'attributes') {
capturedMutations.push({
type: 'attribute',
id: record.target.id,
name: record.attributeName,
display: getComputedStyle(record.target).display,
});
}
}
});
scrapeObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style'],
});
""")
page.click("#adder")
page.click("#reveal")
page.wait_for_selector("#box0")
page.locator("#revealed").wait_for(state="visible")
changes = page.evaluate("""() => {
scrapeObserver.disconnect();
return capturedMutations;
}""")
box_after = page.locator("#box0").count()
input_after = page.locator("#revealed").is_visible()
assert box_before == 0 and input_before is False
assert changes == [
{"type": "added", "id": "box0", "display": "block"},
{
"type": "属性",
"id": "revealed",
"name": "样式",
"display": "内联块"
},
assert box_after == 1 and input_after is True
print("框之前:", box_before)
print("输入可见性之前:", input_before)
print("变化:", changes)
print("变化计数:", len(changes))
print("框之后:", box_after)
print("输入可见性之后:", input_after)
browser.close()
最终运行精确捕获了一个添加的节点和一个样式属性的变化,然后独立确认了一个框和一个可见输入。
## 你将得到的反馈
完成的工作流程返回一个紧凑的变更日志以及独立的最终状态检查:
```json
{
"之前": {
"框匹配": 0,
"输入可见": false
},
"变更": [
{
"类型": "添加",
"id": "box0",
"display": "块"
},
{
"类型": "属性",
"id": "revealed",
"name": "样式",
"display": "内联块"
}
],
"之后": {
"框匹配": 1,
"输入可见": true
}
}
变更记录解释了页面如何变化,而定位器检查确认最终元素是可用的。保持两者可以防止观察到的事件被误认为是完成的用户界面过渡。
选择合适的动态页面信号
| 信号 | 使用场合 | 返回内容 | 主要限制 |
|---|---|---|---|
| MutationObserver | DOM变化的序列或类型很重要 | 添加、移除、文本或属性变更 | 需要有限范围和明确的停止条件 |
| Playwright 定位器等待 | 只有最终可用状态很重要 | 已准备好的元素或确认状态 | 不解释页面如何达到该状态 |
| 网络捕获 | 结构化数据通过请求到达 | 请求和响应载荷 | 不描述后来的DOM转换 |
| 有限轮询 | 没有稳定的事件、请求或定位器 | 定期状态快照 | 重复检查增加噪音和页面工作 |
定位器等待应该是默认的准备原语。当提取也需要变更来源时添加MutationObserver,当结构化响应比渲染的DOM更稳定时,优先使用网络捕获。
常见的MutationObserver故障模式
第一个事件缺失
观察者在操作之后安装。请在点击或导航到状态转换之前注册。
捕获量过大
观察一个较小的根,过滤属性,只存储你需要的字段。永远不要在长期捕获中保留DOM节点。
属性变化重复
框架可能多次写入相同的属性。决定你是否需要每个过渡或仅最后一个值,然后在回调外进行去重。
观察者永远不会停止
一旦预期记录到达或任务截止时间过期,就断开连接。开放式观察会导致无界内存增长。
结论
MutationObserver为客户端DOM变化提供了来源,而Playwright定位器确认可用的最终状态。在操作之前安装观察者,序列化小记录,仅观察相关的变更类,并及时断开连接。将工作流程移动到Scrapeless改变浏览器创建而不改变事件捕获。
准备捕获动态页面状态吗?
加入Scrapeless社区,和开发者一起构建基于浏览器的提取工作流程:Discord · Telegram。
开始使用Scrapeless,查看Scrapeless定价,并将此工作流程与网络请求拦截进行比较,当页面暴露结构化响应时。
常见问题解答
问:MutationObserver比Playwright等待更好吗?
它们回答不同的问题。观察者记录了什么变化;定位器等待确认你需要的元素或状态是否准备好。
问:我应该什么时候安装观察者?
在可以触发变更的操作之前。之后安装会错过你最需要的唯一事件。
问:MutationObserver能看到网络响应吗?
不能。它只能看到DOM变更。当你需要网络载荷时,使用请求/响应事件。
问:为什么过滤观察到的属性?
框架会更改许多属性。一个attributeFilter可以保持捕获在有限和相关的范围内。
问:我应该直接存储MutationRecord对象吗?
不。将它们转换为小的简单对象,因为记录引用DOM节点,并且不方便序列化输出。
问:我如何停止观察者?
在期望的记录到达后或在受限任务截止日期时调用disconnect()。
问:观察公共页面是否总是被允许?
不。请查看网站条款,仅收集必要的公共数据,限制会话及请求量,并在适用时遵循爬虫政策。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



