如何在 Python 和 lxml 中使用 XPath 语法

XML 及其衍生格式已成为各种文档的事实标准,广泛应用于 Spring 框架配置、Struts 配置、Visual Studio 项目文件、Web 服务响应以及 Android UI 布局等场景。它们构成了现代编程基础设施中不可或缺的组件。由于格式无处不在,程序员在处理数据时几乎不可避免地会遇到 XML。如果说 AST(抽象语法树)代表代码,那么 XML 则代表数据;它是描述数据和信息的理想通用语言。因此,掌握读取、解析、导航和操作 XML 文档的能力是程序员的必备技能。

lxml 是一个基于 C 库 libxml2libxslt 的 Python 绑定库,易于使用且功能强大。对于简单的查询(如查找特定标签),可以使用 findfindtext 方法;但对于复杂的查询,则需要更强大的工具——XPath。XPath 是一种迷你语言,允许您以声明方式指定如何选择 XML 文档中的元素。在某些方面,它类似于用于选择 DOM 元素的 CSS 选择器,允许您像浏览 CSS 一样浏览 XML 文档中的元素和属性。路径标识了一组节点,包括元素、属性、文本等。

XPath 在 XSLT 和 XSL 中被大量使用,其核心作用是在输入文档中定位元素并进行转换,从而在输出文档中生成结果,这与 CSS 使用选择器在 HTML 文档中定位元素并应用样式的机制类似。

本文将通过示例 HTML 文件和 lxml API 来说明 XPath 的常用语法。

环境准备

创建示例 HTML 文件

首先,创建一个名为 xpath-test.html 的文件,内容如下:

<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a></li>
     </ul>
 </div>

解析文件

使用 lxml.html 模块读取并解析该文件:

import lxml.html

html = lxml.html.parse("xpath-test.html")

子节点运算符 /

/ 运算符用于分隔父级和子级节点。当路径以 / 开头时,表示这是绝对路径,即从根节点开始查找。

使用绝对路径查找节点

例如,查找 div 节点:

nodes = html.xpath('/html/body/div')

注意lxml 的 HTML 解析器会自动修复文档结构(例如添加缺失的 htmlbody 标签)。因此,根节点通常为 html。您可以打印解析后的 HTML 源文件来验证这一点:

from lxml import etree

print(etree.tostring(html))

输出结果如下(格式已美化):

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a></li>
     </li></ul>
 </div>
</body></html>

选择所有链接文本

基于修复后的文档结构,可以使用绝对路径选择所有链接的文本内容:

nodes = html.xpath('/html/body/div/ul/li/a/text()')
print(nodes)

输出:

['first item', 'second item', 'third item', 'fourth item', 'fifth item']

后代运算符 //

// 运算符用于选择父节点的所有子孙节点。如果路径以 // 开头,意味着搜索从根节点开始,匹配文档中所有符合条件的后代节点,而不仅仅是直接子节点。

修改 HTML 结构

假设我们将 HTML 文件修改为包含嵌套结构:

<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a></li>
    </ul>
    <div>
      <ul>
        <li><a href="link6.html">sixth item</a></li>
      </ul>  
    </div>  
 </div>

获取所有链接

使用后代运算符可以一次性获取所有层级的链接文本,无需关心具体的嵌套深度:

lis = html.xpath('//li/a/text()')
print(lis)

输出:

['first item', 'second item', 'third item', 'fourth item', 'fifth item', 'sixth item']

如果使用绝对路径,可能需要编写多个查询,而后代运算符只需一个表达式即可完成。

限定范围查找

若只想获取第二个 div 内部 ul 列表中的链接,可以结合绝对路径与后代运算符:

lis = html.xpath('/html/body/div/div//li/a/text()')
print(lis)

输出:

['sixth item']

属性 @ 运算符

可以使用 @ 运算符根据元素属性(例如 class 属性)来过滤元素。

按 Class 属性筛选

选择所有 class 属性为 item-0 的元素中的链接文本:

attributes = html.xpath('//li[@class="item-0"]/a/text()')
print(attributes)

输出:

['first item', 'fifth item']

逻辑运算符

XPath 支持逻辑查询,返回布尔值。如果匹配的元素满足查询条件,则返回 True,否则返回 False

测试条件

例如,测试第二个 div 列表中的链接是否包含文本内容为 "first item" 的链接:

logics = html.xpath("//div/div//a/text()='first item'")
print(logics)

输出:

False
说明:本文示例基于 Python 3 环境及 lxml 库。lxml 版本更新通常保持向后兼容,但具体行为可能因解析器配置略有差异。