正则这玩意儿,属于那种“看着像乱码,用起来真香”的技能。我刚工作那会儿,看到别人写一长串 ^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$ 去校验邮箱,第一反应是“这什么东西”,第二反应是“背下来再说”。结果真到自己上手处理日志、写爬虫、做表单校验的时候才发现,正则不是靠背的,是靠理解它的匹配逻辑的。这篇文章我尽量把正则拆开揉碎,从最底层的匹配原理讲到实战里的坑,再给几个直接能抄的模板,目标是让看完的人能自己写出、也能自己读懂正则,而不是到处复制粘贴然后祈祷它能跑。
这篇内容适合谁?刚接触正则、被各种符号劝退的新人;写过一阵子正则但老是匹配不到想要内容的初级开发者;以及想系统梳理一遍、顺带看看不同语言里正则有啥坑的人。
1. 从零理解正则的匹配逻辑:它就是一个“扫描器”
正则表达式本质上是一套描述文本模式的规则,正则引擎拿着这套规则去目标字符串里扫描,看哪些位置能“对上号”。很多人学不会正则,是因为一上来就背元字符表,背完就忘。我先带你把引擎的工作方式搞清楚。
1.1 正则引擎按字符逐个尝试,不是整体匹配
想象一下你在一段文章里找“cat”这个词,你不会直接跳到某个位置说“这里有”,而是一个字符一个字符地看:先看当前位置是不是 c,如果是再看下一个是不是 a,再看下一个是不是 t,全对了才算匹配成功。正则引擎就是这么干的。
比如文本是 "I have a cat.",正则 cat 的匹配过程是:
- 引擎从文本位置 0 开始,先比较
c和I,不匹配,位置前进到 1。 - 继续比较
c和空格,不匹配,位置前进到 2。 - 一直到位置 7(字符 c),
c对上了,接着看下一个字符 a,对上了,再看下一个字符 t,对上了,匹配成功。
这个“逐字符推进、失败就跳到下一个位置重试”的机制,是理解后面一切高级写法的地基。你后面遇到的贪婪匹配、回溯、性能问题,全都是从这个机制里长出来的。
注意:正则默认是“只要在某个位置匹配上就返回结果”,所以如果你用
cat去匹配"The cat and the catalog",它返回的是第一个出现的 cat,而不是你脑子里觉得“应该匹配整个单词”的那个 cat。这就是为什么后面要学\b这种边界符。
1.2 元字符是怎么“偷懒”的:. ^ $ 的本质
正则里真正强大的是元字符,因为它们能让一个字符位对上“多种可能”。但很多新手会在这里犯迷糊,比如以为 . 能匹配“任意字符”就包括换行。实际上在大多数正则引擎里,默认情况下 . 匹配的是“除换行符以外的任意字符”。
.:匹配除换行外的任意单个字符。a.c能匹配abc、a1c、a c,但不能匹配ac(中间必须有字符),也不能匹配跨行的a\nc。^:匹配字符串的开始位置。^abc只在字符串开头是 abc 时匹配。$:匹配字符串的结束位置。abc$只在字符串结尾是 abc 时匹配。
有个常见的坑是,很多人以为 ^ 和 $ 是“匹配字符”,其实它们是“匹配位置”。位置不算字符,所以 ^ 匹配到的结果是长度为 0 的空白。这个概念在后面学断言(lookahead/lookbehind)的时候会再次用到。
^ 和 $ 在换行模式下(多行模式)行为会变,比如在 Python 的 re.MULTILINE 模式下,^ 还能匹配每一行的开头,而不仅仅是整个字符串的开头。这个差异在不同语言里都存在,写跨平台脚本的时候要特别小心。
1.3 字符类 []:让一个位置匹配一组字符中的任意一个
[abc] 表示这个位置可以是 a、b、c 中的任意一个。[a-z] 表示可以是任意小写字母。[0-9] 表示任意数字。这里面有几个容易踩的坑:
第一,[] 里的 ^ 表示“取反”。[^0-9] 匹配任意不是数字的字符。注意这个取反是“一个字符位”的取反,不是说“整段不能是数字”。
第二,[] 里的特殊字符大多不需要转义。比如 [.] 就是匹配一个字面上的点号,不用写成 [\.]。但在 [] 外面,. 就是通配符,要匹配字面点号必须写成 \.。这个规则经常让刚从别的语言转过来的人懵。
第三,[] 里的 - 有两种含义。在 [a-z] 这种位置它就是范围连接符;如果放在 [] 开头或结尾,比如 [-abc] 或 [abc-],它就是一个普通字符,表示匹配 -、a、b、c 中的任意一个。想匹配字面 - 的建议放在开头或结尾,省得转义出问题。
字符类是对“一个位置”的描述,所以 [abc][0-9] 表示的是“先一个字母,再一个数字”,总共占两个字符位,而不是“一个由字母和数字组成的东西”。这个理解偏差是很多新手写正则匹配不到内容的根源。
1.4 预定义字符类:\d \w \s 的快捷方式与隐藏问题
\d 是 [0-9] 的简写,\w 是 [A-Za-z0-9_] 的简写(不同语言可能有差异,比如 Python 3 的 \w 默认还匹配 Unicode 汉字),\s 是空白符的简写,包括空格、制表符、换行等。
这几个简写看起来很省事,但坑也在“简写”两个字上。比如你写 \d+ 想匹配数字,在 JavaScript 里没问题,但在 Python 里默认也能匹配 Unicode 数字(比如阿拉伯文的数字字符),这可能导致意外匹配。反过来,有些老系统用 \w 想匹配“字母数字下划线”,结果它把汉字也算进去了,日志过滤的时候就会出错。
如果你要写一个跨语言、跨平台都稳定的正则,最稳妥的做法是:明确指定字符范围。比如只想匹配 ASCII 数字,就写 [0-9],不要写 \d;只想匹配 ASCII 字母,就写 [A-Za-z],不要写 \w。当然,如果你明确要匹配 Unicode 字符,那 \w 反而方便。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 量词:控制“重复次数”的核心,以及贪婪匹配带来的麻烦
单个字符类解决了“这个位置能放什么”的问题,但实际场景里你经常需要“若干个数字”“连续 4 到 8 个字母”这种需求,这就是量词出场的时候。
2.1 量词语法与常见误区:* + ? {n,m}
a*:a 出现 0 次或多次,也就是“有没有都行,有就尽量多匹配”。a+:a 出现 1 次或多次,至少得有一个。a?:a 出现 0 次或 1 次,也就是“可选”。a{3}:正好 3 次。a{3,}:至少 3 次。a{3,5}:3
