跳转到正文

Python 解析库详解

数据提取是爬虫的核心环节。获取到网页源码后,我们需要从中提取出有价值的信息。Python 提供了多种强大的解析工具:

  1. 正则表达式:最基础但也最万能的文本匹配工具。
  2. lxml (XPath):基于 XML 路径语言,解析速度快,适合大规模抓取。
  3. Beautiful Soup:API 友好,容错性强,适合初学者和格式不规范的网页。
  4. pyquery:jQuery 风格的语法,前端开发者上手极快。
  5. parsel:把 XPath、CSS 与正则统一到一套链式 API,工程化首选。
  6. 智能解析:不写选择器,靠算法或模型自动提取正文与列表。

本章将逐一介绍这些工具的使用方法。


一、正则表达式

正则表达式是处理字符串的强大工具,用于字符串的检索、替换、匹配验证。

1.1 常用匹配规则

模式描述
\w匹配字母、数字及下划线
\s匹配任意空白字符
\d匹配任意数字
.匹配任意字符(除换行符)
*匹配前面的字符 0 次或多次
+匹配前面的字符 1 次或多次
?匹配前面的字符 0 次或 1 次(非贪婪)
^匹配字符串开头
$匹配字符串结尾
[...]匹配字符组中的任意一个
(...)分组,用于提取内容

1.2 match 方法

从字符串开头开始匹配:

python
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())   # 匹配的位置范围

提取目标内容(使用分组):

python
result = re.match('^Hello\s(\d+)\sWorld', content)
print(result.group(1))  # 1234567

通用匹配 .*

python
result = re.match('^Hello.*Demo$', content)

贪婪与非贪婪

python
# 贪婪匹配 .* - 尽可能多匹配
result = re.match('^He.*(\d+).*Demo$', content)
print(result.group(1))  # 7(只匹配到最后一个数字)

# 非贪婪匹配 .*? - 尽可能少匹配
result = re.match('^He.*?(\d+).*Demo$', content)
print(result.group(1))  # 1234567

修饰符

python
# 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多行匹配

转义匹配

python
content = '(百度) www.baidu.com'
result = re.match('\(百度\) www\.baidu\.com', content)

1.3 search 方法

扫描整个字符串,返回第一个成功匹配的结果:

python
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 方法

返回所有匹配结果(列表形式):

python
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 方法

替换字符串中匹配的内容:

python
content = '54aK54yr5oiR54ix5L2g'
content = re.sub('\d+', '', content)
print(content)  # aKyroiRixLg

1.6 compile 方法

编译正则表达式对象,便于复用:

python
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 基本使用

python
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'))

也可以从文件读取:

python
html = etree.parse('./test.html', etree.HTMLParser())

2.3 选取节点

所有节点

python
result = html.xpath('//*')  # 匹配所有节点
result = html.xpath('//li')  # 匹配所有 li 节点

子节点

python
# 直接子节点用 /
result = html.xpath('//li/a')  # li 的直接子节点 a

# 子孙节点用 //
result = html.xpath('//ul//a')  # ul 下所有 a 节点(包括孙节点)

父节点

python
# 使用 .. 获取父节点
result = html.xpath('//a[@href="link4.html"]/../@class')

# 或使用 parent::
result = html.xpath('//a[@href="link4.html"]/parent::*/@class')

2.4 属性匹配与获取

属性过滤

python
result = html.xpath('//li[@class="item-0"]')  # class 为 item-0 的 li 节点

获取属性值

python
result = html.xpath('//li/a/@href')  # 获取所有 a 节点的 href 属性
# ['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

多值属性匹配 (contains)

python
# 当 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)

python
result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()')

2.5 获取文本

python
# 获取直接子节点文本
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 按序选择

python
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()')  # 倒数第三个 li

2.7 节点轴选择

python
# 祖先节点
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 HTMLBeautifulSoup(markup, "lxml")速度快、容错强(推荐)
lxml XMLBeautifulSoup(markup, "xml")唯一支持 XML
html5libBeautifulSoup(markup, "html5lib")最好的容错性

推荐使用 lxml 解析器

python
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')

3.2 基本使用

python
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 story

3.3 节点选择器

选择元素

python
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 节点

获取名称、属性、文本

python
# 节点名称
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

嵌套选择

python
print(soup.head.title.string)  # The Dormouse's story

3.4 关联选择

子节点

python
# 直接子节点(列表)
print(soup.p.contents)

# 直接子节点(迭代器)
for child in soup.p.children:
    print(child)

# 所有子孙节点
for descendant in soup.p.descendants:
    print(descendant)

父节点

python
# 直接父节点
print(soup.a.parent)

# 所有祖先节点
for parent in soup.a.parents:
    print(parent.name)

兄弟节点

python
print(soup.a.next_sibling)       # 下一个兄弟元素
print(soup.a.previous_sibling)   # 上一个兄弟元素

# 所有后续/前面的兄弟节点
for sibling in soup.a.next_siblings:
    print(sibling)

3.5 方法选择器

find_all

python
# 按节点名查询
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 返回第一个匹配的元素,而非列表:

python
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 选择器

python
# 使用 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 初始化

python
from pyquery import PyQuery as pq

# 字符串初始化
doc = pq(html)

# URL 初始化
doc = pq(url='http://example.com')

# 文件初始化
doc = pq(filename='demo.html')

4.2 基本 CSS 选择器

python
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 查找节点

子节点

python
items = doc('.list')

# 所有子孙节点
lis = items.find('li')

# 直接子节点
lis = items.children()

# 筛选子节点
lis = items.children('.active')

父节点

python
# 直接父节点
container = items.parent()

# 所有祖先节点
parents = items.parents()

# 筛选祖先节点
parent = items.parents('.wrap')

兄弟节点

python
li = doc('.list .item-0.active')

print(li.siblings())           # 所有兄弟节点
print(li.siblings('.active'))  # 筛选兄弟节点

4.4 遍历

python
lis = doc('li').items()  # 返回生成器

for li in lis:
    print(li, type(li))  # 每个元素都是 PyQuery 类型

4.5 获取信息

获取属性

python
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'))

获取文本

python
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 节点操作

python
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 伪类选择器

python
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 上

bash
pip install parsel

5.1 三种语法混用

python
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第一个节点的属性字典{}
python
# 选择器 + 正则一步到位,省去先取文本再 re.search 的两段式写法
sel.css(".score::text").re_first(r"[\d.]+")

# 提供默认值,避免下游反复判空
name = sel.css("h2::text").get(default="").strip()

💡 为什么推荐 parsel

  1. 不会抛异常.get() 无匹配返回 None 而非报错,比 lxml 的索引访问健壮得多。
  2. 链式组合:复杂结构可以逐层收窄,代码可读性远高于一条长 XPath。
  3. 与 Scrapy 完全一致:先用 parsel 写解析逻辑,后期迁移到 Scrapy 时解析代码一行不用改

5.3 相对路径的陷阱

python
for item in sel.css(".item"):
    # ❌ 错误:以 / 或 // 开头会从文档根节点重新查找,每次拿到的都是全文档第一个
    item.xpath("//a/text()").get()

    # ✅ 正确:用 ./ 或 .// 限定在当前节点范围内
    item.xpath(".//a/text()").get()

这是 XPath 嵌套遍历中最高频的 bug,lxml 下同样存在。


六、智能解析:不写选择器的提取

上述所有方法都依赖为每个站点手写选择器。当目标站从 10 个涨到 1000 个时,规则的编写和维护成本会超过爬虫本身——页面改版一次,规则就废一批。

智能解析试图回答:能否不写规则,自动识别页面里的标题、正文、发布时间和列表项?

6.1 详情页正文提取的算法思路

传统方案基于文本密度符号密度的统计特征,不依赖任何站点先验知识:

步骤做法
预处理删除 scriptstylenavfooteraside 等噪声标签及其内容
标题提取取所有 h1~h3 节点文本,与 <title> 做相似度比对,取最相似者
时间提取先查 meta 标签(article:published_time 等),再用日期正则扫描全文
正文定位逐节点计算文本密度(文本字符数 / 标签数)与符号密度(标点数 / 文本长度),综合打分取最高者

核心洞察:正文节点的特征是「文字多、标签少、标点密度符合自然语言」;导航和广告区域则是「文字少、链接多、标点稀疏」。这两个指标的组合足以区分绝大多数页面。

6.2 列表页提取的算法思路

列表页的特征是存在一组结构高度相似的兄弟节点

  1. 候选分组:遍历所有父节点,找出「子节点数量 ≥ N 且子节点结构相似度高」的节点作为候选组;
  2. 相似度计算:比较兄弟节点的标签路径、类名、子节点数是否一致;
  3. 聚类合并:把结构相同的候选组合并;
  4. 打分选优:按成员数量、平均文本长度、链接密度综合打分,取分数最高的组;
  5. 字段提取:组内每个成员按位置提取标题(最长文本)和链接(第一个 a[href])。

6.3 现成方案

方案原理特点
trafilatura统计 + 规则混合当前综合效果最好,支持正文/元数据/评论分离
readability-lxml移植自 Firefox 阅读模式成熟稳定,只做正文
newspaper3k规则 + NLP面向新闻,附带关键词与摘要,维护较慢
gne文本密度 + 符号密度中文新闻页效果好
python
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 抽取往往比写规则更划算:

python
# 关键工程手法:先压缩 HTML 再送模型,否则 token 成本不可控
cleaned = trafilatura.extract(html)          # 先用传统方法去噪
# 再让模型按固定 JSON Schema 输出结构化字段
维度手写选择器统计智能解析LLM 抽取
准确率最高(针对性)中(正文类好,字段类差)高(复杂结构也能处理)
站点适配成本高,每站都要写
单页成本极低极低较高(token 费用)
页面改版鲁棒性
适用规模站点少、量大站点多、只要正文站点多、量小、字段复杂

💡 实践中的组合策略

三层降级:核心站点用手写选择器(准确率优先)→ 长尾站点用 trafilatura 兜底(零成本)→ 结构特殊且量小的用 LLM 抽取(成本可控)。

LLM 不适合大批量抽取,成本会随页数线性增长;但用它自动生成选择器规则(喂一个样本页,让模型产出 XPath),再用规则批量跑,是当前性价比最高的用法。


七、总结与选择建议

特点适用场景
lxml (XPath)速度最快、功能强大大规模爬虫、性能敏感
Beautiful Soup容错性好、API 友好初学者、HTML 不规范
pyqueryjQuery 风格、CSS 选择器熟悉 jQuery 的开发者
parselXPath/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 —— 依赖语义化属性

优先使用 iddata-*、语义化 class;避免依赖节点位置索引和完整层级链。


📚 模块与官方文档

解析库

模块安装官方文档
re标准库docs.python.org · 正则 HOWTO
lxmlpip install lxmllxml.de · lxml.html · XPath 用法
beautifulsoup4pip install beautifulsoup4官方文档(中文) · 英文原版
pyquerypip install pyquerypyquery.readthedocs.io
parselpip install parselparsel.readthedocs.io · Selector API
cssselectpip install cssselectcssselect.readthedocs.io
jsonpath-ng(JSON 提取)pip install jsonpath-ngGitHub
jmespath(JSON 查询)pip install jmespathjmespath.org

智能解析

模块安装官方文档
trafilaturapip install trafilaturatrafilatura.readthedocs.io · Python API
readability-lxmlpip install readability-lxmlGitHub
newspaper3kpip install newspaper3knewspaper.readthedocs.io
gnepip install gneGitHub

规范参考

资源说明
W3C XPath 1.0XPath 语法权威规范
MDN - CSS 选择器CSS 选择器完整列表
W3C Selectors Level 4现代选择器规范
regex101正则在线调试(记得切到 Python 模式)

← 返回 Python 深度研究