
📖 开篇导读
在上一节课中,我们学习了使用 Requests 和 BeautifulSoup 抓取静态网页的基本方法。但面对复杂的网页结构、糟糕的HTML代码或海量数据时,BeautifulSoup 的解析效率有时会显得力不从心。此外,很多网页的数据并不是直接写在 HTML 中,而是通过 AJAX 动态加载的。要应对这些挑战,我们需要更强大的工具和技巧。
本课是爬虫进阶级别的内容。我们将学习两种更高效、更灵活的解析方式:正则表达式和 XPath。配合lxml库,解析速度比 BeautifulSoup 提升数倍,且 XPath 语法简洁强大。此外,我们还会介绍如何分析 AJAX 请求,直接抓取后端返回的 JSON 数据,绕过复杂的页面解析。
💡 工作场景:
- 海量数据爬取:lxml 解析速度比纯 Python 的解析器快得多,适合高并发场合。
- 结构清晰但复杂:当 HTML 层级很深时,XPath 能一行定位元素。
- 动态内容:通过 Chrome 开发者工具分析网络请求,直接抓取 API 接口。
- 数据提取:正则表达式在提取特定格式文本(如邮箱、手机号、URL)时极其高效。
学完本课,你将能够独立分析并抓取各类复杂网站数据,包括动态加载的内容,并具备应对初级反爬虫的能力。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 熟练使用正则表达式提取网页中的特定模式数据(链接、邮箱、价格等) | 提取非结构化文本 |
| 2️⃣ | 掌握 XPath 语法,能够在 lxml 中快速定位元素 | 高效解析 HTML/XML |
| 3️⃣ | 理解 lxml 库的安装和使用,对比 BeautifulSoup 的优势 | 性能优化 |
| 4️⃣ | 能够分析网页 AJAX 请求,直接抓取 JSON 数据 | 绕过页面解析 |
| 5️⃣ | 了解如何保持会话(Session)和模拟登录(登录过程) | 爬取需认证的数据 |
| 6️⃣ | 综合运用上述技术完成一个复杂爬虫案例 | 独立解决实际问题 |
🔥 面试考点:“XPath 和 CSS 选择器有何区别?”“如何抓取 AJAX 动态加载的数据?”“lxml 比 BeautifulSoup 快在哪里?为什么?”“简述正则表达式在爬虫中的使用场景。”
📚 知识点理论精讲
一、正则表达式在爬虫中的高效使用
正则表达式(Regular Expression)擅长从字符串中提取符合特定格式的子串。在爬虫中,它非常适合:
- 从 HTML 文本中直接提取邮箱、电话号码、身份证号等。
- 清理文本(去除多余空白、HTML 标签)。
- 从 JavaScript 变量中提取数据(当数据嵌在 script 标签中时)。
1.1 Python re 模块回顾
import re
text = "My email is john@example.com and tom@test.com."
pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
emails = re.findall(pattern, text)
print(emails)
1.2 贪婪与非贪婪
爬虫中经常需要匹配 HTML 标签内的内容,建议使用非贪婪 .*?。
html = '<div class="title">Python教程</div><div>其他</div>'
match = re.search(r'<div class="title">(.*?)</div>', html)
print(match.group(1)) # Python教程
1.3 常用正则示例
| 目标 | 正则表达式 |
|---|---|
| 邮箱 | [\w\.-]+@[\w\.-]+\.\w+ |
| 手机号(中国) | 1[3-9]\d{9} |
| IP 地址 | \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} |
| URL | https?://[^\s]+ |
| 数字(整数/小数) | \d+(?:\.\d+)? |
💡 工作应用:清理从网页中提取的文本时,经常用
re.sub(r'<[^>]+>', '', html)去除 HTML 标签。
二、XPath 语法与 lxml 库
XPath(XML Path Language)是一种在 XML/HTML 文档中查找信息的语言。它比 CSS 选择器更强大、更灵活,可以沿父子、祖先、兄弟关系导航,还能使用函数。
2.1 安装 lxml
pip install lxml
2.2 使用 lxml 解析 HTML
from lxml import etree
html = '<html><body><div id="content">Hello</div></body></html>'
# 创建解析器
parser = etree.HTMLParser()
tree = etree.parse(StringIO(html), parser) # 从字符串解析
# 或者直接使用 etree.HTML()
root = etree.HTML(html)
2.3 XPath 基本语法
| 表达式 | 描述 |
|---|---|
/ | 从根节点选取 |
// | 从任意位置选取 |
. | 当前节点 |
.. | 父节点 |
@ | 选取属性 |
* | 通配符 |
text() | 获取文本 |
常用示例
# 选取所有 div 元素
root.xpath('//div')
# 选取所有 class 为 "title" 的元素
root.xpath('//*[@class="title"]')
# 选取 id 为 "main" 的 div 下的所有 a 标签
root.xpath('//div[@id="main"]//a')
# 获取 a 标签的 href 属性
root.xpath('//a/@href')
# 获取文本内容(不包括子标签)
root.xpath('//h1/text()')
# 选取包含特定文本的节点
root.xpath('//a[contains(text(), "下一页")]')
2.4 谓语(Predicate)
谓语用来过滤节点,写在方括号中。
# 第一个 a 标签
root.xpath('(//a)[1]')
# 最后一个 a 标签
root.xpath('(//a)[last()]')
# 位置小于3的 a 标签
root.xpath('(//a)[position()<3]')
# 有 href 属性的 a 标签
root.xpath('//a[@href]')
# 属性值为特定值
root.xpath('//input[@name="username"]')
2.5 轴(Axis)
可以沿着父子、祖先、兄弟等关系移动。
# 选取当前节点的所有子节点
root.xpath('//div/child::*')
# 选取父节点
root.xpath('//a/parent::*')
# 选取祖先节点
root.xpath('//a/ancestor::div')
# 选取前面的兄弟
root.xpath('//a/preceding-sibling::div')
2.6 函数
XPath 有很多内置函数:contains()、starts-with()、substring()、count()、position() 等。
# 包含
root.xpath('//div[contains(@class, "article")]')
# 以某字符串开头
root.xpath('//a[starts-with(@href, "/topic/")]')
# 统计某节点下子节点数量
root.xpath('count(//div[@id="list"]/ul/li)')
2.7 lxml 与 BeautifulSoup 性能对比
- BeautifulSoup:纯 Python,易用但较慢。
- lxml:底层 C 实现,解析速度极快,支持 XPath。
⚠️ 高频坑点:lxml 对不规范的 HTML 有较强容错能力,但有时会补全或添加
html、body标签,导致 XPath 预期不一致。建议使用etree.HTML()解析,然后直接写 XPath。
三、处理动态加载内容(Ajax)
很多现代网站通过 JavaScript 异步加载数据,HTML 源码中并没有这些数据。这时需要分析网络请求,直接请求后端 API。
3.1 分析 XHR 请求步骤
- 打开浏览器开发者工具(F12)→ Network → XHR 标签。
- 刷新页面,观察加载数据的请求。
- 找到返回 JSON 数据的请求,复制 URL 和请求参数。
- 用 Python 模拟该请求,直接拿到结构化数据。
3.2 示例:爬取 GitHub API
import requests
url = 'https://api.github.com/repos/python/cpython/stargazers'
params = {'per_page': 5}
headers = {'User-Agent': 'Mozilla/5.0'}
resp = requests.get(url, params=params, headers=headers)
data = resp.json()
for user in data:
print(user['login'])
3.3 携带 Cookie 和 Referer
某些 API 需要验证身份,可以使用 Session 保持登录状态。
session = requests.Session()
session.headers.update({'User-Agent': 'Mozilla/5.0'})
# 首先登录(post 用户名密码)
login_resp = session.post('https://example.com/login', data={...})
# 然后访问需要登录的 API
data = session.get('https://example.com/api/data').json()
💻 代码案例实操
案例1:使用正则提取网页中的所有邮箱
"""
regex_emails.py
从本地 HTML 文件或任意文本中提取邮箱地址
"""
import re
import requests
def extract_emails(text):
pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
return re.findall(pattern, text)
# 示例:从百度首页提取(可能没有邮箱,换个例子)
url = 'https://www.sina.com.cn/'
headers = {'User-Agent': 'Mozilla/5.0'}
resp = requests.get(url, headers=headers)
emails = extract_emails(resp.text)
print(f"找到 {len(emails)} 个邮箱:")
for e in emails[:10]:
print(e)
案例2:XPath 提取某新闻网站标题列表
"""
xpath_news.py
使用 lxml 和 XPath 提取新闻标题和链接
"""
import requests
from lxml import etree
url = 'https://news.sina.com.cn/'
headers = {'User-Agent': 'Mozilla/5.0'}
resp = requests.get(url)
resp.encoding = 'utf-8'
tree = etree.HTML(resp.text)
# 观察新浪新闻首页结构,假设标题在 <a> 标签内,且 class 包含"news-title"
# 实际需根据网站调整 XPath,此处做示例
titles = tree.xpath('//a[contains(@href, "doc-")]//text()')
links = tree.xpath('//a[contains(@href, "doc-")]/@href')
print("新闻标题与链接:")
for title, link in zip(titles[:10], links[:10]):
print(f"{title.strip()}\n{link}\n")
案例3:使用 XPath 爬取豆瓣电影 Top250(对比 BeautifulSoup)
"""
xpath_douban.py
用 lxml 与 XPath 重写豆瓣 Top250 爬虫,展示速度优势
"""
import requests
from lxml import etree
import time
def fetch_page(start):
url = f'https://movie.douban.com/top250?start={start}'
headers = {'User-Agent': 'Mozilla/5.0'}
resp = requests.get(url, headers=headers)
resp.encoding = 'utf-8'
return resp.text
def parse_with_xpath(html):
tree = etree.HTML(html)
movies = []
items = tree.xpath('//div[@class="item"]')
for item in items:
title = item.xpath('.//span[@class="title"]/text()')[0]
rating = item.xpath('.//span[@class="rating_num"]/text()')[0]
people = item.xpath('.//div[@class="star"]/span[4]/text()')[0].replace('人评价', '')
movies.append((title, rating, int(people)))
return movies
start_time = time.time()
all_movies = []
for start in range(0, 250, 25):
html = fetch_page(start)
movies = parse_with_xpath(html)
all_movies.extend(movies)
print(f"lxml+XPath 耗时: {time.time() - start_time:.2f}秒, 共{len(all_movies)}条")
案例4:分析 XHR 请求抓取动态数据(微博热搜为例)
"""
weibo_hot_search.py
通过分析 API 获取微博热搜榜(示例接口)
"""
import requests
# 实际微博热搜接口可能需要 token,这里用一个公开的示例
url = 'https://weibo.com/ajax/side/hotSearch'
headers = {
'User-Agent': 'Mozilla/5.0',
'Referer': 'https://weibo.com/'
}
resp = requests.get(url, headers=headers)
if resp.status_code == 200:
data = resp.json()
for idx, item in enumerate(data.get('data', {}).get('realtime', [])[:20], 1):
title = item.get('word')
print(f"{idx}. {title}")
else:
print("请求失败", resp.status_code)
案例5:使用 Session 保持登录状态(模拟 GitHub 登录)
"""
github_login_session.py
演示如何用 Session 保持登录,访问需要登录的页面
"""
import requests
session = requests.Session()
# 1. 先访问登录页获取 authenticity_token(模拟)
login_url = 'https://github.com/login'
session.headers.update({'User-Agent': 'Mozilla/5.0'})
login_page = session.get(login_url)
from lxml import etree
tree = etree.HTML(login_page.text)
token = tree.xpath('//input[@name="authenticity_token"]/@value')[0]
# 2. 提交登录表单
post_data = {
'commit': 'Sign in',
'authenticity_token': token,
'login': 'your_username',
'password': 'your_password'
}
session.post('https://github.com/session', data=post_data)
# 3. 访问需要登录的页面(如个人仓库列表)
profile_url = 'https://github.com/settings/repositories'
resp = session.get(profile_url)
print(resp.status_code) # 200 表示已登录
案例6:lxml 与 XPath 提取嵌套结构的商品信息
"""
xpath_nested_products.py
模拟电商网站的商品列表抓取
"""
html = """
<div class="product">
<h3>商品A</h3>
<span class="price">¥199</span>
<div class="reviews">好评率98%</div>
</div>
<div class="product">
<h3>商品B</h3>
<span class="price">¥299</span>
<div class="reviews">好评率95%</div>
</div>
"""
from lxml import etree
tree = etree.HTML(html)
products = tree.xpath('//div[@class="product"]')
for prod in products:
name = prod.xpath('.//h3/text()')[0]
price = prod.xpath('.//span[@class="price"]/text()')[0]
review = prod.xpath('.//div[@class="reviews"]/text()')[0]
print(f"{name} -> {price}, {review}")
案例7:综合实战——抓取动态加载的评论(分析请求参数)
"""
ajax_comments.py
豆瓣电影短评抓取(通过 Ajax 接口)
"""
import requests
import json
# 豆瓣电影 ID 例如 1292052(肖申克的救赎)
movie_id = '1292052'
start = 0
comments = []
while True:
url = f'https://movie.douban.com/subject/{movie_id}/comments'
params = {
'start': start,
'limit': 20,
'status': 'P',
'sort': 'new_score'
}
headers = {'User-Agent': 'Mozilla/5.0', 'Referer': f'https://movie.douban.com/subject/{movie_id}/'}
resp = requests.get(url, params=params, headers=headers)
# 注意豆瓣评论页的HTML,不是直接JSON,这里仅演示思路,实际应分析XHR
# 假设是直接返回 JSON,下面为模拟
# 真实一般需要解析HTML或找到真正的API
if resp.status_code != 200:
break
# 模拟从HTML中解析评论...
start += 20
if start >= 100:
break
print("完成评论抓取")
注意:很多网站的反爬机制会验证 Referer、Cookie、请求头等,实际中需要更细致的模拟。
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | XPath 表达式写错,导致结果为空 | 提取不到数据 | 在浏览器开发者工具中用 $x() 测试 XPath |
| 2 | lxml 解析时自动添加 html/body | 相对路径 /html/body 可能失效 | 使用 // 或忽略顶层,直接写相对路径 |
| 3 | 忘记处理动态加载内容 | 抓不到数据 | 检查 XHR,直接请求 API |
| 4 | 正则表达式贪婪匹配 | 匹配到过多内容 | 用 .*? 非贪婪 |
| 5 | 混淆 findall 与 search | 返回结果不符合预期 | 按需选择,findall 返回所有匹配 |
| 6 | 忽略编码问题 | 中文乱码 | 设置 resp.encoding = 'utf-8' 或根据网页 meta 指定 |
| 7 | 不设置 User-Agent | 请求被拒绝 | 每次请求都添加浏览器 UA |
| 8 | 高频请求未加延时 | IP 被封 | time.sleep(random.uniform(0.5, 2)) |
| 9 | 忽略 Cookies 和 Session | 无法保持登录状态 | 使用 requests.Session() |
| 10 | 硬编码 XPath 位置索引 | 网页结构微调后崩溃 | 尽量使用 id、class 等稳定属性 |
📝 课后实战练习题
第1题:正则提取网页中的所有 URL
编写函数 extract_urls(html),使用正则提取所有以 http:// 或 https:// 开头的 URL,去重后返回列表。
第2题:XPath 提取知乎热榜问题
抓取知乎首页的热榜(https://www.zhihu.com/hot),使用 lxml 提取每个问题的标题、链接和热度值。
第3题:爬取动态加载的腾讯新闻
打开腾讯新闻客户端首页,分析 XHR 请求,找到新闻列表的 API 接口,用 Python 请求并解析 JSON,提取新闻标题和发布时间。
第4题:登录豆瓣并抓取我的标记
模拟登录豆瓣(使用你的测试账号),然后访问“我读过的书”页面,提取书名和评分。注意处理验证码(可选)。
第5题:正则与 XPath 结合
某网页中,商品价格格式为<span class="price">¥1,234.56</span>,先用正则提取价格数字,再转换为浮点数,并与 XPath 获取的商品名称一起存储。
第6题:爬取分页的 Ajax 评论
找到一个有分页评论的网站(如新闻评论区),分析其加载更多评论的 API 请求,循环抓取所有评论内容,保存为 JSON。
第7题:构建代理池(选做)
使用免费代理网站抓取代理 IP,验证可用性后存入列表,并在后续请求中随机使用代理。
🔜 下节课预告
正则和 XPath 让我们能高效解析数据,但网站的反爬机制也越来越复杂。下一节课我们将学习爬虫反爬入门,包括 User-Agent 伪装、代理 IP、Cookie 维持、验证码识别等内容。
第46课:爬虫反爬入门:请求头、代理IP、Cookie会话维持实战
内容包括:
- 常见的反爬手段(User-Agent、IP 限速、验证码、字体反爬)
- 请求头伪装与浏览器指纹
- 代理 IP 的原理与使用
- Cookie 持久化与会话维持
- 验证码识别初步
学完本课,你将能够应对大多数初级反爬措施,提高爬虫的可用性。
🌟 学习鼓励:正则和 XPath 是爬虫进阶的重要工具。正则看似繁琐,但能解决很多文本提取问题;XPath 则是解析结构化文档的利器。请多做练习,在真实网站上尝试抓取,并总结不同网站的结构规律。经过本课学习,你已经具备独立开发复杂爬虫的能力了!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

1170

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



