shadow DOM不是超级英雄电影中的恶棍,也不是DOM的黑暗面。 shadow DOM只是一种解决文档对象模型(或简称DOM)中缺少的树封装方法。 网页通常使用来自外部源的数据和小部件,如果它们没有封装,那么样式可能会影响HTML中不必要的部分,迫使开发人员使用特定的选择器和!important 规则来避免样式冲突。 尽管如此,在编写大型程序时,这些努力似乎并不是那么有效,并且大量的时间被浪费在防止CSS和JavaScript的冲突上。 Shadow DOM…
Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?
在我最近的一个自动化项目中,我正在编写代码以单击网页上的某个元素。这是一个带有 id 的非常简单的元素avatar
。令我惊讶的是,Selenium 未能找到该元素并抛出异常NoSuchElementException
。我更仔细地检查了那个元素,发现这个元素在一些奇怪的元素里面shadow-root
。事实上,有一个元素树,包括avatar
在那个元素里面shadow-root
。
一个快速的谷歌搜索显示这shadow-root
不是一个常规的 DOM 元素,它是影子 DOM 的一部分。到目前为止,Selenium WebDriver 无法与之交互。w3c有一个提案正在等待中来支持它。
什么是 DOM?
在了解 shadow DOM 之前,您应该首先熟悉 DOM。
文档对象模型 ( DOM )通过 在内存中表示文档的结构(例如表示网页的 HTML)将网页连接到脚本或编程语言
https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model#HTML_DOM
简而言之,当 Web 浏览器获取 HTML 页面时,它会解析文档并将其转换为加载到内存中的 DOM。这是一个非常简单的 HTML 文档:
<html lang="en"> <head> <title>A simple web page</title> </head> <body> <h1>Hello world</h1> <p>I am rendered!</p> </body> </html>
该文档的 HTML DOM 表示形式如下所示:

如您所见,DOM 是一个树状结构。请随时阅读本文中有关 DOM 的更多信息。
什么是shadow DOM?
shadow DOM 是一种在 HTML 文档中实现封装的方法。通过实现它,您可以隐藏文档一部分的样式和行为,并与同一文档的其他代码分开,这样就不会受到干扰。
Shadow DOM 允许将隐藏的 DOM 树附加到常规 DOM 树中的元素——这个 shadow DOM 树从一个 shadow root 开始,在它的下面可以附加到任何你想要的元素,就像普通 DOM 一样。
有一些 shadow DOM 术语需要注意:
- Shadow host:shadow DOM 附加到的常规 DOM 节点。
- Shadow tree: shadow DOM 中的 DOM tree。
- Shadow boundary: shadow DOM 结束的地方,常规 DOM 开始的地方。
- Shadow root: Shadow tree的根节点。
以上部分摘自MDN。您可以在此处阅读有关 shadow DOM的更多信息。
如何访问Shadow DOM?
您可以通过在主页面上下文中运行常规 JavaScript(如果其模式为open
.
让我们看下面的 HTML 代码片段:
<div> <div id="shell"> #shadow-root (open) <div id="avatar"></div> </div> <a href="./logout.html">Logout</a> </div>
如您所见,它既有影子 DOM(avatar
)也有常规 DOM 元素(Logout
链接)。
要使用 JavaScript 访问影子 DOM 元素,您首先需要查询 shadow host 元素,然后才能访问其shadowRoot
属性。一旦您可以访问shadowRoot
,您就可以像常规 JavaScript 一样查询 DOM 的其余部分。
var host = document.getElementById('shell'); var root = host.shadowRoot; var avatar = root.getElementById('avatar');
如何编写 Selenium 代码来访问 shadow DOM?
我们可以利用该特性,将一段 JavaScript 注入浏览器,以获取 shadow DOM 中的目标元素。一旦我们有了目标元素,我们就可以将其解析为 aWebElement
并且可以对该元素执行任何有效的操作。
Java 代码:
WebElement host = driver.findElement(By.id("shell")); JavascriptExecutor js = (JavascriptExecutor)driver; WebElement shadowRoot = (WebElement)(js.executeScript("return arguments[0].shadowRoot", host)); shadowRoot.findElement(By.id("avatar")).click();
Python 代码:
host = driver.find_element_by_id("shell")) shadowRoot = driver.execute_script("return arguments[0].shadowRoot", host) shadowRoot.find_elemen_by.id("avatar")).click()
有时我观察到 WebDriver 的click()
方法会抛出一些异常。在这种情况下,我们可以直接使用 JavaScript 的click()
方法。
Java 代码:
WebElement host = driver.findElement(By.id("shell")); JavascriptExecutor js = (JavascriptExecutor)driver; js.executeScript("arguments[0].shadowRoot.getElementById('avatar').click()", host);
Python 代码:
host = driver.find_element_by_id("shell")) driver.execute_script("return arguments[0].shadowRoot.getElementById('avatar').click()", host)
嵌套的shadow DOM
有时会有一个更复杂的 DOM 结构,其中我们有多个相互嵌套的shadow DOM。如果您检查 Chrome 浏览器的下载页面 – chrome://downloads/,您会发现以下 DOM 结构:
如您所见,shadow DOM 有三层相互嵌套。如果您想访问<div id="leftContent">
位于第三个影子 DOM 内的目标元素怎么办?
好吧,您可以应用我们目前在本教程中学到的相同原则——编写 JavaScript 以首先访问shadow host,然后通过访问shadowRoot
host上的属性来获取shadow DOM。一旦您可以访问第一个 shadow DOM,您就可以遍历它并尝试访问第二个 shadow DOM 的根,依此类推。
document.getElementsByTagName('downloads-manager')[0] .shadowRoot .getElementById('toolbar') .shadowRoot .getElementById('toolbar') .shadowRoot .getElementById('leftContent')
如果我们想点击那个元素,我们可以将完整的 JavaScript 注入浏览器:
Java 代码:
WebElement host = driver.findElement(By.tagName("downloads-manager")); JavascriptExecutor js = (JavascriptExecutor)driver; js.executeScript("arguments[0].shadowRoot.getElementById('toolbar').shadowRoot.getElementById('toolbar').shadowRoot.getElementById('leftContent').click()", host);
Python 代码:
firstHost = driver.find_element_by_tag_name("downloads-manager") driver.execute_script("return arguments[0].shadowRoot.getElementById('toolbar').shadowRoot.getElementById('toolbar').shadowRoot.getElementById('leftContent').click()", host)
或者:
WebDriverWait(self.driver, 5, 0.5).until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'kat-tab[tab-id="DATE_RANGE_REPORTS"]'))) script = 'document.querySelector("kat-tab[tab-id=DATE_RANGE_REPORTS]").querySelector("span[slot=label]").click();' self.driver.execute_script(script)
我们可以优化上面的代码,写一个可以返回任何shadow host的shadow DOM 的辅助方法:
Java 代码:
public WebElement getShadowRoot(WebElement host) { JavascriptExecutor js = (JavascriptExecutor)driver; WebElement shadowRoot = (WebElement) js.executeScript("return arguments[0].shadowRoot", host); return shadowRoot; }
Python 代码:
def getShadowRoot(host): shadowRoot = driver.executeScript("return arguments[0].shadowRoot", host) return shadowRoot
每次需要访问 shadow DOM 时,我们都可以使用这个辅助方法:
Java 代码:
//Get first shadow host and access its shadow root WebElement host1 = driver.findElement(By.tagName("downloads-manager")); WebElement root1 = getShadowRoot(host1); //Get second shadow host and access its shadow root WebElement host2 = shadowRoot1.findElement(By.id("toolbar")); WebElement root2 = getShadowRoot(host2); //Get third shadow host and access its shadow root WebElement host2 = shadowRoot2.findElement(By.id("toolbar")); WebElement root3 = expandRootElement(host2); //Get the target element inside the third shadow DOM WebElement downloads = root3.findElement(By.id("leftContent")).getText(); assert downloads.getText().contains('Downloads');
Python 代码:
# Get first shadow host and access its shadow root host1 = driver.find_element_by_tag_name("downloads-manager") root1 = getShadowRoot(host1) # Get second shadow host and access its shadow root host2 = shadowRoot1.find_element_by_id("toolbar") root2 = getShadowRoot(host2) # Get third shadow host and access its shadow root host2 = shadowRoot2.find_element_by_id("toolbar") root3 = expandRootElement(host2) # Get the target element inside the third shadow DOM downloads = root3.find_element_by_id("leftContent") assert 'Downloads' in downloads.text
挑战
您可以应用本教程的学习内容并在 Chrome 下载页面的搜索栏中输入一些文本吗?如果您检查搜索栏并仔细观察,您会发现它位于嵌套的第三个影子 DOM 内。请随时寻求帮助或在下面的评论中发布您的解决方案。