4.1 re 模块基础:search、match、findall
正则表达式就像爬虫界的瑞士军刀,虽然 BeautifulSoup 更适合处理结构化 HTML,但面对一些非结构化文本或需要快速提取特定模式的内容时,re 模块就派上大用场了。Python 的 re 模块提供了强大的正则功能,其中最常用的三个方法是 search、match 和 findall。
search 方法会在整个字符串中搜索第一个匹配项,match 则只从字符串开头匹配,而 findall 会返回所有匹配结果的列表。这三个方法各有千秋,选择哪个取决于你的具体需求。
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 搜索首个匹配 | re.search(pattern, string) | 在整个字符串中查找第一个匹配项,返回 Match 对象或 None |
| 开头匹配 | re.match(pattern, string) | 仅在字符串开头匹配,返回 Match 对象或 None |
| 查找所有匹配 | re.findall(pattern, string) | 返回所有匹配项的列表,无匹配时返回空列表 |
下面是一个完整的示例,展示如何使用这三个方法:
# 导入 re 模块用于正则表达式操作
import re
# 定义测试字符串,包含电话号码和邮箱
test_text = "联系电话:138-1234-5678,客服邮箱:support@example.com,备用电话:010-87654321"
try:
# 使用 search 查找第一个电话号码(匹配数字和连字符的组合)
phone_search = re.search(r'\d{3}-\d{4}-\d{4}|\d{3}-\d{7}', test_text)
if phone_search:
# 获取匹配的完整字符串
first_phone = phone_search.group()
print(f"找到的第一个电话号码: {first_phone}")
else:
print("未找到电话号码")
# 使用 match 尝试从开头匹配(这里会失败,因为开头不是电话号码)
phone_match = re.match(r'\d{3}-\d{4}-\d{4}', test_text)
if phone_match:
print(f"开头匹配的电话号码: {phone_match.group()}")
else:
print("开头没有匹配的电话号码")
# 使用 findall 查找所有邮箱地址
email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
all_emails = re.findall(email_pattern, test_text)
print(f"找到的所有邮箱: {all_emails}")
# 使用 findall 查找所有电话号码(包括两种格式)
phone_pattern = r'\d{3}-\d{4}-\d{4}|\d{3}-\d{7}'
all_phones = re.findall(phone_pattern, test_text)
print(f"找到的所有电话号码: {all_phones}")
except re.error as e:
# 捕获正则表达式语法错误
print(f"正则表达式错误: {e}")
except Exception as e:
# 捕获其他可能的异常
print(f"发生错误: {e}")注意事项:
- 正则表达式中的特殊字符需要正确转义,避免语法错误
- search 和 match 返回的是 Match 对象,需要调用 group() 方法获取匹配内容
- findall 直接返回字符串列表,使用更简单但功能相对有限
- 复杂的正则表达式建议先在在线工具中测试验证
这节讲了 re 模块的三个核心方法:search、match 和 findall,它们分别适用于不同的匹配场景。掌握这些基础方法能让你在处理文本提取任务时更加得心应手。
4.2 常用模式:匹配数字、链接、邮箱、电话
在爬虫实战中,我们经常需要从网页中提取特定类型的数据,比如价格、网址、联系方式等。这时候就需要掌握一些常用的正则表达式模式。虽然每个网站的格式可能略有不同,但掌握通用模式能让你快速上手。
数字匹配是最基础的需求,从简单的整数到带小数点的价格;链接匹配要处理各种协议和域名格式;邮箱和电话的格式相对标准化,但也存在多种变体。关键是理解模式的构建逻辑,而不是死记硬背。
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 匹配整数/浮点数 | re.findall(r'\d+.?\d*', text) | 匹配整数和小数,注意可能匹配到日期等非目标数字 |
| 匹配URL链接 | re.findall(r'https?😕/[^\s<>"{} | ^`$$$$]+', text) |
| 匹配邮箱地址 | re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}', text) | 标准邮箱格式,支持常见的特殊字符 |
| 匹配电话号码 | re.findall(r'1[3-9]\d | \d{3,4}-\d{7,8}', text) |
下面是一个实用的示例,展示如何从混合文本中提取各种数据:
# 导入 re 模块进行正则匹配
import re
# 模拟从网页提取的混合文本内容
web_content = """
商品价格:¥299.99,原价:599元
官方网站:https://www.example-shop.com
客服邮箱:service@store-example.com
联系电话:13800138000 或 010-12345678
产品链接:http://product.example.com/item?id=12345
"""
try:
# 提取价格(匹配货币符号后的数字)
price_pattern = r'[¥$€]?\s*(\d+\.?\d*)'
prices = re.findall(price_pattern, web_content)
print(f"提取的价格: {prices}")
# 提取所有URL链接(支持http和https)
url_pattern = r'https?://[^\s<>"{}|\\^`$$$$]+'
urls = re.findall(url_pattern, web_content)
print(f"提取的链接: {urls}")
# 提取邮箱地址(标准格式)
email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
emails = re.findall(email_pattern, web_content)
print(f"提取的邮箱: {emails}")
# 提取电话号码(手机号11位,固话带区号)
phone_pattern = r'1[3-9]\d{9}|\d{3,4}-\d{7,8}'
phones = re.findall(phone_pattern, web_content)
print(f"提取的电话: {phones}")
# 提取纯数字(可能包含不需要的数据,需谨慎使用)
all_numbers = re.findall(r'\d+\.?\d*', web_content)
print(f"所有数字: {all_numbers}")
except re.error as e:
# 处理正则表达式错误
print(f"正则表达式错误: {e}")
except Exception as e:
# 处理其他异常
print(f"提取过程中出错: {e}")注意事项:
- 价格匹配可能误匹配到其他数字,最好结合上下文验证
- URL 模式可能无法覆盖所有特殊情况,如包含中文字符的链接
- 电话号码格式因地区而异,实际项目中需要根据目标地区调整
- 提取的数据通常需要进一步清洗和验证,不能直接使用
这节介绍了四种常用数据类型的正则匹配模式,掌握了这些模式就能应对大部分基础的数据提取需求。记住,正则表达式是工具,关键是要理解其原理并根据实际情况调整。
4.3 分组提取与命名捕获
当需要从复杂文本中提取多个相关字段时,简单的匹配就不够用了。这时候就要用到正则表达式的分组功能。分组就像给正则表达式的不同部分贴上标签,让我们能够分别获取各个部分的内容。
普通分组使用圆括号 (),通过 group(1)、group(2) 等方法按顺序获取;命名分组则使用 (?P...) 语法,可以通过名称来访问对应的匹配内容。命名分组让代码更具可读性,特别是在处理复杂模式时。
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 普通分组提取 | match.group(1), match.group(2) | 按分组顺序提取,group(0) 是完整匹配 |
| 命名分组提取 | match.group('name') | 通过分组名称提取,代码更易维护 |
| 获取所有分组 | match.groups() | 返回所有分组的元组 |
| 获取命名分组字典 | match.groupdict() | 返回命名分组的字典 |
下面是一个详细的示例,展示如何使用分组提取结构化数据:
# 导入 re 模块用于正则分组操作
import re
# 模拟日志文本,包含时间、IP、状态码等信息
log_entry = "2023-12-25 14:30:22 - 192.168.1.100 - GET /api/users - 200 - 125ms"
try:
# 使用普通分组提取日志各字段
log_pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) - (\d+\.\d+\.\d+\.\d+) - (\w+) ([^ ]+) - (\d{3}) - (\d+)ms'
log_match = re.search(log_pattern, log_entry)
if log_match:
# 按顺序提取各分组内容
date = log_match.group(1)
time = log_match.group(2)
ip = log_match.group(3)
method = log_match.group(4)
endpoint = log_match.group(5)
status = log_match.group(6)
response_time = log_match.group(7)
print(f"普通分组提取结果:")
print(f"日期: {date}, 时间: {time}, IP: {ip}")
print(f"方法: {method}, 接口: {endpoint}, 状态: {status}, 响应时间: {response_time}ms")
# 使用命名分组提取相同信息(更清晰的方式)
named_log_pattern = r'(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) - (?P<ip>\d+\.\d+\.\d+\.\d+) - (?P<method>\w+) (?P<endpoint>[^ ]+) - (?P<status>\d{3}) - (?P<response_time>\d+)ms'
named_match = re.search(named_log_pattern, log_entry)
if named_match:
# 通过名称提取分组内容
log_dict = named_match.groupdict()
print(f"\n命名分组提取结果:")
for key, value in log_dict.items():
print(f"{key}: {value}")
# 也可以单独访问特定字段
print(f"\n单独访问: 状态码={named_match.group('status')}, IP={named_match.group('ip')}")
# 批量处理多条日志
multiple_logs = [
"2023-12-25 14:30:22 - 192.168.1.100 - GET /api/users - 200 - 125ms",
"2023-12-25 14:31:05 - 10.0.0.50 - POST /api/login - 401 - 89ms",
"2023-12-25 14:32:18 - 172.16.0.25 - DELETE /api/users/123 - 204 - 67ms"
]
print(f"\n批量处理多条日志:")
for i, log in enumerate(multiple_logs, 1):
match = re.search(named_log_pattern, log)
if match:
print(f"日志{i}: {match.group('method')} {match.group('endpoint')} -> {match.group('status')}")
except re.error as e:
# 捕获正则表达式编译错误
print(f"正则表达式错误: {e}")
except AttributeError as e:
# 捕获匹配失败导致的属性错误
print(f"匹配失败,无法提取分组: {e}")
except Exception as e:
# 捕获其他异常
print(f"处理过程中出错: {e}")注意事项:
- 分组索引从1开始,group(0) 始终是完整匹配
- 命名分组的名称必须是有效的Python标识符
- 如果某个分组没有匹配到内容,对应位置会是 None
- 复杂的分组嵌套可能导致索引混乱,建议优先使用命名分组
这节讲解了正则表达式的分组提取功能,包括普通分组和命名分组两种方式。分组让正则表达式不仅能匹配,还能结构化地提取数据,大大提升了数据处理的效率和代码的可维护性。
4.4 正则 vs 解析器:何时该用哪种方式
在爬虫开发中,经常会面临一个选择:用正则表达式还是用 HTML 解析器(如 BeautifulSoup)来提取数据?这个问题困扰过很多初学者。其实两者各有优势,关键是要根据具体情况选择合适的工具。
正则表达式适合处理简单、规则的文本模式,速度快且内存占用少;而解析器适合处理复杂的 HTML 结构,能准确理解标签层次关系,容错性强。如果强行用正则解析复杂的 HTML,可能会写出又长又脆弱的表达式。
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 正则适用场景 | re.findall(pattern, text) | 简单文本模式、固定格式数据、性能要求高 |
| 解析器适用场景 | soup.find_all('tag', class_='class') | 复杂HTML结构、需要理解DOM层次、标签嵌套深 |
| 混合使用策略 | 先解析后正则 | 先用解析器定位大致区域,再用正则提取精确内容 |
下面通过对比示例来展示两种方法的差异和适用场景:
# 导入必要的模块
import re
from bs4 import BeautifulSoup
# 模拟复杂的HTML内容
html_content = """
<div class="product-list">
<div class="product-item" data-id="1001">
<h3 class="product-name">iPhone 15 Pro</h3>
<p class="price">价格: ¥8999.00</p>
<p class="stock">库存: 25件</p>
<div class="rating">
<span class="stars">★★★★☆</span>
<span class="count">(128人评价)</span>
</div>
</div>
<div class="product-item" data-id="1002">
<h3 class="product-name">Samsung Galaxy S24</h3>
<p class="price">价格: ¥6999.00</p>
<p class="stock">库存: 0件</p>
<div class="rating">
<span class="stars">★★★☆☆</span>
<span class="count">(89人评价)</span>
</div>
</div>
</div>
"""
try:
print("=== 方案1: 纯正则表达式提取 ===")
# 尝试用正则提取产品信息(复杂且容易出错)
try:
# 这个正则表达式又长又难维护
product_pattern = r'<div class="product-item"[^>]*data-id="(\d+)"[^>]*>.*?<h3[^>]*>(.*?)</h3>.*?价格: ¥([\d.]+).*?库存: (\d+)件.*?★{2,5}.*?$(\d+)人评价$'
products_regex = re.findall(product_pattern, html_content, re.DOTALL)
for pid, name, price, stock, rating_count in products_regex:
print(f"ID: {pid}, 名称: {name}, 价格: {price}, 库存: {stock}, 评价数: {rating_count}")
except Exception as e:
print(f"正则方案失败: {e}")
print("\n=== 方案2: BeautifulSoup解析器提取 ===")
# 使用BeautifulSoup解析HTML结构
soup = BeautifulSoup(html_content, 'html.parser')
product_items = soup.find_all('div', class_='product-item')
for item in product_items:
try:
# 安全地提取各个字段
product_id = item.get('data-id', '未知')
name_elem = item.find('h3', class_='product-name')
name = name_elem.get_text() if name_elem else '未知'
price_elem = item.find('p', class_='price')
price_text = price_elem.get_text() if price_elem else ''
# 在已定位的文本中使用简单正则
price_match = re.search(r'¥([\d.]+)', price_text)
price = price_match.group(1) if price_match else '0.00'
stock_elem = item.find('p', class_='stock')
stock_text = stock_elem.get_text() if stock_elem else ''
stock_match = re.search(r'(\d+)件', stock_text)
stock = stock_match.group(1) if stock_match else '0'
rating_elem = item.find('span', class_='count')
rating_text = rating_elem.get_text() if rating_elem else ''
rating_match = re.search(r'(\d+)人评价', rating_text)
rating_count = rating_match.group(1) if rating_match else '0'
print(f"ID: {product_id}, 名称: {name}, 价格: {price}, 库存: {stock}, 评价数: {rating_count}")
except AttributeError as e:
print(f"解析单个产品时出错: {e}")
except Exception as e:
print(f"处理产品项时发生错误: {e}")
print("\n=== 方案3: 简单文本用正则,复杂结构用解析器 ===")
# 示例:从纯文本中提取邮箱(正则更合适)
plain_text = "联系我们: support@company.com 或 sales@business.org"
emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', plain_text)
print(f"从纯文本提取邮箱: {emails}")
# 示例:从HTML中提取链接(解析器更合适)
links = soup.find_all('a', href=True)
print(f"从HTML提取链接数量: {len(links)} (本例中无链接)")
except Exception as e:
print(f"整体处理过程中出错: {e}")注意事项:
- 不要试图用正则表达式解析复杂的 HTML/XML,这是反模式
- 对于简单的、格式固定的文本数据,正则表达式效率更高
- 最佳实践通常是:用解析器处理 HTML 结构,用正则处理提取后的文本内容
- 性能敏感的场景可以考虑正则,但要确保模式的健壮性
这节对比了正则表达式和 HTML 解析器的适用场景,核心原则是:结构化数据用解析器,简单文本模式用正则。明智地选择工具能让代码既高效又易于维护。