Selector:CSS 与 XPath
selector 是你交给 tab.query()(以及提取模型中 selector=)的字符串,用来指向某个元素。Pydoll 会说两种 selector 语言,CSS 和 XPath,并会替你挑选合适的引擎:如果字符串以 / 或 ./ 开头,它就作为 XPath 运行,否则作为 CSS selector 运行。这一页会教你足够的两种语言知识,让你能在页面上找到任何东西。
只有 query() 才需要 selector。find() 方法接受的是普通属性(参见 Element finding);当你需要表达一种 find() 无法表达的关系时,才动用 selector。
试试看:在下面输入一个 selector,匹配到的元素就会高亮。它运行的正是浏览器使用的那套 querySelectorAll / XPath,所以在这里匹配到什么,在你的自动化里就匹配到什么。
from pydoll.sync import Chrome
def main():
with Chrome() as browser:
tab = browser.start()
tab.go_to('https://en.wikipedia.org/wiki/Python_(programming_language)')
# CSS: 通过 id 定位文章标题
title = tab.query('#firstHeading')
print(title.text())
# XPath: 第一个 href 中提到 python.org 的链接
link = tab.query("//a[contains(@href, 'python.org')]")
print(link.get_attribute('href'))
main()
import asyncio
from pydoll import Chrome
async def main():
async with Chrome() as browser:
tab = await browser.start()
await tab.go_to('https://en.wikipedia.org/wiki/Python_(programming_language)')
# CSS: 通过 id 定位文章标题
title = await tab.query('#firstHeading')
print(await title.text())
# XPath: 第一个 href 中提到 python.org 的链接
link = await tab.query("//a[contains(@href, 'python.org')]")
print(link.get_attribute('href'))
asyncio.run(main())
上面两个查询都经过同一个 query() 调用。Pydoll 看到第二个查询开头的 //,就把它当作 XPath 处理。
什么时候用哪一种
大多数情况下 CSS 就够了,而且读起来更自然。当你需要 CSS 做不到的事情时,再动用 XPath。
- CSS 按 id、class、标签、属性和位置来选取,并能在页面中向下和横向移动。它是更简短、更常见的语言。
- XPath 能做到上述所有这些,此外还能按可见文本匹配、向上走到父节点或祖先节点,并表达诸如“包含这段文本的那一行”之类的条件。如果你需要按文本找到某个元素,或者从子节点往上导航回它所在的容器,那就是 XPath 的活儿。
一个粗略的规则:从 CSS 开始,一旦发现自己想说“文本是 X 的那个元素”或“Y 的父节点”,就切换到 XPath。
CSS 参考
下面的代码片段都假定 tab 已经启动。给它们中的任何一个传入 find_all=True,就能得到一个列表而不是第一个匹配项。
按 id、class 和标签选取
组合符
组合符描述元素之间的关系。
属性 selector
伪类
伪类按位置或状态来选取。
XPath 参考
路径
按属性和文本匹配
这正是你需要 XPath 的地方。CSS 无法按可见文本选取,XPath 可以。
匹配前先规范化文本
渲染出来的文本常常带有多余的空白。//button[normalize-space(text())='Submit'] 会把连续的空格折叠成一个并去掉两端的空白,所以即使 HTML 的缩进参差不齐,它也能匹配。
轴:向任意方向移动
轴指明从当前节点出发要往哪个方向走。这是 XPath 的优势:你可以往上走到父节点,或横向走到兄弟节点,而这是 CSS 做不到的。
| 轴 | 方向 | 找到 |
|---|---|---|
parent:: |
向上 | 直接父节点 |
ancestor:: |
向上 | 任意深度处的任何祖先节点 |
following-sibling:: |
横向 | 该节点之后的兄弟节点 |
preceding-sibling:: |
横向 | 该节点之前的兄弟节点 |
child:: |
向下 | 直接子节点 |
descendant:: |
向下 | 任何后代节点 |
你会经常见到的简写://div/p 就是 //div/child::p,@id 就是 attribute::id,而 .. 就是 parent::node()。
实例演练
下面这些用到了后面这个示例表单。它展示了你在真实页面中最常碰到的模式:通过旁边的文本找到某个元素,以及从一个控件往上走到它所在的行。
<form id="signup">
<div class="field">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<span class="error" style="display:none;">Invalid email</span>
</div>
<div class="field">
<input type="checkbox" id="newsletter" name="newsletter">
<label for="newsletter">Subscribe to the newsletter</label>
</div>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</form>
通过 label 找到 input
你知道 label 的文本,却不知道 input 的 id。先找到 label,再横向走一步到 input:
找到某个字段旁边的错误信息
is_visible() 报告的是该元素是否真的显示出来了,这在这里很重要,因为这个 span 一开始是隐藏的。
区分两个按钮
提交按钮就是那个 type='submit' 的,所以你永远不必依赖它的位置:
读取某个 checkbox 的 label
for 属性把一个 label 和它的控件绑在一起,所以你可以直接跳到它:
从一个控件往上走到它所在的行
在表格中,你常常拿着一个按钮,想要它所在的那一行。从该元素出发,用一个沿树向上攀爬的 XPath 来查询:
get_attribute() 从你已经定位到的元素上同步读取一个值,所以它不需要 await。
用变量构建 selector
当你要匹配的值来自你的程序时,用 f-string 来构建这个字符串。XPath 的字符串字面量里没有转义字符,所以用值里不包含的那种引号把它包起来,两种引号都有时改用 concat():
def xpath_literal(value):
if "'" not in value:
return f"'{value}'"
if '"' not in value:
return f'"{value}"'
parts = value.split("'")
return 'concat(' + ", \"'\", ".join(f"'{part}'" for part in parts) + ')'
def row_for(tab, product_name):
return tab.query(f'//tr[td[text()={xpath_literal(product_name)}]]')
laptop_row = row_for(tab, "Kid's Laptop")
def xpath_literal(value):
if "'" not in value:
return f"'{value}'"
if '"' not in value:
return f'"{value}"'
parts = value.split("'")
return 'concat(' + ", \"'\", ".join(f"'{part}'" for part in parts) + ')'
async def row_for(tab, product_name):
return await tab.query(f'//tr[td[text()={xpath_literal(product_name)}]]')
laptop_row = await row_for(tab, "Kid's Laptop")
让 selector 保持稳定
挑选那些改版不太可能动到的属性,并尽量依靠最简单能奏效的表达式。
对于简单的查找,CSS 比 XPath 略快一点,但这个差距是每次查询几毫秒,很少值得为它去优化。选那个读起来清晰、又能挺过页面变化的 selector。
下一步
- Element finding:把这些 selector 用到
query(),以及基于属性的find()。 - DOM traversal:从你已经拿到的某个元素出发遍历这棵树。
- Structured extraction:把这些 selector 放进模型的
Field(selector=...)里,以拉取带类型的数据。