Python 解析库详解
数据提取是爬虫的核心环节。获取到网页源码后,我们需要从中提取出有价值的信息。Python 提供了多种强大的解析工具:
- 正则表达式:最基础但也最万能的文本匹配工具。
- lxml (XPath):基于 XML 路径语言,解析速度快,适合大规模抓取。
- Beautiful Soup:API 友好,容错性强,适合初学者和格式不规范的网页。
- pyquery:jQuery 风格的语法,前端开发者上手极快。
- parsel:把 XPath、CSS 与正则统一到一套链式 API,工程化首选。
- 智能解析:不写选择器,靠算法或模型自动提取正文与列表。
本章将逐一介绍这些工具的使用方法。
一、正则表达式
正则表达式是处理字符串的强大工具,用于字符串的检索、替换、匹配验证。
1.1 常用匹配规则
| 模式 | 描述 |
|---|---|
\w | 匹配字母、数字及下划线 |
\s | 匹配任意空白字符 |
\d | 匹配任意数字 |
. | 匹配任意字符(除换行符) |
* | 匹配前面的字符 0 次或多次 |
+ | 匹配前面的字符 1 次或多次 |
? | 匹配前面的字符 0 次或 1 次(非贪婪) |
^ | 匹配字符串开头 |
$ | 匹配字符串结尾 |
[...] | 匹配字符组中的任意一个 |
(...) | 分组,用于提取内容 |
1.2 match 方法
从字符串开头开始匹配:
import re
content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello\s\d{3}\s\d{4}\s\w{10}', content)
print(result.group()) # 匹配的内容
print(result.span()) # 匹配的位置范围提取目标内容(使用分组):
result = re.match('^Hello\s(\d+)\sWorld', content)
print(result.group(1)) # 1234567通用匹配 .*:
result = re.match('^Hello.*Demo$', content)贪婪与非贪婪:
# 贪婪匹配 .* - 尽可能多匹配
result = re.match('^He.*(\d+).*Demo$', content)
print(result.group(1)) # 7(只匹配到最后一个数字)
# 非贪婪匹配 .*? - 尽可能少匹配
result = re.match('^He.*?(\d+).*Demo$', content)
print(result.group(1)) # 1234567修饰符:
# re.S 使 . 匹配包括换行符在内的所有字符
content = '''Hello 1234567 World_This
is a Regex Demo
'''
result = re.match('^He.*?(\d+).*?Demo$', content, re.S)| 修饰符 | 描述 |
|---|---|
re.I | 忽略大小写 |
re.S | 使 . 匹配包括换行符 |
re.M | 多行匹配 |
转义匹配:
content = '(百度) www.baidu.com'
result = re.match('\(百度\) www\.baidu\.com', content)1.3 search 方法
扫描整个字符串,返回第一个成功匹配的结果:
content = 'Extra stings Hello 1234567 World_This is a Regex Demo'
result = re.search('Hello.*?(\d+).*?Demo', content)
print(result.group(1)) # 1234567建议:为了匹配方便,尽量使用
search而非match。
1.4 findall 方法
返回所有匹配结果(列表形式):
html = '''
<li data-view="2">一路上有你</li>
<li data-view="7"><a href="/2.mp3" singer="任贤齐">沧海一声笑</a></li>
<li data-view="4"><a href="/3.mp3" singer="齐秦">往事随风</a></li>
'''
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)
for result in results:
print(result[0], result[1], result[2])
# /2.mp3 任贤齐 沧海一声笑
# /3.mp3 齐秦 往事随风1.5 sub 方法
替换字符串中匹配的内容:
content = '54aK54yr5oiR54ix5L2g'
content = re.sub('\d+', '', content)
print(content) # aKyroiRixLg1.6 compile 方法
编译正则表达式对象,便于复用:
pattern = re.compile('\d{2}:\d{2}')
result1 = re.sub(pattern, '', '2016-12-15 12:00')
result2 = re.sub(pattern, '', '2016-12-17 12:55')二、使用 XPath (lxml)
XPath,全称 XML Path Language,即 XML 路径语言。它最初用于搜寻 XML 文档,但同样适用于 HTML 文档。
官方文档:https://www.w3.org/TR/xpath/
2.1 XPath 常用规则
| 表达式 | 描述 |
|---|---|
nodename | 选取此节点的所有子节点 |
/ | 从当前节点选取直接子节点 |
// | 从当前节点选取子孙节点 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
示例://title[@lang='eng'] 表示选择所有名称为 title,且属性 lang 的值为 eng 的节点。
2.2 基本使用
from lxml import etree
text = '''
<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>
</ul>
</div>
'''
html = etree.HTML(text) # 自动修正 HTML
result = etree.tostring(html)
print(result.decode('utf-8'))也可以从文件读取:
html = etree.parse('./test.html', etree.HTMLParser())2.3 选取节点
所有节点
result = html.xpath('//*') # 匹配所有节点
result = html.xpath('//li') # 匹配所有 li 节点子节点
# 直接子节点用 /
result = html.xpath('//li/a') # li 的直接子节点 a
# 子孙节点用 //
result = html.xpath('//ul//a') # ul 下所有 a 节点(包括孙节点)父节点
# 使用 .. 获取父节点
result = html.xpath('//a[@href="link4.html"]/../@class')
# 或使用 parent::
result = html.xpath('//a[@href="link4.html"]/parent::*/@class')2.4 属性匹配与获取
属性过滤
result = html.xpath('//li[@class="item-0"]') # class 为 item-0 的 li 节点获取属性值
result = html.xpath('//li/a/@href') # 获取所有 a 节点的 href 属性
# ['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']多值属性匹配 (contains)
# 当 class 有多个值时,使用 contains
text = '<li class="li li-first"><a href="link.html">first item</a></li>'
html = etree.HTML(text)
result = html.xpath('//li[contains(@class, "li")]/a/text()')多属性匹配 (and)
result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()')2.5 获取文本
# 获取直接子节点文本
result = html.xpath('//li[@class="item-0"]/a/text()')
# ['first item', 'fifth item']
# 获取所有子孙节点文本
result = html.xpath('//li[@class="item-0"]//text()')
# ['first item', 'fifth item', '\n ']2.6 按序选择
result = html.xpath('//li[1]/a/text()') # 第一个 li(注意从 1 开始)
result = html.xpath('//li[last()]/a/text()') # 最后一个 li
result = html.xpath('//li[position()<3]/a/text()') # 前两个 li
result = html.xpath('//li[last()-2]/a/text()') # 倒数第三个 li2.7 节点轴选择
# 祖先节点
result = html.xpath('//li[1]/ancestor::*')
result = html.xpath('//li[1]/ancestor::div')
# 属性
result = html.xpath('//li[1]/attribute::*')
# 子节点
result = html.xpath('//li[1]/child::a[@href="link1.html"]')
# 子孙节点
result = html.xpath('//li[1]/descendant::span')
# 后续节点
result = html.xpath('//li[1]/following::*[2]')
# 后续同级节点
result = html.xpath('//li[1]/following-sibling::*')三、使用 Beautiful Soup
Beautiful Soup 是 Python 的 HTML/XML 解析库,借助网页的结构和属性来解析网页。
官方文档:https://www.crummy.com/software/BeautifulSoup/bs4/doc/
3.1 解析器选择
| 解析器 | 使用方法 | 优势 |
|---|---|---|
| Python 标准库 | BeautifulSoup(markup, "html.parser") | 内置、无需安装 |
| lxml HTML | BeautifulSoup(markup, "lxml") | 速度快、容错强(推荐) |
| lxml XML | BeautifulSoup(markup, "xml") | 唯一支持 XML |
| html5lib | BeautifulSoup(markup, "html5lib") | 最好的容错性 |
推荐使用 lxml 解析器:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')3.2 基本使用
html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.prettify()) # 格式化输出
print(soup.title.string) # The Dormouse's story3.3 节点选择器
选择元素
print(soup.title) # <title>The Dormouse's story</title>
print(type(soup.title)) # <class 'bs4.element.Tag'>
print(soup.title.string) # The Dormouse's story
print(soup.head) # <head>...</head>
print(soup.p) # 第一个 p 节点获取名称、属性、文本
# 节点名称
print(soup.title.name) # title
# 所有属性
print(soup.p.attrs) # {'class': ['title'], 'name': 'dromouse'}
# 单个属性
print(soup.p['class']) # ['title']
print(soup.p.attrs['name']) # dromouse
# 文本内容
print(soup.p.string) # The Dormouse's story嵌套选择
print(soup.head.title.string) # The Dormouse's story3.4 关联选择
子节点
# 直接子节点(列表)
print(soup.p.contents)
# 直接子节点(迭代器)
for child in soup.p.children:
print(child)
# 所有子孙节点
for descendant in soup.p.descendants:
print(descendant)父节点
# 直接父节点
print(soup.a.parent)
# 所有祖先节点
for parent in soup.a.parents:
print(parent.name)兄弟节点
print(soup.a.next_sibling) # 下一个兄弟元素
print(soup.a.previous_sibling) # 上一个兄弟元素
# 所有后续/前面的兄弟节点
for sibling in soup.a.next_siblings:
print(sibling)3.5 方法选择器
find_all
# 按节点名查询
soup.find_all(name='ul')
soup.find_all(name='li')
# 按属性查询
soup.find_all(attrs={'id': 'list-1'})
soup.find_all(id='list-1')
soup.find_all(class_='element') # class 是关键字,加下划线
# 按文本查询(支持正则)
import re
soup.find_all(text=re.compile('link'))find
find 返回第一个匹配的元素,而非列表:
soup.find(name='ul')
soup.find(class_='list')其他方法
| 方法 | 描述 |
|---|---|
find_parents() / find_parent() | 所有/直接父节点 |
find_next_siblings() / find_next_sibling() | 后面所有/第一个兄弟节点 |
find_previous_siblings() / find_previous_sibling() | 前面所有/第一个兄弟节点 |
3.6 CSS 选择器
# 使用 select 方法
soup.select('.panel .panel-heading') # class 选择器
soup.select('ul li') # 层级选择器
soup.select('#list-2 .element') # id + class 选择器
# 嵌套选择
for ul in soup.select('ul'):
print(ul.select('li'))
# 获取属性
for ul in soup.select('ul'):
print(ul['id'])
# 获取文本
for li in soup.select('li'):
print(li.get_text()) # 或 li.string四、使用 pyquery
pyquery 是仿照 jQuery 的 API 设计的解析库,如果你熟悉 jQuery,这个库会非常顺手。
官方文档:http://pyquery.readthedocs.io
4.1 初始化
from pyquery import PyQuery as pq
# 字符串初始化
doc = pq(html)
# URL 初始化
doc = pq(url='http://example.com')
# 文件初始化
doc = pq(filename='demo.html')4.2 基本 CSS 选择器
html = '''
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
</div>
'''
from pyquery import PyQuery as pq
doc = pq(html)
print(doc('#container .list li')) # 选择所有 li 节点4.3 查找节点
子节点
items = doc('.list')
# 所有子孙节点
lis = items.find('li')
# 直接子节点
lis = items.children()
# 筛选子节点
lis = items.children('.active')父节点
# 直接父节点
container = items.parent()
# 所有祖先节点
parents = items.parents()
# 筛选祖先节点
parent = items.parents('.wrap')兄弟节点
li = doc('.list .item-0.active')
print(li.siblings()) # 所有兄弟节点
print(li.siblings('.active')) # 筛选兄弟节点4.4 遍历
lis = doc('li').items() # 返回生成器
for li in lis:
print(li, type(li)) # 每个元素都是 PyQuery 类型4.5 获取信息
获取属性
a = doc('.item-0.active a')
print(a.attr('href')) # link3.html
print(a.attr.href) # link3.html
# 多个节点时,需遍历
for item in doc('a').items():
print(item.attr('href'))获取文本
a = doc('.item-0.active a')
print(a.text()) # third item(纯文本)
li = doc('.item-0.active')
print(li.html()) # <a href="link3.html"><span class="bold">third item</span></a>注意:多个节点时,text() 返回所有文本(空格分隔),html() 只返回第一个。
4.6 节点操作
li = doc('.item-0.active')
# 添加/移除 class
li.removeClass('active')
li.addClass('active')
# 修改属性
li.attr('name', 'link')
# 修改内容
li.text('changed item')
li.html('<span>changed item</span>')
# 移除节点
wrap = doc('.wrap')
wrap.find('p').remove()
print(wrap.text())4.7 伪类选择器
doc('li:first-child') # 第一个 li
doc('li:last-child') # 最后一个 li
doc('li:nth-child(2)') # 第二个 li
doc('li:gt(2)') # 第三个之后的 li
doc('li:nth-child(2n)') # 偶数位置的 li
doc('li:contains(second)') # 包含 second 文本的 li五、使用 parsel
parsel 是从 Scrapy 中抽离出来的独立解析库,也是 Scrapy 内部 response.css() / response.xpath() 的实现。它的价值在于把 XPath、CSS 选择器和正则统一到一套链式 API 上。
pip install parsel5.1 三种语法混用
from parsel import Selector
sel = Selector(text=html)
# XPath 与 CSS 可以在同一条链上交替使用
sel.css(".item").xpath("./a/@href").getall()
sel.xpath('//div[@class="item"]').css("a::text").get()
# CSS 扩展语法:::text 取文本,::attr(name) 取属性(标准 CSS 没有这两个)
sel.css("a.name::text").get()
sel.css("a.name::attr(href)").getall()5.2 提取方法的语义差异
| 方法 | 返回 | 无匹配时 |
|---|---|---|
.get() | 第一个结果的字符串 | None(可传 default="") |
.getall() | 所有结果的列表 | [] |
.re(pattern) | 所有正则匹配的列表 | [] |
.re_first(pattern) | 第一个正则匹配 | None |
.attrib | 第一个节点的属性字典 | {} |
# 选择器 + 正则一步到位,省去先取文本再 re.search 的两段式写法
sel.css(".score::text").re_first(r"[\d.]+")
# 提供默认值,避免下游反复判空
name = sel.css("h2::text").get(default="").strip()💡 为什么推荐 parsel
- 不会抛异常:
.get()无匹配返回None而非报错,比lxml的索引访问健壮得多。 - 链式组合:复杂结构可以逐层收窄,代码可读性远高于一条长 XPath。
- 与 Scrapy 完全一致:先用 parsel 写解析逻辑,后期迁移到 Scrapy 时解析代码一行不用改。
5.3 相对路径的陷阱
for item in sel.css(".item"):
# ❌ 错误:以 / 或 // 开头会从文档根节点重新查找,每次拿到的都是全文档第一个
item.xpath("//a/text()").get()
# ✅ 正确:用 ./ 或 .// 限定在当前节点范围内
item.xpath(".//a/text()").get()这是 XPath 嵌套遍历中最高频的 bug,lxml 下同样存在。
六、智能解析:不写选择器的提取
上述所有方法都依赖为每个站点手写选择器。当目标站从 10 个涨到 1000 个时,规则的编写和维护成本会超过爬虫本身——页面改版一次,规则就废一批。
智能解析试图回答:能否不写规则,自动识别页面里的标题、正文、发布时间和列表项?
6.1 详情页正文提取的算法思路
传统方案基于文本密度与符号密度的统计特征,不依赖任何站点先验知识:
| 步骤 | 做法 |
|---|---|
| 预处理 | 删除 script、style、nav、footer、aside 等噪声标签及其内容 |
| 标题提取 | 取所有 h1~h3 节点文本,与 <title> 做相似度比对,取最相似者 |
| 时间提取 | 先查 meta 标签(article:published_time 等),再用日期正则扫描全文 |
| 正文定位 | 逐节点计算文本密度(文本字符数 / 标签数)与符号密度(标点数 / 文本长度),综合打分取最高者 |
核心洞察:正文节点的特征是「文字多、标签少、标点密度符合自然语言」;导航和广告区域则是「文字少、链接多、标点稀疏」。这两个指标的组合足以区分绝大多数页面。
6.2 列表页提取的算法思路
列表页的特征是存在一组结构高度相似的兄弟节点:
- 候选分组:遍历所有父节点,找出「子节点数量 ≥ N 且子节点结构相似度高」的节点作为候选组;
- 相似度计算:比较兄弟节点的标签路径、类名、子节点数是否一致;
- 聚类合并:把结构相同的候选组合并;
- 打分选优:按成员数量、平均文本长度、链接密度综合打分,取分数最高的组;
- 字段提取:组内每个成员按位置提取标题(最长文本)和链接(第一个
a[href])。
6.3 现成方案
| 方案 | 原理 | 特点 |
|---|---|---|
trafilatura | 统计 + 规则混合 | 当前综合效果最好,支持正文/元数据/评论分离 |
readability-lxml | 移植自 Firefox 阅读模式 | 成熟稳定,只做正文 |
newspaper3k | 规则 + NLP | 面向新闻,附带关键词与摘要,维护较慢 |
gne | 文本密度 + 符号密度 | 中文新闻页效果好 |
import trafilatura
downloaded = trafilatura.fetch_url(url)
# 一行拿到正文 + 元数据,无需任何选择器
result = trafilatura.extract(downloaded, output_format="json",
include_comments=False, with_metadata=True)6.4 LLM 抽取:2026 年的新选项
书成书时这条路线还不存在。现在对结构复杂、样式多变、量不大的页面,直接让大模型按 schema 抽取往往比写规则更划算:
# 关键工程手法:先压缩 HTML 再送模型,否则 token 成本不可控
cleaned = trafilatura.extract(html) # 先用传统方法去噪
# 再让模型按固定 JSON Schema 输出结构化字段| 维度 | 手写选择器 | 统计智能解析 | LLM 抽取 |
|---|---|---|---|
| 准确率 | 最高(针对性) | 中(正文类好,字段类差) | 高(复杂结构也能处理) |
| 站点适配成本 | 高,每站都要写 | 零 | 零 |
| 单页成本 | 极低 | 极低 | 较高(token 费用) |
| 页面改版鲁棒性 | 差 | 好 | 好 |
| 适用规模 | 站点少、量大 | 站点多、只要正文 | 站点多、量小、字段复杂 |
💡 实践中的组合策略
三层降级:核心站点用手写选择器(准确率优先)→ 长尾站点用 trafilatura 兜底(零成本)→ 结构特殊且量小的用 LLM 抽取(成本可控)。
LLM 不适合大批量抽取,成本会随页数线性增长;但用它自动生成选择器规则(喂一个样本页,让模型产出 XPath),再用规则批量跑,是当前性价比最高的用法。
七、总结与选择建议
| 库 | 特点 | 适用场景 |
|---|---|---|
| lxml (XPath) | 速度最快、功能强大 | 大规模爬虫、性能敏感 |
| Beautiful Soup | 容错性好、API 友好 | 初学者、HTML 不规范 |
| pyquery | jQuery 风格、CSS 选择器 | 熟悉 jQuery 的开发者 |
| parsel | XPath/CSS/正则统一 API,不抛异常 | 工程首选,与 Scrapy 无缝衔接 |
| 正则 | 不依赖 DOM 结构 | 从 JS 变量、非结构化文本中提取 |
| 智能解析 | 无需规则 | 站点极多、只需正文 |
推荐组合:
- 高性能场景:
lxml+ XPath - 通用场景:
Beautiful Soup+lxml解析器 - jQuery 用户:
pyquery - 工程化项目:
parsel(解析逻辑可直接迁移到 Scrapy) - 千站级采集:
trafilatura兜底 + 核心站点手写规则
💡 选择器的健壮性原则
无论用哪个库,定位路径越短越稳。
- ❌
body > div:nth-child(3) > div > div.content > p—— 页面加一个 div 就失效 - ✅
[data-testid="content"] p或//div[@class="content"]//p—— 依赖语义化属性
优先使用 id、data-*、语义化 class;避免依赖节点位置索引和完整层级链。
📚 模块与官方文档
解析库
| 模块 | 安装 | 官方文档 |
|---|---|---|
re | 标准库 | docs.python.org · 正则 HOWTO |
lxml | pip install lxml | lxml.de · lxml.html · XPath 用法 |
beautifulsoup4 | pip install beautifulsoup4 | 官方文档(中文) · 英文原版 |
pyquery | pip install pyquery | pyquery.readthedocs.io |
parsel | pip install parsel | parsel.readthedocs.io · Selector API |
cssselect | pip install cssselect | cssselect.readthedocs.io |
jsonpath-ng(JSON 提取) | pip install jsonpath-ng | GitHub |
jmespath(JSON 查询) | pip install jmespath | jmespath.org |
智能解析
| 模块 | 安装 | 官方文档 |
|---|---|---|
trafilatura | pip install trafilatura | trafilatura.readthedocs.io · Python API |
readability-lxml | pip install readability-lxml | GitHub |
newspaper3k | pip install newspaper3k | newspaper.readthedocs.io |
gne | pip install gne | GitHub |
规范参考
| 资源 | 说明 |
|---|---|
| W3C XPath 1.0 | XPath 语法权威规范 |
| MDN - CSS 选择器 | CSS 选择器完整列表 |
| W3C Selectors Level 4 | 现代选择器规范 |
| regex101 | 正则在线调试(记得切到 Python 模式) |