
正则表达式这玩意儿刚接触Python的人十有八九觉得它像天书——一堆反斜杠加括号看着比密码还难懂。但真在项目里跑过几轮爬虫、处理过几万条脏数据之后你会发现自己根本离不开它。字符串查找、格式校验、数据清洗、日志分析随便哪个场景都能看到它的身影。这篇就基于我自己多年的Python使用经验把正则表达式这块掰开揉碎了讲清楚从语法基础到爬虫实战、再到各种坑和排查思路尽量让刚入门的朋友也能照着用起来。我最早用正则其实就是为了从一个网页里把商品价格抠出来。那时候还是用正则表达式硬匹配后来换成XPath、CSS选择器但正则依然是兜底方案——遇到结构不规则、标签嵌套混乱的HTML正则反而最能打。所以不管你是要做爬虫、搞数据分析还是写自动化脚本正则都是绕不开的一门基本功。1. 正则表达式到底解决什么问题1.1 从一个真实需求说起假设你现在拿到一个纯文本文件里面有几千行这样的数据订单号A20240001金额98.50元时间2024-03-15 14:30 订单号B20240567金额1200.00元时间2024/03/16 09:12 订单号C20240888金额66元时间2024.03.17你想把订单号、金额、时间三列分别提取出来存成结构化数据。用普通的字符串方法做split分割、strip清理、再判断格式写出来的代码又长又脆换一种格式就歇菜。这时候正则表达式就是最顺手的工具一行匹配模式搞定import re text 订单号A20240001金额98.50元时间2024-03-15 14:30 pattern r订单号(\w)金额([\d.])元时间([\d. /:-]) m re.search(pattern, text) if m: print(m.groups()) # (A20240001, 98.50, 2024-03-15 14:30)这就是正则的核心价值它帮你用一段“模板”描述一类字符串的结构规律而不是笨拙地逐个字符去判断。简单说你告诉正则引擎“我要找什么样形状的东西”它按你的规则去文本里扫描找出所有符合条件的片段或者帮你替换、校验、拆分。1.2 正则的适用边界这个必须开篇就说清楚正则不是万能的。早期我犯过的最大错误就是想用正则去解析HTML、解析JSON、解析各种嵌套结构。结果就是写了一坨超长的匹配模式一遇到换行、注释、属性顺序变化就直接崩溃。HTML的标签可以嵌套JSON的结构是递归的这类数据用正则处理本质上是“用错误的工具去解决错误的问题”。正则真正擅长的是这几类任务格式校验邮箱、手机号、身份证号、日期格式是否合法。信息提取从日志、网页文本、配置文件中抽取关键内容。批量替换把文本里的某类模式统一替换成另一格式。字符串拆分按多个分隔符统一切割。文本过滤找出包含特定关键词组合的段落。而解析HTML、JSON、XML这类有层级嵌套的数据正确做法是交给专门的解析库比如解析HTML用BeautifulSoup、lxml解析JSON直接json.loads。我见过不少新手拿正则生啃一个JSON字符串最后把自己绕晕了得不偿失。正则解决的是“线性的、平面的”文本模式问题碰上“递归的、嵌套的”结构果断换工具。2. 核心语法与匹配逻辑拆解2.1 字符匹配的基础单位正则的本质是描述一组字符串的规则它由普通字符和元字符组成。普通字符就是字面意思比如匹配a就是找字母a。但正则的威力都在元字符上。我按自己的理解把常用的元字符分成几组字符类与任意匹配模式含义示例.匹配除换行外的任意单个字符a.c匹配abc、a1c\d匹配一个数字等价于[0-9]\d\d匹配42\w匹配字母、数字、下划线等价于[a-zA-Z0-9_]\w匹配一个单词\s匹配空白字符包括空格、制表符、换行a\sb匹配a b[abc]字符集合匹配a、b、c中任意一个[Pp]ython匹配Python或python[^abc]排除型字符集合[^0-9]匹配任意非数字字符这里有个容易踩的坑在字符集合内部很多元字符会“退化”成普通字符。比如[.]就只匹配句点本身而不是任意字符。所以如果你要匹配一个点号写\.或者[.]都可以但不要直接写.不然它会匹配掉你不想匹配的东西。位置锚定模式含义^匹配字符串开头$匹配字符串结尾\b匹配单词边界\B匹配非单词边界位置锚定很常用比如校验一个字符串是不是纯数字^\d$。如果忘了加^和$\d会在abc123def里照样匹配出123结果就不符合“纯数字”的预期。这个细节在实际写校验正则时特别重要。2.2 量词与贪婪模式的坑量词决定一个模式重复多少次模式含义*重复0次或多次重复1次或多次?重复0次或1次{n}重复n次{n,}重复至少n次{n,m}重复n到m次比如\d{4}用来匹配四位年份\d{1,3}匹配1到3位数字。手机号校验可以写成^1[3-9]\d{9}$意思是以1开头第二位是3到9之间的数字后面跟9位数字一共11位。真正让人头大的是贪婪与非贪婪。默认情况下量词是贪婪的它会尽量匹配更多的字符。举个例子import re text ahello/abworld/b # 贪婪模式 print(re.findall(r.*, text)) # 输出[ahello/abworld/b] # 非贪婪模式 print(re.findall(r.*?, text)) # 输出[a, /a, b, /b]同样的模式只是加了个?结果天差地别。贪婪模式会一路吃到最后一个才停下非贪婪模式则是找到一个最短的满足条件就停。实际提取HTML标签内容时几乎都要用非贪婪模式否则很容易把一大片内容全吞进去。这个坑我在爬虫里踩过无数次后面专门有一节细说。2.3 分组与反向引用括号在正则里不只是为了“分组显示”它还能捕获匹配到的内容。re.search之后通过.group(1)、.group(2)就能取出对应分组的内容。比如提取订单号m re.search(r订单号(\w), text) print(m.group(1)) # A20240001分组还有一个更高级的用法叫反向引用。匹配重复出现的单词时很管用比如(\w)\s\1就能匹配hello hello这种连续重复的词。\1引用了第一个分组的匹配内容。再比如非捕获分组(?:...)它只分组但不捕获不会占用分组编号适合用在不希望结果里多出无用分组的情况。比如匹配http或https可以写(?:http|https)://这样不会产生多余的分组干扰编号。还有命名分组(?Pname...)取数据时用m.group(name)比记忆数字索引清晰得多。代码一长group(3)和group(4)真的分不清谁是谁命名分组能救你一命。举个例子text 金额98.50元 m re.search(r金额(?Pamount[\d.])元, text) print(m.group(amount)) # 98.502.4 常用元字符速查表为了让你日常写的时候不用翻文档我把最常用的整理成一张表放这里语法作用.匹配除换行外任意字符^/$匹配开头 / 结尾\d/\D数字 / 非数字\w/\W单词字符 / 非单词字符\s/\S空白 / 非空白\b/\B单词边界 / 非边界*//?0次或多次 / 1次或多次 / 0次或1次{n,m}重复n到m次[...]/[^...]字符集合 / 排除集合(...)分组捕获(?:...)非捕获分组(?Pname...)命名分组|或匹配左边或右边\b单词边界3. Python正则的实操要点3.1 re模块的核心API别再只会searchPython的re模块是写正则的主要战场。核心函数就这几个但每个都有自己的脾气re.match(pattern, string)从字符串开头开始匹配开头不满足就直接返回None。它跟^作用类似很多人刚学时分不清match和search记住这一点就够了match限定必须从头开始。re.search(pattern, string)扫描整个字符串找到第一个满足模式的位置。re.findall(pattern, string)返回所有匹配结果的列表。注意如果模式里有分组返回的不再是字符串列表而是元组列表。re.finditer(pattern, string)返回一个迭代器每一项是一个Match对象。当匹配数量很大时用finditer更省内存而且可以通过m.group()取分组信息。re.sub(pattern, repl, string)替换所有匹配的文本。repl可以是一个字符串也可以是一个函数函数写法在动态替换时很实用。re.split(pattern, string)按照模式拆分字符串比str.split()灵活得多可以一次按多个分隔符切。这里有个细节findall遇分组时的行为。新手最懵的就是这个。比如re.findall(r(\w)(\w), testexample.com) # 输出 [(test, example)]注意是元组列表如果你想既拿到整体匹配、又拿到分组用finditer更好。或者把整体也包成一个分组((\w)(\w))。3.2 编译与性能别反复造轮子在许多示例代码里大家习惯直接写re.findall(pattern, text)。这种写法最直观但如果你在循环里调用几千次每次Python都要重新编译一次正则表达式性能损耗非常明显。正确做法是用re.compile预编译pattern re.compile(r\d{4}-\d{2}-\d{2}) for line in lines: m pattern.search(line) ...编译后的Pattern对象可以直接复用search、findall等方法速度有明显提升。特别是爬虫批量处理列表页、日志分析跑几十万行文本的场景compile带来的收益不是玄学是实打实的。另外re.compile还可以传一些有用的标志位re.I忽略大小写。re.S让.也能匹配换行符。这个在匹配跨多行的文本时几乎必用因为默认.不匹配换行一跨行就匹配失败。re.M多行模式让^和$匹配每一行的开头和结尾而不只是整个字符串的开头结尾。re.X忽略模式中的空白符允许写注释让复杂正则可读性提升不少。这几个标志可以组合使用比如re.compile(pattern, re.I | re.S)。用re.S匹配HTML多行内容是我的常规操作不然.遇到换行就断正则全废。3.3 常用的正则模板拿过去就能改下面这些都是我在各种项目里反复用、验证过没问题的常见正则可以直接抄# 邮箱简化版能覆盖绝大多数情况 email_pattern r[\w.-][\w-]\.[\w.-] # 中国大陆手机号 phone_pattern r1[3-9]\d{9} # 身份证号18位 id_card_pattern r\d{17}[\dXx] # IPv4地址 ipv4_pattern r((?:\d{1,3}\.){3}\d{1,3}) # 日期支持 2024-03-15 / 2024/03/15 / 2024.03.15 date_pattern r\d{4}[-/.]\d{1,2}[-/.]\d{1,2} # 匹配中文字符 chinese_pattern r[\u4e00-\u9fa5] # 提取HTML标签内的文本 html_text_pattern r([^]) # URL链接 url_pattern rhttps?://[\w./?%-]需要注意这些是“够用就好”的实用型正则不是终极完美版。像邮箱正则真要严格按照RFC标准写能写出一长串看似疯狂的模式但实际业务里多数场景并不需要那么严格。符合业务需求、能处理你遇到的数据就是好正则。3.4 匹配中文的各种坑处理中文时正则有几个需要注意的点。首先Python 3的字符串默认就是Unicode直接用[\u4e00-\u9fa5]可以匹配中文字符。但如果你在Windows的CMD、老项目里遇到编码问题字符串本身的编码就可能出问题正则倒是次要的。其次\w在Python 3的默认情况下是能匹配Unicode单词字符的也就是说它也能匹配中文这一点跟一些老教程里描述的不一样。自己写代码时如果只想匹配英文单词最好用[A-Za-z0-9_]显式指定否则\w可能把中文也吞进去提取结果里多出一堆不想要的内容。还有就是匹配连续的中文不要写[\u4e00-\u9fa5]以外花里胡哨的东西这个范围覆盖了绝大多数常用汉字。如果遇到生僻字、扩展区字符范围可能要扩大比如加\u3400-\u4dbf扩展A区一般场景用不到知道有这回事就行。4. 爬虫场景中的正则实战4.1 从HTML里提取信息的几种姿势爬虫是正则表达式最典型的应用场景之一。虽然现在很多人用XPath和CSS选择器但正则依然有它不可替代的位置尤其是在处理接口返回的JSONP、嵌套严重的标签、或者是用XPath写了半天都定位不到的时候正则往往一个模式就解决问题。从HTML提取信息常见的做法是先把整个页面文本拿到来然后用re.search或re.findall去抠目标内容。比如html div classproduct h3无线鼠标/h3 span classprice89.00/span /div div classproduct h3机械键盘/h3 span classprice299.00/span /div # 提取商品名称 names re.findall(rh3(.*?)/h3, html) # 提取价格 prices re.findall(rspan classprice(.*?)/span, html) print(names) # [无线鼠标, 机械键盘] print(prices) # [89.00, 299.00]看到.*?了吗这就是前面说的非贪婪匹配。如果把?去掉第一个h3(.*)/h3会从第一个h3一路吞到最后一个/h3结果就变成整个列表连在一起。这个区别写爬虫的人早晚会遇到一次。从HTML提取数据我不建议一上来就写超复杂的正则。我的习惯是先用re.findall快速验证模式再回来看数据质量。如果HTML结构比较稳定正则完全够用如果结构经常变那还是该上BeautifulSoup就上BeautifulSoup别硬扛。4.2 数据清洗与格式统一爬下来的数据往往带着各种杂质——空格、换行、HTML实体、重复标点。这时候正则就是清洗利器。举几个我实际用过的例子# 去掉所有HTML标签 clean_text re.sub(r[^], , html_text) # 合并连续空白字符为单个空格 clean_space re.sub(r\s, , text) # 去掉首尾空白 clean_trim text.strip() # 把全角数字转半角简化版 half_width re.sub(r[-], lambda m: str(int(m.group(0))), text) # 手机号脱敏改成 138****1234 masked re.sub(r(1[3-9]\d)\d{4}(\d{4}), r\1****\2, text)这里重点说下替换函数。re.sub的第二个参数传函数意味着你可以对每个匹配结果做任意处理。比如上面全角转半角的写法每次匹配到一个全角数字就调用函数转成半角。这种动态替换的能力是简单的字符串replace做不到的。再分享一个常见需求把爬到的金额数字规范化。原始数据可能是89.00 元、价格89元、299.00这些乱七八糟的格式你想要统一转成浮点数def extract_amount(text): m re.search(r([\d,](?:\.\d)?), text.replace(,, )) return float(m.group(1)) if m else None print(extract_amount(价格89.00 元)) # 89.0 print(extract_amount(原价1,299元)) # 1299.0注意我先把,去掉了因为千分位逗号会干扰数字匹配。这些细节都是处理真实数据时一点点试出来的。4.3 实战从JSONP接口里抠数据有些老接口返回的不是标准JSON而是JSONP格式比如callback({status:1,data:{name:李四,age:25}});如果直接用json.loads解析会直接报错因为整段不是合法JSON。这时候先用正则把括号里的内容抠出来再接json.loadsimport json resp_text callback({status:1,data:{name:李四,age:25}}); m re.search(rcallback\((.*)\);?, resp_text) if m: data json.loads(m.group(1)) print(data[data][name]) # 李四这也是正则的经典用法之一从非结构化文本里先把结构化片段剥离出来再交给专业工具去解析。高阶一点的玩法是动态匹配回调函数名(\w)\((.*)\)然后m.group(1)就是函数名m.group(2)就是JSON字符串更加通用。5. 常见问题与排查技巧实录5.1 匹配不到先排除这些低级错误我最常被问到的问题就是“我这正则怎么啥都匹配不出来”。排查思路就一条把模式拆到最细逐层验证。常见的坑有忘加re.S导致跨行匹配失败。HTML源码动辄几百行中间必有换行。你的.*匹配不到换行自然就失败了。解决加上re.S或者改用[\s\S]*。在原始字符串里忘了处理反斜杠。强烈建议写正则时用r...原始字符串比如r\d否则\d在普通字符串里会被当成转义轻则报错重则静默匹配错误。字符集合里的特殊字符没转义。比如想匹配a.b你写a.b结果.匹配了任意字符axb也被匹配了。想匹配字面点号要写a\.b。中文与编码问题。用[\u4e00-\u9fa5]没问题但如果你把正则模式和源文本的编码搞混了可能啥也匹配不到。先确保两边都是同样编码一般UTF-8最稳妥。我调试正则的土办法是找一条最小样本先把固定文字部分写出来再逐步加入可变部分。每次只改一个点验证一次。这样定位问题非常快。5.2 匹配过多贪婪模式惹的祸正则“匹配过头”几乎都是贪婪模式的锅。前面说过.*会吃到底这在提取多个结构相似的内容时是灾难。解决方法是能不用.就不用尽量用更精确的字符类比如\d、[\w-]。必须用.时后面加?变非贪婪(.?)。用排除字符集代替.比如提取标题title([^])/title比title(.*?)/title更稳因为它永远不会跨过下一个。我在爬虫里最常用的是第三种写法。[^]这招在提取HTML标签内容时特别稳因为内容是文本里面不应该有符号。用排除字符集天然避免了贪婪问题连非贪婪都不用加。5.3 性能陷阱灾难性回溯怎么写出来的某些正则遇到特殊输入时性能会急剧退化甚至卡死。典型的例子是嵌套量词 模糊匹配组合比如(a)这类模式。在处理长字符串时正则引擎的回溯次数可能指数级增长程序看起来像死循环。实际写代码时尽量避免“量词套量词”比如(.?)、(\w*\d*)。不要把每个字符都搞成可选。对性能敏感的大文本处理建议用re.compile预编译不要在循环里反复构建。能锚定的就锚定^和$能大幅减少扫描范围。使用finditer而非findall大文本里一次性构建列表很耗内存。实在不行就分开多次匹配不要试图一个正则解决所有问题。我处理几十MB的日志文件时如果发现某个正则跑了几秒还没出结果第一反应就是看有没有灾难性回溯风险。砍掉嵌套量词性能立刻恢复。5.4 调试小工具推荐写复杂正则时别光靠肉眼在代码里猜。我常用的调试方法是在Python里写一个小脚本把正则、测试文本扔进去定期打印结果。在线正则测试工具如regex101能实时显示分组、匹配位置还能解释每一步匹配过程调试复杂正则非常省力。自己写个debug函数把findall的结果逐条打印出来看看是不是自己想要的。有朋友还会用re.DEBUG标志来查看编译过程但那个输出比较底层一般调试用不上。先把模式逻辑理清楚比啥工具都强。6. 我在实际项目中的几点体会正则这东西刚学时觉得难用多了觉得爽真正深入之后反而会越来越谨慎。我现在写正则前会先问自己三个问题这个需求正则是真是最适合的工具吗模式里有没有潜在的性能陷阱数据格式出现变化时这个正则会不会静默出错想清楚了再动手能省下后面一堆排查时间。有个经验特别想分享给新手正则模式宁可写窄不要写宽。写窄了匹配不到你马上能发现写宽了静默匹配到不该匹配的数据可能藏在代码里跑好几个星期都没人发现直到某天数据统计对不上账才知道后悔。我见过太多因为.*太贪婪导致爬虫数据串位、分析结果混乱的案例了。要匹配一个数字就写\d别写.要匹配几个字符就明确字符范围别图省事用太模糊的模式。还有一点重要项目里别忘了给正则加注释。re.X模式允许你在正则里写空格和注释比如pattern re.compile(r (?Pyear\d{4}) # 年 [-/.] # 分隔符 (?Pmonth\d{1,2}) # 月 [-/.] # 分隔符 (?Pday\d{1,2}) # 日 , re.X)这样几个月后回来看代码你还能看懂自己在干什么。否则一长串正则放在那里连自己都要琢磨半天更别提接手你代码的同事了。最后建议反正则表达式当成一个需要持续积累的“工具箱”——不需要背所有语法但常用的元字符、贪婪与非贪婪的概念、分组引用的用法必须烂熟于心。下一回不管是写爬虫、处理日志还是做数据清洗你都会感谢当初认真学了正则的自己。