拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python正则表达式核心实战:从匹配规则到踩坑避雷指南

正则表达式这东西Python新手基本都会遇到。学的时候觉得不就是匹配字符串吗等真的动手写才发现要么匹配不到要么匹配多了那个*和看起来差不多用起来结果完全不一样。我自己当年学的时候也在这个地方卡了很久翻了不少教程大部分都是把语法列一遍然后给几个例子就完了真正遇到问题还是不知道怎么排查。所以这篇东西我打算用比较实际的方式来写。不搞那种冗长的语法手册而是围绕Python正则最核心的、新手最容易卡住的知识点和场景把每个关键点讲透配实际的代码和踩坑记录。无论你是刚装好Python想找个方向练习还是已经在写爬虫遇到了文本提取的需求这篇文章都能帮你少走弯路。1. 先别急着写代码正则表达式到底在解决什么问题1.1 从生活场景理解模式匹配在没有正则表达式的时候你想从一个字符串里找出所有的手机号只能这样写text 联系人张三 13812345678李四 13987654321 # 你只能用切片和判断去硬找这个字符串短还好说如果是一整页网页源码几百KB的文本用Python原生字符串方法去处理代码会膨胀到没法看。而正则表达式做的事情就是你描述一个模式它帮你在文本里把所有符合这个模式的内容都找出来。就像你在一个巨大的图书馆里找所有书脊上有Python字样的书不需要一本一本翻只需要一个检索规则。这个概念理解到位了后面所有语法都是在为描述模式服务的。1.2 Python里正则的两个基本角色字符与元字符正则表达式里的字符分两种。一种是普通字符比如你写一个字母a它就只能匹配文本里的a另一种是元字符比如\d、\w、*、、[它们有特殊的含义。新手最容易困惑的就是这两种角色的区别为什么\d能匹配数字但不是数字为什么[本身不能被直接匹配我的建议是先记住最核心的几组元字符其他的用到再查元字符含义等价说明\d匹配一个数字等价于[0-9]\w匹配字母、数字、下划线等价于[a-zA-Z0-9_]\s匹配空白字符空格、制表符、换行.匹配除换行以外的任意字符注意是任意*前面的字符出现0次或多次贪婪匹配前面的字符出现1次或多次贪婪匹配?前面的字符出现0次或1次还用于关闭贪婪[]字符集匹配括号里的任意一个字符注意是一个^匹配字符串开头在[]里表示取反$匹配字符串结尾注意和\Z的区别看到一个正则表达式的时候先用眼睛把元字符找出来剩下的就是普通字符。元字符决定匹配规则普通字符决定匹配对象这个区分一旦清楚了阅读正则表达式的能力会立刻提升。1.3 新手最常见的误区拿正则当字符串函数用很多新手学正则的时候会有一个错觉觉得正则能搞定一切字符串处理结果把简单问题复杂化。实际上Python自带的字符串方法已经覆盖了很多常见需求text hello world # 判断开头结尾用 startswith/endswith 就行没必要用正则 print(text.startswith(hello)) # 替换固定文本用 str.replace 更直观什么时候用正则一个简单的判断标准当你要匹配的内容本身是一类东西而不是一个具体东西的时候。比如匹配以数字开头的行、匹配所有邮箱地址这类需求字符串原生方法做不了才轮到正则上场。2. Python正则的核心API掌握这五个就够用2.1 match、search、findall的分工差异Python的re模块提供了几个常用函数新手容易搞混的是match和search以及findall的返回类型。re.match只从字符串的开头位置开始匹配。如果开头就不符合哪怕中间有符合的内容也返回None。re.search则扫描整个字符串找到第一个符合模式的位置就停下。import re text 我的电话是13812345678赶紧联系 # match 从头开始匹配text 开头是我的不是数字所以返回 None result_match re.match(r\d{11}, text) print(result_match) # None # search 会扫描整个字符串找到第一个连续11位数字 result_search re.search(r\d{11}, text) print(result_search.group()) # 13812345678findall和前面两个有本质区别它返回的是所有匹配结果组成的列表而不是一个match对象。如果正则里有分组用小括号括起来的部分findall的行为会再变一下这个问题后面专门讲这里先记住findall是提取数据的利器但不适合拿来判断有没有匹配。2.2 sub、split不光是查找还要会替换和切分正则不只是用来找的re.sub用来替换所有匹配的内容re.split用来按模式切分字符串。这两个函数的数据处理能力被很多人低估了。import re text 2024-01-15 和 2024/02/20 是两个日期 # 把各种格式的日期分隔符统一成 - result re.sub(r[/.], -, text) print(result) # 2024-01-15 和 2024-02-20 是两个日期 # 按多个标点切分句子 sentence Python很棒;但是正则有点难需要多练。 parts re.split(r[;。], sentence) print(parts) # [Python很棒, 但是正则有点难, 需要多练, ]re.sub的高级用法是传一个函数作为替换内容这样可以对匹配到的内容做动态处理。比如想把文本里所有数字都乘以2import re def double(match): return str(int(match.group()) * 2) text 今年是2024年我买了3本书 result re.sub(r\d, double, text) print(result) # 今年是4048年我买了6本书这个技巧在处理日志分析、数据清洗时非常实用建议直接记住。2.3 两个小细节re.IGNORECASE与re.VERBOSE新手容易忽略的是编译标志。re.IGNORECASE简写re.I让正则忽略大小写这个很好理解。重点说re.VERBOSE简写re.X它允许你在正则里写注释和换行大幅提升可读性import re # 没有 VERBOSE 的正则一长串看着就头疼 pattern1 r^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$ # 加了 VERBOSE 之后可以像写普通代码一样排版 pattern2 re.compile(r ^(\d{4}) # 年份4位数字 [-/] # 分隔符- 或 / (\d{1,2}) # 月份1到2位数字 [-/] # 分隔符 (\d{1,2}) # 日期1到2位数字 $ , re.VERBOSE)写复杂的正则时我会默认加上re.VERBOSE。否则隔一个月回头看自己写的一长串正则会非常痛苦。这个东西不是锦上添花是刚需。3. 从零写一个手机号码正则实操拆解与进阶案例3.1 13位数字手机号码正则表达式的完整推导很多新手直接在搜索引擎搜13位数字手机号码正则表达式怎么写搜出来就复制粘贴。但如果不理解每一步的推导过程换个场景你就废了。我们一步步拆第一步确定边界。手机号是11位不是13位。搜13位数字手机号码正则的人往往混淆了以13开头这个条件。比如号码是13812345678你说它是13开头的号码不代表号码只有13位。所以先明确需求是匹配以13开头的11位手机号。第二步分析结构。中国手机号的规律是第一位是1第二位目前是3到9后面9位任意数字。那么正则就是import re pattern r^1[3-9]\d{9}$这个正则拆开看^和$确保匹配的是整个字符串而不是一段文本里恰好有11位数字。1第一位必须是数字1。[3-9]第二位的取值范围是3到9注意这是一个字符位。\d{9}后面9个数字。{9}表示前面的\d正好出现9次。验证一下import re pattern r^1[3-9]\d{9}$ phones [13812345678, 19876543210, 12812345678, 123456789012] for p in phones: result re.match(pattern, p) print(p, -, 匹配 if result else 不匹配)注意我这里是用了re.match它本身就是从开头匹配所以^可以省略。但如果你用的是re.search^必须带。还有一点实际业务里手机号通常出现在一堆文字中间比如请回电13812345678。这时候^和$就失灵了因为手机号不在开头也不在结尾。正确做法是给手机号加边界在前面用(?!\d)表示前面不能是数字后面用(?!\d)表示后面不能是数字import re pattern r(?!\d)1[3-9]\d{9}(?!\d) text 请回电13812345678或者13987654321也行注意不是123456789012345 print(re.findall(pattern, text)) # 输出[13812345678, 13987654321]这个负向断言(?!\d)和(?!\d)在提取场景里几乎必用因为不加的话13位以上的长数字串会被提取出错误的子串。3.2 进阶案例一从爬虫HTML中提取邮箱链接新手学正则有一个普遍动力是为了爬虫。爬虫的第一步往往是从HTML里提取信息。虽然专业的爬虫框架有更成熟的解析方案但正则始终是轻量级的首选工具。看这个场景从一堆HTML源码中提取邮箱地址。import re html div p联系邮箱supportexample.com/p p商务合作businesstest.com.cn/p p广告联系adsub.domain.co/p /div pattern r[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,} emails re.findall(pattern, html) print(emails)这个正则的结构[a-zA-Z0-9._%-]是邮箱用户名部分是固定分隔符[a-zA-Z0-9.-]是域名部分点号用反斜杠转义了因为在正则里点号是任意字符的通配符最后\.[a-zA-Z]{2,}是顶级域名至少2个字母。这个例子我用了findall直接取结果列表没有判断匹配对象的步骤是提取数据的标准姿势。3.3 进阶案例二格式化日期字符串数据清洗时经常遇到日期格式混乱的问题有2024-01-15有2024/1/5有2024.01.05需要统一成一种格式。import re text 日期1: 2024-01-15, 日期2: 2024/2/20, 日期3: 2024.03.05 pattern r(\d{4})[-/.](\d{1,2})[-/.](\d{1,2}) def reformat(match): year, month, day match.groups() return f{year}年{int(month)}月{int(day)}日 result re.sub(pattern, reformat, text) print(result) # 日期1: 2024年1月15日, 日期2: 2024年2月20日, 日期3: 2024年3月5日这里的核心是分组()把年份、月份、日期分别捕获然后在函数里重新组合。如果你只需要提取而不需要重排match.group(1)、match.group(2)这种方式也足够直观。3.4 进阶案例三统计文章中出现次数最多的关键词正则配合collections.Counter可以实现简单的词频统计。这个案例结合了python和正则表达式两个热搜词的场景也是新手练习的常用方向。import re from collections import Counter article Python是一种编程语言python非常适合数据分析。 学习Python的过程中正则表达式是必须掌握的技能之一。 正则表达式虽然刚开始学起来有点难但掌握了它处理文本会非常高效。 # 提取所有中英文单词简单版本只匹配连续字母和中文字符 words re.findall(r[a-zA-Z]|[\u4e00-\u9fff], article.lower()) # 统计词频 counter Counter(words) print(counter.most_common(5))这段代码里的[\u4e00-\u9fff]是中文的Unicode范围\u4e00是一字所在编码区间的起点\u9fff是终点。新手在中英文混排的场景里很容易在这里卡住后面第4部分会专门讲中文匹配。4. 新手踩坑实录这些坑我全都踩过4.1 贪婪匹配与懒惰匹配为什么结果比预期多*和默认是贪婪的意思是它们会尽可能匹配多的内容。比如你有一个HTML标签b加粗/b想提取加粗两个字import re text b加粗/b 和 b加粗2/b # 错误示范贪婪匹配 result re.findall(rb(.*)/b, text) print(result) # [加粗/b 和 b加粗2]为什么会这样因为.*会一直往右吞字符直到最后一个/b出现才停下来。它不会在第一个/b就停止。解法是加?把贪婪变成懒惰result re.findall(rb(.*?)/b, text) print(result) # [加粗, 加粗2](.*?)里的?表示前面的*匹配到的内容尽可能少够用就行。这个区别在一切有开始标记和结束标记的提取任务中都会遇到。我的经验是在写.*的时候养成反射性加?的习惯除非你明确要贪婪匹配。4.2 转义地狱为什么写[\d\.]还是不对正则里有特殊含义的字符比如.、*、(、)如果你想匹配它们的本来面目需要在前面加反斜杠\。新手很容易在字符集[]里犯迷糊以为里面也需要全部转义。比如想匹配一个数字或者一个点号写成[\d\.]。实际上点号在字符集[]内部就是普通字符不需要转义。[\d.]和[\d\.]的效果一样。但如果你不转义也能用很多教程还是倾向于让你转义因为万一你以后把这个表达式挪到字符集外面忘了转义就会出错。真正麻烦的是在Python字符串里写正则时的双重转义。注意这两行的区别# 正则里的 \d 表示数字Python字符串里要写成 \\d pattern1 \\d # 正确 pattern2 \d # 错误Python 会把 \d 当作普通字符 d带一个警告更推荐的做法是使用原始字符串也就是在字符串前面加一个rpattern r\d # 推荐加了r之后反斜杠就原样传给正则引擎不用再纠结转义了。这是Python正则最重要的一个写法习惯。我看到很多新手的错误代码一半以上都和忘记加r有关。4.3 中文匹配Unicode范围怎么记正则匹配中文正统写法是用Unicode编码范围[\u4e00-\u9fff]。import re text 我最近在学Python的re模块感觉re真的很好用 # 提取所有中文字符 chinese_chars re.findall(r[\u4e00-\u9fff], text) print(chinese_chars) # [我, 最, 近, 在, 学, 的, 模, 块, 感, 觉, 真, 的, 很, 好, 用]这里的\u4e00对应一字\u9fff对应这个区间的末尾。这个范围覆盖了绝大多数常用汉字但生僻字可能在范围之外。如果只是做一般的中文文本处理这个范围完全够用。另外注意re模块在没有指定re.UNICODE标志时处理中文字符的边界可能和你的直觉不同。比如\w在默认情况下匹配中文字符Python3里\w会匹配Unicode的字母数字但\b词的边界对中文不友好。所以判断一个中文字符是否存在最好明确用[\u4e00-\u9fff]。4.4 性能陷阱灾难性回溯与re.compile正则的性能问题新手可能体会不深但一旦开始处理大数据量的文本就会踩到。最典型的场景是嵌套量词比如(a)b这种写法。当文本里全是一长串a而没有b时正则引擎会尝试无数种匹配路径导致程序卡死。这个现象叫灾难性回溯。import re # 这个表达式在匹配长字符串时会很慢 bad_pattern r(a)$ # 实际测试里几十个a后面没有b就足以卡顿解决方案有两个一是重写正则避免嵌套量词二是给量词加一个上限比如{1,10}限制尝试次数。另外一个小优化是re.compile的使用。如果一个正则表达式要在循环里用很多次每次直接re.findall(pattern, text)会重复编译模式消耗额外时间。提前编译一次import re pattern re.compile(r(\d{4})-(\d{2})-(\d{2})) for line in large_file: match pattern.search(line) # 复用编译后的模式 # 处理逻辑对于新手阶段re.compile的性能提升可能感觉不到但这是一个好的习惯。更重要的是编译后可以给它起一个有意义的名字代码可读性会好很多。5. 正则的边界什么时候千万别用正则5.1 经典反例解析HTML/JSON正则表达式很强但它不是万能锤。HTML和JSON这种有嵌套结构的文本理论上就不适合用正则解析。比如你要匹配一个div里面的所有内容如果div里面还嵌套了div正则的(.*?)只会匹配到第一个闭合标签就停了无法正确判断层级。举个现实的例子有人用正则去爬网页标题。看起来是提取title(.*?)/title确实绝大多数情况都能成功。但一旦遇到title标签属性里有特殊字符或者标题里有/title这种字符串虽然罕见但不是不可能正则解法就直接崩了。这种场景正确做法是用BeautifulSoup这类DOM解析器。同样的道理适用于JSON。JSON是严格嵌套的结构化格式直接用json.loads就能解析成Python字典。如果非要写一个正则去提取嵌套的JSON字段最后一定会写出一个没人维护得动的怪物。一个简单的判断标准你要处理的内容如果本身有层级结构就别用正则如果只是一串扁平的文本正则高效又方便。5.2 正则能力自查清单经过上面的学习你可以拿这个清单自查一下[ ] 看到\d、\w、\s、.、*、、?、[]能立刻说出含义[ ] 能区分re.match、re.search、re.findall的差异[ ] 知道findall在正则包含分组时返回的是组而不是完整匹配[ ] 能写出提取11位手机号的正则并处理数字边界[ ] 知道(.*?)和(.*)的区别[ ] 知道为什么写正则时建议加r前缀[ ] 知道用[\u4e00-\u9fff]匹配中文字符[ ] 能说出至少一个不该用正则的场景每一项背后对应的都是一个实际的坑。如果自查发现哪一项不确定回到前面的章节再读一遍对应的部分或者打开Python交互环境敲一敲。5.3 新手练习路径从环境搭建到每日一练最后聊一下练习环境。很多新手卡在正则表达式之前其实是卡在Python还没装好或者写完代码不知道在哪跑。如果你还没装Python建议直接去Python官网下载最新稳定版安装时注意勾选Add Python to PATH。编辑器方面我推荐用VSCode装好Python扩展用起来比较轻量。不建议新手一开始就折腾复杂的IDE反而被工具分散了注意力。练正则不需要独立环境直接在Python交互式shell里就能跑。我自己当年构建了一套简单的练法分享给你定义一组测试文本包含正常数据和干扰数据。比如练习手机号提取时文本里既有正确手机号也有座机号、乱写的13位数字确保你的正则是准的。随时用re.findall打印结果验证预期。正则这东西写出来和写对了完全是两回事必须以输出为准。把每天写爬虫、处理文本时遇到的提取需求先想一想要不要用正则、怎么写再去查语法。学习正则最好的产出是整理自己的常用正则片段。比如手机号、邮箱、URL、日期、IP地址这些高频模式完全可以沉淀成自己的工具函数库。以后写爬虫、做数据分析清洗直接调用效率翻倍。我个人在实际操作中的体会是正则表达式的学习曲线不是线性的它更像是在爬台阶刚学会\d和*的时候觉得很简单一用起来发现匹配多了学会懒惰匹配和分组之后又觉得进阶了接着就碰到中文和性能问题。每上一个台阶你都能处理更复杂的文本场景。坚持把每一个卡住的问题弄明白正则就会从玄学变成顺手工具。别怕慢这东西实战几次就熟了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门