Python · 6 分钟阅读
数据解析工具:XPath
[[toc]]
原文搬运自知乎 [@皮大大],由 Richie 重新整理与现代化。
XPath(XML Path)是在 XML / HTML 文档中按“路径”定位节点的语言。除了
手写正则,XPath 几乎是爬虫解析数据最高效的入门方式。
| 工具 | 速度 | 适用 |
|---|---|---|
re(正则) |
较快 | 文本、规则清晰时 |
lxml |
极快 | HTML / XML 结构化解析 |
BeautifulSoup |
慢 | 入门友好、容错强 |
parsel |
快 | Scrapy 内置 |
建议:能用 XPath 解决的问题,就别用正则。XPath 不容易因为换行/空格
出错,且维护性远好于一段复杂的re.search。
1. 安装
# macOS:
pip install -U lxml
# Ubuntu / Debian:
sudo apt install python3-lxml
# Windows (推荐 conda):
conda install lxml
验证:
python -c "from lxml import etree; print(etree.LXML_VERSION)"
2. 解析原理
XPath 使用分两步:
- 实例化
etree解析对象; - 调用
xpath(...)方法并传入表达式,得到节点 / 文本 / 属性的列表。
两种实例化方式:
from lxml import etree
# 1) 从 HTML 字符串
html = "<html><body><h1>hi</h1></body></html>"
tree = etree.HTML(html)
# 2) 从本地文件
tree = etree.parse("test.html")
etree.HTML会 自动补全缺失的标签(HTML 经常不闭合),容错性强。
3. 核心语法速查
3.1 三个最重要的符号
| 符号 | 含义 |
|---|---|
/ |
从根节点开始,单层匹配 |
// |
跨层匹配,或“从任意位置开始” |
. |
当前节点 |
3.2 常用谓语(筛选条件)
| 写法 | 含义 |
|---|---|
//a[1] |
第一个 a(索引从 1 开始) |
//a[last()] |
最后一个 a |
//a[@class] |
拥有 class 属性的 a |
//a[@class="name"] |
class 恰为 "name" 的 a |
//a[contains(@class,"du")] |
class 包含 "du" 的 a |
//a[text()="床前明月光"] |
文本内容恰好等于某串 |
//a[contains(text(),"床前")] |
文本包含某串 |
3.3 常用轴 / 函数
| 表达式 | 含义 | <br /> |
|---|---|---|
//li/a/text() |
文本 | <br /> |
//a/@href |
属性值 | <br /> |
//ul/li[position()<=3] |
前 3 个 li |
<br /> |
| `//ul/li | //ul/li/b` | 或(多分支) |
string(//title) |
把节点及子节点拼成一个字符串 | <br /> |
4. 示例 HTML
下面的 test.html 在后续所有例子里复用:
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>古代诗人及作品</title>
</head>
<body>
<div>
<p>诗人姓名</p>
</div>
<div class="name">
<p>李白</p>
<p>白居易</p>
<p>李清照</p>
<p>杜甫</p>
<p>王安石</p>
<a href="http://www.tang.com" title="李世民" target="_self">
<span> this is span </span>
古代诗人写的诗词真的非常棒
</a>
<a href="" class="du">床前明月光,疑是地上霜</a>
<img src="http://www.baidu.com/tang.jpg" alt="" />
</div>
<div class="tang">
<ul>
<li><a href="http://www.baidu.com" title="百度">朝辞白帝彩云间,千里江陵一日还</a></li>
<li><a href="http://www.sougou.com" title="搜狗">清明时节雨纷纷,路上行人欲断魂</a></li>
<li><a href="http://www.360.com" alt="360">秦时明月汉时关,万里长征人未还</a></li>
<li><a href="http://www.sina.com" title="必应">君子赠人以言,庶人赠人以财</a></li>
<li><b>苏轼</b></li>
<li><i>苏洵</i></li>
<li><a href="http://www.google.cn" id="谷歌">欢迎使用谷歌浏览器</a></li>
</ul>
</div>
</body>
</html>
5. 常见操作
5.1 获取单个 / 多个节点
from lxml import etree
tree = etree.parse("test.html")
# 节点(Element 对象列表)
titles = tree.xpath("/html/head/title")
print(titles) # [<Element title at 0x...>]
print(titles[0].text)
# 文本
title_text = tree.xpath("/html/head/title/text()")[0]
print(title_text) # 古代诗人及作品
⚠️ Xpath 返回 列表。
[0]取出第一个;想取唯一值时务必确认结果个数。
5.2 跨层 + 任意位置
# 所有 <p>
ps = tree.xpath("//p")
print(len(ps)) # 6
5.3 属性定位
# 找 class="name" 的 div
name_div = tree.xpath('//div[@class="name"]')
# 取 class="name" 下所有 <p> 文本
names = tree.xpath('//div[@class="name"]/p/text()')
print(names) # ['李白', '白居易', '李清照', '杜甫', '王安石']
# 索引定位(第 3 个 <p>)
print(tree.xpath('//div[@class="name"]/p[3]/text()')) # ['李清照']
5.4 取属性值
# 拿某个 <a> 的 href
href = tree.xpath('//div[@class="tang"]//li/a[1]/@href')[0]
# 多个
hrefs = tree.xpath('//div[@class="tang"]//li/a/@href')
print(hrefs)
5.5 拿一列 <li> 下的全部文本
# 第 1 个 <li> 下 a/b/i 全部合并
items = tree.xpath(
'//div[@class="tang"]//li/a/text() | '
'//div[@class="tang"]//li/b/text() | '
'//div[@class="tang"]//li/i/text()'
)
print(items)
注意
|是 XPath 的“或”运算符,优先级低于/,必要时用括号分组。
6. 实战:从小说网站抓取古龙作品列表
import requests
import pandas as pd
from lxml import etree
def fetch(url: str) -> str:
"""很多老站点默认 GBK 编码,先按 gbk 解码。"""
resp = requests.get(
url,
headers={"User-Agent": "Mozilla/5.0"},
timeout=10,
)
resp.raise_for_status()
return resp.content.decode("gbk", errors="replace")
def parse(html: str) -> pd.DataFrame:
tree = etree.HTML(html)
hrefs = tree.xpath("//tbody/tr//a/@href")
names = tree.xpath("//tbody/tr//a/text()")
base = "https://www.kanunu8.com/book"
return pd.DataFrame({
"name": names,
"url": [base + h for h in hrefs],
})
if __name__ == "__main__":
url = "https://www.kanunu8.com/zj/10867.html"
df = parse(fetch(url))
print(df.head())
df.to_excel("gulong.xlsx", index=False)
⚠️ 实际抓取时务必:
- 遵守目标网站的服务条款与
robots.txt;- 限速(一般 ≤ 1 QPS);
- 站点结构常变,XPath 选择器要可配置,别写死。
7. 在浏览器里抄 XPath
- 打开 DevTools(F12)→ Elements 面板。
- 选中目标节点,右键 → Copy → Copy XPath。
- 浏览器复制的 XPath 通常是
/html/body/div[3]/...,过于脆弱。改成
//div[@class="xxx"]/...这种“语义化”版本。
Chrome DevTools 的 $x("//p") 控制台命令可以直接跑 XPath,结果会高亮。
8. lxml 的 CSS 选择器(可选)
lxml 也支持 cssselect:
pip install cssselect
from lxml import etree
tree = etree.parse("test.html")
# 等价于 //div[@class="name"]/p
for p in tree.cssselect("div.name > p"):
print(p.text)
习惯 CSS 的人会觉得这个更顺手。
9. 常见问题
- 结果总是空? 先
print(etree.tostring(tree)[:200])看 HTML 是否成功加载。 - HTML 不规范?
etree.HTML会补全;但有些残缺页面连 lxml 也救不了,
上BeautifulSoup兜底。 - 想拿整段 HTML(含子节点)? 用
etree.tostring(node, pretty_print=True).decode()。 xpath(...)返回<Element>还是字符串? 看表达式里有没有text()/
@attr—— 没有就返回节点。- XPath 1.0 vs 2.0? lxml 只支持 XPath 1.0(部分 2.0 扩展)。够用。
10. 速查表
| 需求 | XPath 表达式 |
|---|---|
所有 a 节点 |
//a |
第一个 a |
//a[1] |
class 含 name 的 div |
//div[contains(@class, "name")] |
| 取文本 | //a/text() |
| 取属性 | //a/@href |
| 多条件 | //a[@class="x" and text()="y"] |
| 多分支 | //a |
| 在某个节点下找子节点 | ./p / .//p |
| 最后一个 | //li[last()] |