python selenium(4+)+chromedriver最新版 定位爬取嵌套shadow-root(open)中内容

废话不多说,直接开始

本文以无界作为本文测试案例,抓取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提供的例子:

python 复制代码
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"

python 复制代码
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。

这里主要看下,相比前一个简单例子的区别:

  1. 这里可以很明显看到wujie ,这个词在之后抓取其他shadow-root网页会很常见。该测试案例网站也有介绍章节。
  2. shadow Dom内嵌<html >,同时注意到下方的script脚本,有些网站会写到是异步脚本动态加载js,该过程需要在selenium中使用 timesleep或者until 来解决动态加载问题。
    如下(异步加载js):
    参考前一个例子来抓取仓库地址
python 复制代码
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

python 复制代码
	#加不加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 节点的行为(伪装其父节点为全局文档),绕过验证逻辑。

于是修改后代码如下:

python 复制代码
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 ,代码和上述大差不差

相关推荐
是十一月末9 分钟前
Python语法之正则表达式详解以及re模块中的常用函数
开发语言·python·正则表达式
灵封~1 小时前
PythonQt练习
python
pzx_0012 小时前
【时间序列预测】基于Pytorch实现CNN_LSTM算法
人工智能·pytorch·python·算法·cnn·lstm
@半良人2 小时前
Python迭代器-大数据量的处理
python
hakesashou2 小时前
python中什么是驼峰法
python
工业互联网专业3 小时前
Python毕业设计选题:基于大数据的淘宝电子产品数据分析的设计与实现-django+spark+spider
大数据·python·数据分析·spark·django·课程设计·spider
fc&&fl4 小时前
python+docker实现分布式存储的demo
开发语言·python·docker
叫我:松哥6 小时前
基于Python 哔哩哔哩网站热门视频数据采集与可视化分析设计与实现,有聚类有网络语义研究
开发语言·python·信息可视化·网络爬虫·matplotlib·聚类分析·网络语义分析
Srlua6 小时前
基于深度学习的手势识别算法
人工智能·python
MC何失眠8 小时前
vulnhub靶场【哈利波特】三部曲之Fawkes
网络·python·学习·网络安全