ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

正则表达式入门实战:从文本匹配到数据清洗的核心技巧

正则表达式入门实战:从文本匹配到数据清洗的核心技巧

你有没有过这种经历:一整天泡在数据堆里,从几百行日志里手工复制用户ID,或者用Excel一个个分列去拆一段地址,眼都快看花了,结果还漏了几条。如果因为“懒”去搜了一下怎么自动化,大概率会看到一个词——正则表达式。很多人第一次见它就被吓退了,那堆反斜杠和括号看起来像乱码,甚至有人直接用“正则”当动词用,说“这类文本正则一下就行”。

但我可以负责任地讲,正则不是高深莫测的黑魔法,它就是一套描述“文本形状”的规则。你描述得越准确,它匹配得就越准。学它的过程更像是在学一门“给文字拍X光片”的本事:你能一眼看出哪些部分是邮箱、哪些是电话、哪些藏在标签里的链接。这篇内容不只是罗列语法,我会把正则为什么是现在这个样子、每个符号背后的设计思维讲清楚,再带四个从易到难的练习,最后聊聊只有写过很多正则才踩得到、查得出的坑。

1. 先想明白正则到底解决什么问题

1.1 一个让人崩溃的日常场景

假设你手头有一份导出的表格,里面是一堆姓名和手机号混在一起的文本,比如“张三,138xxxx1234;李四,186xxxx5678;王五,159xxxx4321”。现在要把手机号单独列出来,你会怎么做?Ctrl + C 再 Ctrl + V 一个个复制?如果只有十条,那确实无所谓。如果是十万条,按照一条三秒来算,不吃不喝也得干上三天。

正则的做法就很直接:用1[3-9]\d{9}这种模式把“以1开头、第二位是3到9、后面随便9个数字”的片段全部抓出来。它不管前面是中文名还是逗号,只在文本里寻找符合条件的形状。

正则的英文叫 Regular Expression,直译是“规则表达式”。它表达的是一种模式(Pattern),不是某种具体的值。就好比你说“找一件红色T恤”,而不是“找那件胸前有个黄色笑脸的红色T恤”。前者是模式,后者是精确值。正则天生适合处理后者没法描述的模糊场景。

1.2 正则的定位:给文本处理开了一扇快速通道

从软件工程的角度看,正则是一种“声明式”的编程思路。一般我们处理文本是命令式的:先遍历每个字符,再判断是否是数字,再判断长度……一步步说清楚怎么办。正则则不一样,你只需要声明“我想要的文本长什么样”,引擎就会自动去匹配。

这种声明式的设计有几个非常实际的好处。第一,代码省了,原本几十行的循环判断可以压缩成一行;第二,可读性上去之后,维护成本反而降低,因为模式一眼就能看出在找什么;第三,它是跨语言通用的,你在命令行里写的规则,换到编程语言、编辑器、数据库里,核心思路基本一致。

正则几乎是所有文本处理工具的底层能力。文本编辑器里用它做高级查找替换,命令行里用它过滤日志,爬虫里用它提取数据,数据库里用它做字段校验,甚至在表单里判断用户输入的邮箱、密码、身份证号是否符合格式,背后全是正则。

1.3 哪种人最值得花时间学正则

如果你属于下面这几类人,正则是值得认真花一个下午去练的:

  • 写代码的同学:日志分析、参数提取、代码重构改名,正则几乎是每天都要碰的基本功。
  • 做数据清洗的运营同学:导出的数据脏乱差,需要批量处理手机号、邮箱、日期格式,正则比Excel高效太多。
  • 做内容编辑或产品运营的人:批量给文章加链接、整理错别字、替换统一格式,会正则的人十分钟干完别人两小时的活。
  • 安全分析、系统运维这类靠日志吃饭的岗位:在海量日志里找攻击特征、统计某类错误码,正则就是你的放大镜。

换句话说,正则不是程序员的专属技能,只要你经常和文字打交道,它就值得学。

2. 正则核心语法:不背语法表,靠理解功能去记

2.1 字面量与字符组:正则最小的积木

正则最基本的能力,就是匹配一个或多个确定的字符。比如你直接写一个cat,它就能在文本里找到所有出现cat的地方。这看上去很简单,但正则真正的威力来自“字符组”(Character Class),也就是用方括号[]表示“这几个字符中任意一个”。

[abc]表示匹配单个字符,这个字符可以是 a、b 或 c。[0-9]表示任意一个阿拉伯数字,[a-z]表示任意小写字母。你还可以组合出[a-zA-Z0-9_]这种“所有字母、数字和下划线”的字符组。

这背后透露了一个核心逻辑:正则描述的是“集合”。[0-9]本质上是“一个属于数字集合的字符”。如果文本里某个位置上是数字就一定命中,不是数字就跳过。

为了方便,正则里还有一些预定义的字符组简写。\d等价于[0-9],\w等价于[a-zA-Z0-9_],\s表示空白字符(空格、制表符、换行符)。它们的价值不仅仅是少打字,更重要的是语义明确:看到\d就知道这里必须是数字,看到\w就知道这里只能是单词常见字符。

在刚开始学正则时,不需要背下所有简写,先记住\d、\w、\s这三个最常用的即可,等练多了自然就记住了。

2.2 量词:描述重复次数是正则的命根子

如果正则只能匹配单个字符,那它的能力就太弱了。真正让正则强大的是“量词”(Quantifier),用来描述某个字符或组出现多少次。

正则里最常用的三个量词是:

  • *表示前面的字符出现 0 次或多次。
  • +表示出现 1 次或多次。
  • ?表示出现 0 次或 1 次。

举一个很直观的例子:匹配一个微信号@abc123,你要的是 @ 后面一串字母数字。写@\w+,这里的+就是告诉引擎“@后面至少得有一个字母或数字,且可以一直往后吃到最后一个符合条件的字符”。

除此之外,还有更精确的花括号量词。\d{4}表示必须出现 4 个数字,[a-z]{2,5}表示小写字母出现 2 到 5 个,\d{3,}表示至少 3 个数字且上不封顶。

我建议你在理解量词时,把它当作口语里的“几个”。*是“有没有都行,有就多个”,+是“至少一个,多了更好”,?是“最多一个,可有可无”。量词的粒度既能用在单个字符后面,也能用在分组后面,比如(ab)+表示ab这个整体至少出现一次。

2.3 锚点与分组:定位到哪里、提取哪一段

锚点(Anchor)解决的是“位置”问题,它不匹配具体字符,而是匹配一个位置。^表示字符串开头,$表示字符串结尾。

如果你写^\d+,意思是“从字符串开头开始,尽量抓数字”;如果你写\d+$,意思是“在字符串结尾之前,尽量抓数字”。锚点的价值在于它可以卡住边界,避免从文本中间匹配到你不需要的内容。

分组(Group)用圆括号()表示,它有两个作用:第一,把多个字符打包成一个整体,配合量词使用;第二,捕获匹配到的内容,方便之后单独提取。

比如要从一个日期字符串2024-06-15里分别提取年、月、日,你可以写(\d{4})-(\d{2})-(\d{2})。匹配成功后,三个括号里的内容会被分别存到第 1 组、第 2 组、第 3 组。这在编程语言里对应的就是match[1]、match[2]、match[3]这种操作。

分组还有一个进阶版本叫非捕获分组(?:)。如果只是为了包裹整体而不需要提取,用(?:)可以省掉不必要的分组回溯,性能稍好,也不用担心组号错乱。尤其是在写比较复杂的校验规则时,非捕获分组能让你省掉很多麻烦。

2.4 转义与字符类:反斜杠为什么总是成对出现

正则里有一批字符有特殊含义,比如.表示“任意一个字符(某些模式里不包括换行)”,*、+、?、[]、()都有自己的功能。如果你想匹配它们本身,就需要在前面加反斜杠转义,写成\.、\*、\\。

举个常见的例子:要匹配一个 IP 地址,比如192.168.1.1,如果你直接写\d+.\d+.\d+.\d+,由于.是“任意字符”的元字符,它也能匹配到192-168-1-1这种诡异的文本。正确写法应该是\d+\.\d+\.\d+\.\d+。

很多新手困惑的是,为什么在编程语言里写正则时,反斜杠变成了双份,比如要匹配一个数字,代码里写的是\\d而不是\d。

这是因为编程语言(比如 Python、Java)的字符串本身也有转义机制。\d在字符串里先被当成普通字符来处理,传一半就被消化掉了。为了把反斜杠“安全送到”正则引擎,字符串里要写\\d。如果用原生字符串语法(比如 Python 的r"\\d"),正则里就只要写一个反斜杠。这个细节特别容易让人心态崩溃,写之前先搞清楚语言字符串的转义规则,能省掉不少调试泪。

3. 动手练习:四道从易到难的新手实操题

3.1 练习一:教你精确匹配一个手机号

手机号校验是每个新人第一次接触正则时最常见的练习,但也最容易踩坑。大陆手机号目前的结构是:第一位必须是 1,第二位目前是 3 到 9,后面还有 9 位数字。

最简单的写法:

^1[3-9]\d{9}$

拆开看:^锁定开头,1匹配开头数字 1,[3-9]匹配第二位数字 3 到 9,\d{9}匹配后面 9 个数字,$锁定结尾。这个模式只可能是 11 位的手机号,如果中间夹了空格或连字符就匹配不到。

如果你想宽松一点,允许手机号带分隔符,比如用户输入的是138-1234-5678或138 1234 5678,可以写成:

^1[3-9]\d{9}$|^1[3-9]\d-\d{4}-\d{4}$

但要注意,这么写存在一个隐患:直接多个条件用|连接时,如果条件本身有重叠,你可能匹配到并不完整的内容。更好的办法是把分隔符部分设置为可选分组:

^1[3-9]\d(?:[- ]?\d{4}){2}$

这个练习的重点是体会“锚点 + 字符组 + 量词”的组合用法,以及为什么不同需求要微调正则结构。

3.2 练习二:从 HTML 标签里提取链接地址

爬虫场景里经常会遇到“拿到一堆 a 标签源代码,要把 href 属性值全部抠出来”的需求。比如代码里有这段内容:

<a href="https://example.com/page/1" class="link">第一页</a> <a href='/about' class='nav'>关于我们</a> <link rel="stylesheet" href="/static/css/app.css">

新手容易犯的错是直接写<a href=".*">,这样会贪婪地把第一个 href 到最后一个结束符之间的全部内容吞掉。正确做法是限定提取范围,并且兼容单双引号。

推荐练习用的模式:

href=["']([^"']+)["']

解释一下:href=是字面量,["']匹配单引号或双引号,([^"']+)捕获“引号内部的非引号字符”,["']再匹配结束引号。因为用了否定字符组,匹配会停在下一个引号处,不会跨标签乱跳。

这里还能顺便体会有没有全局标志(g)的区别:如果没有全局标志,匹配到第一个就停了;加上全局标志后,会把文本里所有符合条件的都提取出来。

练习时建议用带高亮和分组面板的工具,一边改一边看捕获组的变化,这是理解“捕获”概念最快的方式。

3.3 练习三:数据清洗场景下的通用替换

正则不只是用来“找”,还经常用来“换”。比如你从某个后台导出的表格里,电话和手机号混在一个文本字段里,形如手机:138xxxx1234 / 固话:010-12345678。现在你想把手机号单独摘出来,固话单独摘出来,可以这样:

手机号提取:1[3-9]\d{9}固话提取:0\d{2,3}-?\d{7,8}

如果想把多余的空格、前后空白清理干净,一条替换就搞定:\s+替换成空字符串。如果想统一日期格式,把2024/06/15变成2024-06-15,替换式写(\d{4})/(\d{2})/(\d{2})替换为\1-\2-\3。

这个练习的真实价值在于,正则替换是文本处理里效率最高的武器之一,少则几秒、多则几分钟就能完成一个原本要写几十行脚本的工作。遇到重复性替换,先停下来想想能不能用一个正则搞定,养成这个习惯之后效率会拉开一大截。

3.4 练习四:密码强度校验的常见写法

密码校验是另一个经典场景,难点在于“必须同时满足多个条件”。比如要求密码长度 8 到 20 位,必须包含至少一个小写字母、至少一个大写字母、至少一个数字。

一个典型的写法:

^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,20}$

这里的关键是(?=...),它叫“正向先行断言”(正向前瞻)。它的作用是“从这里往后的文本,必须满足括号里的模式,但它不消费字符”。通俗点儿讲,它像一个检查关卡,只检查不拿东西。三个断言相当于三个守门员,分别盯着小写、大写、数字,全部通过之后,再从开头用[A-Za-z\d]{8,20}真正匹配这整段字符串。

很多新手卡在这个练习上的原因是搞不懂:为什么我用.*[a-z].*却还是失败?因为.*是贪婪的,它会先把整个字符串吃掉,再往后退来满足后面的条件,在同时校验多个字符类别时,顺序处理很难兼顾。断言则让每个条件各自独立检查,互不干扰。理解断言是正则从入门到进阶的一道分水岭,这个练习能帮你把“检查”和“提取”在脑子里分开。

4. 常见问题与排错技巧,都是写过才懂的坑

4.1 贪婪匹配与懒惰匹配:为什么结果总比想象的长

默认情况下,量词都是贪婪的,也就是说它会在满足整体匹配的前提下,尽可能多地吞字符。新手写<a href=".*">时,.*会从第一个href=一直吃到行尾最后一个">才结束,中间的内容全被吞掉。这就是为什么很多人在网页提取时,明明要一个链接,结果把整篇文章都“匹配”出来了。

解决办法是让量词变成懒惰模式,也就是在量词后面加一个?,写成.*?或.+?。懒惰模式会让引擎在“能匹配成功”的前提下,尽可能少地吞字符。比如<a href="(.*?)"就能在碰到第一个"就停下来。

注意一个误区:很多人以为.*?是“匹配到最少字符”,然后疯狂加问号。懒惰只是改变了匹配优先级,不影响最终匹配的合法性。在多个相同结构的文本里提取时,懒惰匹配搭配否定字符组[^"]*往往更稳,因为否定字符组直接限制了字符范围,比依赖“少吞”的方式更可控。

4.2 灾难性回溯:一个“看着没问题”的正则把程序卡死

这是正则领域隐藏最深、最让人头疼的问题。考虑一个模式(a+)+c去匹配一串很长的aaaa...但末尾没有c的文本。引擎会尝试让外层的+和里层的+反复排列组合,穷举所有拆分方式,才能确认匹配失败。

当输入的a足够多时,这种回溯是指数级的,程序看起来就是卡死了,几秒后报超时。这个现象叫“灾难性回溯”(Catastrophic Backtracking)。任何人写正则都应该保持一点点敬畏,尤其是在用嵌套量词写用户输入校验时。

我在实际项目里遇到过一起印象深刻的案例:某个登录接口的密码规则正则^(\w+)+$,用户只要输入一串二十几个字符居然导致接口耗时飙升,排查半天才定位到是正则回溯问题。后来换成了^\w+$,问题立刻消失。记住一条铁律:能不用嵌套量词就别用,尽量让模式保持线性匹配。

4.3 中文匹配与特殊字符的几大误区

正则默认的\w在大多数原生实现里不包含中文,即使某些语言开启了 Unicode 模式,\w的语义也可能不符合预期。匹配中文时比较通用可靠的写法是显式指定汉字范围。

很多语言支持[\u4e00-\u9fa5],在其他的工具或语言里可能需要写成类似[一-龥]。如果你不确定某个字符的 Unicode 范围,可以先在文档里查到码点再写。

还有一个容易忽略的问题:.默认匹配不了换行符。比如你用.*打算匹配一段跨行文字,结果发现只匹配了第一行。解决办法大体有三种:一是把模式改成[\s\S]*,显式包含所有空白和非空白字符;二是开启单行模式,让.也能匹配换行;三是直接用多行模式配合锚点。不同语言、不同工具对这些选项的开关语法不太一样,排查问题先确认你的模式处于什么模式之下。

4.4 我常用的调试思路和习惯

调试正则与其靠肉眼盯,不如把每一步拆开看。我踩过多次坑之后的固定习惯是:

  1. 先在一个有“分组高亮”和“匹配信息”的在线测试工具里调试,肉眼确认分组捕获是否准确。
  2. 用最简单固定的测试文本验证边界:空串、只含一个字符、正常范围、超长文本,每种都跑一遍。
  3. 逐步剥离功能:先不加断言不加锚点,确认核心匹配,再一层层加上锚点和断言,任何一步出问题都更容易定位。
  4. 遇到性能疑问时,拿超长文本实测耗时,不要只看短文本上的表现。有些灾难性回溯在短文本上根本显不出来。
  5. 替换场景里,先做一次“只替换首次匹配”的小范围测试,确认替换式中的分组引用没有写错再全局执行。

这些习惯看起来琐碎,但能帮你把正则调试从“玄学”变成“工程”。尤其是第二点和第四点,几乎每次认真测试都能发现边界问题。

5. 把正则真正用起来:我的几点实战心得

5.1 先写测试用例再写正则

很多同学一上来就拿着模式去匹配正式数据,遇到不对就改,改完再试,反复横跳。更好的顺序是先写出 5 到 10 个“应该匹配”和“不应该匹配”的样本,再开始写正则。

举个例子,如果要做手机号校验,先列出:13812345678(应通过)、12812345678(应拒绝)、1381234(应拒绝)、1381234567a(应拒绝)。把这组用例先摆在那儿,之后无论怎么改正则,都能立刻跑一遍验证。这不但科学,还能避免“只盯着成功用例看、完全无视漏匹配”的问题。

5.2 保持可读性,给正则写注释

正则难读是出了名的,所以更要在写复杂模式时努力提高可读性。各个语言几乎都支持在正则里用(?#注释)或开启x模式写注释。比如密码规则这个练习里,每个(?=...)断言都可以配一行注释,别人接手时也不用从头猜你的思路。

另外,能用命名分组的地方就尽量用命名分组。名字比编号可读性高太多,也更不容易在改正则时引错组号。比如提取日期时写成(?P<year>\d{4})-(?P<month>\d{2}),后面用result.group('year')就能拿年份,读起来一目了然。等你过几个月再回来维护自己的代码时,会感谢当时留了注释的自己。

5.3 正则有边界,知道什么时候该换工具

正则的适用场景是“文本模式匹配”,但并非所有文本问题都适合用正则。比如解析 HTML 这种层级嵌套非常严重的内容,或者解析编程语言这种语法结构复杂的文本,正则就成了劣质方案,容易写出脆弱、复杂、难以维护的模式。

此外,有些场景用正则处理明显比手写循环慢。如果你是在大量日志里做统计,可以考虑先用更快的工具做粗过滤,再用正则做细提取。如果有性能瓶颈,也可以考虑使用不回溯的“正则引擎”或者采用状态机式的专门解析器。

我的体会是:正则是工具箱里极其顺手的一把好工具,但它不是唯一工具。出色地处理文本问题,既要知道正则的强项,也要知道它不适合碰哪些地方。这样用起来心里有数,才能真正发挥它的效率。希望上面这些基础拆解、练习和踩坑记录,能帮你少走点弯路。你先拿几个小例子练上几轮,配上自己的真实数据试一遍,很快就会发现,正则不仅不难,还特别能带来成就感。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进