废话不多说,直接开始
本文以无界作为本文测试案例,抓取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 os
import 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 By
def 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_root
shadow_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_root
nest_shadow_host = shadow_root.find_element(By.CSS_SELECTOR, '#nested_shadow_host')
nest_shadow_root = nest_shadow_host.shadow_root
nest_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_root
html_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 返回都是none
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")')
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_root
html_element = shadow_root.find_element(By.CSS_SELECTOR,'html')
button = html_element.find_element(By.CSS_SELECTOR,".el-button")
#方法2 Ans
assert button.text == '仓库地址'
#方法1 Ans
assert result.text == '仓库地址'
print(button.text)
print(result.text)
except Exception as e:
print("Error:", e)
finally:
driver.quit()
最后成功拿到 仓库地址(好的,现在就偷仓库东西去了)
课后作业!
试试抓取 某企鹅下的 某moba手游论坛下的用户名,日期!
实验链接:彻底疯狂!
I got it ,代码和上述大差不差