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

如何使用 Playwright 和 Scrapeless 抓取 Shadow DOM

Daniel Kim
Daniel Kim

Lead Scraping Automation Engineer

30-Jul-2026

TL;DR:

  • Shadow DOM可以显示在宿主元素的光DOM中不存在的文本。 JavaScript可能会在自定义元素加载后在shadow root内部创建可见内容。
  • Playwright CSS和文本定位器可以遍历开放的shadow roots。 相同的选择器在document.querySelector中失败,因为文档级DOM查询在shadow边界停止。
  • 闭合的shadow roots和XPath需要不同的预期。 Playwright定位器不会穿透闭合的roots,XPath选择器不支持自动遍历Shadow DOM。
  • Shadow DOM提取逻辑与浏览器传输独立。 本地验证工作流程后,可以在需要托管浏览器或受管理出口时,通过Scrapeless CDP连接重用相同定位器。
  • 免费开始。 新的Scrapeless账户包含免费的Scraping Browser运行时——请在app.scrapeless.com注册。

一个Shadow DOM组件可以显示在宿主元素的普通光DOM子元素中缺失的文本。本指南使用Playwright遍历一个真实的开放shadow root,展示了为什么文档级选择器返回空结果,并将该提取逻辑与在云浏览器中运行相同代码的Scrapeless Scraping Browser连接分开。

Shadow DOM对爬虫的影响

Shadow DOM为自定义元素提供了一个封装的子树。页面的主文档可以看到宿主标签,但普通DOM API并不会自动从宿主穿越到其shadow root。

在下面使用的公共MDN演示中,源代码包含一个带有data-text属性的<popup-info>宿主。一个延迟脚本升级该标签,调用attachShadow({ mode: "open" }),并创建一个内部的.info span。浏览器从该span中显示帮助文本。最初的HTML根本不包含该span。

MDN的Shadow DOM指南定义了开放和闭合roots之间的边界和区别。这一区别很重要:开放root暴露了element.shadowRoot;闭合root通过该API返回null

为什么使用Playwright与Scrapeless

Playwright定位器可以穿透开放的shadow roots进行正常的CSS和文本查找。Playwright定位器文档指出两个例外:XPath不穿透shadow roots,且不支持闭合模式的roots。

遍历代码不依赖于Chromium的运行位置。一个本地浏览器对于公共演示已经足够;当目标需要托管浏览器、管理指纹、住宅出口或持久的云会话时,Scrapeless Scraping Browser则变得很有用。Playwright通过connect_over_cdp API连接到该浏览器。

前提条件

  • Python 3.10或更高版本。
  • playwright 1.59.0或当前兼容版本。
  • 为下面完全可执行的回退安装本地Chrome/Chromium运行时。
  • 一个资助的Scrapeless账户和用于云连接的SCRAPELESS_API_KEY。验证账户的新浏览器会话返回代码14500,因此云连接块被标记为前提条件缺口,而不是显示为成功。

安装

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

验证主机已经在/usr/bin/google-chrome下安装了Google Chrome。如果您的机器没有,请使用项目的正常浏览器安装命令安装Playwright管理的Chromium。

连接到Scrapeless Scraping Browser

注意:此连接需要资助的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开发者文档是连接参数的当前来源。一旦page存在,Shadow DOM查找就是标准的Playwright。

检查宿主及其开放root

MDN 组件演示是小型公开的:

python Copy
TARGET_URL = (
    "https://mdn.github.io/web-components-examples/"
    "popup-info-box-web-component/"
)

page.goto(TARGET_URL, wait_until="domcontentloaded")
page.wait_for_selector("popup-info .info")

host = page.locator("popup-info")
print("host light-DOM text:", repr(host.inner_text()))
print("shadow mode:", host.evaluate("element => element.shadowRoot.mode"))
text Copy
host light-DOM text: ''
shadow mode: open

空的主机文本并不能证明该组件没有数据。这说明主机没有普通的子文本。实际的 .info 节点位于自定义元素升级后创建的 open shadow root 之下。

证明文档选择器在边界处停止

从文档运行一个普通的浏览器 DOM 查询:

python Copy
plain_match = page.evaluate(
    "document.querySelector('popup-info .info') !== null"
)
print("document query found inner node:", plain_match)
text Copy
document query found inner node: False

选择器是有效的 CSS。失败的原因在于范围:document.querySelector 搜索文档树,而不向下深入到主机的 shadow tree 中。

让 Playwright 定位器穿透开放根

Playwright 的普通定位器链跨越开放根:

python Copy
info = page.locator("popup-info .info")
record = {
    "host": "popup-info",
    "shadow_mode": host.evaluate("element => element.shadowRoot.mode"),
    "text": info.inner_text(),
    "source_url": page.url,
}

print("inner matches:", info.count())
print("help text:", record["text"])
print("text chars:", len(record["text"]))
text Copy
inner matches: 1
help text: 您的卡片验证代码 (CVC) 是额外的安全特性 — 它是您卡片背面的最后 3 或 4 个数字。
text chars: 118

该文本是组件的真实 data-text 值复制到 shadow-root span 中。字符计数包括标点和空格,因此它是一个有用的保护措施,防止意外提取空元素或图标的替代文本。

准备将同一定位器移动到托管的浏览器吗?创建一个免费的 Scrapeless 账户 ,并保持浏览器创建后的每一行不变。

完整的本地验证脚本

此脚本在本地 Chrome 中运行整个诊断。用上述 CDP 连接替换 launch(...) 将其移动到 Scrapeless,而不改变提取逻辑:

python Copy
from playwright.sync_api import sync_playwright

TARGET_URL = (
    "https://mdn.github.io/web-components-examples/"
    "popup-info-box-web-component/"
)

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")
    page.wait_for_selector("popup-info .info")

    host = page.locator("popup-info")
    info = page.locator("popup-info .info")
    host_text = host.inner_text()
    shadow_mode = host.evaluate("element => element.shadowRoot.mode")
    plain_match = page.evaluate(
        "document.querySelector('popup-info .info') !== null"
    )
    help_text = info.inner_text()

    assert shadow_mode == "open"
    assert not plain_match
    assert "卡片验证代码" in help_text

    print("page title:", page.title())
    print("host light-DOM text:", repr(host_text))
    print("shadow mode:", shadow_mode)
    print("document query found inner node:", plain_match)
    print("inner matches:", info.count())
    print("help text:", help_text)
    print("text chars:", len(help_text))
    browser.close()

运行证明边界而不仅仅显示成功的定位器:light-DOM 文本为空,文档选择器返回 false,根报告为 open,而 Playwright 返回一条 118 字符的帮助字符串。

开放和封闭根的含义

开放根

attachShadow({mode: "open"}) 使 host.shadowRoot 可用。Playwright 定位器可以用 CSS/文本选择器遍历它。嵌套的开放根以相同的方式处理,尽管每个组件可能会在不同的时间表上加载。

封闭根

attachShadow({mode: "closed"}) 使 host.shadowRoot 返回 null。Playwright 的普通定位器无法穿透它。请勿将脚本注入或浏览器仪表化呈现为通用解决方案:封闭根是明确的封装边界,改变页面代码可能会改变您打算测量的行为。

插槽内容

<slot> 在 shadow-tree 布局中显示 light-DOM 子元素。分配的节点仍然属于主机的 light DOM,因此它们的所有权和选择器行为与在 shadow root 内部创建的元素不同。在决定哪个侧面拥有数据之前,请检查实时 DOM。

常见的 Shadow DOM 失败模式

主机存在但内部定位器超时

自定义元素可能尚未升级,它的 JavaScript 可能已失败,或者选择器可能针对一个已关闭的根。在内部元素上等待以作为真实的准备就绪标记,并在更改选择器之前检查控制台错误。

XPath 找不到任何内容

使用 CSS 或文本定位器。Playwright 文档中描述的影子遍历不适用于 XPath 选择器。

嵌套组件返回部分文本

定位最深的稳定元素,而不是在大型宿主上调用 inner_text()。这可以避免混合插槽文本、视觉上隐藏的标签和嵌套组件输出。

源 HTML 没有内部节点

当 JavaScript 构造影子树时,这是正常的。使用浏览器。HTTP 解析器只能看到源响应,无法观察加载后创建的子树。

结论

影子 DOM 抓取是一个范围问题,在成为选择器问题之前。证明哪个树拥有节点,等待自定义元素升级,并使用 Playwright 的 CSS/文本定位器针对开放的根。保持关闭根边界的诚实。浏览器传输是单独的:在公共测试表面本地运行,然后在目标需要托管的反检测浏览器或管理出口时切换浏览器创建到 Scrapeless CDP 端点。

开始使用 Scrapeless,查看 Scrapeless 定价,并阅读 Chrome DevTools 协议控制什么 以了解周围的 CDP 架构。

常见问题

问:Playwright 能否抓取开放的 Shadow DOM 根?

可以。Playwright 的 CSS 和文本定位器可以穿透开放的影子根,因此像 page.locator("popup-info .info") 这样的定位器可以到达内部节点。

问:为什么 document.querySelector 对相同选择器失败?

document.querySelector 会搜索文档树并在影子宿主处停止。您必须自己通过开放根查询或使用执行该遍历的 Playwright 定位器。

问:Playwright 能否穿透封闭的影子根?

不能,无法通过其正常的定位器行为。封闭的根从 host.shadowRoot 返回 null,并且是一个已记录的不支持边界。

问:XPath 在 Shadow DOM 内部有效吗?

无效。Playwright 的官方定位器指导表示 XPath 选择器无法穿透影子根;请使用 CSS 或文本定位器。

问:如何处理嵌套的开放影子根?

将定位器链向稳定的内部元素并等待该元素。Playwright 可以跨越多个开放根,但每个自定义元素可能会异步升级。

问:我需要浏览器进行 Shadow DOM 抓取吗?

是的,当 JavaScript 创建影子树时。普通的 HTTP 响应包含宿主标记和脚本,而不是执行后生成的实时子树。

问:Shadow DOM 抓取是否合法?

DOM 机制并不决定权限。请查看目标的条款和 机器人指令,使用公共数据,限制请求量,并寻求法律建议以应对敏感或商业收集。

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

最受欢迎的文章

目录