7.1 为什么要学习正则表达式
7.1.1 现实中的问题
想象一下这些场景:
- 从一段文字中找出所有的电话号码
- 验证用户输入的邮箱格式是否正确
- 从日志文件中提取错误信息
- 清理文本中的多余空格和标点
这些任务如果用普通的字符串方法会很麻烦,但用正则表达式就很简单!
7.1.2 正则表达式能做什么
- 查找:在文本中搜索特定模式的字符串
- 验证:检查字符串是否符合特定格式
- 提取:从复杂文本中抽取出需要的信息
- 替换:将匹配的文本替换为其他内容
- 分割:按特定模式分割字符串
7.2 初识正则表达式
7.2.1 最简单的匹配:字面匹配
1
2
3
4
5
6
7
8
9
10
11
|
import re
# 示例1:查找包含"python"的文本
text = "我喜欢学习python编程,python很有趣"
result = re.findall("python", text)
print(result) # 输出: ['python', 'python']
# 示例2:查找精确匹配
text = "apple banana apple orange"
result = re.findall("apple", text)
print(result) # 输出: ['apple', 'apple']
|
小提示
re.findall() 会找到所有匹配的内容,返回一个列表
7.2.2 使用re.search()查找第一个匹配
1
2
3
4
5
6
7
8
|
import re
# 示例:查找第一个数字
text = "我的电话是123456,你的电话是789012"
match = re.search(r"\d+", text) # \d代表数字,+代表一个或多个
if match:
print("找到数字:", match.group()) # 输出: 找到数字: 123456
print("位置:", match.start(), "到", match.end()) # 输出: 位置: 5 到 11
|
【课堂练习】7.2.1 基础练习1:查找关键词
任务: 在下面的文本中查找所有"数据"这个词
1
2
|
text = "数据分析很重要,数据采集是第一步,数据处理需要技巧"
# 你的代码写在这里
|
要求:
- 使用
re.findall() 方法
- 输出找到的所有"数据"
预期输出:
7.3 基础元字符
7.3.1 字符类:匹配一组字符
| 元字符 |
说明 |
示例 |
匹配结果 |
[abc] |
匹配a、b或c |
[aeiou] |
匹配任意元音字母 |
[0-9] |
匹配0到9的数字 |
[0-9] |
匹配单个数字 |
[a-z] |
匹配小写字母 |
[a-z] |
匹配单个小写字母 |
[A-Z] |
匹配大写字母 |
[A-Z] |
匹配单个大写字母 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import re
# 示例1:匹配元音字母
text = "Hello World"
vowels = re.findall(r"[aeiou]", text, re.IGNORECASE) # 忽略大小写
print("元音字母:", vowels) # 输出: ['e', 'o', 'o']
# 示例2:匹配数字
text = "房间号是305,价格是299元"
numbers = re.findall(r"[0-9]", text)
print("单个数字:", numbers) # 输出: ['3', '0', '5', '2', '9', '9']
# 示例3:使用 [0-9]+ 匹配连续数字
numbers = re.findall(r'[0-9]+', text)
print("连续数字:", numbers) # 输出: ['305', '299']
|
【课堂练习】字符类匹配练习1:从文本中提取不同类型的信息
任务: 从文本中提取不同类型的信息
1
|
text = "产品型号:iPhone15 Pro,价格:$999,库存:256台,评分:A+级"
|
要求:
- 提取所有大写字母(使用
[A-Z])
- 提取所有小写字母(使用
[a-z])
- 提取所有数字(使用
[0-9])
- 提取产品名称中的字母(连续的字母字符)
预期输出:
1
2
3
4
5
|
大写字母: ['I', 'P', 'P', 'A']
小写字母: ['h', 'o', 'n', 'e', 'r', 'o']
单个数字: ['1', '5', '9', '9', '9', '2', '5', '6']
价格数字: ['999']
产品名称字母: ['iPhone', 'Pro']
|
请编写代码完成以上任务。
7.3.2 预定义字符类
| 字符类 |
等价于 |
说明 |
\d |
[0-9] |
数字 |
\D |
[^0-9] |
非数字 |
\w |
[a-zA-Z0-9_] |
单词字符 |
\W |
[^a-zA-Z0-9_] |
非单词字符 |
\s |
[ \t\n\r\f\v] |
空白字符 |
\S |
[^ \t\n\r\f\v] |
非空白字符 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import re
# 示例1:提取所有数字
text = "苹果5个,香蕉3根,橙子8个"
numbers = re.findall(r"\d", text)
print("数字:", numbers) # 输出: ['5', '3', '8']
# 示例2:提取所有单词
text = "Hello, 世界! How are you?"
words = re.findall(r"\w+", text)
print("单词:", words) # 输出: ['Hello', '世界', 'How', 'are', 'you']
# 示例3:找到所有空白位置
text = "Hello World\nPython"
spaces = re.findall(r"\s", text)
print("空白字符:", [repr(s) for s in spaces]) # 输出: [' ', ' ', ' ', '\n']
|
【课堂练习】7.3.1 基础练习2:提取信息
任务: 从文本中提取不同类型的信息
1
|
text = "订单号:A1001,价格:299元,数量:3件,总计:897元"
|
要求:
- 提取所有数字
- 提取所有字母
- 提取所有中文文字(提示:中文的Unicode范围是
[\u4e00-\u9fff])
预期输出:
1
2
3
|
数字: ['1', '0', '0', '1', '2', '9', '9', '3', '8', '9', '7']
字母: ['A']
中文: ['订单号', '价格', '元', '数量', '件', '总计', '元']
|
7.4 重复匹配
7.4.1 量词:控制重复次数
| 量词 |
说明 |
示例 |
匹配 |
* |
0次或多次 |
a* |
“”, “a”, “aa”, “aaa” |
+ |
1次或多次 |
a+ |
“a”, “aa”, “aaa” |
? |
0次或1次 |
a? |
“”, “a” |
{n} |
恰好n次 |
a{3} |
“aaa” |
{n,} |
至少n次 |
a{2,} |
“aa”, “aaa”, “aaaa” |
{n,m} |
n到m次 |
a{2,4} |
“aa”, “aaa”, “aaaa” |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import re
# 示例1:匹配连续的数字
text = "价格是299元,折扣后279元,原价320元"
prices = re.findall(r"\d+", text) # + 表示1个或多个数字
print("价格:", prices) # 输出: ['299', '279', '320']
# 示例2:匹配特定长度的数字
text = "邮编:100001,电话:13812345678"
# 匹配6位数字(邮编)
postcodes = re.findall(r"\d{6}", text)
print("邮编:", postcodes) # 输出: ['100001']
# 匹配11位数字(手机号)
phones = re.findall(r"\d{11}", text)
print("手机号:", phones) # 输出: ['13812345678']
# 示例3:可选匹配
text = "颜色:red,颜色:blue,颜色:"
colors = re.findall(r"颜色:(\w+)?", text)
print("颜色:", colors) # 输出: ['red', 'blue', '']
|
【课堂练习】量词匹配练习1
任务: 从文本中提取不同类型的信息,练习使用各种量词
1
2
3
4
5
6
7
8
|
text = """
用户信息:
用户名:john_doe123,密码:***,年龄:25
手机号:13812345678,邮编:100001
订单:A1001, B202, C30005, D4
评分:★★★★☆ (4.5分)
备注:这个商品很好!!!
"""
|
要求:
- 提取所有用户名中的字母
- 提取所有精确3位的数字
- 提取所有3位及以上的数字
- 提取所有2-4位的数字
- 提取评分中的星星数量
预期输出:
1
2
3
4
5
|
用户名字母: ['john', 'doe']
精确3位数字: ['123', '138']
3位及以上数字: ['123', '13812345678', '100001', '005']
2-4位数字: ['25', '100', '01', '1001', '202', '300', '05', '4.5']
星星数量: ['★★★★']
|
请编写代码完成以上任务。
7.4.2 贪婪 vs 非贪婪匹配
1
2
3
4
5
6
7
8
9
10
11
12
|
import re
# 示例:贪婪匹配 vs 非贪婪匹配
text = "<div>内容1</div><div>内容2</div>"
# 贪婪匹配(默认):尽可能匹配多的字符
greedy = re.findall(r"<div>.*</div>", text)
print("贪婪匹配:", greedy) # 输出: ['<div>内容1</div><div>内容2</div>']
# 非贪婪匹配:尽可能匹配少的字符
non_greedy = re.findall(r"<div>.*?</div>", text)
print("非贪婪匹配:", non_greedy) # 输出: ['<div>内容1</div>', '<div>内容2</div>']
|
【课堂练习】7.4.1 基础练习3:提取多种信息
任务: 从文本中提取不同类型的信息
1
2
3
4
5
6
7
|
text = """
商品信息:
名称:智能手机,价格:2999元
名称:耳机,价格:399元
名称:笔记本电脑,价格:5999元
联系电话:138-1234-5678
"""
|
要求:
- 提取所有商品名称(提示:在"名称:“后面的文字)
- 提取所有价格数字
- 提取电话号码(格式:xxx-xxxx-xxxx)
预期输出:
1
2
3
|
商品名称: ['智能手机', '耳机', '笔记本电脑']
价格: ['2999', '399', '5999']
电话号码: ['138-1234-5678']
|
7.5 位置匹配
7.5.1 边界匹配
| 边界 |
说明 |
示例 |
^ |
字符串开始 |
^Hello |
$ |
字符串结束 |
world$ |
\b |
单词边界 |
\bword\b |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import re
# 示例1:匹配以特定内容开头的行
text = """苹果很好吃
香蕉也很甜
苹果汁很好喝"""
starts_with_apple = re.findall(r"^苹果", text, re.MULTILINE) # 多行模式
print("以'苹果'开头:", starts_with_apple) # 输出: ['苹果']
# 示例2:匹配完整的单词
text = "cat category catfish catch"
# 只匹配完整的cat单词
whole_cat = re.findall(r"\bcat\b", text)
print("完整单词cat:", whole_cat) # 输出: ['cat']
# 匹配所有包含cat的单词
all_cat = re.findall(r"\b\w*cat\w*\b", text)
print("包含cat的单词:", all_cat) # 输出: ['cat', 'category', 'catfish', 'catch']
|
7.6 实际应用示例
7.6.1 提取邮箱地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import re
# 简单的邮箱匹配
text = """
联系我们:
客服邮箱:service@company.com
销售邮箱:sales@company.cn
技术支持:support@test.org
无效邮箱:user@, @company.com
"""
# 简单的邮箱匹配模式
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
emails = re.findall(email_pattern, text, re.IGNORECASE)
print("找到的邮箱:", emails)
# 输出: ['service@company.com', 'sales@company.cn', 'support@test.org']
|
7.6.2 提取电话号码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import re
# 匹配多种电话号码格式
text = """
联系方式:
手机:138-1234-5678
固话:010-6234-5678
紧急:110
无效:123-456
"""
# 匹配手机号和固话
phone_pattern = r'\b\d{3}-\d{4}-\d{4}\b'
phones = re.findall(phone_pattern, text)
print("电话号码:", phones) # 输出: ['138-1234-5678', '010-6234-5678']
# 匹配所有3位以上的数字
all_numbers = re.findall(r'\b\d{3,}\b', text)
print("所有长数字:", all_numbers) # 输出: ['110']
|
7.6.3 提取URL链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import re
# 提取网页中的URL
text = """
欢迎访问我们的网站:
主站:https://www.example.com
博客:http://blog.example.com/path/to/article
图片:https://example.com/image.jpg
无效:www.example, http://
"""
# 简单的URL匹配
url_pattern = r'https?://[^\s]+'
urls = re.findall(url_pattern, text)
print("找到的URL:")
for url in urls:
print("-", url)
|
【课堂练习】7.6.1 综合练习1:清理数据
任务: 清理和格式化文本数据
1
2
3
4
5
6
|
text = """
用户输入的数据:
姓名: 张三 ,年龄:25岁,邮箱:zhangsan@email.com
姓名:李四,年龄:30,邮箱:lisi@test.cn
姓名: 王五 ,年龄:abc,邮箱:invalid-email
"""
|
要求:
- 提取所有正确的姓名(去掉前后空格)
- 提取所有有效的年龄(只能是数字)
- 提取所有有效的邮箱地址
- 清理数据,输出结构化的信息
预期输出:
1
2
3
|
有效的用户数据:
姓名: 张三, 年龄: 25, 邮箱: zhangsan@email.com
姓名: 李四, 年龄: 30, 邮箱: lisi@test.cn
|
7.7 替换和分割
7.7.1 使用re.sub()进行替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import re
# 示例1:隐藏敏感信息
text = "我的电话是138-1234-5678,邮箱是user@example.com"
# 隐藏电话号码
hidden_phone = re.sub(r'\d{3}-\d{4}-\d{4}', '***-****-****', text)
print("隐藏电话:", hidden_phone)
# 隐藏邮箱
hidden_email = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '***@***.***', text)
print("隐藏邮箱:", hidden_email)
# 示例2:格式化文本
text = "价格是299元,折扣后279元,原价320元"
# 给所有数字添加人民币符号
formatted = re.sub(r'(\d+)', r'¥\1', text)
print("格式化后:", formatted) # 输出: 价格是¥299元,折扣后¥279元,原价¥320元
|
7.7.2 使用re.split()进行分割
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import re
# 示例1:按多种分隔符分割
text = "苹果,香蕉;橙子 梨子,桃子"
# 按逗号、分号、空格分割
fruits = re.split(r'[,; ]+', text)
print("水果列表:", fruits) # 输出: ['苹果', '香蕉', '橙子', '梨子', '桃子']
# 示例2:复杂的文本分割
text = "姓名:张三 年龄:25 城市:北京"
# 按空格分割,但保留键值对
items = re.split(r'\s+', text)
print("分割结果:", items) # 输出: ['姓名:张三', '年龄:25', '城市:北京']
# 进一步提取键值
for item in items:
key_value = re.split(r':', item)
print(f"{key_value[0]}: {key_value[1]}")
|
7.8 分组提取
7.8.1 使用括号分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import re
# 示例1:提取日期各部分
text = "今天是2024-01-15,明天是2024-01-16"
# 使用分组提取年、月、日
dates = re.findall(r'(\d{4})-(\d{2})-(\d{2})', text)
print("日期分组:", dates) # 输出: [('2024', '01', '15'), ('2024', '01', '16')]
for year, month, day in dates:
print(f"年: {year}, 月: {month}, 日: {day}")
# 示例2:提取URL的各个部分
text = "网站: https://www.example.com:8080/path/to/page"
# 提取协议、域名、端口、路径
pattern = r'(https?)://([^:/]+)(?::(\d+))?(/.*)?'
match = re.search(pattern, text)
if match:
protocol = match.group(1) # https
domain = match.group(2) # www.example.com
port = match.group(3) # 8080
path = match.group(4) # /path/to/page
print(f"协议: {protocol}")
print(f"域名: {domain}")
print(f"端口: {port}")
print(f"路径: {path}")
|
7.8.2 命名分组(更易读)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import re
# 使用命名分组
text = "订单号:ORD20240115001,金额:299.00元"
pattern = r'订单号:(?P<order_id>\w+),金额:(?P<amount>\d+\.?\d*)元'
match = re.search(pattern, text)
if match:
order_id = match.group('order_id')
amount = match.group('amount')
print(f"订单ID: {order_id}")
print(f"金额: {amount}")
|
【课堂练习】7.8.1 综合练习2:解析日志
任务: 从服务器日志中提取信息
1
2
3
4
5
|
log_lines = [
'192.168.1.1 - - [15/Jan/2024:10:30:45] "GET /index.html HTTP/1.1" 200 1234',
'10.0.0.2 - - [15/Jan/2024:10:31:22] "POST /api/login HTTP/1.1" 201 567',
'172.16.0.5 - - [15/Jan/2024:10:32:15] "GET /static/css/style.css HTTP/1.1" 304 -'
]
|
要求:
- 提取IP地址
- 提取时间
- 提取请求方法(GET/POST等)
- 提取请求路径
- 提取状态码
预期输出:
1
2
3
|
IP: 192.168.1.1, 时间: 15/Jan/2024:10:30:45, 方法: GET, 路径: /index.html, 状态: 200
IP: 10.0.0.2, 时间: 15/Jan/2024:10:31:22, 方法: POST, 路径: /api/login, 状态: 201
IP: 172.16.0.5, 时间: 15/Jan/2024:10:32:15, 方法: GET, 路径: /static/css/style.css, 状态: 304
|
7.9 与BeautifulSoup结合使用
7.9.1 在BeautifulSoup中使用正则表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import re
import requests
from bs4 import BeautifulSoup
# 获取网页内容
url = "https://xuexiqu.cn/python_web_crawler/2.链接和多媒体标签.html"
response = requests.get(url)
response.encoding = "utf-8"
soup = BeautifulSoup(response.text, "lxml")
# 使用正则表达式查找特定链接
# 查找所有包含"jd.com"的链接
jd_links = soup.find_all('a', href=re.compile(r'jd\.com'))
print("京东链接:")
for link in jd_links:
print(f"- {link.text}: {link['href']}")
# 查找所有图片
images = soup.find_all('img', src=re.compile(r'\.(jpg|png|gif)$', re.IGNORECASE))
print("\n图片:")
for img in images:
print(f"- {img.get('alt', '无描述')}: {img['src']}")
|
7.10 本章总结
7.10.1 重点回顾
- 基础匹配:字面匹配是最简单的开始
- 字符类:
[abc], \d, \w, \s 等用于匹配特定类型的字符
- 量词:
*, +, ?, {n} 控制重复次数
- 位置匹配:
^, $, \b 匹配特定位置
- 分组:
() 用于提取和分组内容
7.10.2 实用技巧
- 从简单模式开始,逐步复杂化
- 使用
re.findall() 快速测试模式
- 用
re.search() 查找第一个匹配
- 用
re.sub() 进行替换操作
- 用分组
() 提取需要的信息
7.10.3 学习建议
- 多练习:正则表达式需要大量练习才能掌握
- 从小处着手:从简单的匹配开始,逐步增加复杂度
- 使用工具:在线正则表达式测试工具可以帮助调试
- 结合实际:在实际项目中应用学到的知识
记住:正则表达式是一个强大的工具,虽然开始可能觉得复杂,但随着练习会变得越来越容易!