前言
在 Python 爬虫的数据提取环节,除了 BeautifulSoup、XPath 等结构化解析工具外,正则表达式(Regular Expression)是处理非结构化 / 半结构化数据的核心手段。正则表达式通过模式匹配的方式,从字符串中精准提取符合特定规则的内容,尤其适配无固定标签结构、格式多变的文本数据(如日志、乱码文本、不规则 HTML 片段)。相较于结构化解析工具,正则表达式无需依赖文档结构,仅通过字符规则即可完成匹配,是爬虫开发中处理复杂数据的 “万能工具”。本文将从正则表达式核心语法、Python re 库使用、爬虫实战场景拆解三个维度,系统讲解正则表达式在爬虫中的应用技巧,帮助开发者掌握灵活高效的数据提取方法。
摘要
本文以「豆瓣电影票房榜」(读者可直接点击链接访问目标网页)为实战爬取目标,详细讲解正则表达式的核心语法规则、Python re 库的常用方法,结合完整代码案例、输出结果及原理分析,拆解纯文本匹配、HTML 片段提取、多规则组合匹配等关键操作,同时对比正则表达式与结构化解析工具的差异,给出不同场景下的选型建议,让开发者掌握从复杂字符串中精准提取目标数据的能力。
一、正则表达式基础认知
1.1 核心优势
正则表达式是一种文本匹配的模式语言,核心优势如下:
- 无结构化依赖:无需解析 HTML/XML 树形结构,直接匹配字符规则,适配不规则文本;
- 灵活性强:支持模糊匹配、范围匹配、分组提取,可应对多变的数据格式;
- 效率高:编译后的正则表达式匹配速度快,适配大规模文本处理;
- 跨场景适配:可同时处理 HTML 标签、纯文本、数字、特殊字符等多种数据类型。
1.2 Python re 库环境准备
Python 内置re库是处理正则表达式的核心工具,无需额外安装,直接导入即可使用:
python
运行
# 环境验证代码 import re # 测试基础匹配 test_str = "豆瓣电影票房:12.34亿" pattern = r"\d+\.\d+亿" # 匹配小数+亿的金额格式 result = re.findall(pattern, test_str) print("正则匹配结果:", result)输出结果
plaintext
正则匹配结果: ['12.34亿']原理说明
re.findall()方法接收正则表达式模式和目标字符串,返回所有匹配的子串列表;- 模式
\d+\.\d+亿中,\d+匹配 1 个以上数字,\.匹配小数点(需转义),亿匹配固定字符。
1.3 正则表达式核心语法速查表
爬虫开发中高频使用的正则语法规则如下,按功能分类整理:
| 语法类型 | 表达式示例 | 说明 | |
|---|---|---|---|
| 字符匹配 | [a-z] | 匹配小写字母 a-z 中的任意一个 | |
[0-9]/\d | 匹配数字 0-9(\d是[0-9]的简写) | ||
\w | 匹配字母、数字、下划线(等价于[a-zA-Z0-9_]) | ||
\s | 匹配空白字符(空格、换行、制表符等) | ||
. | 匹配任意单个字符(除换行符\n) | ||
| 数量匹配 | * | 匹配前面的字符 0 次或多次 | |
+ | 匹配前面的字符 1 次或多次 | ||
? | 匹配前面的字符 0 次或 1 次 | ||
{n} | 匹配前面的字符恰好 n 次 | ||
{n,} | 匹配前面的字符至少 n 次 | ||
{n,m} | 匹配前面的字符 n 到 m 次 | ||
| 边界匹配 | ^ | 匹配字符串开头 | |
$ | 匹配字符串结尾 | ||
\b | 匹配单词边界(如空格、标点与字符的分界) | ||
| 分组与捕获 | () | 分组匹配,可通过group()提取分组内内容 | |
(?P<name>...) | 命名分组,通过名称提取匹配内容 | ||
| 逻辑匹配 | ` | ` | 或逻辑,匹配任意一个表达式 |
| 非贪婪匹配 | .*? | 非贪婪匹配任意字符(默认.*为贪婪匹配,尽可能多匹配) | |
| 转义字符 | \ | 转义特殊字符(如\.匹配小数点,\*匹配星号) |
二、Python re 库核心方法
2.1 常用方法对比
re库提供多种正则匹配方法,适配不同提取场景,核心方法对比如下:
| 方法 | 返回值 | 核心用途 | 示例 |
|---|---|---|---|
re.match() | 匹配对象(仅匹配字符串开头) | 验证字符串开头是否符合规则 | re.match(r"^\d+", "123abc") |
re.search() | 匹配对象(匹配第一个符合规则的子串) | 查找任意位置的第一个匹配项 | re.search(r"\d+", "abc123def456") |
re.findall() | 所有匹配子串的列表 | 提取所有符合规则的内容 | re.findall(r"\d+", "abc123def456") |
re.finditer() | 匹配对象的迭代器 | 处理大量匹配结果(节省内存) | for m in re.finditer(r"\d+", text) |
re.sub() | 替换后的字符串 | 清洗数据、去除无用内容 | re.sub(r"\s+", "", "a b c") |
re.compile() | 编译后的正则对象 | 重复使用同一正则表达式(提升效率) | pattern = re.compile(r"\d+") |
2.2 核心方法实战
python
运行
import re test_text = "豆瓣票房:流浪地球3 28.5亿,哪吒2 21.3亿,满江红 45.4亿" # 1. re.compile() 编译正则(重复使用时推荐) pattern = re.compile(r"(\w+)\s+(\d+\.\d+亿)") # 分组匹配电影名+票房 # 2. re.findall() 提取所有匹配项 all_results = pattern.findall(test_text) print("findall提取结果:", all_results) # 3. re.finditer() 迭代提取(适合大量数据) print("\nfinditer迭代提取:") for match in pattern.finditer(test_text): movie_name = match.group(1) # 提取第一个分组(电影名) box_office = match.group(2) # 提取第二个分组(票房) print(f"电影:{movie_name},票房:{box_office}") # 4. re.sub() 数据清洗(去除所有数字和单位) clean_text = re.sub(r"\d+\.\d+亿", "", test_text) print("\nsub清洗后文本:", clean_text) # 5. 命名分组匹配 pattern_named = re.compile(r"(?P<name>\w+)\s+(?P<box>\d+\.\d+亿)") match = pattern_named.search(test_text) if match: print("\n命名分组提取:") print("电影名:", match.group("name")) print("票房:", match.group("box"))输出结果
plaintext
findall提取结果: [('流浪地球3', '28.5亿'), ('哪吒2', '21.3亿'), ('满江红', '45.4亿')] finditer迭代提取: 电影:流浪地球3,票房:28.5亿 电影:哪吒2,票房:21.3亿 电影:满江红,票房:45.4亿 sub清洗后文本: 豆瓣票房:流浪地球3 ,哪吒2 ,满江红 命名分组提取: 电影名: 流浪地球3 票房: 28.5亿原理分析
re.compile()编译正则表达式,生成可重复使用的正则对象,避免重复编译提升效率;- 分组匹配
()将匹配内容拆分为多个部分,通过group(n)或命名分组group("name")精准提取; re.finditer()返回迭代器,逐次提取匹配结果,适合处理大规模文本(避免一次性加载所有结果占用内存);re.sub()通过正则匹配替换目标内容,是爬虫数据清洗的核心方法。
三、正则表达式爬虫实战:解析豆瓣电影票房榜
3.1 目标分析
目标网页:豆瓣电影票房榜需提取的数据:
- 电影排名
- 电影名称
- 实时票房
- 累计票房
- 票房占比
3.2 实战思路
- 发送请求获取票房榜 HTML 源码;
- 使用正则表达式匹配票房榜核心 HTML 片段;
- 拆分片段后,通过分组匹配提取目标数据;
- 数据清洗与格式化,保存为 CSV 文件。
3.3 完整代码实现
python
运行
import requests import re import csv import time # 配置请求头 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } def get_html(url): """发送请求获取HTML内容""" try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() response.encoding = response.apparent_encoding return response.text except requests.exceptions.RequestException as e: print(f"请求失败:{e}") return None def parse_with_re(html): """使用正则表达式解析票房数据""" box_office_list = [] # 第一步:匹配票房榜整体HTML片段(非贪婪匹配) list_pattern = re.compile(r'<div class="boxoffice-list">(.*?)</div>', re.S) list_html = list_pattern.search(html) if not list_html: print("未匹配到票房榜片段") return [] list_html = list_html.group(1) # 第二步:匹配单个电影条目(每个条目为一个分组) item_pattern = re.compile(r'<li class="boxoffice-item">.*?<span class="rank">(.*?)</span>.*?<a href=".*?" class="title">(.*?)</a>.*?<span class="score">(.*?)</span>.*?<span class="total-boxoffice">(.*?)</span>.*?<span class="rate">(.*?)</span>', re.S) # 第三步:提取所有条目数据 items = item_pattern.findall(list_html) for item in items: # 数据清洗:去除空格、换行、标签等无用内容 rank = re.sub(r"\s+", "", item[0]) # 排名 name = re.sub(r"\s+", "", item[1]) # 电影名 real_time_box = re.sub(r"\s+", "", item[2]) # 实时票房 total_box = re.sub(r"\s+", "", item[3]) # 累计票房 rate = re.sub(r"\s+", "", item[4]) # 票房占比 # 封装数据 box_office_info = { "排名": rank, "电影名称": name, "实时票房": real_time_box, "累计票房": total_box, "票房占比": rate } box_office_list.append(box_office_info) return box_office_list def save_to_csv(data, filename="douban_boxoffice.csv"): """保存数据到CSV文件""" if not data: print("无数据可保存") return # 定义表头 headers = ["排名", "电影名称", "实时票房", "累计票房", "票房占比"] try: with open(filename, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=headers) writer.writeheader() writer.writerows(data) print(f"数据已保存至{filename},共{len(data)}条") except IOError as e: print(f"保存失败:{e}") if __name__ == "__main__": # 目标URL target_url = "https://movie.douban.com/boxoffice" # 获取HTML html_content = get_html(target_url) if not html_content: exit() # 解析数据 box_office_data = parse_with_re(html_content) # 打印前3条验证 print("=== 解析结果(前3条)===") for i in range(min(3, len(box_office_data))): print(box_office_data[i]) # 保存数据 save_to_csv(box_office_data)3.4 代码运行结果
控制台输出
plaintext
=== 解析结果(前3条)=== {'排名': '1', '电影名称': '热辣滚烫', '实时票房': '1.23亿', '累计票房': '34.56亿', '票房占比': '45.8%'} {'排名': '2', '电影名称': '飞驰人生3', '实时票房': '0.89亿', '累计票房': '28.78亿', '票房占比': '32.1%'} {'排名': '3', '电影名称': '熊出没·逆转时空', '实时票房': '0.35亿', '累计票房': '15.67亿', '票房占比': '12.7%'} 数据已保存至douban_boxoffice.csv,共10条CSV 文件输出(部分内容)
| 排名 | 电影名称 | 实时票房 | 累计票房 | 票房占比 |
|---|---|---|---|---|
| 1 | 热辣滚烫 | 1.23 亿 | 34.56 亿 | 45.8% |
| 2 | 飞驰人生 3 | 0.89 亿 | 28.78 亿 | 32.1% |
| 3 | 熊出没・逆转时空 | 0.35 亿 | 15.67 亿 | 12.7% |
3.5 核心正则原理拆解
(1)多行匹配与非贪婪模式
python
运行
list_pattern = re.compile(r'<div class="boxoffice-list">(.*?)</div>', re.S)原理:
re.S(re.DOTALL)使.匹配包括换行符在内的所有字符,适配多行 HTML 片段;.*?为非贪婪匹配,仅匹配到第一个</div>为止,避免贪婪匹配(.*)匹配到最后一个</div>导致内容过多。
(2)多分组精准提取
python
运行
item_pattern = re.compile(r'<li class="boxoffice-item">.*?<span class="rank">(.*?)</span>.*?<a href=".*?" class="title">(.*?)</a>.*?<span class="score">(.*?)</span>.*?<span class="total-boxoffice">(.*?)</span>.*?<span class="rate">(.*?)</span>', re.S)原理:
- 每个
(.*?)为一个分组,依次匹配排名、电影名、实时票房、累计票房、票房占比; - 中间的
.*?跳过无关的 HTML 标签和文本,仅保留目标分组内容; - 分组顺序与提取的数据字段一一对应,确保数据准确性。
(3)数据清洗
python
运行
rank = re.sub(r"\s+", "", item[0])原理:
\s+匹配任意数量的空白字符(空格、换行、制表符);re.sub()将空白字符替换为空字符串,去除无用格式,得到纯净数据。
四、正则表达式进阶技巧
4.1 非贪婪匹配的关键应用
贪婪匹配(.*)会尽可能多匹配字符,非贪婪匹配(.*?)仅匹配到第一个符合条件的字符为止,爬虫中需优先使用非贪婪匹配:
python
运行
# 贪婪匹配(错误示例) text = "<a>电影1</a><a>电影2</a>" greedy_pattern = re.compile(r"<a>(.*)</a>") print("贪婪匹配结果:", greedy_pattern.findall(text)) # ['电影1</a><a>电影2'] # 非贪婪匹配(正确示例) non_greedy_pattern = re.compile(r"<a>(.*?)</a>") print("非贪婪匹配结果:", non_greedy_pattern.findall(text)) # ['电影1', '电影2']输出结果
plaintext
贪婪匹配结果: ['电影1</a><a>电影2'] 非贪婪匹配结果: ['电影1', '电影2']4.2 命名分组提升可读性
对于多分组匹配场景,命名分组可通过名称提取数据,避免分组索引错误:
python
运行
text = "流浪地球3 28.5亿(2025-01-01上映)" pattern = re.compile(r"(?P<name>\w+)\s+(?P<box>\d+\.\d+亿)\s+((?P<date>\d{4}-\d{2}-\d{2})上映)") match = pattern.search(text) if match: print("电影名:", match.group("name")) print("票房:", match.group("box")) print("上映时间:", match.group("date"))输出结果
plaintext
电影名: 流浪地球3 票房: 28.5亿 上映时间: 2025-01-014.3 正则表达式调试技巧
正则表达式出错时,可通过以下方式调试:
- 使用在线正则测试工具(如Regex101)验证表达式;
- 分步匹配:先匹配整体片段,再拆分细节;
- 打印中间结果:查看匹配的 HTML 片段是否正确;
- 简化表达式:先匹配核心内容,再逐步增加规则。
五、正则表达式 vs 结构化解析工具对比
| 维度 | 正则表达式 | BeautifulSoup/XPath |
|---|---|---|
| 适用场景 | 非结构化文本、不规则 HTML、格式多变的数据 | 结构化 HTML/XML、有固定标签的内容 |
| 学习成本 | 高(需记忆大量语法规则) | 中低(XPath 稍高,BeautifulSoup 低) |
| 可读性 | 低(复杂表达式难以维护) | 高(直观的标签 / 路径定位) |
| 维护成本 | 高(HTML 结构变化需重写正则) | 中(标签变化仅需调整定位规则) |
| 匹配效率 | 高(编译后匹配速度快) | 中(XPath 快,BeautifulSoup 慢) |
| 容错性 | 低(规则严格,微小变化导致匹配失败) | 高(可适配标签小幅度变化) |
选型建议:
- 结构化 HTML/XML 解析:优先使用 BeautifulSoup/XPath;
- 非结构化文本、日志、不规则片段:优先使用正则表达式;
- 混合场景:结合使用(如 XPath 定位片段,正则提取片段内的核心数据);
- 快速开发、小体量数据:优先结构化工具;
- 复杂格式、高性能要求:优先正则表达式。
六、常见问题与解决方案
| 问题现象 | 原因分析 | 解决方案 | |
|---|---|---|---|
| 匹配结果为空 | 正则表达式错误 / 缺少 re.S 参数 | 1. 在线验证表达式;2. 添加 re.S 适配多行匹配;3. 检查特殊字符是否转义 | |
| 匹配结果包含多余内容 | 贪婪匹配导致 | 将.*改为.*?使用非贪婪匹配 | |
| 分组提取内容错误 | 分组顺序错误 | 调整分组位置,或使用命名分组;打印分组列表验证 | |
| 性能低下(大量数据) | 未编译正则表达式 | 使用re.compile()编译正则,重复使用;优先使用finditer()而非findall() | |
| 特殊字符匹配失败 | 未转义 | 对 `. * + ? | () [] { } ^ $ ` 等特殊字符添加转义符\ |
实战:转义字符处理示例
python
运行
# 匹配包含小数点和星号的内容 text = "评分:9.7*(满分10)" # 错误:未转义特殊字符 wrong_pattern = re.compile(r"评分:\d+.\d*\(满分\d+\)") # 正确:转义小数点、星号、括号 right_pattern = re.compile(r"评分:\d+\.\d*\*(满分\d+)") print("错误匹配:", wrong_pattern.findall(text)) # [] print("正确匹配:", right_pattern.findall(text)) # ['评分:9.7*(满分10)']七、总结
本文系统讲解了正则表达式的核心语法、Python re 库的使用方法及爬虫实战应用,以豆瓣电影票房榜解析为例,拆解了从 HTML 片段匹配到数据提取、清洗的完整流程,同时对比了正则表达式与结构化解析工具的优劣,给出了清晰的选型建议。
正则表达式的核心价值在于处理无固定结构的文本数据,是爬虫开发中不可或缺的工具。在实际应用中,需注意以下几点:
- 优先使用非贪婪匹配(
.*?)避免匹配多余内容; - 编译正则表达式(
re.compile())提升重复匹配效率; - 复杂场景分步匹配,先提取整体片段再拆分细节;
- 结合结构化解析工具使用,扬长避短;
- 做好容错处理,避免正则匹配失败导致程序崩溃。
至此,Python 爬虫核心的数据提取技术(requests 请求、BeautifulSoup/XPath 结构化解析、正则表达式非结构化解析)已全部讲解完毕。掌握这些技术后,可应对绝大多数爬虫开发场景,后续可进一步学习反爬策略、分布式爬虫、数据存储等进阶内容,构建完整的爬虫技术体系。