🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

使用 MutationObserver 和 Playwright 捕获动态 DOM 变化

Isabella Garcia
Isabella Garcia

Web Data Collection Specialist

30-Jul-2026

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 或更高版本。
  • playwright 1.59.0 或当前兼容版本。
  • 本地 Chrome/Chromium 用于完整的实时示例。
  • 一个资金充足的 Scrapeless 账户和 SCRAPELESS_API_KEY 用于云连接。验证账户返回代码 14500,因此该连接是一个标记的前提条件缺口。

安装

bash Copy
python -m pip install "playwright==1.59.0"

连接到 Scrapeless 抓取浏览器

注意:该块需要资金充足的抓取浏览器余额。最终的验证账户返回 余额不足,请先充值; 以下完整观察者工作流程在本地 Chrome 中运行。

python Copy
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 Copy
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 Copy
box before: 0
input visible before: False

设备的初始 HTML 包含隐藏输入,但不包含框。这种差异就是为什么观察者必须监视子列表和属性变更的原因。

第二步 — 在操作之前安装观察者

存储纯字典而不是 DOM 节点。MutationRecord 公开了变更类型、目标、已更改的属性以及添加/移除的节点。

python Copy
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 Copy
page.click("#adder")
page.click("#reveal")
page.wait_for_selector("#box0")
page.locator("#revealed").wait_for(state="visible")

Playwright 的 自动等待指导 解释了定位器为何是正确的准备检查。观察者日志是补充证据,而不是具体最终状态断言的替代。

第 4 步 — 断开观察者并读取记录

python Copy
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 Copy
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 Copy
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,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录