正则表达式在爬虫中的应用

网络爬虫专题 · 掌握正则表达式提取数据

专题:Python网络爬虫系统学习

关键词:Python, 网络爬虫, 正则表达式, re模块, findall, 匹配模式, 数据提取, 文本清洗

一、正则表达式概述

1.1 什么是正则表达式

正则表达式(Regular Expression,简称 regex 或 RE)是一种用于描述字符串匹配模式的表达式。它使用单个字符串来描述和匹配一系列符合某个句法规则的字符串。正则表达式最初由数学家在 20 世纪 50 年代提出,随后被引入到 Unix 工具(如 grep、sed、awk)中,如今几乎所有主流编程语言都内置了正则表达式的支持。

正则表达式的核心价值在于:它提供了一种高度简洁和强大的方式来处理文本。对于爬虫开发人员来说,正则表达式是必不可少的工具——无论使用 Requests 直接获取 HTML 内容后手动解析,还是使用 BeautifulSoup 等高级解析库,正则都在数据清洗和精确提取环节发挥着关键作用。

1.2 爬虫中正则的三大用途

第一,提取特定格式数据。网页中往往包含大量结构化数据:URL 链接、邮箱地址、电话号码、日期时间、价格信息等。这些数据遵循固定的格式模式,非常适合用正则表达式直接抓取。

第二,文本清洗。从网页抓取的原始内容通常包含大量噪声:HTML 标签、JavaScript 代码、CSS 样式、多余的空白字符等。正则表达式可以高效地移除这些干扰项,保留有价值的文本内容。

第三,数据验证。在爬虫采集到数据后,往往需要验证数据的格式是否符合预期。例如验证邮箱格式是否正确、手机号是否合法、URL 是否完整等。正则表达式提供了标准的验证手段,避免无效数据进入后续处理流程。

1.3 Python re 模块简介

Python 的 re 模块是标准库中用于正则表达式操作的核心模块,无需额外安装即可使用。re 模块提供了丰富的函数接口,支持从简单的字符串匹配到复杂的模式搜索和替换。它使用 NFA(非确定型有限自动机)算法实现,功能强大且性能优秀。使用时只需在代码开头执行 import re 即可引入该模块。

import re # 导入正则模块,Python 内置,无需 pip 安装

re 模块的设计遵循 "先编译、后使用" 和 "直接使用" 两种模式。在需要多次使用同一正则表达式的场景下,建议先编译以提高性能,后面会详细讨论。

二、re 模块核心函数

2.1 re.match() — 从字符串开头匹配

re.match() 尝试从字符串的起始位置匹配一个模式。如果起始位置匹配成功,则返回一个 Match 对象;如果起始位置不匹配,则返回 None。注意:match 只检查字符串的开头,即使模式在其他位置也能匹配,只要开头不匹配就返回空。

import re result = re.match(r'\d+', '2026年是蛇年') if result: print(result.group()) # 输出:2026 # match 从头匹配,以下匹配失败 result2 = re.match(r'蛇年', '2026年是蛇年') # 返回 None

2.2 re.search() — 搜索第一个匹配

re.search() 扫描整个字符串并返回第一个成功的匹配。与 match 不同,search 不限制从开头匹配,它会在字符串中任意位置查找第一个符合模式的子串。找到后立即返回 Match 对象,不再继续向后搜索。

import re text = '联系方式:13800138000,备用:13912345678' phone = re.search(r'1[3-9]\d{9}', text) if phone: print(phone.group()) # 输出:13800138000(只返回第一个匹配) # 要获取所有匹配,需使用 findall 或 finditer

2.3 re.findall() — 查找所有匹配(返回列表)

re.findall() 是爬虫开发中使用频率最高的函数。它会搜索整个字符串,以列表形式返回所有不重叠的匹配结果。如果模式中包含分组,则返回元组组成的列表;如果包含多个分组,每个匹配返回一个元组。

import re html = '价格:¥29.9,原价:¥99.0,折扣价:¥19.9' prices = re.findall(r'¥(\d+\.?\d*)', html) print(prices) # 输出:['29.9', '99.0', '19.9'] # 多分组情况:提取标签名和属性 tags = re.findall(r'<(\w+)([^>]*)>', '') print(tags) # 输出:[('div', ' class="main"'), ('a', ' href="/"')]

2.4 re.finditer() — 查找所有匹配(返回迭代器)

re.finditer() 与 findall 功能类似,但它返回一个迭代器,生成 Match 对象而非字符串。当匹配结果非常多时,迭代器模式可以节省内存,因为不需要一次性将所有结果加载到内存中。同时,通过 Match 对象可以获取分组详情、匹配位置等信息。

import re text = '苹果 12元,香蕉 5元,橘子 8元' for match in re.finditer(r'(\w+)\s+(\d+)元', text): print(f'商品:{match.group(1)},价格:{match.group(2)}元') # 输出: # 商品:苹果,价格:12元 # 商品:香蕉,价格:5元 # 商品:橘子,价格:8元

2.5 re.sub() — 替换匹配的文本

re.sub() 用于替换字符串中所有匹配正则表达式的子串。它接收三个主要参数:模式、替换字符串、原始字符串。替换字符串中可以使用 \1、\2 等反向引用,也可使用命名分组 \g<name>。该函数在爬虫数据清洗阶段非常实用。

import re # 去除 HTML 标签 html = '<p>这是一段<strong>重要</strong>文本</p>' clean_text = re.sub(r'<[^>]+>', '', html) print(clean_text) # 输出:这是一段重要文本 # 格式化手机号:138 0013 8000 phone = '13800138000' formatted = re.sub(r'(\d{3})(\d{4})(\d{4})', r'\1 \2 \3', phone) print(formatted) # 输出:138 0013 8000

2.6 re.split() — 按模式分割字符串

re.split() 根据正则表达式的匹配结果来分割字符串,返回分割后的列表。相比字符串自带的 split() 方法,re.split() 可以按更复杂的模式进行分割,例如按多个标点符号或按空白字符分割。

import re # 按标点符号和空白分割 text = 'Python,Java;JavaScript C++ Rust' langs = re.split(r'[,;.\s]+', text) print(langs) # 输出:['Python', 'Java', 'JavaScript', 'C++', 'Rust'] # 使用分组保留分隔符 text2 = '第1章 基础 第2章 进阶 第3章 高级' parts = re.split(r'(第\d章)', text2) print(parts) # 输出:['', '第1章', ' 基础 ', '第2章', ' 进阶 ', '第3章', ' 高级']

2.7 re.compile() — 编译正则,提高性能

re.compile() 将正则表达式的字符串形式编译成一个 Pattern 对象。编译后的 Pattern 对象拥有 match()、search()、findall() 等相同方法。当同一个正则表达式需要被多次使用时,预编译可以显著提升性能,因为正则的编译过程只需执行一次。此外,编译时还可以指定 flags 参数(如 re.IGNORECASE 忽略大小写)。

import re # 编译正则表达式(推荐在循环或多次使用时采用) pattern = re.compile(r'https?://[^\s\'"<>]+') urls = pattern.findall(long_html_text) # 编译时指定 flag:忽略大小写 pattern_ci = re.compile(r'python', re.IGNORECASE) print(pattern_ci.findall('Python python PYTHON')) # 输出:['Python', 'python', 'PYTHON']

三、正则语法基础

3.1 元字符速查表

元字符是正则表达式中具有特殊含义的字符。掌握它们是使用正则表达式的第一步。以下是 Python re 模块中常用的元字符:

元字符含义示例
.匹配除换行符以外的任意单个字符a.b 匹配 "acb"、"a b" 但不匹配 "ab"
^匹配字符串的开头^Python 匹配以 Python 开头的字符串
$匹配字符串的结尾com$ 匹配以 com 结尾的字符串
*匹配前一个字符 0 次或多次ab*c 匹配 "ac"、"abc"、"abbc" 等
+匹配前一个字符 1 次或多次ab+c 匹配 "abc"、"abbc" 但不匹配 "ac"
?匹配前一个字符 0 次或 1 次ab?c 匹配 "ac"、"abc" 但不匹配 "abbc"
{n}匹配前一个字符恰好 n 次\d{4} 匹配 4 位数字,如 "2026"
{n,}匹配前一个字符至少 n 次\d{3,} 匹配至少 3 位数字
{n,m}匹配前一个字符 n 到 m 次\d{2,4} 匹配 2 到 4 位数字
\转义字符\. 匹配点号本身,\\匹配反斜杠
|或操作cat|dog 匹配 "cat" 或 "dog"
(...)分组捕获(\d{3})-(\d{4}) 捕获两组数字
[...]字符集合[aeiou] 匹配任意一个元音字母
[^...]否定字符集合[^0-9] 匹配任意非数字字符

3.2 字符类详解

字符类用于表示一类字符的集合,是正则中非常实用的特性。Python re 模块提供了多种快捷字符类:

在爬虫开发中,最常用的字符类是 \d(提取数字)、\s(处理空白)以及自定义字符类如 [一-鿿](提取中文字符)。

3.3 量词与贪婪匹配

量词用于指定前一个字符或分组出现的次数。Python re 模块默认使用贪婪匹配模式,即量词会尽可能多地匹配字符。例如,模式 <.+> 在字符串 "<div>内容</div>" 中会匹配整个 "<div>内容</div>",而不是只匹配 "<div>"。

在量词后面加上问号即可切换为懒惰匹配模式(非贪婪模式),使量词尽可能少地匹配字符:

import re text = '<div>内容1</div><div>内容2</div>' # 贪婪模式:匹配尽可能多的字符 greedy = re.findall(r'<div>(.*)</div>', text) print(greedy) # 输出:['内容1</div><div>内容2'] # 懒惰模式:匹配尽可能少的字符 lazy = re.findall(r'<div>(.*?)</div>', text) print(lazy) # 输出:['内容1', '内容2'](这才是我们想要的)

常见的懒惰量词包括 *?(0 次或多次,尽可能少)、+?(1 次或多次,尽可能少)、??(0 次或 1 次,尽可能少)、{n,m}?(n 到 m 次,尽可能少)。在爬虫中提取 HTML 内容时,几乎总是使用懒惰匹配来避免过度匹配。

3.4 分组与捕获

分组使用圆括号 () 将正则表达式的一部分括起来,实现多个维度的功能:

import re url = 'https://example.com/articles/12345?page=2' # 命名分组示例 pattern = re.compile(r'https?://(?P<domain>[^/]+)/(?P<path>\S+)') match = pattern.search(url) if match: print(match.group('domain')) # 输出:example.com print(match.group('path')) # 输出:articles/12345?page=2

四、爬虫常用正则模式

以下是网络爬虫开发中使用频率最高的正则表达式模式集合,熟练掌握这些模式可以大幅提高数据提取效率。

4.1 提取 URL 链接

网页中 URL 的格式较为固定,通常以 http:// 或 https:// 开头,不包含空白字符和引号。以下模式可以匹配绝大多数 URL:

import re html = '<a href="https://example.com/page?q=1">链接1</a>' url_pattern = re.compile(r'https?://[^\s\'"<>]+') urls = url_pattern.findall(html) print(urls) # 输出:['https://example.com/page?q=1']

4.2 提取邮箱地址

邮箱地址的格式为 用户名@域名.后缀。用户名部分允许字母、数字、点号、下划线、百分号、加号和减号:

import re text = '请联系 support@example.com 或 admin@test.org.cn' email_pattern = re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}') emails = email_pattern.findall(text) print(emails) # 输出:['support@example.com', 'admin@test.org.cn']

4.3 提取手机号码

中国大陆手机号以 1 开头,第二位为 3-9 中的任意数字,后面紧跟 9 位数字:

import re text = '联系电话:13812345678,备用:15987654321' phone_pattern = re.compile(r'1[3-9]\d{9}') phones = phone_pattern.findall(text) print(phones) # 输出:['13812345678', '15987654321']

4.4 提取数字(整数和小数)

在爬取商品信息、价格数据等场景中,提取数字是最常见的需求之一:

import re text = '价格¥29.90,销量 1586 件,评分 4.8 分' num_pattern = re.compile(r'\d+\.?\d*') numbers = num_pattern.findall(text) print(numbers) # 输出:['29.90', '1586', '4.8']

4.5 提取中文字符

在处理中文网页时,常常需要单独提取汉字部分以过滤掉英文、数字和标点:

import re text = 'Python爬虫2026教程——从入门到精通' chinese = re.findall(r'[一-鿿]+', text) print(chinese) # 输出:['爬虫', '教程', '从入门到精通']

4.6 提取 HTML 标签内容

在没有使用 BeautifulSoup 的情况下,可以直接用正则从 HTML 中提取特定标签内的文本。注意使用懒惰匹配以防止跨标签捕获:

import re html = '<h1>标题内容</h1><p>段落文本</p><p>另一段</p>' content_pattern = re.compile(r'<h1>(.*?)</h1>') headers = content_pattern.findall(html) print(headers) # 输出:['标题内容'] # 提取所有 <p> 标签内容 p_pattern = re.compile(r'<p>(.*?)</p>') paragraphs = p_pattern.findall(html) print(paragraphs) # 输出:['段落文本', '另一段']

4.7 提取 JSON 中的指定字段值

当爬虫获取到 JSON 格式的响应数据时(许多网站 API 返回 JSON),可以使用正则快速提取特定字段的值,避免完整解析 JSON 的开销:

import re json_data = '{"name": "张三", "email": "zhangsan@example.com", "age": 28}' # 提取 name 字段的值 name = re.search(r'"name"\s*:\s*"([^"]+)"', json_data) if name: print(name.group(1)) # 输出:张三

五、正则与 BeautifulSoup 结合

5.1 在 find_all() 中使用正则

BeautifulSoup 的 find_all() 方法允许传入正则表达式作为参数。当需要查找所有符合特定模式 class 属性或 id 属性的标签时,这种方式非常高效:

import re from bs4 import BeautifulSoup html = ''' <div class="product-price">¥29.99</div> <div class="product-name">商品名称</div> <div class="product-price sale">¥19.99</div> ''' soup = BeautifulSoup(html, 'html.parser') # 查找所有 class 中包含 "price" 的标签 price_tags = soup.find_all(class_=re.compile(r'price')) for tag in price_tags: print(tag.text) # 输出: # ¥29.99 # ¥19.99 # 查找所有 id 以 "product" 开头的标签 product_tags = soup.find_all(id=re.compile(r'^product'))

5.2 在 text 参数中使用正则

BeautifulSoup 的 text 参数(在最新版本中更名为 string)同样支持正则表达式,用于按文本内容匹配标签:

import re from bs4 import BeautifulSoup html = ''' <ul> <li>苹果 10元</li> <li>香蕉 5元</li> <li>葡萄 15元</li> <li>测试</li> </ul> ''' soup = BeautifulSoup(html, 'html.parser') # 查找所有文本中包含数字和"元"的 li 标签 items = soup.find_all('li', text=re.compile(r'\d+元')) for item in items: print(item.text) # 输出: # 苹果 10元 # 香蕉 5元 # 葡萄 15元

5.3 在 CSS 选择器中使用正则

BeautifulSoup 的 select() 方法虽然不支持直接在 CSS 选择器中使用正则,但可以结合 has-class 检测或其他属性匹配技巧来变通实现。此外,通过 select() 获取标签集合后,再配合正则进行二次筛选是一种常见的组合用法:

import re from bs4 import BeautifulSoup html = ''' <div class="article-2026">2026年文章</div> <div class="article-2025">2025年文章</div> <div class="note">笔记</div> ''' soup = BeautifulSoup(html, 'html.parser') # 用 select 获取所有 div,再用正则筛选 all_divs = soup.select('div') year_divs = [div for div in all_divs if re.search(r'article-\d{4}', div.get('class', ''))] for div in year_divs: print(div.text) # 输出: # 2026年文章 # 2025年文章

六、实战示例

6.1 提取页面所有图片 URL

爬取网页中的图片 URL 是最常见的任务之一。图片链接通常出现在 <img> 标签的 src 属性中,也可能在 data-src 或 data-original 等延迟加载属性中:

import re import requests # 获取页面内容(示例用静态字符串替代) html = ''' <img src="https://example.com/images/photo1.jpg" alt="照片1"> <img data-src="https://example.com/images/photo2.jpg" class="lazy"> <img src="https://example.com/icons/icon.png"> ''' # 提取所有 src 或 data-src 属性中的图片 URL img_pattern = re.compile(r'(?:src|data-src)=["\']([^"\']+\.(?:jpg|png|gif|webp))["\']', re.IGNORECASE) img_urls = img_pattern.findall(html) for url in img_urls: print(url) # 输出: # https://example.com/images/photo1.jpg # https://example.com/images/photo2.jpg # https://example.com/icons/icon.png

6.2 提取文章发布时间

新闻网站和博客通常会以不同格式在页面中标注发布时间。以下模式可以匹配常见的日期时间格式:

import re html = ''' <span class="date">2026-05-01</span> <time datetime="2026-05-01T10:30:00+08:00">2026年5月1日</time> <div class="publish">2026/05/01 10:30</div> ''' # 匹配多种日期格式 date_pattern = re.compile( r'\d{4}[-/年]\d{1,2}[-/月]\d{1,2}' ) dates = date_pattern.findall(html) print(dates) # 输出:['2026-05', '2026-05', '2026/05/01'] # 更精确的模式:匹配完整日期 full_date_pattern = re.compile( r'\d{4}[-/年]\d{1,2}[-/月]\d{1,2}[日]?\s*\d{1,2}:\d{2}' )

6.3 提取价格和数字

电商爬虫中提取价格信息是核心需求。价格常常带有货币符号,可能包含千分位分隔符:

import re html = ''' <span class="price">¥1,299.00</span> <span class="old-price">¥1,599.00</span> <span class="discount">-18%</span> ''' # 提取所有带 ¥ 符号的价格 price_pattern = re.compile(r'¥([\d,]+\.\d{2})') prices = price_pattern.findall(html) print(prices) # 输出:['1,299.00', '1,599.00'] # 去除千分位逗号并转为浮点数 clean_prices = [float(p.replace(',', '')) for p in prices] print(clean_prices) # 输出:[1299.0, 1599.0]

6.4 清洗 HTML 文本

从网页抓取的文本经常混杂着 HTML 标签、多余的空白字符、JavaScript 代码等。使用正则表达式可以高效地完成一系列清洗操作:

import re def clean_html_text(html): """清洗 HTML 文本,返回纯文本""" # 1. 移除 JavaScript 代码块 text = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.DOTALL | re.IGNORECASE) # 2. 移除 CSS 样式块 text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL | re.IGNORECASE) # 3. 移除 HTML 标签 text = re.sub(r'<[^>]+>', '', text) # 4. 将 HTML 实体转换为对应字符 text = re.sub(r'&', '&', text) text = re.sub(r'<', '<', text) text = re.sub(r'>', '>', text) text = re.sub(r' ', ' ', text) text = re.sub(r'"', '"', text) # 5. 将连续空白(包括换行)压缩为单个空格 text = re.sub(r'\s+', ' ', text) # 6. 去除首尾空白 return text.strip() # 测试 dirty_html = ''' <div> <h1>标题</h1> <p>这是一段<strong>文本</strong>。</p> <script>alert('hello')</script> </div> ''' clean = clean_html_text(dirty_html) print(clean) # 输出:标题 这是一段文本。

七、正则性能优化

7.1 预编译正则表达式

在爬虫中,同一个正则表达式可能需要对成千上万个页面重复使用。此时预编译(re.compile)可以带来显著的性能提升。编译后的 Pattern 对象内部已经将正则字符串解析为有限状态机,后续每次调用都省去了编译步骤:

import re # 不推荐:每次循环都重新编译 def extract_urls_slow(html_list): url_list = [] for html in html_list: urls = re.findall(r'https?://[^\s\'"<>]+', html) url_list.extend(urls) return url_list # 推荐:预编译一次,多次使用 URL_PATTERN = re.compile(r'https?://[^\s\'"<>]+') def extract_urls_fast(html_list): url_list = [] for html in html_list: urls = URL_PATTERN.findall(html) url_list.extend(urls) return url_list # 在大型爬虫项目中,预编译可节省 30%-50% 的正则执行时间

7.2 避免回溯灾难

正则引擎在匹配失败时会进行回溯——尝试所有可能的路径。某些模式会导致指数级的回溯次数,严重时甚至导致程序挂起(称为"灾难性回溯"或 Catastrophic Backtracking)。

常见的危险模式:嵌套量词如 (a+)+b、使用贪婪量词匹配可选内容、过多的分支结构。以下是一些优化建议:

# 危险:灾难性回溯模式 # 如果输入较长且不匹配,以下模式可能导致 CPU 100% dangerous = re.compile(r'(\d+)+!') # 安全:明确指定匹配范围 safe = re.compile(r'\d+!') # 另一个危险模式:嵌套贪婪 dangerous2 = re.compile(r'<div>(.*)*</div>') # 安全方案 safe2 = re.compile(r'<div>(.*?)</div>')

7.3 使用原始字符串

Python 中定义正则表达式时务必使用原始字符串(在字符串前加 r 前缀),如 r'\d+'。原始字符串会取消反斜杠的转义处理,避免正则表达式中的转义字符被 Python 字符串解析器先行处理:

# 不推荐:使用普通字符串 pattern1 = re.compile('\\d+\\.\\d+') # 需要双重转义,容易出错 # 推荐:使用原始字符串(r 前缀) pattern2 = re.compile(r'\d+\.\d+') # 清晰直观 # 文件名示例 # 不写 r 前缀时,\s 会被 Python 解释为转义序列 # 写了 r 前缀后,\s 原样传递给 re 模块 # 匹配反斜杠本身 # 普通字符串需要 4 个反斜杠 bad = re.compile('\\\\\\\\') # 原始字符串只需 2 个 good = re.compile(r'\\\\')

7.4 合理选择正则 vs 其他方法

虽然正则表达式功能强大,但并非所有场景都适合使用。在实际的爬虫项目中,应根据具体情况选择最合适的工具:

总结来说,正则表达式的定位是"精确的瑞士军刀"——处理精确格式的数据时无出其右者,但不适合作为解析复杂文档结构的重型工具。在爬虫项目中,推荐采用"解析库 + 正则"的组合策略:使用 BeautifulSoup 或 lxml 解析文档结构,使用正则提取结构内部的格式化数据。

核心要点总结:

1. 正则表达式是爬虫工程师必须掌握的核心技能,在数据提取、清洗、验证三个环节中都有应用。

2. re 模块的 findall 是爬虫中最常用的函数,search 用于首次匹配,sub 用于替换清洗。

3. 懒惰匹配 .*? 在爬虫场景中远优于贪婪匹配 .*,能有效避免跨标签匹配。

4. 爬虫中最常用的模式:URL 提取、邮箱/手机号提取、数字与价格提取、中文字符提取。

5. 正则与 BeautifulSoup 结合使用可以发挥各自优势——Soup 负责结构解析,正则负责精确提取。

6. 性能优化三原则:预编译避免重复编译、使用原始字符串避免转义混淆、避免嵌套量词防止回溯灾难。

7. 不滥用正则——处理 HTML 结构应优先使用专用解析器,正则只适合提取特定格式的文本数据。