Python爬虫进阶:用XPath与lxml构建健壮的数据提取管道
如果你曾经尝试过用Python写爬虫,大概率会遇到一个绕不开的问题:如何从那些结构复杂、嵌套混乱的HTML页面里,精准地提取出你想要的数据?正则表达式虽然强大,但面对现代网页的动态生成和层层嵌套,常常显得力不从心,写出来的表达式既难维护,又容易因为页面结构的微小变动而失效。
这时候,XPath就像一把精准的手术刀。它不是什么新潮的技术,但却是每个处理结构化文档(无论是HTML还是XML)的开发者工具箱里,最值得信赖的工具之一。尤其是在Python生态中,配合lxml库,XPath能让你从“勉强能用”的爬虫,进化到“稳定可靠”的数据采集系统。这篇文章不会重复那些基础的语法手册,而是聚焦于如何在实际的、复杂的爬虫项目中,构建一套基于XPath和lxml的、具备容错性和可维护性的数据提取方案。我们会从核心概念讲起,然后深入到实战中的高级技巧、性能优化和异常处理,最后用一个完整的、模拟真实复杂场景的案例来串联所有知识点。
1. 超越基础:理解XPath在DOM树中的导航逻辑
很多教程一上来就罗列//div[@class="content"]这样的表达式,这当然没错,但如果你不理解XPath背后的“上下文”概念,就很难写出灵活且健壮的表达式。XPath的本质是在一棵由节点构成的树(DOM树)中进行导航和查询。
1.1 上下文节点:你的查询起点
每次执行XPath查询,都有一个隐含的“当前节点”,即上下文节点。在lxml中,当你对一个etree元素调用.xpath()方法时,这个元素就是初始的上下文节点。
from lxml import etree
html = """
<html>
<body>
<div id="main">
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</div>
<div id="sidebar">
<p>Side note</p>
</div>
</body>
</html>
"""
tree = etree.HTML(html)
main_div = tree.xpath('//div[@id="main"]')[0] # 获取id为main的div元素
# 此时,main_div是上下文节点
# 查询其下的所有p标签
paragraphs_in_main = main_div.xpath('.//p')
print([p.text for p in paragraphs_in_main]) # 输出:['Paragraph 1', 'Paragraph 2']
# 对比:如果从根节点(tree)开始,使用相同的相对路径'.//p',结果会不同
all_paragraphs_from_root = tree.xpath('.//p')
print([p.text for p in all_paragraphs_from_root]) # 输出:['Paragraph 1', 'Paragraph 2', 'Side note']
注意:表达式开头的
.代表从当前上下文节点开始。main_div.xpath('.//p')和main_div.xpath('//p')有天壤之别。前者只在main_div的子孙中查找,后者会从文档根节点重新开始全局查找,通常会返回非预期的更多结果。
1.2 轴(Axes):定义搜索方向
轴定义了相对于上下文节点的搜索方向。这是XPath强大但常被忽略的部分。
| 轴名称 | 缩写 | 描述 | 示例(上下文节点为某个<li>) |
|---|---|---|---|
child |
(默认) | 上下文节点的直接子节点 | li/span 选择<li>下的直接<span>子元素 |
descendant |
// |
上下文节点的所有后代节点 | li//span 选择<li>下任意层级的<span>元素 |
parent |
.. |
上下文节点的父节点 | ../@class 选择父节点的class属性 |
ancestor |
无 | 上下文节点的所有祖先节点 | ancestor::div 选择所有祖先<div>节点 |
following-sibling |
无 | 上下文节点之后的所有同级节点 | following-sibling::li 选择后面所有的<li>兄弟节点 |
preceding-sibling |
无 | 上下文节点之前的所有同级节点 | preceding-sibling::a 选择前面所有的<a>兄弟节点 |
attribute |
@ |
上下文节点的属性 | @href 选择href属性值 |
在实战中,following-sibling轴尤其有用。比如,在一个没有规律类名的表格中,你想获取某个特定表头后的所有数据行:
# 假设表格结构不规则,但知道“价格”列在“产品名”列之后
tree.xpath('//th[text()="产品名"]/following-sibling::td')
1.3 谓词(Predicates):进行精细过滤
谓词是放在方括号[]内的表达式,用于对节点集进行过滤。它可以是位置索引、属性判断、函数调用等。
- 位置索引:
//div[@class="item"][1]选择第一个具有class="item"的div。注意:XPath索引从1开始。 - 属性存在性检查:
//a[@href]选择所有拥有

378

被折叠的 条评论
为什么被折叠?



