侧边栏壁纸
博主头像
Setyg的博客

当秋风染红落叶的时候,就是新生~

  • 累计撰写 86 篇文章
  • 累计创建 66 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Python 爬虫无法获取 Shadow DOM 元素的原因与解决

Setyg
2026-09-17 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

用 Selenium 抓页面时,有时会发现某个元素在浏览器开发者工具里明明看得到,代码里却怎么都定位不到,返回 NoSuchElementException。看 DOM 结构会发现它挂在一段 #shadow-root (open) 下面——这就是 Shadow DOM。

一、为什么定位不到

Shadow DOM 是 Web Components 的封装机制:组件可以把自身内部的 HTML 结构、样式和行为封装在一个独立的 DOM 子树里,与主文档的 DOM 树隔离。

关键在于:driver.find_element() 以及所有常规的 XPath、CSS 选择器,只在主文档的 light DOM 里查找,不会进入任何 shadow root。所以像下面这样写,无论选择器多准确都会失败:

# 找不到:CSS 选择器不会穿透 shadow 边界
driver.find_element(By.CSS_SELECTOR, "#shadow-button")

要拿到里面的元素,必须先定位到 shadow host(宿主元素),再进入它的 shadow root,然后在 root 内部查找。逐层进入,是这个问题的核心思路。

二、方法一:Selenium 4 的 shadow_root 属性(推荐)

Selenium 4 为 WebElement 提供了 shadow_root 属性,直接返回该元素挂载的 ShadowRoot 对象;如果该元素没有 shadow root,会抛 NoSuchShadowRoot 异常。

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

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

# 1. 先定位 shadow host
host = driver.find_element(By.CSS_SELECTOR, "my-widget")

# 2. 拿到它的 shadow root
shadow_root = host.shadow_root

# 3. 在 shadow root 内部查找真正的目标元素
button = shadow_root.find_element(By.CSS_SELECTOR, "#shadow-button")
button.click()

拿到的是一个真正的 WebElement,所以 click()、send_keys()、.text 这些方法都能正常用。

这个属性对浏览器版本有要求:Chromium 96+、Firefox 96+、Safari 16.4+ 才支持。如果目标环境是更早的浏览器,用下面的方法二。

三、方法二:用 execute_script 穿透

通过注入 JavaScript 读取 element.shadowRoot,是兼容性最好的做法。

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

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

host = driver.find_element(By.CSS_SELECTOR, "my-widget")

# 方式 A:直接返回 root 内部的元素(最省事,推荐)
button = driver.execute_script(
    "return arguments[0].shadowRoot.querySelector('#shadow-button')", host
)
button.click()

# 方式 B:先把 shadow root 取出来,再用 Selenium 的方法继续找
shadow_root = driver.execute_script("return arguments[0].shadowRoot", host)
button = shadow_root.find_element(By.CSS_SELECTOR, "#shadow-button")
button.click()

两种方式都可行。方式 A 把查询交给浏览器执行,一步到位,不受 Selenium 版本差异影响,出问题时也最容易排查;方式 B 拿到 ShadowRoot 后可以用 Selenium 的等待、条件判断等方法,写起来更顺手。

注意传给 execute_script 的必须是通过 Selenium 定位到的 host 元素,不能在 JS 里用 document.querySelector(...).shadowRoot 这样链式去取——那样拿到的 host 是主文档里的元素没错,但一旦涉及后面要讲的 closed 模式或嵌套结构就会失效,直接用 arguments[0] 传入最稳妥。

四、嵌套 Shadow DOM:逐层进入

Web Components 互相嵌套时,shadow root 也会层层套下去。这时无法用一条选择器直达最内层,只能一层一层往下钻:

from selenium.webdriver.common.by import By

# 第一层
sr1 = driver.find_element(By.CSS_SELECTOR, "outer-widget").shadow_root

# 第二层:在外层 root 里找到内层 host,再取它的 shadow root
sr2 = sr1.find_element(By.CSS_SELECTOR, "inner-widget").shadow_root

# 第三层:目标元素
target = sr2.find_element(By.CSS_SELECTOR, "#shadow-button")
target.click()

每一层都用上一层的 root 作为查找起点,这是处理嵌套结构的通用写法。

五、不确定元素在哪一层:遍历查找

如果页面结构复杂、手写层级容易出错,可以写一个帮助函数,逐层遍历页面上所有的 shadow root 去找目标元素。核心是:只在 shadow host 上递归,而不是遍历所有后代元素。

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

def find_in_shadow_dom(driver, css_selector):
    """广度优先遍历页面上所有 open shadow root,找到即返回,找不到返回 None。"""
    roots = [driver]           # 待搜索的根,可以是 driver,也可以是 ShadowRoot
    while roots:
        next_roots = []
        for root in roots:
            found = root.find_elements(By.CSS_SELECTOR, css_selector)
            if found:
                return found[0]

            # 收集当前根下所有挂了 shadow root 的宿主元素
            for el in root.find_elements(By.CSS_SELECTOR, "*"):
                try:
                    next_roots.append(el.shadow_root)
                except NoSuchShadowRoot:
                    pass    # 该元素没有 shadow root,跳过
        roots = next_roots
    return None


button = find_in_shadow_dom(driver, "#shadow-button")
if button:
    button.click()
else:
    print("Element not found.")

几个说明:

  • find_elements 在找不到时返回空列表而不是抛异常,所以可以直接用 if found 判断。
  • 用 el.shadow_root 配合捕获 NoSuchShadowRoot 来筛选宿主元素,比在 JS 里 filter(e => e.shadowRoot) 更贴近 Selenium 原生写法,也不需要额外传递对象进 JS 上下文。
  • 这个函数每次都从最外层重新遍历,页面元素多时开销不小;如果同一个页面上要抓多个 shadow 元素,建议把中间结果(各层的 root)缓存下来复用。
  • 只在「不知道元素在第几层」时才需要它。层级已知的情况下,第四节逐层进入的写法更快也更清晰。

六、几个容易踩的坑

1. find_element_by_* 系列方法已被移除

不少较早的教程和示例代码里还在用这种写法:

# Selenium 4.3.0 起已移除,会报 AttributeError
shadow_root.find_elements_by_css_selector(css_selector)
shadow_root.find_elements_by_tag_name("*")

这类 find_element_by_* / find_elements_by_* 方法已在 Selenium 4.3.0 中正式移除(更早的 4.1 起就已废弃),继续使用会抛 AttributeError: 'WebDriver' object has no attribute 'find_elements_by_css_selector'。统一改成下面这种形式:

shadow_root.find_element(By.CSS_SELECTOR, css_selector)
shadow_root.find_elements(By.CSS_SELECTOR, "*")
shadow_root.find_element(By.TAG_NAME, "button")

2. closed 模式的 shadow root 拿不到

attachShadow() 可以指定 mode: 'open' 或 mode: 'closed'。当组件使用 closed 模式时,它刻意把 element.shadowRoot 设为 null,外部脚本无法通过这个属性访问内部结构——上述所有方法都会失效。::shadow 这类选择器同样只对 open 模式生效。

开发者工具里 closed 的 shadow root 会显示成 #shadow-root (closed),可以用这一点快速判断。遇到 closed 模式的常规应对思路:

  • 优先看页面是否提供了其他可达的入口(比如公开的 API、页面上的普通按钮);
  • 用 CDP 在页面加载前重写 Element.prototype.attachShadow,把 mode: "closed" 强制改成 "open",之后就能按正常流程访问了。

这类做法依赖浏览器实现细节,属于「能解决但不够稳定」的手段,用在爬虫场景没问题,但不要写进对稳定性要求高的自动化测试里。

3. ShadowRoot 不是 WebElement

这个区别很容易混:host.shadow_root 返回的是 ShadowRoot 对象,不能在它上面直接调 click() 或 send_keys()。必须先在它内部 find_element 定位到一个真正的 WebElement,再对那个元素做操作。

4. 拿到 root 不等于元素已就绪

Shadow DOM 常常是组件异步渲染出来的。host.shadow_root 拿到的可能是内容还没填充的、innerHTML 为空的根。这时可以配合显式等待:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
shadow_root = wait.until(
    lambda d: d.find_element(By.CSS_SELECTOR, "my-widget").shadow_root
)
button = wait.until(
    lambda _: shadow_root.find_element(By.CSS_SELECTOR, "#shadow-button")
)

expected_conditions 里的 presence_of_element_located 等方法接收的是 (By, locator) 形式,不能直接把 ShadowRoot 当成 driver 传进去,所以这里用 lambda 自己封装等待条件更灵活。

5. 元素找到了但点击失败

如果报 ElementClickInterceptedException,通常是元素还没滚动到可视区域,或者被浮层遮挡。可以先 driver.execute_script("arguments[0].scrollIntoView()", el),或者改用 ActionChains 派发点击、乃至直接用 JS 点击。这类问题的根源和 Shadow DOM 无关,属于常规的元素交互排查。

七、方法选择建议

场景推荐做法
Selenium 4 + 较新的浏览器host.shadow_root 属性,写法最简洁
浏览器版本较老,或需要最高兼容性execute_script 返回 arguments[0].shadowRoot.querySelector(...)
结构固定的嵌套 shadow DOM逐层 .shadow_root 往下进入
不知道元素在第几层遍历所有 shadow root 的帮助函数
closed 模式常规方法无效,需用 CDP 改写 attachShadow

小结

核心只有一句话:常规选择器不穿透 shadow 边界,必须先定位 host、拿到 shadow root,再在 root 内部查找。Selenium 4 的 shadow_root 属性已经把这件事变成了两行代码,旧代码里那些 find_elements_by_* 的写法需要一并迁移到 find_element(By.XXX, ...)。真正无解的情况只有 closed 模式,那需要绕过浏览器限制,且稳定性要打折扣。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区