XPath匹配
XPath匹配
XPath 是用于在 XML/HTML 文档中导航和查询节点的语言。
一、XPath 基础语法
1. 节点选择
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 示例 XML
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book category="cooking">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="children">
<title lang="en">Harry Potter</title>
<author>J.K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="web">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<year>2003</year>
<price>49.99</price>
</book>
</bookstore>
1
2
3
4
5
6
7
8
9
10
11
from lxml import etree
import requests
# 加载示例 XML
url = "http://example.com/books.xml"
# response = requests.get(url)
# tree = etree.fromstring(response.content)
# 或者使用本地文件
tree = etree.parse("books.xml")
root = tree.getroot()
2. 基本路径表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 选择所有节点
* # 所有元素节点
@* # 所有属性节点
node() # 所有类型节点
# 绝对路径(从根开始)
/bookstore # 根元素 bookstore
/bookstore/book # bookstore 的直接子 book 元素
# 相对路径(从当前节点)
./book # 当前节点下的 book 元素
../author # 父节点下的 author 元素
# 任意位置(递归查找)
//book # 文档中所有 book 元素
//book/title # 所有 book 下的 title 元素
二、谓词(条件筛选)
1. 位置索引
1
2
3
4
5
6
//book[1] # 第一个 book 元素
//book[last()] # 最后一个 book 元素
//book[last()-1] # 倒数第二个
//book[position()<3] # 前两个 book 元素
# 注意:XPath 索引从 1 开始,不是 0!
2. 属性筛选
1
2
3
4
5
//book[@category] # 有 category 属性的 book
//book[@category='web'] # category="web" 的 book
//book[@category!='web'] # category 不等于 "web"
//book[@lang='en'] # lang="en" 的 book
//book[not(@category)] # 没有 category 属性的 book
3. 内容筛选
1
2
3
4
5
6
7
//title[text()='Harry Potter'] # 文本等于
//title[contains(text(), 'Harry')] # 文本包含
//title[starts-with(text(), 'Harry')] # 文本以...开头
//title[ends-with(text(), 'Potter')] # 文本以...结尾(XPath 2.0)
//book[author='J.K. Rowling'] # 子元素内容等于
//book[price>35] # 数值比较
//book[year>2004] # 数值比较
4. 组合条件
1
2
3
4
//book[@category='web' and price>40] # 与运算
//book[@category='web' or @category='cooking'] # 或运算
//book[price>20 and year>2004] # 多个条件
//book[author='J.K. Rowling'][price>25] # 链式筛选
三、获取节点内容
1. 获取文本
1
2
3
4
//title/text() # 所有 title 的文本内容
//book/author/text() # 所有 author 的文本
//book[1]/title/text() # 第一个 book 的 title 文本
string(//book[1]) # 第一个 book 的所有文本(拼接)
2. 获取属性
1
2
3
4
//book/@category # 所有 book 的 category 属性
//title/@lang # 所有 title 的 lang 属性
//book[1]/@category # 第一个 book 的 category
//book[@category='web']/@category # 特定 book 的 category
3. 获取完整节点
1
2
//book # 返回 book 元素节点本身
//book[1] # 返回第一个 book 元素
四、轴(Axes) - 按关系选择
1. 常用轴
1
2
3
4
5
6
7
8
9
10
//book/child::title # book 的子节点 title(等价于 //book/title)
//title/parent::book # title 的父节点 book
//book/ancestor::* # book 的所有祖先节点
//book/descendant::* # book 的所有后代节点
//book/following::* # book 之后的所有节点
//book/preceding::* # book 之前的所有节点
//book/following-sibling::* # book 之后的同级节点
//book/preceding-sibling::* # book 之前的同级节点
//book/self::* # book 自身
//book/attribute::* # book 的所有属性
2. 轴的实际应用
1
2
3
4
5
6
7
8
9
10
11
# 选择某个 author 的所有兄弟节点
//author[text()='J.K. Rowling']/following-sibling::*
# 选择某个节点的所有后代文本
//bookstore/descendant::text()
# 选择某个节点之前的所有 price 节点
//book[@category='web']/preceding::price
# 选择某个节点的所有属性
//book[1]/attribute::*
五、函数(Functions)
1. 字符串函数
1
2
3
4
5
6
7
concat(//author[1], ' - ', //title[1]) # 字符串连接
substring(//title[1], 1, 5) # 子字符串
string-length(//title[1]) # 字符串长度
translate(//title[1], 'aeiou', 'AEIOU') # 字符替换
normalize-space(//title[1]) # 去除首尾空格并压缩内部空格
upper-case(//title[1]) # 转为大写(XPath 2.0)
lower-case(//title[1]) # 转为小写(XPath 2.0)
2. 数值函数
1
2
3
4
5
6
7
8
sum(//book/price) # 所有 price 的总和
avg(//book/price) # 平均值(XPath 2.0)
min(//book/year) # 最小值(XPath 2.0)
max(//book/year) # 最大值(XPath 2.0)
count(//book) # book 的数量
floor(//book[1]/price) # 向下取整
ceiling(//book[1]/price) # 向上取整
round(//book[1]/price) # 四舍五入
3. 节点集函数
1
2
3
4
5
6
count(//book) # 节点数量
position() # 当前节点位置
last() # 最后一个位置
name(//book[1]) # 节点名称
local-name(//book[1]) # 本地名称(无命名空间)
namespace-uri(//book[1]) # 命名空间 URI
六、运算符
1
2
3
4
5
6
7
8
//book[price > 20 and price < 40] # 数值比较
//book[price = 29.99] # 等于
//book[price != 29.99] # 不等于
//book[price >= 30] # 大于等于
//book[price <= 30] # 小于等于
//book[1] | //book[2] # 并集(两个节点集的合并)
//book/title | //book/author # 合并 title 和 author
七、命名空间处理
1
2
3
4
5
6
<!-- 带命名空间的 XML -->
<root xmlns:bk="http://example.com/books">
<bk:book>
<bk:title>XML Guide</bk:title>
</bk:book>
</root>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from lxml import etree
xml = '''<root xmlns:bk="http://example.com/books">
<bk:book>
<bk:title>XML Guide</bk:title>
</bk:book>
</root>'''
root = etree.fromstring(xml)
# 方法1:使用命名空间前缀
ns = {'bk': 'http://example.com/books'}
titles = root.xpath('//bk:book/bk:title/text()', namespaces=ns)
print(titles) # ['XML Guide']
# 方法2:使用本地名称
titles = root.xpath('//*[local-name()="book"]/*[local-name()="title"]/text()')
print(titles) # ['XML Guide']
# 方法3:使用通配符加条件
titles = root.xpath('//*[name()="bk:book"]/*[name()="bk:title"]/text()')
print(titles) # ['XML Guide']
八、Python lxml 中的 XPath 使用
1. 基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from lxml import etree
# 解析 XML
tree = etree.parse('books.xml')
root = tree.getroot()
# 执行 XPath 查询
# 返回元素列表
books = root.xpath('//book')
for book in books:
print(book.tag, book.attrib)
# 返回文本列表
titles = root.xpath('//title/text()')
print(titles) # ['Everyday Italian', 'Harry Potter', 'XQuery Kick Start']
# 返回属性列表
categories = root.xpath('//book/@category')
print(categories) # ['cooking', 'children', 'web']
# 返回布尔值
has_web = root.xpath('boolean(//book[@category="web"])')
print(has_web) # True
# 返回数字
book_count = root.xpath('count(//book)')
print(book_count) # 3.0
# 返回字符串
first_title = root.xpath('string(//book[1]/title)')
print(first_title) # 'Everyday Italian'
2. 编译 XPath 表达式(提高性能)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from lxml import etree
tree = etree.parse('books.xml')
# 编译常用表达式
find_books = etree.XPath('//book')
find_titles = etree.XPath('//title/text()')
find_expensive_books = etree.XPath('//book[price>35]')
# 使用编译后的表达式
books = find_books(tree)
titles = find_titles(tree)
expensive_books = find_expensive_books(tree)
print(f"Found {len(books)} books")
print(f"Titles: {titles}")
3. 使用变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from lxml import etree
tree = etree.parse('books.xml')
# 方法1:使用 XPath 类
find_books_by_category = etree.XPath('//book[@category=$cat]')
web_books = find_books_by_category(tree, cat='web')
print(f"Web books: {len(web_books)}")
# 方法2:使用 format(注意安全性)
category = 'web'
# 注意:这种方法有注入风险,如果 category 来自用户输入,请使用方法1
books = tree.xpath(f'//book[@category="{category}"]')
# 方法3:字符串拼接(不推荐,有注入风险)
category = 'web'
books = tree.xpath('//book[@category="' + category + '"]')
九、高级技巧和示例
1. 复杂查询示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查找价格在 25-50 之间的书
//book[price>=25 and price<=50]
# 查找 2005 年出版的书,按价格降序
//book[year=2005] sortby(price descending) # XPath 2.0
# 查找作者名包含 "James" 的书
//book[contains(author, 'James')]
# 查找第二本书的第三个兄弟节点
//book[2]/following-sibling::*[3]
# 查找所有有文本内容的节点
//*[text() and normalize-space(text())!='']
# 查找深度为3的节点
//*[count(ancestor::*) = 2]
# 查找属性值包含特定字符串
//book[contains(@category, 'child')]
2. 实际网页抓取示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from lxml import html
import requests
# 获取网页
url = 'https://books.toscrape.com/'
response = requests.get(url)
tree = html.fromstring(response.content)
# 提取所有书籍信息
books = []
# 方法1:分别提取每个字段
titles = tree.xpath('//h3/a/text()')
prices = tree.xpath('//div[@class="product_price"]/p[@class="price_color"]/text()')
availabilities = tree.xpath('//div[@class="product_price"]/p[@class="instock availability"]/text()[normalize-space()]')
for i in range(len(titles)):
books.append({
'title': titles[i].strip(),
'price': prices[i].strip(),
'availability': availabilities[i].strip() if i < len(availabilities) else 'N/A'
})
# 方法2:一次提取每本书的所有信息
book_elements = tree.xpath('//article[@class="product_pod"]')
for book_el in book_elements:
title = book_el.xpath('.//h3/a/text()')[0].strip()
price = book_el.xpath('.//p[@class="price_color"]/text()')[0].strip()
availability = book_el.xpath('.//p[@class="instock availability"]/text()[normalize-space()]')[0].strip()
books.append({
'title': title,
'price': price,
'availability': availability
})
print(f"Found {len(books)} books")
3. 处理动态属性
1
2
3
4
5
6
7
8
9
10
11
12
# 假设 class 属性是动态生成的
# <div class="product-12345">...</div>
# <div class="product-67890">...</div>
# 使用 starts-with
elements = tree.xpath('//div[starts-with(@class, "product-")]')
# 使用 contains
elements = tree.xpath('//div[contains(@class, "product-")]')
# 使用 translate 忽略大小写
elements = tree.xpath('//div[contains(translate(@class, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "product-")]')
十、调试和测试 XPath
1. 在浏览器中测试
- Chrome/Edge:F12 → Console →
$x('//your/xpath') - Firefox:F12 → Console →
$x('//your/xpath')
2. 在线测试工具
3. Python 调试技巧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from lxml import etree
def debug_xpath(tree, xpath):
"""调试 XPath 表达式"""
try:
result = tree.xpath(xpath)
print(f"XPath: {xpath}")
print(f"结果类型: {type(result)}")
print(f"结果数量: {len(result)}")
if len(result) > 0:
print("前5个结果:")
for i, item in enumerate(result[:5]):
if isinstance(item, etree._Element):
print(f" {i+1}. 元素: {item.tag}, 属性: {item.attrib}")
if item.text and item.text.strip():
print(f" 文本: {item.text.strip()[:50]}...")
else:
print(f" {i+1}. {type(item).__name__}: {str(item)[:50]}...")
else:
print("未找到匹配项")
print("-" * 50)
except Exception as e:
print(f"错误: {e}")
# 使用示例
tree = etree.parse('books.xml')
debug_xpath(tree, '//book[price>30]')
debug_xpath(tree, '//title[contains(text(), "Harry")]')
十一、性能优化
1. 避免低效表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ❌ 低效:递归搜索两次
//bookstore//book//title
# ✅ 高效:一次定位
//book/title
# ❌ 低效:使用多个 // 开头
//div//p//span
# ✅ 高效:具体路径
//div[@id="content"]//span
# ❌ 低效:过度使用 *
//*[@class="price"]
# ✅ 高效:指定标签名
//span[@class="price"]
2. 使用索引优化
1
2
3
4
5
6
# 如果只需要第一个结果
(//book)[1] # 先找到所有,再取第一个(低效)
//book[1] # 直接找第一个(高效)
# 限制结果数量
//book[position() <= 10] # 只取前10个
十二、常见问题解决
1. 处理特殊字符
1
2
3
4
5
6
7
8
9
10
11
12
13
# XPath 中的引号转义
# 单引号包含双引号
elements = tree.xpath('//div[@class="foo"]')
# 双引号包含单引号
elements = tree.xpath("//div[@class='foo']")
# 包含两种引号
elements = tree.xpath('//div[contains(text(), \'"quote"\')]')
elements = tree.xpath("//div[contains(text(), \"'quote'\")]")
# 使用 concat 函数
elements = tree.xpath('//div[contains(text(), concat(\'"\', "quote", \'"\'))]')
2. 处理命名空间(简化版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 移除命名空间以便简化 XPath
def remove_namespaces(tree):
"""移除所有命名空间"""
for elem in tree.iter():
if elem.tag.startswith('{'):
elem.tag = elem.tag.split('}', 1)[1]
for attr in list(elem.attrib):
if attr.startswith('{'):
del elem.attrib[attr]
return tree
# 使用
tree = etree.parse('with_namespace.xml')
clean_tree = remove_namespaces(tree)
# 现在可以使用简单的 XPath
titles = clean_tree.xpath('//book/title/text()')
3. 处理默认命名空间
1
2
3
4
5
6
<!-- XML 有默认命名空间 -->
<root xmlns="http://example.com">
<book>
<title>XML Guide</title>
</book>
</root>
1
2
3
# 需要为默认命名空间分配前缀
ns = {'def': 'http://example.com'}
titles = tree.xpath('//def:book/def:title/text()', namespaces=ns)
十三、练习和测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 测试你的 XPath 技能
test_xml = '''
<catalog>
<product id="1">
<name>Laptop</name>
<price>999.99</price>
<category>Electronics</category>
<stock>15</stock>
</product>
<product id="2">
<name>Book</name>
<price>19.99</price>
<category>Education</category>
<stock>100</stock>
</product>
<product id="3">
<name>Phone</name>
<price>699.99</price>
<category>Electronics</category>
<stock>25</stock>
</product>
</catalog>
'''
tree = etree.fromstring(test_xml)
# 练习:
# 1. 选择所有电子产品
# 2. 选择价格超过 500 的产品
# 3. 选择库存小于 20 的产品
# 4. 选择产品名称包含 "oo" 的产品
# 5. 获取所有产品 ID
# 6. 计算所有产品的总价值(价格 × 库存)
掌握 XPath 需要练习,建议:
- 使用浏览器的开发者工具练习
- 尝试不同的网站,编写 XPath 提取数据
- 理解轴和谓词的组合使用
- 注意性能,编写高效的 XPath 表达式
本文由作者按照
CC BY 4.0
进行授权
