在网络爬虫与数据采集场景中,网页数据解析是核心步骤之一。当我们通过请求工具(如requests
、aiohttp
)获取到网页的 HTML/XML 源码后,需要从中精准提取目标数据(如文本、链接、属性等)。
———————————————————————————————————————————目录
3、Beautiful Soup:Python 友好的解析库
目前 Python 生态中,常用的解析工具包括XPath、Beautiful Soup、pyquery和parsel。它们各有特点:有的基于路径表达式,有的模仿前端语法,有的专注于高效解析。本文将系统讲解这四种工具的使用方法、优缺点及适用场景。
1、前置知识:网页结构基础
网页数据解析的前提是理解 HTML/XML 的树形结构:
- 网页由标签(元素) 嵌套组成(如
<div>
、<a>
、<p>
); - 标签可包含属性(如
<a href="https://example.com">
中的href
); - 标签之间可包含文本内容(如
<p>hello</p>
中的hello
)。
所有解析工具的核心目标都是:通过某种规则定位到目标标签,再提取其文本或属性。
2、XPath:基于路径的高效解析
XPath(XML Path Language)是一种用于在 XML/HTML 文档中定位节点的语言,语法简洁且功能强大,是解析网页的 “利器”。
2.1 XPath 的核心概念
- 节点:HTML/XML 中的元素(如标签、文本、属性);
- 路径表达式:通过 “路径” 描述节点位置(类似文件系统的目录路径);
- 轴(Axes):定义节点与当前节点的关系(如父节点、子节点、祖先节点)。
2.2 安装与基本使用
XPath 本身是一种语法,在 Python 中需结合解析库(如lxml
)使用:
pip install lxml # lxml是支持XPath的高效解析库
基本用法示例:
from lxml import etree
# 示例HTML源码
html = """
<html>
<body>
<div class="container">
<h1>标题</h1>
<ul>
<li><a href="link1.html">链接1</a></li>
<li><a href="link2.html">链接2</a></li>
</ul>
</div>
</body>
</html>
"""
# 解析HTML为可操作的对象
tree = etree.HTML(html)
2.3 常用 XPath 表达式
表达式 | 含义 | 示例(基于上述 HTML) | 结果 |
---|---|---|---|
/ |
从根节点开始(绝对路径) | /html/body/div/h1 |
定位到<h1>标题</h1> |
// |
从任意位置匹配(相对路径) | //li/a |
定位到所有<a> 标签 |
. |
当前节点 | //div[@class="container"]/. |
定位到class="container" 的div |
.. |
父节点 | //a/.. |
定位到<a> 的父节点<li> |
@属性名 |
提取属性值 | //a/@href |
['link1.html', 'link2.html'] |
text() |
提取文本内容 | //h1/text() |
['标题'] |
[索引] |
按索引筛选(索引从 1 开始) | //li[1]/a/text() |
['链接1'] |
[条件] |
按条件筛选 | //a[text()="链接2"]/@href |
['link2.html'] |
contains(@属性, 值) |
模糊匹配属性 | //div[contains(@class, "cont")] |
定位到class="container" 的div |
2.4 高级用法:轴与函数
轴示例:
//a/parent::*
:获取<a>
的父节点(所有类型);//div/child::li
:获取div
的直接子节点<li>
;//h1/following-sibling::*
:获取<h1>
之后的同级节点。
常用函数:
count(//li)
:统计<li>
标签数量;starts-with(@href, "link")
:匹配href
以link
开头的标签;last()
:获取最后一个节点(如//li[last()]/a/text()
)。
2.5 XPath 的优缺点
- 优点:语法简洁,定位精准,支持复杂条件,解析速度快(基于
lxml
); - 缺点:需要学习专门语法,对不规范 HTML 的容错性较弱(需依赖
lxml
的自动修复)。
2.6 演示代码
"""
文件名: 1
作者: 墨尘
日期: 2025/8/8
项目名: pythonProject
备注:
"""
from lxml import etree
# 模拟豆瓣电影 Top250 页面的简化 HTML
html = """
<html>
<body>
<div class="item">
<div class="info">
<em class="top250">1</em>
<a href="https://movie.douban.com/subject/1292052/" class="title">肖申克的救赎</a>
<div class="bd">
<p class="quote">
<span class="inq">希望让人自由。</span>
</p>
</div>
</div>
</div>
<div class="item">
<div class="info">
<em class="top250">2</em>
<a href="https://movie.douban.com/subject/1291561/" class="title">霸王别姬</a>
<div class="bd">
<p class="quote">
<span class="inq">风华绝代。</span>
</p>
</div>
</div>
</div>
</body>
</html>
"""
# 解析 HTML
tree = etree.HTML(html)
# 1. 绝对路径(/ 从根节点开始)
# 含义:从 html 根节点逐层找 body -> div -> info -> em
result = tree.xpath('/html/body/div[@class="item"]/div[@class="info"]/em/text()')
print("1. 绝对路径(/):", result) # 输出:['1', '2']
# 2. 相对路径(// 任意位置匹配)
# 含义:不管层级,直接找所有 class=title 的 a 标签文本
result = tree.xpath('//a[@class="title"]/text()')
print("2. 相对路径(//):", result) # 输出:['肖申克的救赎', '霸王别姬']
# 3. 当前节点(.)
# 含义:先定位到 info 节点,再从当前节点找 em(类似“就近搜索”)
info_nodes = tree.xpath('//div[@class="info"]')
for node in info_nodes:
em_text = node.xpath('./em/text()')[0] # . 表示当前 info 节点内
print(f"3. 当前节点(.)- 第 {em_text} 名电影") # 输出:第1名电影 / 第2名电影
# 4. 父节点(..)
# 含义:先找 span 标签,再通过 .. 找父节点 p 的文本
result = tree.xpath('//span[@class="inq"]/../text()') # .. 是 span 的父节点 p
print("4. 父节点(..):", [text.strip() for text in result]) # 输出:['希望让人自由。', '风华绝代。']
# 5. 提取属性值(@属性名)
# 含义:找所有 a 标签的 href 属性
result = tree.xpath('//a[@class="title"]/@href')
print("5. 提取属性值(@属性名):", result) # 输出电影详情页链接
# 6. 提取文本内容(text())
# 含义:找所有 em 标签的文本(排名)
result = tree.xpath('//em[@class="top250"]/text()')
print("6. 提取文本内容(text()):", result) # 输出:['1', '2']
# 7. 按索引筛选([索引],从 1 开始)
# 含义:找第 1 个 li(这里用 div 模拟,取第 1 部电影标题)
result = tree.xpath('//div[@class="item"][1]/a/text()')
print("7. 按索引筛选([1]):", result) # 输出:['肖申克的救赎']
# 8. 按条件筛选([条件])
# 含义:找标题是“霸王别姬”的 a 标签的 href
result = tree.xpath('//a[text()="霸王别姬"]/@href')
print("8. 按条件筛选([条件]):", result) # 输出对应电影链接
# 9. 模糊匹配(contains(@属性, 值))
# 含义:找 class 包含 "top" 的 em 标签(实际是 top250,contains 模糊匹配)
result = tree.xpath('//em[contains(@class, "top")]/text()')
print("9. 模糊匹配(contains):", result) # 输出:['1', '2']
2.7 演示效果
3、Beautiful Soup:Python 友好的解析库
Beautiful Soup 是 Python 的一个 HTML/XML 解析库,以 “人性化” 的 API 著称,适合新手使用。它不依赖特定语法,而是通过 Python 风格的方法定位节点。
3.1 安装与解析器选择
pip install beautifulsoup4 # 核心库
# 需搭配解析器(推荐lxml,兼顾速度与容错性)
pip install lxml
Beautiful Soup 支持多种解析器,差异如下:
解析器 | 优点 | 缺点 |
---|---|---|
html.parser |
Python 内置,无需额外安装 | 速度较慢,容错性一般 |
lxml |
速度快,容错性强(自动修复不规范 HTML) | 需要额外安装 |
html5lib |
容错性极强(模拟浏览器解析) | 速度最慢 |
推荐优先使用lxml
:
from bs4 import BeautifulSoup
# 解析HTML(指定lxml解析器)
soup = BeautifulSoup(html, 'lxml') # html为前文示例的HTML源码
3.2 核心用法:定位与提取
Beautiful Soup 通过标签名、属性、文本等条件定位节点,常用方法如下:
3.2.1 基础定位:find()
与find_all()
find(name, attrs, text, ...)
:返回第一个匹配的节点;find_all(name, attrs, text, limit, ...)
:返回所有匹配的节点(列表)。
示例:
# 按标签名定位
h1 = soup.find('h1') # 第一个<h1>标签
lis = soup.find_all('li') # 所有<li>标签
# 按属性定位
container = soup.find('div', attrs={'class': 'container'}) # class为container的div
# 简化写法(class是关键字,需加下划线)
container = soup.find('div', class_='container')
# 按文本定位
link2 = soup.find('a', text='链接2') # 文本为“链接2”的<a>标签
# 提取数据
print(h1.text) # 提取文本:标题
print(link2['href']) # 提取属性:link2.html
3.2.2 CSS 选择器:select()
Beautiful Soup 支持 CSS 选择器语法(与前端一致),通过select()
方法使用:
# 选择所有<a>标签
a_tags = soup.select('a')
# 选择class为container的div下的ul
ul = soup.select('div.container > ul') # > 表示直接子节点
# 选择第2个li标签(索引从0开始)
li2 = soup.select('li')[1]
# 提取属性与文本
print(li2.select_one('a')['href']) # select_one()返回第一个匹配节点
print(li2.select_one('a').text)
3.3 高级用法:遍历与嵌套
Beautiful Soup 的节点对象可直接遍历子节点:
# 遍历container的所有子节点
container = soup.find('div', class_='container')
for child in container.children: # 直接子节点
print(child)
# 嵌套定位(先找父节点,再找子节点)
ul = container.find('ul')
links = ul.find_all('a') # 仅在ul下查找<a>
3.4 Beautiful Soup 的优缺点
- 优点:API 直观(Python 风格),无需学习新语法,容错性强,适合处理不规范 HTML;
- 缺点:解析速度较慢(相比
lxml
),复杂条件定位不如 XPath 灵活。
3.5 演示代码
"""
文件名: 2
作者: 墨尘
日期: 2025/8/8
项目名: pythonProject
备注:
"""
from bs4 import BeautifulSoup
# 定义HTML内容
html = """
<div class="news-list">
<article class="news-item">
<h2 class="title">
<a href="/news/1001" target="_blank">人工智能在医疗领域的新突破</a>
</h2>
<div class="meta">
<span class="time">2023-10-01</span>
<span class="category">科技</span>
</div>
<p class="summary">研究人员开发的AI系统可精准识别早期癌症症状,准确率达98%...</p>
</article>
<article class="news-item">
<h2 class="title">
<a href="/news/1002" target="_blank">全球气候变化峰会达成新协议</a>
</h2>
<div class="meta">
<span class="time">2023-10-02</span>
<span class="category">国际</span>
</div>
<p class="summary">来自195个国家的代表同意采取更激进的措施减少碳排放...</p>
</article>
<article class="news-item">
<h2 class="title">
<a href="/news/1003" target="_blank">新型可再生能源技术取得重大进展</a>
</h2>
<div class="meta">
<span class="time">2023-10-03</span>
<span class="category">能源</span>
</div>
<p class="summary">科学家研发的新型太阳能电池转换效率突破30%,成本降低50%...</p>
</article>
</div>
"""
# 解析HTML,使用lxml解析器
soup = BeautifulSoup(html, 'lxml')
# 1. 提取所有新闻的标题和对应的链接
print("1. 所有新闻标题和链接:")
# 使用find_all找到所有新闻项
news_items = soup.find_all('article', class_='news-item')
for item in news_items:
# 在每个新闻项中查找标题
title_tag = item.find('h2', class_='title').find('a')
title = title_tag.text
link = title_tag['href']
print(f"标题:{title},链接:{link}")
# 2. 提取所有新闻的发布时间和分类
print("\n2. 所有新闻发布时间和分类:")
# 使用CSS选择器定位
meta_tags = soup.select('article.news-item .meta')
for meta in meta_tags:
time = meta.find('span', class_='time').text
category = meta.find('span', class_='category').text
print(f"时间:{time},分类:{category}")
# 3. 筛选出"科技"分类的新闻摘要
print("\n3. 科技分类的新闻摘要:")
# 先找到分类为科技的新闻项
tech_news = soup.find('article', class_='news-item',
attrs={'data-category': None}) # 这里用另一种方式筛选
# 更精确的方式:遍历所有新闻项并判断分类
for item in news_items:
category = item.find('span', class_='category').text
if category == '科技':
summary = item.find('p', class_='summary').text
print(summary)
# 4. 统计新闻总数
print(f"\n4. 新闻总数:{len(news_items)}")
# 额外演示:使用CSS选择器提取所有摘要
summaries = [p.text for p in soup.select('article.news-item p.summary')]
print("\n所有新闻摘要:")
for i, summary in enumerate(summaries, 1):
print(f"{i}. {summary}")
3.6 演示效果
4、pyquery:模仿 jQuery 的解析工具
pyquery 是一款模仿 jQuery 语法的解析库,适合熟悉前端(JavaScript/jQuery)的开发者。它的语法与 jQuery 几乎一致,降低了前端开发者的学习成本。
4.1 安装与基本使用
pip install pyquery
基本用法:
from pyquery import PyQuery as pq
# 解析HTML
doc = pq(html) # html为前文示例的HTML源码
# 也可直接加载URL或文件:doc = pq(url='https://example.com')
4.2 核心用法:jQuery 风格选择器
pyquery 的语法与 jQuery 高度一致,常用方法如下:
方法 / 语法 | 含义 | 示例(基于前文 HTML) | 结果 |
---|---|---|---|
$('选择器') |
定位节点(支持 CSS 选择器) | doc('div.container h1') |
定位到<h1>标题</h1> |
.text() |
提取文本内容 | doc('h1').text() |
标题 |
.attr('属性') |
提取属性值 | doc('a').attr('href') |
link1.html (第一个<a> ) |
.find('选择器') |
查找子节点 | doc('ul').find('li') |
所有<li> 标签 |
.eq(索引) |
按索引筛选(从 0 开始) | doc('li').eq(1).find('a').text() |
链接2 |
.parent() |
获取父节点 | doc('a').parent() |
<a> 的父节点<li> |
4.3 高级用法:遍历与筛选
# 遍历所有<a>标签
for a in doc('a').items(): # .items()返回可迭代对象
print(a.text(), a.attr('href'))
# 筛选文本包含“链接1”的<a>
link1 = doc('a:contains("链接1")')
print(link1.attr('href')) # link1.html
# 链式操作(类似jQuery)
doc('div.container').find('ul').children('li').eq(0).text() # 链接1
4.4 pyquery 的优缺点
- 优点:语法与 jQuery 一致,适合前端开发者,API 简洁,支持链式操作;
- 缺点:解析速度一般,复杂条件定位能力弱于 XPath,容错性不如 Beautiful Soup。
4.5 演示代码
"""
文件名: 3
作者: 墨尘
日期: 2025/8/8
项目名: pythonProject
备注:
"""
from pyquery import PyQuery as pq
# 定义HTML内容
html = """
<div class="product-detail">
<div class="product-images">
<img src="/img/main.jpg" alt="智能手机主图" class="main-img">
<div class="thumbnails">
<img src="/img/thumb1.jpg" alt="侧面图">
<img src="/img/thumb2.jpg" alt="背面图">
<img src="/img/thumb3.jpg" alt="细节图">
</div>
</div>
<div class="product-info">
<h1 class="title">超级智能手机 Pro Max (128GB) - 星空蓝</h1>
<div class="price-wrap">
<span class="original-price">¥6999</span>
<span class="current-price">¥5999</span>
<span class="discount">直降1000元</span>
</div>
<div class="specifications">
<div class="spec-item">
<span class="name">处理器</span>
<span class="value">骁龙8 Gen2</span>
</div>
<div class="spec-item">
<span class="name">内存</span>
<span class="value">8GB + 128GB</span>
</div>
<div class="spec-item">
<span class="name">电池</span>
<span class="value">5000mAh</span>
</div>
</div>
<div class="tags">
<span class="tag">正品保障</span>
<span class="tag">次日达</span>
<span class="tag">支持分期</span>
</div>
</div>
</div>
"""
# 解析HTML
doc = pq(html)
# 1. 提取商品名称和当前售价
product_name = doc('.product-info .title').text()
current_price = doc('.product-info .current-price').text()
original_price = doc('.product-info .original-price').text()
print(f"1. 商品名称:{product_name}")
print(f" 当前售价:{current_price}")
print(f" 原价:{original_price}")
# 2. 提取所有商品图片链接
# 主图
main_img = doc('.product-images .main-img').attr('src')
# 缩略图(使用find方法查找子节点)
thumbnail_imgs = [img.attr('src') for img in doc('.product-images .thumbnails').find('img').items()]
print("\n2. 商品图片链接:")
print(f" 主图:{main_img}")
print(f" 缩略图:{thumbnail_imgs}")
# 3. 提取商品规格信息
specifications = {}
# 遍历所有规格项(使用.items()方法获取可迭代对象)
for item in doc('.specifications .spec-item').items():
# 链式操作:在当前项中查找name和value
spec_name = item.find('.name').text()
spec_value = item.find('.value').text()
specifications[spec_name] = spec_value
print("\n3. 商品规格:")
for name, value in specifications.items():
print(f" {name}:{value}")
# 4. 提取商品标签列表
tags = [tag.text() for tag in doc('.tags .tag').items()]
print("\n4. 商品标签:", tags)
# 5. 计算价格差
# 去除价格中的非数字字符并转换为整数
current_price_num = int(current_price.replace('¥', ''))
original_price_num = int(original_price.replace('¥', ''))
price_diff = original_price_num - current_price_num
print(f"\n5. 价格差:¥{price_diff}")
4.6 演示结果
5、parsel:Scrapy 生态的解析利器
parsel 是 Scrapy 框架内置的解析库,专为爬虫设计,同时支持 XPath 和 CSS 选择器,兼顾灵活性与效率。
5.1 安装与基本使用
pip install parsel
基本用法:
from parsel import Selector
# 解析HTML
selector = Selector(text=html) # html为前文示例的HTML源码
5.2 核心用法:XPath 与 CSS 双支持
parsel 的Selector
对象同时支持xpath()
和css()
方法,提取数据用get()
(单个结果)或getall()
(所有结果):
5.2.1 XPath 语法
# 提取<h1>文本
h1_text = selector.xpath('//h1/text()').get() # '标题'
# 提取所有<a>的href
hrefs = selector.xpath('//a/@href').getall() # ['link1.html', 'link2.html']
5.2.2 CSS 语法
# 提取class为container的div下的ul
ul = selector.css('div.container > ul')
# 提取第2个<li>的文本
li2_text = selector.css('li:nth-child(2) a::text').get() # '链接2'
# 注意:CSS提取文本用::text,属性用::attr(属性名)
5.3 高级用法:嵌套解析
parsel 支持对节点进行二次解析(类似 Beautiful Soup 的嵌套定位):
# 先定位到ul,再在ul内部解析子节点
ul_selector = selector.css('ul').get() # 获取ul的HTML字符串
sub_selector = Selector(text=ul_selector) # 二次解析
li_texts = sub_selector.css('li a::text').getall() # ['链接1', '链接2']
5.4 parsel 的优缺点
- 优点:同时支持 XPath 和 CSS,解析效率高(基于
lxml
),与 Scrapy 无缝集成; - 缺点:主要用于爬虫场景,单独使用时功能不如其他工具丰富。
5.5 演示代码
from parsel import Selector
# 定义HTML内容
html = """
<div class="blog-container">
<article class="blog-post featured">
<h2 class="post-title">
<a href="/posts/python-basics" rel="bookmark">Python 基础入门:从零基础到精通</a>
</h2>
<div class="post-meta">
<span class="author">作者:张三</span>
<span class="date">发布时间:2023-09-15</span>
<span class="comments">评论:24</span>
</div>
<div class="post-content">
<p>本文将介绍Python的基础知识,包括变量、数据类型、函数...</p>
<a href="/posts/python-basics" class="read-more">阅读更多 →</a>
</div>
<div class="post-tags">
<a href="/tags/python" class="tag">Python</a>
<a href="/tags/basics" class="tag">入门</a>
</div>
</article>
<article class="blog-post">
<h2 class="post-title">
<a href="/posts/web-scraping" rel="bookmark">网络爬虫实战:从零构建一个数据采集工具</a>
</h2>
<div class="post-meta">
<span class="author">作者:李四</span>
<span class="date">发布时间:2023-09-10</span>
<span class="comments">评论:18</span>
</div>
<div class="post-content">
<p>本文将教你如何使用Scrapy框架构建一个高效的网络爬虫...</p>
<a href="/posts/web-scraping" class="read-more">阅读更多 →</a>
</div>
<div class="post-tags">
<a href="/tags/scrapy" class="tag">Scrapy</a>
<a href="/tags/crawling" class="tag">爬虫</a>
</div>
</article>
</div>
"""
# 解析HTML
selector = Selector(text=html)
# 1. 提取所有博客文章的标题和链接(使用XPath)
titles = selector.xpath('//article[@class="blog-post"]//h2[@class="post-title"]/a/text()').getall()
links = selector.xpath('//article[@class="blog-post"]//h2[@class="post-title"]/a/@href').getall()
print("1. 博客文章标题和链接:")
for title, link in zip(titles, links):
print(f"标题:{title}")
print(f"链接:{link}\n")
# 2. 提取每篇文章的作者、发布时间和评论数(混合使用CSS和XPath)
articles = selector.css('article.blog-post')
print("2. 文章详细信息:")
for article in articles:
# 使用CSS选择器
author = article.css('.post-meta .author::text').get()
# 使用XPath
date = article.xpath('.//span[@class="date"]/text()').get()
comments = article.css('.post-meta .comments::text').get()
print(f"{author}")
print(f"{date}")
print(f"{comments}\n")
# 3. 提取所有文章的标签列表(使用CSS)
all_tags = []
tag_elements = selector.css('.post-tags .tag')
for tag in tag_elements:
tag_name = tag.css('::text').get()
tag_link = tag.css('::attr(href)').get()
all_tags.append((tag_name, tag_link))
print("3. 所有标签:")
for name, link in all_tags:
print(f"{name}: {link}")
# 4. 筛选出"精选"文章的简介(使用XPath条件筛选)
featured_intro = selector.xpath(
'//article[contains(@class, "featured")]//div[@class="post-content"]/p/text()'
).get()
print(f"\n4. 精选文章简介:{featured_intro}")
# 5. 统计所有文章的评论总数(数据处理)
comments_count = selector.css('.post-meta .comments::text').getall()
# 提取数字并求和
total_comments = sum(int(comment.split(':')[1]) for comment in comments_count)
print(f"\n5. 总评论数:{total_comments}")
# 6. 嵌套解析示例
# 先获取所有文章的HTML,再进行二次解析
article_htmls = selector.css('article.blog-post').getall()
print("\n6. 嵌套解析获取每篇文章的第一个标签:")
for html in article_htmls:
sub_selector = Selector(text=html)
first_tag = sub_selector.xpath('//div[@class="post-tags"]/a[1]/text()').get()
print(f"第一个标签:{first_tag}")
5.6 演示结果
6、工具对比与选择建议
6.1 工具对比
工具 | 核心语法 | 解析速度 | 容错性 | 适用场景 | 学习成本 |
---|---|---|---|---|---|
XPath(lxml) | 路径表达式 | 快 | 中 | 复杂条件定位、高效解析 | 中 |
Beautiful Soup | Python 方法 / CSS | 慢 | 高 | 简单解析、处理不规范 HTML、新手入门 | 低 |
pyquery | jQuery 语法 | 中 | 中 | 前端开发者、简单解析 | 低(前端背景) |
parsel | XPath/CSS | 快 | 中 | Scrapy 爬虫、需要混合使用 XPath 和 CSS 的场景 | 中 |
6.2 选择建议:
- 新手入门:优先选 Beautiful Soup(API 直观);
- 前端开发者:选 pyquery(语法熟悉);
- 高效 / 复杂解析:选 XPath(lxml);
- Scrapy 爬虫:必选 parsel(原生支持)。
7、实践技巧
- 处理动态内容:若网页数据由 JavaScript 动态生成(如 Ajax 加载),需先通过
Selenium
或Playwright(后面会讲到)
获取渲染后 HTML,再解析; - 反爬与容错:对不规范 HTML,优先用 Beautiful Soup(
html5lib
解析器)或lxml
(自动修复); - 调试工具:
- 浏览器开发者工具(F12)可直接测试 XPath/CSS 选择器(Elements 面板右键 “Copy XPath/CSS selector”);
- 用
print()
输出中间结果,验证定位是否正确。