简介这份PDF面向正在学习Python、希望系统掌握正则表达式的入门与进阶读者聚焦文本匹配、提取、替换和校验等常见任务帮助读者理解正则语法在Python中的实际用法。压缩包内共1个PDF文件体积约661KB内容围绕正则表达式基础规则、常用元字符、量词、分组与捕获、匹配模式以及Python re模块的调用方式展开适合边读边练、快速查阅。已有260人学习或下载说明该主题对初学者和需要处理日志、爬虫文本、表单校验的开发者具有一定参考价值。读者可从中梳理正则表达式的知识框架结合示例理解匹配与替换思路并在实际编码中对照使用减少因语法细节不熟而反复试错的时间。1. 正则表达式到底是什么一句话把“要找的东西”描述出来在快递柜前找包裹你手里没有取件码只知道三件事收件人手机号 13 开头、尾号是 8、柜子在 B 区。你要找的不是“某一个特定包裹”而是“符合这套特征的那一类包裹”。正则表达式就是把这套特征写下来的记号它是一种专门描述“字符串长什么样”的小型语法写一条模式pattern交给匹配引擎让它在文本里挑出符合形状的片段或者判断整段文本是否符合这个形状。值得花时间搞懂它是因为最难处理的文本处理工作几乎都卡在“形状”上从日志里找时间戳和错误码、校验手机号和邮箱、从脏数据里提取数字。用普通字符串方法写代码又长又脆弱用正则一行就能说清。它并不神秘只是一套约定密集的小语言认识这些基本规则就通了。2. 为什么非要用正则从字符串查找的三个难点说起2.1 难点一要找的东西不是一串固定的字随手从服务日志里挑一行出来大概长这样[2024-05-11 09:31:02] ERROR useralice id88213 cost127ms想从这一行里取四样东西时间、级别、用户名、耗时。用最朴素的字符串方法能做的无非是line.find(ERROR)、line.split( )、line.startswith([)。这些方法有一个共同的假设要找的内容是固定的一串字。只要这串字写死了它们又快又准。现实里要的东西几乎全是“一类形状”。日期是“四位数字、一个短横、两位数字、一个短横、两位数字”手机号是“1 开头的 11 位数字”订单号是“两个字母加 8 位数字”金额是“一串数字可能带小数点可能带货币符号”。这些需求用in判断“有没有”只能拿到一个是非答案拿不到位置和原文用split切只要格式稍微变一下例如多了一个空格、混进一行不带方括号的堆栈信息整套切法就会当场失败。更麻烦的是“统计”这类需求。给一份几百兆的文本问里面出现过多少个形如 2024-05-11 的日期、每个日期各出现几次。手写循环也能做但要自己管理下标、自己判断边界、自己处理“月份是 13 这种明显不是日期的串”。写到后面你会发现代码里绝大部分行都在描述“什么样算日期”而不是在解决真正的问题。2.2 难点二手写判断很快变成在造一门小语言先看一段没有正则、纯手写的手机号校验def is_valid_phone(text: str) - bool: 朴素实现只接受 1 开头的 11 位纯数字 if len(text) ! 11: # 长度必须刚好 11 return False if text[0] ! 1: # 首位必须是 1 return False for ch in text: # 剩下每一位都必须是数字字符 if ch 0 or ch 9: return False return True这段代码逻辑没错len判断长度text[0]判断首位循环逐字判断是否为数字字符。用ch 0 or ch 9而不是ch.isdigit()是因为isdigit()对全角数字、上标数字也会返回真反而更松。参数只有一个待校验的字符串返回值是布尔值。问题是它几乎不可维护。需求稍微动一下比如允许带86前缀、允许中间有空格或短横、允许括号包住区号这段代码就得推倒重写而且每加一条规则就多一层缩进和一个分支。你实际上是在用 if 和 for 手工拼装一门“描述字符串形状”的迷你语言只是这门语言只有你自己看得懂而且它一定会在某个边界情况上出错。这类反复返工的经历做过文本清洗的人大概都有一堆经验。2.3 正则的解法不描述“哪一个”描述“哪一类”正则的思路换了个方向。它不去写“第一位是什么、第二位是什么”而是给出一张形状说明书允许出现哪些字符、每个字符出现几次、从哪儿开始到哪儿结束。就像配钥匙你不是把某一把钥匙的照片给锁匠而是给一张齿形图纸哪几个位置凸起、哪几个位置凹陷、总长多少。拿着图纸锁匠能配出成千上万把形状一致的钥匙。这张“图纸”由一小撮元字符画成常用的就那么十几个写法含义能匹配的例子.任意一个字符默认不含换行a、7、#\d一个数字0到9\w一个“词字符”字母、数字、下划线a、9、_\s一个空白字符空格、制表符、换行[abc]方括号里任意一个字符a、b、c[^abc]不在方括号里的任意一个字符d、1*?前面那个东西出现 0 次以上、1 次以上、0 或 1 次见下一章{m,n}前面那个东西出现 m 到 n 次\d{2,4}^$一行的开头、一行的结尾位置不是字符()把一段括起来当作整体或单独取出分组|或左边或右边把手机号校验用正则重写只需要一条模式import re # r 前缀表示原始字符串反斜杠不交给 Python 解释原样传给正则引擎 PHONE re.compile(r^1[3-9]\d{9}$) def is_valid_phone(text: str) - bool: 用正则校验1 开头第二位 3-9后面 9 位数字整串必须完全符合 return PHONE.match(text) is not Nonere.compile把模式编译成一个可复用的对象^和$分别锚定整串的开头和结尾1是字面字符[3-9]是字符类表示这一位只能是 3 到 9\d{9}表示再来 9 个数字。PHONE.match(text)从开头尝试匹配返回一个匹配对象或None所以用is not None转成布尔值。需求再加前缀也只需要改模式字符串函数体不动。2.4 编译一次、反复使用正则不是一次性消耗品初学者常见的写法是把模式直接塞进re.search()里每次调用都重新解析一遍模式文本。模式短的时候无感一旦放进循环里跑几十万行日志解析开销就变成实打实的成本。把它编译一次放模块级常量是几乎零成本的优化。匹配成功返回的那个对象也不只是一个“真值”它带着位置和分组信息import re LOG re.compile( r^\[(?Pts[\d\-: ])\]\s(?PlevelERROR|WARN|INFO)\suser(?Puser\w)\sid(?Poid\d), re.MULTILINE, # 让 ^ 匹配每一行的开头而不只是整段文本开头 ) for m in LOG.finditer(log_text): print(m.group(ts), m.group(level), m.group(user))模式里的(?Pname...)是命名分组给括号里的内容起个名字re.MULTILINE是编译标志改变^和$的行为让它们按行生效finditer返回一个迭代器逐条产出匹配对象比findall省内存而且保留了位置信息。每个匹配对象上group(user)取命名分组的内容m.span()取这段内容在原文本里的起止下标。同一份模式被编译一次、在循环里复用这才是正则该有的用法。3. 正则的语法基础字符、量词、分组、锚点怎么配合3.1 字符类先确定“这一位允许出现谁”写正则的顺序应该是先定字符集再定次数最后定边界。字符类负责第一步这一位上允许出现哪些字符。\d是数字\w是字母数字下划线\s是空白字符反斜杠大写就是取反\D非数字、\W非词字符、\S非空白。方括号是自定义集合[aeiou]是五个元音里任意一个[3-9]是 3 到 9 的任意一位[^0-9]是“不是数字的任意字符”。方括号里的短横表示范围想匹配短横本身就把它放在开头或结尾写成[-*/]。有个细节是中文。\w在 Python 3 的字符串模式下默认能匹配中文字符所以\w并不等于“英文单词”。要精确匹配汉字写[\u4e00-\u9fa5]更稳妥要匹配“一个或多个英文单词字符”写[A-Za-z0-9_]更明确。这类隐含行为是踩坑高发区测试时用中文样本跑通了上线遇到纯英文数据反而对不上还查不出原因。.是通配符能匹配除换行以外的任意字符。很多人以为它什么都能匹配结果遇到多行文本里换行处匹配失败误以为模式写错了。想让它跨越换行要么加re.DOTALL标志要么老老实实用[\s\S]。3.2 量词与贪婪再确定“这一位出现几次”字符类定了“是谁”量词定“几个”。*是 0 次或多次是 1 次或多次?是 0 次或 1 次{3}是恰好 3 次{2,5}是 2 到 5 次{2,}是至少 2 次。默认情况下量词是贪婪的能多吃就多吃匹配到满足条件的最后一个位置。拿引号举例import re text 他说第一句话然后又说第二句话 print(re.findall(r[^]*, text)) # [第一句话, 第二句话] print(re.findall(r.*, text)) # [第一句话然后又说第二句话]第一个模式用[^]*表示“任意多个非引号字符”天然被引号卡住所以能正确切出两段。第二个模式用.*贪婪地把整行吞掉只吐出一个大匹配。逻辑说明[^]这个写法的意思是“只要不是引号吃多少都行”用字符类代替通配符是解决贪婪问题最稳的一招。参数上findall在有分组时返回的是分组内容而不是整体匹配这里没有分组返回的就是完整匹配串。量词后面加个?就变成懒惰模式*?、?、{2,5}?意思是“尽量少匹配”。懒惰不是万能解药它只是把默认方向反过来真正想清楚边界在哪通常还是靠锚点和字符类。3.3 分组与捕获把一段模式圈起来单独取出来圆括号有两个作用把一段模式当成整体来用量词以及把匹配到的内容单独捕获出来。(ab)表示“ab 这个整体重复一次以上”(\d{4})-(\d{2})-(\d{2})表示整体是一条日期同时把年、月、日分别存进三个组。组是按左括号出现的顺序编号的group(1)是第一个group(0)或group()是整体。分组一多数字编号就记不住了所以有命名分组(?Pyear\d{4})用m.group(year)取名或者m.groupdict()一次拿到全部命名分组组成的字典。还有一种不捕获的分组(?:...)只把括号当整体用不占编号也不出现在结果里。它在拼接复杂模式时很有用比如(?:ERROR|WARN)只是想说“这两个词任选一个”并不想把级别单独取出来。少一个捕获组findall的返回结果就从元组变回字符串很多人以为自己写错了其实是分组数量变了。3.4 锚点与边界匹配到哪儿停锚点匹配的是位置不是字符。^是一行或整串的开头$是结尾\b是词边界也就是“一边是词字符、另一边不是”的那个位置。\bcat\b能匹配 “a cat here” 里的 cat但不会匹配 “concatenate” 里的 cat这种边界判断很多场景都会用到。^和$的行为受标志影响。默认它们只在整个字符串的开头和结尾生效加上re.MULTILINE它们就按行生效每行的开头和结尾都算。反过来\A和\Z永远只认整串的头尾不受标志影响。做日志解析时想要按行匹配就必须加MULTILINE否则只有第一行能被^命中后面全部漏掉而模式本身看起来毫无问题。锚点还用在校验上。没有^和$的\d{11}会匹配“任意位置出现的连续 11 位数字”一段 20 位数字里也能挑出 11 位来校验形同虚设。做整串校验时两个锚点一定要成对出现。4. 常见误区与易混概念这四组东西最容易搞混4.1 易混点一match、search、fullmatch 不是一回事先说易混点。re.match只从字符串开头尝试匹配re.search会扫描整段文本找第一个符合的位置re.fullmatch要求整段文本完全匹配才算成功。三个函数名长得很像参数也一模一样很容易随手写一个就用。为什么会混它们失败时都返回None成功时都返回匹配对象从返回值上看不出区别。加上很多人第一次写校验时用match加上$也跑通了就以为match本身就是全串匹配等到模式忘了写$出现 “1234567890123” 被判定为合法手机号才明白问题在哪。怎么区分记住三个关键词。match是“必须从第一个字符开始”后面多出来的内容它不管search是“只要存在就行”位置随意fullmatch是“整段必须恰好是它”。做格式校验用fullmatch不用自己补锚点做抽取用search或finditer只有明确知道内容就在开头时match才合适。判断成功与否时写is not None比直接if m:更清楚虽然匹配对象在布尔上下文里也等价于真。4.2 易混点二字符串转义与正则转义反斜杠要写几遍先说易混点。想匹配一个真正的反斜杠或者一个点号正则里要写成\\和\.但写进 Python 字符串字面量时反斜杠自己还要被 Python 解释器处理一遍于是变成\\\\和\\.看着就头大。为什么会混两套转义规则叠在一起一层是 Python 字符串的一层是正则引擎的。\d在 Python 里其实是个无效转义不同版本处理方式不一样还可能弹出警告\b更坑它在 Python 字符串里是退格符正则里却是词边界同一个写法两种含义。怎么区分一律用原始字符串r...。r\d{3}里的反斜杠原样传给正则引擎不用数自己写了几个。唯一的例外是模式本身要以反斜杠结尾这种情况原始字符串写不出来得退回普通字符串。养成所有模式都加r的习惯能省掉一大半莫名其妙的失败。4.3 易混点三贪婪不等于“性能差”但回溯会先说易混点。很多人听说“贪婪匹配慢”就把所有量词都改成懒惰的*?结果匹配结果变了性能也没见好。为什么会混慢的根源不是贪婪或懒惰而是回溯。正则引擎在遇到需要“退一步再试”的情况时会逐个位置回退重试嵌套量词会把重试次数乘起来。像(a)$这种写法遇到一长串 a 后面跟一个非 a 字符引擎会尝试指数级的分支组合文本长一点就会卡住看起来像程序死机。怎么区分判断标准是有没有嵌套量词、有没有“同一段内容能被多种方式匹配”的歧义。.*单独用通常没问题(.*)*、(\w\s?)这类就要警惕。优化方向是减少歧义能写字符类就写字符类[^]*比.*精确得多能加锚点就加锚点让引擎早点失败确实需要处理长文本时考虑把大模式拆成几个小模式分步处理。真遇到正则跑不动先把日志里的样本拿出来单独跑一次看耗时是不是随长度暴涨这比凭感觉改量词靠谱。4.4 易混点四正则不是解析器别拿它拆 HTML 和 JSON先说易混点。既然正则可以描述形状那用它把网页里的链接、标签内容提取出来好像也顺理成章。为什么会混简单样本上它确实能用。一段格式整齐的 HTML用a href(.*?)就能拿到链接比引入解析库快得多。于是很容易把这个做法带到真实数据上。怎么区分看结构有没有嵌套和递归。正则本质上处理的是“线性”的字符串没办法正确表达任意深度的嵌套结构而 HTML 和 JSON 恰恰是嵌套的。属性里带引号、标签里套标签、注释里藏着半截标签都会让模式失效。稳妥的分工是结构化数据交给专门的解析库正则只负责解析之后的小块文本清洗比如从一段纯文本里提取日期、金额、编号。这条界线守住能少写很多难以排查的正则。5. 什么时候该用正则、什么时候该放手5.1 该用的三类场景第一类是格式校验。手机号、邮箱、身份证、订单号、车牌号特点是规则明确、长度有限、允许的字符集固定一条模式加fullmatch就能解决比手写循环清晰得多。第二类是从半结构化文本里抽字段。日志行、配置文件里某一行、一段别人发来的通知文本格式大致稳定但没有严格的语法用命名分组把需要的几个字段圈出来finditer逐行处理几十行代码就能完成过去几百行的工作。第三类是批量查找替换。把一批文档里的旧字段名统一换掉或者把某种写法的日期统一成另一种写法re.sub配一个回调函数比逐字符处理省事也更容易回滚。5.2 该放手的信号场景建议原因解析 HTML、XML、JSON用专门的解析库结构有嵌套正则表达不了递归校验复杂语法如完整表达式用词法或语法分析工具规则组合爆炸模式无法维护模式长度超过两三行拆成几个小模式分步处理出问题时无法定位是哪一段导致的需要在模式里塞业务逻辑写普通函数正则只描述形状不承载判断还有一个容易被忽略的信号当一条正则写完之后你自己需要花五分钟才能看懂它匹配的是什么就应该停下来。可以加注释、可以用re.VERBOSE把模式拆成多行带说明的写法也可以干脆换一种实现。可读性差的正则三个月后由谁接手都是负担尤其是自己接手。在真实项目里取舍标准可以再粗略一些能用两三行普通字符串方法写清楚、不会反复变动的逻辑就别上正则规则会频繁调整、格式相对固定的文本处理正则更合适。我吃过一次亏为了一行日志里一个特别别扭的字段写了一条超长的模式当时测试全部通过两个月后格式变化这条模式没有匹配到也没有报错只是静默匹配不到东西排查了大半天才定位到问题所在。从那以后我每次写正则都会强制走一遍这三步先拿真实样本而不是自己编的样本试再确认匹配失败时的行为是不是静默通过最后给模式留一句注释说明它想干什么。希望帮到你。本文还有配套的精品资源点击获取