Python正则表达式入门:从模式匹配到数据提取实战
文章目录
- Python正则表达式入门:从模式匹配到数据提取实战
- 前言
- 一、`re` 模块快速上手
- 1.1 第一个正则示例
- 1.2 原生字符串 `r"..."`
- 二、核心匹配函数
- 2.1 `search()`:搜索第一个匹配
- 2.2 `match()`:从字符串开头匹配
- 2.3 `findall()`:查找所有匹配
- 2.4 `finditer()`:返回迭代器(推荐大文本使用)
- 2.5 `sub()`:替换匹配内容
- 三、常用正则模式速查
- 3.1 字符类
- 3.2 量词
- 3.3 边界与锚点
- 3.4 贪婪与非贪婪
- 四、捕获组与高级用法
- 4.1 分组捕获
- 4.2 命名分组
- 4.3 前瞻与后顾
- 4.4 常用模式速查表
- 五、编译正则:提升性能
- 六、实战案例:简易网页爬虫辅助工具
- 总结
- ✅ 亮点总结
- 适用场景
- 扩展方向
前言
正则表达式(Regular Expression)是处理文本的瑞士军刀。无论是验证用户输入、提取网页数据、清洗日志文件,还是批量替换文本,正则表达式都能用几行代码完成看似复杂的任务。Python内置的re模块提供了完整的正则表达式支持。
学习正则表达式的现实意义:在面试中,正则表达式是常见考点;在爬虫开发中,它是数据提取的核心工具;在日志分析中,它是错误排查的利器。然而,正则表达式也有一个"名声"——它看起来像天书,容易把简单问题复杂化。本文的目标是让你掌握正则的核心套路,学会"用合适的工具做合适的事",避免陷入过度依赖正则的陷阱。本文将从零带你掌握正则表达式的核心用法。
一、re模块快速上手
1.1 第一个正则示例
正则表达式最简单的入门方式就是从一个具体的例子开始。下面这个例子演示了如何从一段文本中提取手机号码——这是正则表达式最经典的应用场景之一。
importre text="我的手机号是13812345678,请尽快联系我"# 匹配中国大陆手机号pattern=r"1[3-9]\d{9}"match=re.search(pattern,text)ifmatch:print(f"找到手机号:{match.group()}")# 找到手机号: 13812345678print(f"起始位置:{match.start()}")# 7print(f"结束位置:{match.end()}")# 18关键解析:re.search()在整个字符串中搜索第一个匹配,返回一个Match对象。如果没找到匹配,它返回None——所以在生产代码中务必先检查返回值是否为None,否则直接调用.group()会抛出AttributeError。
1.2 原生字符串r"..."
正则表达式中大量使用反斜杠,使用原生字符串r"..."可以避免转义混乱:
# 繁琐的普通字符串pattern1="\\d+\\.\\d+"# 简洁的原生字符串(推荐)pattern2=r"\d+\.\d+"print(pattern1==pattern2)# True二、核心匹配函数
re模块提供了多个匹配函数,各自有不同的语义和使用场景。理解它们的区别是正则入门的关键。常见面试题:“match()和search()有什么区别?”——答案很简单,match()只从字符串开头匹配,search()搜索整个字符串。
2.1search():搜索第一个匹配
text="价格:¥19.9,折扣价:¥9.9"pattern=r"¥(\d+\.\d+)"match=re.search(pattern,text)print(match.group())# ¥19.9print(match.group(1))# 19.9 (第一个捕获组)2.2match():从字符串开头匹配
# match() 只匹配开头print(re.match(r"\d+","123abc"))# <re.Match object>print(re.match(r"\d+","abc123"))# None (不在开头)# search() 搜索任意位置print(re.search(r"\d+","abc123"))# <re.Match object>2.3findall():查找所有匹配
text="我有苹果5个,橘子12个,香蕉8个"pattern=r"(\w+)(\d+)个"# 有捕获组时返回元组列表result=re.findall(pattern,text)print(result)# [('苹果', '5'), ('橘子', '12'), ('香蕉', '8')]forfruit,countinresult:print(f"{fruit}:{count}个")2.4finditer():返回迭代器(推荐大文本使用)
text="Python 3.9, Python 3.10, Python 3.11"pattern=r"Python (\d+\.\d+)"formatchinre.finditer(pattern,text):print(f"版本{match.group(1)}位于位置{match.start()}")2.5sub():替换匹配内容
# 脱敏手机号text="联系人: 张三 13812345678, 李四 13987654321"masked=re.sub(r"(\d{3})\d{4}(\d{4})",r"\1****\2",text)print(masked)# 联系人: 张三 138****5678, 李四 139****4321# 使用函数进行替换defcensor(match):word=match.group()returnword[0]+"*"*(len(word)-1)text="这个项目简直太棒了,效率非常高"result=re.sub(r"[了得]",censor,text)print(result)# 这个项目简直太棒*,效率非常高三、常用正则模式速查
正则表达式的元字符是构建匹配模式的"积木"。这些符号初看可能很抽象,但每个都有明确的使用场景。学习技巧:不要试图一次记住所有元字符——先熟练\d、\w、.、*、+、?这几个最常用的,剩下的需要时再查即可。
3.1 字符类
patterns={r"\d":"匹配任意数字 [0-9]",r"\D":"匹配任意非数字",r"\w":"匹配字母数字下划线 [a-zA-Z0-9_]",r"\W":"匹配非字母数字下划线",r"\s":"匹配空白字符(空格、制表符、换行等)",r"\S":"匹配非空白字符",r".":"匹配任意字符(除换行符)",}test="A 1 _ 中 @"print(re.findall(r"\d",test))# ['1']print(re.findall(r"\w",test))# ['A', '1', '_', '中']print(re.findall(r"\s",test))# [' ', ' ', ' ', ' ']3.2 量词
test_cases={r"a*":"a出现0次或多次",r"a+":"a出现1次或多次",r"a?":"a出现0次或1次",r"a{3}":"a恰好出现3次",r"a{2,4}":"a出现2到4次",r"a{2,}":"a出现至少2次",}print(re.findall(r"a*","baaac"))# ['', 'aaa', '', '']print(re.findall(r"a+","baaac"))# ['aaa']print(re.findall(r"a{2,3}","baaaaac"))# ['aaa']print(re.findall(r"\d{3,4}","电话: 010-1234567"))# ['010', '1234']3.3 边界与锚点
边界匹配是正则表达式中的重要概念,它不消耗字符,只匹配"位置"。^匹配行首,$匹配行尾,\b匹配单词边界。理解边界匹配对于精确匹配非常重要——比如你想匹配独立的单词 “cat” 而不是 “category” 中的 “cat”,就需要用到\bcat\b。re.MULTILINE标志让^和$匹配每行的开头和结尾(而非整个字符串的开头和结尾)。
test="""第一行 第二行 第三行"""# ^ 匹配行首,$ 匹配行尾print(re.findall(r"^第.",test,re.MULTILINE))# ['第一', '第二', '第三']# \b 匹配单词边界text="cat category scatter cat"print(re.findall(r"\bcat\b",text))# ['cat', 'cat']3.4 贪婪与非贪婪
这是正则表达式中最经典的"坑"之一。贪婪匹配是正则的默认行为——量词(*,+,?,{})会尽可能多地匹配字符。这在很多场景下会导致意料之外的结果,比如在HTML中使用.*匹配时可能跨过多个标签。非贪婪匹配通过在量词后加?实现(如*?,+?,??),它会尽可能少地匹配。一个实用的经验法则:在HTML/XML解析、引号内容提取等场景中,几乎总是应该使用非贪婪匹配。
html="<div>内容1</div><div>内容2</div>"# 贪婪匹配(默认):匹配尽可能多的内容greedy=re.findall(r"<div>.*</div>",html)print(greedy)# ['<div>内容1</div><div>内容2</div>']# 非贪婪匹配:加 ? 匹配尽可能少的内容lazy=re.findall(r"<div>.*?</div>",html)print(lazy)# ['<div>内容1</div>', '<div>内容2</div>']四、捕获组与高级用法
4.1 分组捕获
# 提取日期text="今天是2024-01-15,明天是2024-01-16"pattern=r"(\d{4})-(\d{2})-(\d{2})"formatchinre.finditer(pattern,text):year,month,day=match.groups()print(f"{year}年{month}月{day}日")4.2 命名分组
# 使用 (?P<name>...) 给分组命名pattern=r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"text="出生日期: 1995-08-22"match=re.search(pattern,text)ifmatch:print(match.group("year"))# 1995print(match.group("month"))# 08print(match.group("day"))# 22print(match.groupdict())# {'year': '1995', 'month': '08', 'day': '22'}4.3 前瞻与后顾
# 正向肯定前瞻 (?=...):后面必须跟着...text="100元 200美元 300元 400欧元"# 匹配后面跟着"元"的数字result=re.findall(r"\d+(?=元)",text)print(result)# ['100', '300']# 正向肯定后顾 (?<=...):前面必须是...# 匹配前面是"$"的数字text="商品: $199, $299, ¥599"result=re.findall(r"(?<=\$)\d+",text)print(result)# ['199', '299']# 负向前瞻 (?!...):后面不能跟着...text="Python3 Python2 Python"result=re.findall(r"Python(?!\d)",text)print(result)# ['Python'] (不匹配后面有数字的)4.4 常用模式速查表
| 模式 | 含义 | 示例 |
|---|---|---|
\d{3,4} | 3-4位数字 | 区号 |
[\u4e00-\u9fa5] | 中文字符 | 匹配中文 |
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} | Email地址 | 基本邮箱匹配 |
1[3-9]\d{9} | 中国大陆手机号 | 13812345678 |
\d{17}[\dXx] | 18位身份证 | 身份证号 |
https?://[^\s]+ | URL | 网址 |
五、编译正则:提升性能
频繁使用的正则表达式应该预编译。re.compile()将正则模式编译为一个正则对象,后续可以反复使用,避免每次调用re.match()时重复解析和编译。在大批量文本处理中,预编译能带来显著的性能提升——在循环内重复使用同一个正则表达式时,编译版本比未编译版本快30%-50%。最佳实践:将编译后的正则对象定义为模块级别的常量,在整个程序中复用。
importtime# 预编译email_pattern=re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')emails=["user@example.com","invalid-email","test@company.co.uk","no@domain","good@email.org",]foremailinemails:is_valid=bool(email_pattern.match(email))status="✓"ifis_validelse"✗"print(f"{status}{email}")# 性能测试test_text="hello@world.com "*10000start=time.time()for_inrange(1000):re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',"test@example.com")print(f"未编译耗时:{time.time()-start:.3f}s")start=time.time()for_inrange(1000):email_pattern.match("test@example.com")print(f"编译后耗时:{time.time()-start:.3f}s")六、实战案例:简易网页爬虫辅助工具
下面的实战案例将前面学到的知识整合成几个实用的函数——邮箱提取、URL提取、HTML表格解析、敏感信息脱敏。这些函数都是实际爬虫和数据清洗项目中可以直接复用的工具代码。
importredefextract_emails(text):"""从文本中提取所有邮箱地址"""pattern=re.compile(r'\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b')returnpattern.findall(text)defextract_urls(text):"""从文本中提取所有URL"""pattern=re.compile(r'https?://[^\s<>"\']+|www\.[^\s<>"\']+')returnpattern.findall(text)defparse_html_table(html):"""解析简易HTML表格"""# 提取所有行rows=re.findall(r'<tr>(.*?)</tr>',html,re.DOTALL)table_data=[]forrowinrows:# 提取每行中的单元格cells=re.findall(r'<t[dh]>(.*?)</t[dh]>',row,re.DOTALL)# 清理HTML标签clean_cells=[re.sub(r'<.*?>','',cell).strip()forcellincells]table_data.append(clean_cells)returntable_datadefmask_sensitive_info(text):"""脱敏处理:隐藏身份证号、手机号、银行卡号"""text=re.sub(r'(\d{3})\d{4}(\d{4})',r'\1****\2',text)text=re.sub(r'(\d{6})\d{8}(\d{4})',r'\1********\2',text)text=re.sub(r'(\d{4})\s?(\d{4})\s?(\d{4})\s?(\d{4})',r'\1 **** **** \4',text)returntext# 测试sample=""" 联系人: 张三, 邮箱: zhang@test.com 官网: https://www.example.com 手机: 13812345678 身份证: 320102199001011234 <table> <tr><th>姓名</th><th>分数</th></tr> <tr><td>张三</td><td>95</td></tr> <tr><td>李四</td><td>88</td></tr> </table> """print("提取邮箱:",extract_emails(sample))print("提取URL:",extract_urls(sample))print("解析表格:",parse_html_table(sample))print("脱敏后:",mask_sensitive_info(sample))总结
正则表达式是文本处理的利器,Pythonre模块的核心API:
search()查找第一个匹配,findall()查找所有匹配sub()替换匹配内容,split()按模式分割字符串- 使用
r"..."原生字符串避免转义困扰 - 频繁使用的正则用
re.compile()预编译提升性能 - 非贪婪匹配
*?和+?在HTML解析等场景中非常实用
正则表达式初看可能有些晦涩,但一旦掌握,处理文本的效率和优雅程度都会显著提升。建议收藏本文用作速查参考。下一篇我们将进入多线程与并发编程的世界。
✅ 亮点总结
- 系统讲解
re模块五大核心函数(search/findall/sub/split/match),附带速查表 - 元字符、量词、分组、零宽断言循序渐进,从基础模式到复杂匹配
- 预编译
re.compile()提升性能,非贪婪匹配解决 HTML 解析中的常见坑 - 实战案例:邮箱/URL 提取、敏感信息脱敏、HTML 表格解析,即学即用
适用场景
- 数据清洗:从日志/爬虫结果中提取结构化字段(日期、IP、金额等)
- 表单验证:校验用户输入的手机号、邮箱、身份证号等格式合法性
- 代码重构:批量替换项目中的旧 API 调用、统一代码风格
扩展方向
- 学习正则表达式的性能优化,了解回溯陷阱和原子组
- 结合
re的 flags 参数掌握多行匹配、忽略大小写等高级模式 - 探索
regex第三方库,支持更丰富的正则特性(如递归匹配、模糊匹配)