废话不多说,直接开始
本文以无界作为本文测试案例,抓取shadow-root(open)下的内容
shadow Dom in selenium:
首先先讲一下shadow Dom in selenium 版本的区别,链接指向这里
在Selenium 4+版本 以及 chrome ver 96+中,有做出一下修改。摒弃了老版本的driver.find_element_by_css_selector。这点跟版本有关系,请自行查看目前使用的版本。
使用以及安装版本:
selenium 4.27.1 + chrome(131.0.6778.86)最新版本**
selenium pip install selenium==4.27.1
即可(不指定版本也可以,目前默认安装此版本)
chromedriver 最新版本下载指向https://googlechromelabs.github.io/chrome-for-testing/#stable
由于selenium ,chrome+chromedriver 版本一直在迭代,所以也许过两年情况可能不一样了。具体更新见官方。
初入shadow-root:
先看shadow Dom in selenium提供的例子:
import osimport pytest
from selenium.webdriver import Chrome
from selenium.webdriver import Firefox
from selenium.webdriver import Remote
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.common.by import Bydef test_old_code_old_chrome():"""What most people use for Shadow DOM Elements.Still works for Chrome < v96, Edge < v96, Safari"""options = ChromeOptions()options.set_capability('browserVersion', '95.0')sauce_options = {'username': os.environ["SAUCE_USERNAME"],'accessKey': os.environ["SAUCE_ACCESS_KEY"]}options.set_capability('sauce:options', sauce_options)sauce_url = "https://ondemand.us-west-1.saucelabs.com/wd/hub"driver = Remote(command_executor=sauce_url, options=options)driver.get('http://watir.com/examples/shadow_dom.html')shadow_host = driver.find_element_by_css_selector('#shadow_host')shadow_root = driver.execute_script('return arguments[0].shadowRoot', shadow_host)shadow_content = shadow_root.find_element_by_css_selector('#shadow_content')assert shadow_content.text == 'some text'driver.quit()def test_old_code_new_chrome():"""Same code as above, but in Chromium 96+.Selenium 4.0 has same error as Selenium 3.Selenium 4.1 has AttributeError for using the old find_element_* method"""driver = Chrome()driver.get('http://watir.com/examples/shadow_dom.html')shadow_host = driver.find_element_by_css_selector('#shadow_host')shadow_root = driver.execute_script('return arguments[0].shadowRoot', shadow_host)with pytest.raises(AttributeError, match="'ShadowRoot' object has no attribute 'find_element_by_css_selector'"):shadow_root.find_element_by_css_selector('#shadow_content')driver.quit()def test_fix_old_code():"""Same code as above, but using the new By class for find_element()This works in Selenium 4.1."""driver = Chrome()driver.get('http://watir.com/examples/shadow_dom.html')shadow_host = driver.find_element(By.CSS_SELECTOR, '#shadow_host')shadow_root = driver.execute_script('return arguments[0].shadowRoot', shadow_host)shadow_content = shadow_root.find_element(By.CSS_SELECTOR, '#shadow_content')assert shadow_content.text == 'some text'driver.quit()def test_recommended_code():"""Please use this code."""driver = Chrome()driver.get('http://watir.com/examples/shadow_dom.html')shadow_host = driver.find_element(By.CSS_SELECTOR, '#shadow_host')shadow_root = shadow_host.shadow_rootshadow_content = shadow_root.find_element(By.CSS_SELECTOR, '#shadow_content')assert shadow_content.text == 'some text'driver.quit()def test_firefox_workaround():"""Firefox is special."""driver = Firefox()driver.get('http://watir.com/examples/shadow_dom.html')shadow_host = driver.find_element(By.CSS_SELECTOR, '#shadow_host')children = driver.execute_script('return arguments[0].shadowRoot.children', shadow_host)shadow_content = next(child for child in children if child.get_attribute('id') == 'shadow_content')assert shadow_content.text == 'some text'driver.quit()
主要看test_recommended_code这部分代码,可以发现主要抓取some text,并assert
同理于是按照官方给的示例代码,很容易抓到“nested text”
def test_recommended_code():"""Please use this code."""driver = Chrome()driver.get('http://watir.com/examples/shadow_dom.html')shadow_host = driver.find_element(By.CSS_SELECTOR, '#shadow_host')shadow_root = shadow_host.shadow_rootnest_shadow_host = shadow_root.find_element(By.CSS_SELECTOR, '#nested_shadow_host')nest_shadow_root = nest_shadow_host.shadow_rootnest_shadow_content = nest_shadow_root.find_element(By.CSS_SELECTOR, '#nested_shadow_content')assert nest_shadow_content.text == 'nested text'driver.quit()
在控制台我们能得到同样的输出结果:
测试案例:
打开本文开头指向的无界链接,进入F12。
这里主要看下,相比前一个简单例子的区别:
- 这里可以很明显看到wujie,这个词在之后抓取其他shadow-root网页会很常见。该测试案例网站也有介绍章节。
- shadow Dom内嵌<html>,同时注意到下方的script脚本,有些网站会写到是异步脚本动态加载js,该过程需要在selenium中使用 timesleep或者until 来解决动态加载问题。
如下(异步加载js):
参考前一个例子来抓取仓库地址
import time
from selenium import webdriver
from selenium.webdriver.common.by import By# 设置 Chrome options
options = webdriver.ChromeOptions()
options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36')# 初始化 WebDriver
driver = webdriver.Chrome(options=options)
# 在页面中执行自定义 JavaScript 代码try:# 打开目标网页url = "https://wujie-micro.github.io/demo-main-vue/vue3?vue3=%2Fdemo-vue3%2Fhome"driver.get(url)time.sleep(5)# 等待页面加载并找到目标元素所在的 Shadow DOM# wait = WebDriverWait(driver, 20) # 等待最多20秒# shadow_host = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "wujie-app.wujie_iframe")))shadow_host = driver.find_element(By.CSS_SELECTOR,"wujie-app.wujie_iframe")shadow_root = shadow_host.shadow_roothtml_element = shadow_root.find_element(By.CSS_SELECTOR,'html')button = html_element.find_element(By.CSS_SELECTOR,".el-button")assert button.text == '仓库地址'except Exception as e:print("Error:", e)finally:driver.quit()
恭喜你,你会收到一份报错
尝试F12控制台js获取,复制JS路径,控制台输出,说明没问题。
尝试在python selenium运行js
#加不加return 返回都是noneresult = driver.execute_script('return document.querySelector("#app > div.content > div > wujie-app").shadowRoot.querySelector("#app > div:nth-child(2) > div.content > p:nth-child(4) > button > span")')
result返回是none
于是就开始查这个问题查了大半天,百度 csdn,stackoverflow什么的翻了个底朝天,几乎一度快放弃了。。。。到这里以为是被反爬了,但后想了一想该网站应该不是一个商用的客户网站,不涉及到用户信息,应该不会有反爬才对,所以决定再试试。
实在不行了,打算去看看chromedriver的开源代码查查到底为什么,查之前抱着试一试的态度在chromedriver的 issue中搜我的问题竟然找到了解决办法!
只能说十分感谢该老哥!链接指向
感兴趣的可以去看源码,我自己后面去看了下里面这个IsNodeReachable,这里就不贴图分析了。
问题大概是 Shadow DOM 的封装性导致节点在 Chromedriver 的验证中被误判为不可达。
所以解决思路: 通过修改 Shadow DOM 节点的行为(伪装其父节点为全局文档),绕过验证逻辑。
于是修改后代码如下:
import time
from selenium import webdriver
from selenium.webdriver.common.by import By# 设置 Chrome options
options = webdriver.ChromeOptions()
options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36')# 初始化 WebDriver
driver = webdriver.Chrome(options=options)
# 在页面中执行自定义 JavaScript 代码try:# 打开目标网页url = "https://wujie-micro.github.io/demo-main-vue/vue3?vue3=%2Fdemo-vue3%2Fhome"driver.get(url)time.sleep(5)driver.execute_script("""Object.defineProperty(window.document.querySelector('wujie-app').shadowRoot.firstElementChild, "parentNode", {enumerable: true,configurable: true,get: () => window.document,});""")# 等待页面加载并找到目标元素所在的 Shadow DOM# wait = WebDriverWait(driver, 20) # 等待最多20秒# shadow_host = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "wujie-app.wujie_iframe")))#方法1,直接复制js路径,更简单result = driver.execute_script('return document.querySelector("#app > div.content > div > wujie-app").shadowRoot.querySelector("#app > div:nth-child(2) > div.content > p:nth-child(4) > button > span")')#方法2 一步步的,我个人感觉这样更清晰,对于初学,我建议这样层层递进,并学会# find_element(By.CSS_SELECTOR 的使用shadow_host = driver.find_element(By.CSS_SELECTOR,"wujie-app.wujie_iframe")shadow_root = shadow_host.shadow_roothtml_element = shadow_root.find_element(By.CSS_SELECTOR,'html')button = html_element.find_element(By.CSS_SELECTOR,".el-button")#方法2 Ansassert button.text == '仓库地址'#方法1 Ansassert result.text == '仓库地址'print(button.text)print(result.text)
except Exception as e:print("Error:", e)finally:driver.quit()
最后成功拿到 仓库地址(好的,现在就偷仓库东西去了)
课后作业!
试试抓取 某企鹅下的 某moba手游论坛下的用户名,日期!
实验链接:彻底疯狂!
I got it ,代码和上述大差不差