DOM 遍历
拿到一个元素后,你常常还需要它周围的元素:它的子元素、兄弟元素、shadow 根内部的元素,或者 iframe 里的内容。本指南讲的是从一个已知起点在 DOM 树中移动。至于如何先定位到那个起始元素,参见查找元素。
获取子元素
get_children_elements() 返回一个元素的后代。max_depth 控制深入的层数(1 表示只取直接子元素),tag_filter 只保留你指定的标签。
import asyncio
from pydoll.browser.chromium import Chrome
async def main():
async with Chrome() as browser:
tab = await browser.start()
await tab.go_to('https://books.toscrape.com')
container = await tab.find(class_name='row', tag_name='ol')
direct = await container.get_children_elements(max_depth=1)
print(f'{len(direct)} direct children')
# 向下最多 2 层的后代,仅保留链接
links = await container.get_children_elements(max_depth=2, tag_filter=['a'])
print(f'{len(links)} links within two levels')
asyncio.run(main())
获取兄弟元素
get_siblings_elements() 返回与你的元素处于同一层级的元素,不包括它自身。tag_filter 把结果收窄到特定标签。
active = await tab.find(class_name='active')
siblings = await active.get_siblings_elements()
print(f'{len(siblings)} siblings')
link_siblings = await active.get_siblings_elements(tag_filter=['a'])
限定范围的搜索 vs 直接子元素
限定范围的 find() 或 query() 会搜索一个元素的所有后代。当你只想要直接子元素时,使用 CSS 子代组合器 > 或一个 XPath 步进,query() 都能接受:
container = await tab.find(id='cards')
# 子树中任意位置的每一个 .card
all_cards = await container.find(class_name='card', find_all=True)
# 只有那些作为直接子元素的 .card
direct_cards = await container.query('> .card', find_all=True)
当你想探查结构或按标签过滤时,用 get_children_elements();当你想在子树中任意位置匹配特定属性的元素时,用限定范围的 find()/query()。
读取文本和属性
从任何元素上你都能读取它的可见文本和 HTML 属性:
book = await tab.find(class_name='product_pod')
title = await book.find(tag_name='h3')
print(await title.text) # 可见文本
link = await title.find(tag_name='a')
print(link.get_attribute('href')) # 某个属性值
print(link.get_attribute('title'))
text 是一个可 await 的属性;get_attribute(name) 返回属性字符串,属性不存在时返回 None。
shadow DOM
许多组件把内部结构藏在 shadow 根里,常规 DOM 查询看不到它们。先取得 shadow 宿主,获取它的 shadow 根,然后在里面搜索。
host = await tab.find(id='my-component')
shadow = await host.get_shadow_root()
button = await shadow.query('.internal-btn')
await button.click()
在 shadow 根内部,用 query() 配合 CSS
ShadowRoot 上不支持 find() 和 XPath,会抛出 NotImplementedError。搜索 shadow 根时只能用 query() 配合 CSS 选择器。
在 shadow 根内部的 query() 同样接受 find_all、timeout 和 raise_exc 参数:
items = await shadow.query('.item', find_all=True)
dynamic = await shadow.query('#late', timeout=5, raise_exc=False)
Web 组件是可以嵌套的,因此一个 shadow 根里可以包含另一个 shadow 宿主:
outer = await tab.find(tag_name='outer-component')
outer_shadow = await outer.get_shadow_root()
inner = await outer_shadow.query('inner-component')
inner_shadow = await inner.get_shadow_root()
deep = await inner_shadow.query('.deep-btn')
发现页面上的 shadow 根
当你不知道存在哪些 shadow 根时(比如调试,或者像 Cloudflare Turnstile 这样的动态组件),find_shadow_roots() 会返回全部。shadow 宿主经常延迟加载,因此传入 timeout 以轮询直到它们出现:
shadow_roots = await tab.find_shadow_roots(timeout=10)
for sr in shadow_roots:
print(f'mode={sr.mode}, host={sr.host_element}')
checkbox = await sr.query('input[type="checkbox"]', raise_exc=False)
if checkbox:
await checkbox.click()
默认情况下,搜索覆盖主文档(包括同源 iframe)。传入 deep=True 还可触及跨源 iframe(OOPIF)内部的 shadow 根,Turnstile 这类组件用的正是这种:
在 iframe 内部工作
iframe 有它自己的 DOM 上下文。先找到 iframe 元素,然后在它上面调用 find() 或 query();Pydoll 会自动把搜索路由进该框架。对嵌套的 iframe 继续串联即可。
iframe = await tab.query('iframe.embedded-content', timeout=10)
button = await iframe.find(tag_name='button', class_name='submit')
await button.click()
# 嵌套的 iframe
inner = await iframe.find(tag_name='iframe')
link = await inner.find(text='Download PDF')
await link.click()
关于完整的 iframe 指南,包括 CAPTCHA 框架和故障排查,参见 Iframes。
iframe 内部的截图
tab.take_screenshot() 只捕获顶层页面。要捕获 iframe 内容,找到框架内部的一个元素,然后调用 element.take_screenshot()。