欢迎光临
我们一直在努力

Shadow DOM 如何抓取?

随着 Web Components 技术在前端工程中的普及,越来越多网站采用 Shadow DOM 封装组件、隔离样式与逻辑。对于爬虫开发、自动化测试从业者而言,传统的 document.querySelector 等 DOM 选择器无法穿透 Shadow DOM 的隔离边界,常出现元素定位失败、内容抓取为空的问题。本文将从原理到实战,系统讲解 Shadow DOM 的抓取方案,覆盖原生 JavaScript 实现、主流自动化工具用法,以及 Closed Shadow DOM 的破解思路。

一、为什么 Shadow DOM 难以直接抓取?

1.1 Shadow DOM 的核心隔离机制

Shadow DOM 是浏览器原生提供的 DOM 封装能力,它允许将一棵隐藏的 DOM 树(Shadow Tree)附加到常规 DOM 元素(宿主元素 Host)上。这棵内部树与外部文档完全隔离:

  • 外部 CSS 样式无法渗透到 Shadow 内部
  • 全局 DOM 查询方法无法直接选中内部元素
  • 内部的 id、class 命名不会与外部文档冲突

这种设计本意是提升组件复用性和封装性,但也给内容抓取带来了天然屏障。

1.2 Open 与 Closed 两种模式

调用 Element.attachShadow() 创建 Shadow DOM 时,必须通过 mode 参数指定封装模式:

  • mode: 'open':宿主元素的 shadowRoot 属性对外暴露,外部脚本可直接访问,是业务开发中最常见的模式
  • mode: 'closed':宿主元素的 shadowRoot 属性返回 null,外部无法直接获取内部 DOM 树,封装性更强

常规抓取失效的本质是:选择器无法跨越 Shadow Root 边界;而 Closed 模式下连访问入口都被彻底隐藏。

二、原生 JavaScript 抓取方案

2.1 基础:直接访问 Open Shadow Root

对于 Open 模式的 Shadow DOM,抓取逻辑非常直观:先定位宿主元素,再通过 .shadowRoot 进入内部树,继续执行 DOM 查询。

javascript

运行

// 1. 定位 Shadow DOM 的宿主元素
const hostElement = document.querySelector('#custom-component');

// 2. 进入 Shadow Root 边界
const shadowRoot = hostElement.shadowRoot;
if (!shadowRoot) {
console.log('该元素无 Shadow DOM 或为 Closed 模式');
return;
}

// 3. 在 Shadow 内部查询目标元素
const targetNode = shadowRoot.querySelector('.content-text');

// 4. 提取内容或完整 HTML
const text = targetNode.textContent;
const fullInnerHTML = shadowRoot.innerHTML;

2.2 进阶:递归遍历多层嵌套 Shadow DOM

复杂页面中 Shadow DOM 往往存在多层嵌套(Shadow 内部的组件又包含自己的 Shadow),手动逐层访问效率极低。可以实现一个深度优先的递归函数,自动穿透所有 Shadow Root 边界。

javascript

运行

/**
* 在全文档(含所有 Shadow DOM)中查询匹配元素
* @param {string} selector CSS 选择器
* @param {Document|ShadowRoot} root 当前遍历根节点
* @returns {Element[]} 匹配的元素数组
*/
function querySelectorAllInShadow(selector, root = document) {
const result = [];
// 先在当前根节点范围查询
result.push(…Array.from(root.querySelectorAll(selector)));

// 遍历当前根下所有元素,检查是否包含 Shadow Root
const allElements = root.querySelectorAll('*');
for (const el of allElements) {
if (el.shadowRoot) {
// 递归进入 Shadow 内部继续查询
result.push(…querySelectorAllInShadow(selector, el.shadowRoot));
}
}
return result;
}

// 示例:查找页面所有 Shadow 中的 .title 元素
const allTitles = querySelectorAllInShadow('.title');

三、主流自动化工具的 Shadow DOM 抓取

在实际爬虫与自动化场景中,我们更多依赖 Playwright、Selenium、Puppeteer 等工具。这些工具均已提供 Shadow DOM 支持,实现方式各有差异。

3.1 Playwright:原生支持穿透定位

Playwright 对 Shadow DOM 的支持最完善,其 Locator 定位器默认可穿透 Open Shadow DOM,无需手动获取 shadowRoot。

  • 方式一:深度选择器直接定位 使用 >>> 深度选择器,可一步跨 Shadow 边界定位元素:

    javascript

    运行

    // 直接定位 Shadow 内部的提交按钮
    const submitBtn = page.locator('#component-host >>> .submit-btn');
    await submitBtn.click();

  • 方式二:通过 evaluate 提取内容 如需批量获取 Shadow 内部 HTML 或复杂数据,可通过 JS 执行器操作:

    javascript

    运行

    const shadowHTML = await page.evaluate(() => {
    const host = document.querySelector('#component-host');
    return host.shadowRoot?.innerHTML || '';
    });

3.2 Selenium 4+:官方 ShadowRoot API

Selenium 从 4.0 版本开始正式支持 Shadow DOM 操作,通过 shadow_root 属性获取内部根节点。

Python 示例:

python

运行

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://example.com")

# 定位宿主元素
host = driver.find_element(By.CSS_SELECTOR, "#custom-component")
# 获取 Shadow Root
shadow_root = host.shadow_root
# 在 Shadow 内部查询元素
content = shadow_root.find_element(By.CSS_SELECTOR, ".content")
print(content.text)

3.3 Puppeteer:通过 evaluate 桥接

Puppeteer 未内置 Shadow DOM 选择器语法,需通过 page.evaluate 在页面上下文内访问 Shadow Root。

javascript

运行

const host = await page.$('#component-host');
const innerText = await page.evaluate((el) => {
return el.shadowRoot?.querySelector('.text')?.textContent || '';
}, host);

四、Closed Shadow DOM 的破解思路

对于 mode: 'closed' 的 Shadow DOM,element.shadowRoot 会返回 null,常规方法完全无法访问。此时最通用、最稳定的方案是:在页面脚本执行前,劫持 attachShadow 原生方法,将所有创建的 Shadow Root 缓存下来。

4.1 实现原理

重写 Element.prototype.attachShadow,在原生方法执行后,将返回的 Shadow Root 存入全局 WeakMap。后续无论 Open 还是 Closed 模式,都可以通过宿主元素从 WeakMap 中取出对应的 Shadow Root。

4.2 完整注入脚本

必须在 document_start 阶段注入,确保先于页面业务脚本执行:

javascript

运行

(function () {
const shadowCache = new WeakMap();
const nativeAttachShadow = Element.prototype.attachShadow;

// 重写原生方法
Element.prototype.attachShadow = function (options) {
const shadowRoot = nativeAttachShadow.call(this, options);
// 缓存所有 Shadow Root(含 closed)
shadowCache.set(this, shadowRoot);
return shadowRoot;
};

// 暴露全局获取方法
window.getShadowRoot = function (host) {
return shadowCache.get(host);
};
})();

4.3 在自动化工具中注入

以 Playwright 为例,通过 addInitScript 在页面初始化阶段注入:

javascript

运行

const context = await browser.newContext();
await context.addInitScript(() => {
const shadowCache = new WeakMap();
const nativeAttachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function (options) {
const sr = nativeAttachShadow.call(this, options);
shadowCache.set(this, sr);
return sr;
};
window.getShadowRoot = (host) => shadowCache.get(host);
});

注入完成后,即可通过 window.getShadowRoot(host) 获取任意 Closed Shadow DOM 的根节点,后续操作与 Open 模式完全一致。

五、抓取实战中的注意事项

5.1 等待 Shadow DOM 渲染完成

Shadow DOM 几乎都由 JavaScript 动态创建,页面加载完成时可能尚未渲染。需配合等待机制:

  • Playwright:直接使用 locator.waitFor() 等待内部元素出现
  • 通用方案:轮询检测宿主元素的 shadowRoot 是否存在

5.2 区分 Shadow DOM 与 iframe

很多开发者会混淆 Shadow DOM 与 iframe,二者处理逻辑完全不同:

  • Shadow DOM 属于同一文档,只是 DOM 作用域隔离
  • iframe 是独立的浏览上下文,拥有自己的 window 和 document

如果目标内容在 iframe 中,需要先切换上下文再操作,不可套用 Shadow DOM 的抓取方法。

5.3 性能优化

多层嵌套的 Shadow DOM 全局递归会产生性能开销,建议:

  • 明确宿主位置时,定向进入 Shadow 内部查询,不做全局递归
  • 批量提取内容时,优先获取 innerHTML 再统一解析,减少跨上下文调用次数

5.4 反爬对抗

部分网站会利用 Shadow DOM 作为反爬手段,将关键数据放在 Shadow 内部并配合代码混淆。除上述抓取方案外,还需注意注入脚本的隐蔽性,避免因重写原生方法被反爬策略检测。

六、总结

Shadow DOM 的抓取本质是突破其封装边界的过程,核心思路可归纳为三类:

  • Open 模式:通过 element.shadowRoot 直接访问,主流自动化工具均提供便捷 API
  • 多层嵌套:通过递归遍历所有 Shadow Root,实现全文档范围的内容抓取
  • Closed 模式:提前注入脚本劫持 attachShadow,缓存所有 Shadow Root 实例
  • 理解 Shadow DOM 的隔离机制后,无论使用原生 JavaScript 还是自动化工具,底层逻辑都是相通的。掌握以上方法后,绝大多数基于 Shadow DOM 的页面内容都可以被正常抓取。

    赞(0)
    未经允许不得转载:171主机测评 » Shadow DOM 如何抓取?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址