如何从 Selenium 处理 Shadow DOM 中的元素
Posted
技术标签:
【中文标题】如何从 Selenium 处理 Shadow DOM 中的元素【英文标题】:How to handle elements inside Shadow DOM from Selenium 【发布时间】:2016-09-19 22:45:44 【问题描述】:我想在chromedriver
中自动检查文件下载完成。
下载列表中每个条目的html
看起来像
<a is="action-link" id="file-link" tabindex="0" role="link" href="http://fileSource" class="">DownloadedFile#1</a>
所以我使用以下代码来查找目标元素:
driver.get('chrome://downloads/') # This page should be available for everyone who use Chrome browser
driver.find_elements_by_tag_name('a')
当有 3 个新下载时,这将返回空列表。
我发现,只能处理#shadow-root (open)
标签的父元素。
那么如何在这个#shadow-root
元素中找到元素呢?
【问题讨论】:
driver.find_elements_by_id("file-link")
有帮助吗?
没有。这将返回相同的空列表
好的,那么 Css/Xpath 可能仍然作为访问driver.find_elements_by_css_selector(".[id='file-link']")
的方式为您提供了一些价值?
你的语句返回InvalidSelectorException
,driver.find_elements_by_css_selector("[id='file-link']")
返回空列表
@Anderson : 你错过了driver.find_elements_by_css_selector(".[id='file-link']")
后面的.
吗?
【参考方案1】:
您可以使用driver.executeScript()
方法访问网页中的 HTML 元素和 javascript 对象。
在下面的示例中,executeScript
将在 Promise
中返回存在于元素影子树中的所有 <a>
元素的节点列表,其中 id
是 host
。然后你可以执行你的断言测试:
it( 'check shadow root content', function ()
return driver.executeScript( function ()
return host.shadowRoot.querySelectorAll( 'a' ).then( function ( n )
return expect( n ).to.have.length( 3 )
)
)
注意:我不懂 Python,所以我使用了 JavaScript 语法,但它应该以同样的方式工作。
【讨论】:
我不知道这段代码是什么意思 :) 而且我从未在JS
中看到过=>
符号它的用途是什么?...任何人都可以“翻译”这段代码吗?
() => 是一个 lambda 表达式/内联函数语法。我更新了我的答案以使用标准函数声明。【参考方案2】:
有时影子根元素是嵌套的,第二个影子根在文档根中不可见,但在其父访问的影子根中可用。我认为最好使用 selenium 选择器并注入脚本只是为了获取影子根:
def expand_shadow_element(element):
shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
return shadow_root
outer = expand_shadow_element(driver.find_element_by_css_selector("#test_button"))
inner = outer.find_element_by_id("inner_button")
inner.click()
为了说明这一点,我刚刚在 Chrome 的下载页面中添加了一个可测试的示例,单击搜索按钮需要打开 3 个嵌套的影子根元素:
import selenium
from selenium import webdriver
driver = webdriver.Chrome()
def expand_shadow_element(element):
shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
return shadow_root
driver.get("chrome://downloads")
root1 = driver.find_element_by_tag_name('downloads-manager')
shadow_root1 = expand_shadow_element(root1)
root2 = shadow_root1.find_element_by_css_selector('downloads-toolbar')
shadow_root2 = expand_shadow_element(root2)
root3 = shadow_root2.find_element_by_css_selector('cr-search-field')
shadow_root3 = expand_shadow_element(root3)
search_button = shadow_root3.find_element_by_css_selector("#search-button")
search_button.click()
执行其他答案中建议的相同方法的缺点是它对查询进行硬编码,可读性较差,并且您不能将中间选择用于其他操作:
search_button = driver.execute_script('return document.querySelector("downloads-manager").shadowRoot.querySelector("downloads-toolbar").shadowRoot.querySelector("cr-search-field").shadowRoot.querySelector("#search-button")')
search_button.click()
稍后编辑:
我最近尝试访问内容设置(请参阅下面的代码),它有多个影子根元素重叠,现在如果不先扩展另一个,您将无法访问其中一个,而您通常还有动态内容和 3 个以上的影子元素一个进入另一个它使自动化成为不可能。上面的答案在前几次使用过,但足以让一个元素改变位置,并且您需要始终使用检查元素并在树上查看它是否在阴影根中,自动化噩梦。
当您发现按钮此时不可点击时,不仅很难找到内容设置,因为 shadowroot 和动态变化。
driver = webdriver.Chrome()
def expand_shadow_element(element):
shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
return shadow_root
driver.get("chrome://settings")
root1 = driver.find_element_by_tag_name('settings-ui')
shadow_root1 = expand_shadow_element(root1)
root2 = shadow_root1.find_element_by_css_selector('[page-name="Settings"]')
shadow_root2 = expand_shadow_element(root2)
root3 = shadow_root2.find_element_by_id('search')
shadow_root3 = expand_shadow_element(root3)
search_button = shadow_root3.find_element_by_id("searchTerm")
search_button.click()
text_area = shadow_root3.find_element_by_id('searchInput')
text_area.send_keys("content settings")
root0 = shadow_root1.find_element_by_id('main')
shadow_root0_s = expand_shadow_element(root0)
root1_p = shadow_root0_s.find_element_by_css_selector('settings-basic-page')
shadow_root1_p = expand_shadow_element(root1_p)
root1_s = shadow_root1_p.find_element_by_css_selector('settings-privacy-page')
shadow_root1_s = expand_shadow_element(root1_s)
content_settings_div = shadow_root1_s.find_element_by_css_selector('#site-settings-subpage-trigger')
content_settings = content_settings_div.find_element_by_css_selector("button")
content_settings.click()
【讨论】:
嗨,爱德华,我迟到了。我尝试使用您的代码,但似乎shadow_root1
没有 find_element_by_whatever
方法。我做错什么了吗?基本上我有root1 = driver.find_element_by_tag_name('input')
,然后是shadowRoot1 = ExpandShadowElement(root1)
他们一直在更改它,没有时间查看和更新
啊,谢谢!其实我发现我不需要解析shadow DOM,设法登录而不接触它们,不知道为什么......【参考方案3】:
我会将此添加为评论,但我没有足够的声望点--
Eduard Florinescu 的回答很好地说明了一旦你在 shadowRoot 中,你只有与可用的 JS 方法相对应的 selenium 方法——主要是通过 id 选择。
为了解决这个问题,我在 python 字符串中编写了一个更长的 JS 函数,并使用原生 JS 方法和属性(通过 id、children + 索引等查找)来获取我最终需要的元素。
使用 driver.execute_script() 运行 JS 字符串时,您还可以使用此方法访问子元素的 shadowRoots 等
【讨论】:
【参考方案4】:还有可以使用的pyshadow pip 模块,在我的情况下可以使用,下面的例子:
from pyshadow.main import Shadow
from selenium import webdriver
driver = webdriver.Chrome('chromedriver.exe')
shadow = Shadow(driver)
element = shadow.find_element("#Selector_level1")
element1 = shadow.find_element("#Selector_level2")
element2 = shadow.find_element("#Selector_level3")
element3 = shadow.find_element("#Selector_level4")
element4 = shadow.find_element("#Selector_level5")
element5 = shadow.find_element('#control-button') #target selector
element5.click()
【讨论】:
【参考方案5】:为了简单起见,我最初将 Eduard 的解决方案稍微修改为一个循环。但是当 Chrome 更新到 96.0.4664.45 时 selenium 在调用 'return arguments[0].shadowRoot'
时开始返回 dict 而不是 WebElement。
我做了一些修改,发现我可以通过调用 return arguments[0].shadowRoot.querySelector("tag")
让 Selenium 返回一个 WebElement。
这是我的最终解决方案最终的样子:
def get_balance_element(self):
# Loop through nested shadow root tags
tags = [
"tag2",
"tag3",
"tag4",
"tag5",
]
root = self.driver.find_element_by_tag_name("tag1")
for tag in tags:
root = self.expand_shadow_element(root, tag)
# Finally there. GOLD!
return [root]
def expand_shadow_element(self, element, tag):
shadow_root = self.driver.execute_script(
f'return arguments[0].shadowRoot.querySelector("tag")', element)
return shadow_root
干净简单,适合我。
另外,我只能得到这个工作的 Selenium 3.141.0。 4.1 有一个半生不熟的影子 DOM 实现,它只是设法破坏一切。
【讨论】:
Chrome 96+ 旨在使用 Python Selenium 4.1 中的新shadow_dom
属性。我在这里也有 Selenium 3 的 hack:titusfortner.com/2021/11/22/shadow-dom-selenium.html以上是关于如何从 Selenium 处理 Shadow DOM 中的元素的主要内容,如果未能解决你的问题,请参考以下文章
java+selenium,请问该如何定位#shadow-root里面的元素?
如何使用 Python 在 selenium 中编辑#shadow-root(用户代理)值
如何使用 jquery 和 selenium 在“chrome://downloads”访问“shadow-root”下的元素?
有人知道如何使用 selenium webdriver 识别 shadow dom web 元素吗?