shadow_dom

Shadow DOM 处理

现代网页开发中,Shadow DOM 技术被广泛应用于创建独立的组件和小部件。这种技术可以实现组件的封装和样式隔离,但同时也为网页自动化和爬虫带来了挑战。本教程将详细介绍如何使用 DrissionPage 处理 Shadow DOM 元素。

Shadow DOM 基础

什么是 Shadow DOM?

Shadow DOM 是 Web Components 标准的一部分,它允许开发者将 HTML、CSS 和 JavaScript 封装到自定义元素中,使其与主文档的 DOM 隔离。这种隔离使得组件的样式和行为不会受到页面其他部分的影响,也不会影响页面其他部分。

Shadow DOM 的主要构成部分:

  • Shadow Host:附加 Shadow DOM 的常规 DOM 元素
  • Shadow Root:Shadow DOM 的根节点
  • Shadow Tree:Shadow Root 中包含的 DOM 树
  • Shadow Boundary:隔离 Shadow DOM 和常规 DOM 的边界

Shadow DOM 的挑战

Shadow DOM 对网页自动化和爬虫带来的主要挑战:

  1. 隔离性:常规 DOM 查询无法直接穿透 Shadow Boundary
  2. 选择器限制:标准的 CSS 选择器和 XPath 无法直接选择 Shadow DOM 中的元素
  3. 动态性:许多 Shadow DOM 组件是动态创建和销毁的
  4. 嵌套:Shadow DOM 可以嵌套多层

DrissionPage 处理 Shadow DOM

DrissionPage 提供了专门的功能来处理 Shadow DOM,让您能够轻松地访问和操作 Shadow DOM 中的元素。

查找 Shadow Host

首先,我们需要找到包含 Shadow DOM 的宿主元素(Shadow Host):

from DrissionPage import ChromiumPage

page = ChromiumPage()
page.get('https://example.com/page-with-shadow-dom')

# 查找 Shadow Host 元素
shadow_host = page.ele('#shadow-host-id')

访问 Shadow Root

获取到 Shadow Host 后,可以访问其 Shadow Root:

# 获取 Shadow Root
shadow_root = shadow_host.shadow_root

# 确认是否成功获取 Shadow Root
if shadow_root:
    print("成功获取 Shadow Root")
else:
    print("未找到 Shadow Root,可能是开放式 Shadow DOM 或元素没有 Shadow Root")

在 Shadow DOM 中查找元素

获取 Shadow Root 后,可以在其中查找元素,就像在普通 DOM 中一样:

# 在 Shadow DOM 中查找单个元素
button = shadow_root.ele('.shadow-button')

# 在 Shadow DOM 中查找多个元素
items = shadow_root.eles('.shadow-item')

# 使用其他选择器
input_field = shadow_root.ele('@name=shadow-input')
header = shadow_root.ele('tag:h2')

操作 Shadow DOM 中的元素

一旦找到 Shadow DOM 中的元素,您可以像操作普通 DOM 元素一样操作它们:

# 点击 Shadow DOM 中的按钮
shadow_root.ele('.action-button').click()

# 在 Shadow DOM 中的输入框中输入文本
shadow_root.ele('input').input('Hello Shadow DOM')

# 获取 Shadow DOM 中元素的文本
text = shadow_root.ele('.content').text
print(f"Shadow DOM 中的文本: {text}")

# 获取 Shadow DOM 中元素的属性
value = shadow_root.ele('input').attr('value')
print(f"输入框的值: {value}")

处理嵌套的 Shadow DOM

在某些复杂的 Web 应用中,Shadow DOM 可能会嵌套多层。DrissionPage 支持处理这种嵌套结构:

# 获取第一层 Shadow Root
first_shadow_root = page.ele('#outer-host').shadow_root

# 在第一层 Shadow DOM 中查找包含第二层 Shadow DOM 的宿主元素
inner_host = first_shadow_root.ele('#inner-host')

# 获取第二层 Shadow Root
second_shadow_root = inner_host.shadow_root

# 在第二层 Shadow DOM 中查找元素
nested_element = second_shadow_root.ele('.nested-element')

# 操作嵌套的元素
nested_element.click()

一步到位查找 Shadow DOM 中的元素

DrissionPage 提供了一种更简洁的方式,使用特殊语法一步找到 Shadow DOM 中的元素:

from DrissionPage import ChromiumPage

page = ChromiumPage()
page.get('https://example.com/page-with-shadow-dom')

# 使用 >>> 语法穿透 Shadow DOM
element = page.ele('#shadow-host >>> .shadow-element')

# 处理多层嵌套的 Shadow DOM
deeply_nested = page.ele('#outer-host >>> #inner-host >>> .nested-element')

这种语法类似于 CSS 的 Shadow DOM 穿透选择器,使用 >>> 符号表示穿透 Shadow Boundary。

高级应用场景

处理自定义元素

许多使用 Shadow DOM 的页面也使用自定义元素(Custom Elements)。您可以结合处理:

# 查找自定义元素
custom_element = page.ele('tag:my-custom-element')

# 获取其 Shadow Root
shadow_root = custom_element.shadow_root

# 操作自定义元素内部的组件
shadow_root.ele('.internal-button').click()

处理开放式和封闭式 Shadow DOM

Shadow DOM 有两种模式:开放式(open)和封闭式(closed):

# 尝试获取 Shadow Root
host = page.ele('#host')
shadow_root = host.shadow_root

if shadow_root:
    # 开放式 Shadow DOM
    print("这是开放式 Shadow DOM,可以正常访问")
    shadow_root.ele('.content').click()
else:
    # 封闭式 Shadow DOM,需要特殊处理
    print("这可能是封闭式 Shadow DOM,尝试使用JavaScript访问")
    # 使用 JavaScript 技巧访问封闭式 Shadow DOM
    page.run_js('''
    // 获取宿主元素
    const host = document.querySelector('#host');
    // 直接点击内部元素(如果知道其实现细节)
    host.click();
    ''')

处理动态创建的 Shadow DOM

有些 Shadow DOM 是动态创建的,需要等待其出现:

# 触发创建 Shadow DOM 的操作
page.ele('#create-shadow-button').click()

# 等待 Shadow Host 元素出现
page.wait.ele_display('#dynamic-shadow-host')

# 获取动态创建的 Shadow Root
dynamic_host = page.ele('#dynamic-shadow-host')
shadow_root = dynamic_host.shadow_root

# 等待 Shadow DOM 中的特定元素出现
page.wait.ele_display('#dynamic-shadow-host >>> .dynamic-content')

# 操作元素
page.ele('#dynamic-shadow-host >>> .dynamic-button').click()

实战案例:处理具有 Shadow DOM 的视频播放器

以下是一个完整的实例,展示如何自动操作带有 Shadow DOM 的视频播放器:

from DrissionPage import ChromiumPage
import time

# 创建页面对象
page = ChromiumPage()

# 访问带有视频播放器的页面
page.get('https://example.com/video-player')

# 等待播放器加载完成
page.wait.load_complete()

# 找到视频播放器宿主元素
player_host = page.ele('#video-player')

# 获取播放器的 Shadow Root
player_root = player_host.shadow_root

# 点击播放按钮
play_button = player_root.ele('.play-button')
play_button.click()

# 等待视频开始播放
time.sleep(3)

# 调整音量
volume_slider = player_root.ele('.volume-control')
# 使用拖拽操作调整音量
volume_slider.drag_to(volume_slider, offset_x=20, offset_y=0)

# 切换到全屏模式
fullscreen_button = player_root.ele('.fullscreen-button')
fullscreen_button.click()

# 等待几秒钟观看视频
time.sleep(5)

# 退出全屏
page.press_key('Escape')

# 暂停视频
play_button.click()

# 获取视频当前时间
current_time = player_root.ele('.time-display').text
print(f"视频播放时间: {current_time}")

# 获取视频标题
video_title = player_root.ele('.video-title').text
print(f"视频标题: {video_title}")

实战案例:处理带有 Shadow DOM 的Web组件表单

以下是自动填写和提交包含在 Shadow DOM 中的表单的示例:

from DrissionPage import ChromiumPage
import time

# 创建页面对象
page = ChromiumPage()

# 访问包含 Web 组件表单的页面
page.get('https://example.com/webcomponent-form')

# 等待页面加载完成
page.wait.load_complete()

# 获取表单组件的 Shadow Root
form_host = page.ele('tag:custom-form')
form_root = form_host.shadow_root

# 填写表单字段
form_root.ele('input.name').input('张三')
form_root.ele('input.email').input('zhangsan@example.com')

# 选择下拉菜单选项
dropdown_host = form_root.ele('custom-dropdown')
dropdown_root = dropdown_host.shadow_root
dropdown_root.ele('.dropdown-button').click()
dropdown_root.ele('text=选项2').click()

# 勾选复选框
checkbox_host = form_root.ele('custom-checkbox')
checkbox_root = checkbox_host.shadow_root
checkbox_root.ele('.checkbox-input').click()

# 填写文本区域
textarea_host = form_root.ele('custom-textarea')
textarea_root = textarea_host.shadow_root
textarea_root.ele('textarea').input('这是一段测试文本,用于测试 Shadow DOM 中的文本区域。')

# 上传文件
file_upload_host = form_root.ele('custom-file-upload')
file_upload_root = file_upload_host.shadow_root
file_input = file_upload_root.ele('input[type=file]')
file_input.upload(r'C:\path\to\test_file.pdf')

# 提交表单
submit_button = form_root.ele('button[type=submit]')
submit_button.click()

# 等待提交完成
page.wait.load_complete()

# 检查提交后的确认消息
confirmation_host = page.ele('custom-confirmation')
confirmation_root = confirmation_host.shadow_root
success_message = confirmation_root.ele('.success-message').text
print(f"表单提交结果: {success_message}")

处理复杂的电子商务网站

许多现代电子商务网站使用 Shadow DOM 来实现产品展示、购物车和结账功能。以下是自动化浏览和购买流程的示例:

from DrissionPage import ChromiumPage
import time

# 创建页面对象
page = ChromiumPage()

# 访问电子商务网站
page.get('https://example.com/shop')

# 等待产品列表加载
page.wait.load_complete()

# 查找产品组件(可能使用 Shadow DOM)
product_items = page.eles('product-item')

# 选择第一个产品
first_product = product_items[0]
product_shadow = first_product.shadow_root

# 查看产品详情
product_shadow.ele('.product-title').click()

# 等待产品详情页加载
page.wait.load_complete()

# 产品详情页可能也使用 Shadow DOM
product_detail_host = page.ele('product-detail')
detail_shadow = product_detail_host.shadow_root

# 选择产品规格(如尺寸、颜色等)
size_selector_host = detail_shadow.ele('size-selector')
size_shadow = size_selector_host.shadow_root
size_shadow.ele('text=M').click()

color_selector_host = detail_shadow.ele('color-selector')
color_shadow = color_selector_host.shadow_root
color_shadow.ele('.color-option[data-color=blue]').click()

# 添加到购物车
detail_shadow.ele('.add-to-cart-button').click()

# 等待购物车更新
time.sleep(2)

# 打开购物车
cart_button = page.ele('shopping-cart-icon').shadow_root.ele('.cart-icon')
cart_button.click()

# 等待购物车面板打开
time.sleep(1)

# 购物车可能也使用 Shadow DOM
cart_host = page.ele('shopping-cart')
cart_shadow = cart_host.shadow_root

# 进入结账流程
cart_shadow.ele('.checkout-button').click()

# 等待结账页面加载
page.wait.load_complete()

# 结账页面可能包含多个 Shadow DOM 组件
# 填写送货地址
address_form_host = page.ele('address-form')
address_shadow = address_form_host.shadow_root

address_shadow.ele('input[name=fullName]').input('张三')
address_shadow.ele('input[name=address]').input('北京市朝阳区xxx街xxx号')
address_shadow.ele('input[name=phone]').input('13800138000')

# 继续到支付
address_shadow.ele('.continue-button').click()

# 等待支付页面加载
time.sleep(2)

# 选择支付方式
payment_host = page.ele('payment-methods')
payment_shadow = payment_host.shadow_root
payment_shadow.ele('text=支付宝').click()

# 提交订单
page.ele('order-summary').shadow_root.ele('.place-order-button').click()

# 等待订单确认
page.wait.load_complete()

# 获取订单号
order_number = page.ele('order-confirmation').shadow_root.ele('.order-number').text
print(f"订单已提交,订单号: {order_number}")

故障排除

无法访问 Shadow DOM

问题:尝试访问 Shadow Root 时返回 None

可能的原因和解决方案:

  1. 封闭式 Shadow DOM

    # 尝试使用 JavaScript 绕过限制
    page.run_js('''
    // 存储原始 attachShadow 方法
    const originalAttachShadow = Element.prototype.attachShadow;
    
    // 重写 attachShadow 方法
    Element.prototype.attachShadow = function() {
      return originalAttachShadow.call(this, { mode: 'open' });
    };
    
    // 重新加载页面
    location.reload();
    ''')
    
  2. Shadow DOM 尚未创建

    # 等待足够的时间让 JavaScript 创建 Shadow DOM
    page.wait.load_complete()
    time.sleep(1)  # 额外等待时间
    
  3. 宿主元素不正确

    # 使用开发者工具确认正确的宿主元素
    all_possible_hosts = page.eles('.possible-host')
    for host in all_possible_hosts:
        shadow = host.shadow_root
        if shadow:
            print(f"找到 Shadow Root: {host.attr('id') or host.attr('class')}")
    

找不到 Shadow DOM 中的元素

问题:虽然成功获取了 Shadow Root,但无法找到其中的元素

可能的原因和解决方案:

  1. 选择器错误

    # 打印 Shadow DOM 的 HTML 结构进行检查
    shadow_root = page.ele('#host').shadow_root
    print(shadow_root.html)
    
  2. 元素尚未加载

    # 等待 Shadow DOM 中的元素
    shadow_root = page.ele('#host').shadow_root
    page.wait.ele_loaded(shadow_root.ele, '.target-element')
    
  3. 嵌套的 Shadow DOM

    # 检查是否有嵌套的 Shadow DOM
    outer_shadow = page.ele('#outer-host').shadow_root
    inner_host = outer_shadow.ele('#inner-host')
    if inner_host:
        inner_shadow = inner_host.shadow_root
        target = inner_shadow.ele('.target')
    

总结

Shadow DOM 为现代 Web 组件提供了封装和样式隔离,但也为网页自动化带来了挑战。DrissionPage 提供了丰富的功能来处理各种 Shadow DOM 场景:

  1. 基础操作

    • 获取 Shadow Root
    • 在 Shadow DOM 中查找元素
    • 操作 Shadow DOM 中的元素
  2. 高级功能

    • 处理嵌套的 Shadow DOM
    • 使用特殊语法一步查找 Shadow DOM 中的元素
    • 处理开放式和封闭式 Shadow DOM
    • 处理动态创建的 Shadow DOM
  3. 实战应用

    • 操作带有 Shadow DOM 的复杂 UI 组件
    • 处理 Web 组件表单
    • 自动化电子商务网站的购物流程

通过掌握这些技术,您可以有效地自动化和爬取使用现代 Web 组件技术构建的复杂网页。无论是视频播放器、表单组件还是完整的电子商务网站,DrissionPage 都能帮助您轻松处理其中的 Shadow DOM 元素。

使用 Hugo 构建
主题 StackJimmy 设计