我用正则踩过的最贵的一次坑,是一段看起来完全正确的手机号校验:^1[3-9]\d{9}$。单条测试没问题,拿去清洗一份 2000 多行的用户名单时,只提出来 1 条。当时怀疑量词写错、怀疑行尾有不可见字符,甚至怀疑数据本身有问题,最后才反应过来:多行文本里 $ 默认只匹配整个字符串的末尾,也就是最后一行的结尾,前面 1999 行全被"锚点"卡掉了。一个标志位的事,让我对整个下午的清洗结果都失去了信任。正则的难不在于符号多,而在于你以为它匹配的和它实际匹配的之间,隔着几个看不见的路径规则。
一、先把最常用的几个符号钉死
| 写法 | 含义 | 易错点 |
|---|---|---|
. \d \w \s | 除换行外任意一个字符 / 数字 / 字母数字下划线 / 空白 | . 不转义时会匹配句号之外的任何东西,IP 里的点必须写 \. |
* + ? | 0 次以上 / 1 次以上 / 0 或 1 次 | ? 跟在量词后是"非贪婪",跟在括号后是"命名/断言",含义完全不同 |
{n} {n,m} | 恰好 n 次 / n 到 m 次 | {0,} 等价 *,但可读性更好 |
^ $ | 字符串开头 / 结尾(默认不是"行"首尾) | 多行文本忘了 m 标志就会全灭,这是我那次故障的根因 |
[abc] [^abc] | 字符集 / 排除集 | 方括号里 . * + 失去特殊含义,- 位置不对会变成范围 |
(a|b) (?:a|b) | 捕获分组 / 非捕获分组 | 括号只是"不想被 | 拆开"时该用 (?:),白占一个分组序号 |
二、两个例子,看标志位怎么改变结果
手机号(校验整段输入):^1[3-9]\d{9}$
手机号(从多行日志里提取):(?m)^1[3-9]\d{9}$ 或 1[3-9]\d{9}(?!\d)
IPv4:\b(?:\d{1,3}\.){3}\d{1,3}\b
第一行只适合表单校验:输入框里就是这一个值,^ 和 $ 恰好夹住它。第二行才是我清洗名单该写的:m 让 ^ $ 变成行首行尾;或者干脆不锚定,用 (?!\d) 保证 11 位之后不再紧跟数字,避免从一串 19 位的订单号里"截"出一个假手机号。第三行如果把 \. 写成 .,那么 123456789 也能被它匹配走——这是从日志里提 IP 时最常见的误报来源。
标志位里 g(全局)决定"找到第一个就停"还是"遍历全部",i 忽略大小写,m 多行,s 让 . 连换行也能匹配。这四个是我在实际工作里唯一会反复切换的。
三、贪婪与非贪婪:为什么它把整段 HTML 吞了
量词默认是贪婪的,会尽量往后吃。<a href=".*"> 对 <a href="/home">text</a> <a href="/about"> 这种两个标签的行,会从第一个 " 一路吃到最后一个 ",中间的 </a> 全被包含进去。改成 <a href=".*?"> 才是"就近闭合"。同理,抓 HTML 时更稳的做法是排除掉结束符本身:<a href="[^"]*">,用字符集表达"直到遇到引号为止",比依赖非贪婪更不容易出错。
我在调这类表达式时的流程是:写完立刻把正则和一段真实样本一起贴进正则表达式测试,靠实时高亮看边界到底落在哪。眼睛在脑子里推演 .*? 的走位是不可靠的,高亮框一出来就有结论;分组面板还能列出每个捕获组的序号和内容位置,写 $1、$2 替换时不用再数括号。
四、三类让正则变成事故的时刻
- 灾难性回溯:形如
(a+)+b、(\d+[-\s]*)*@的嵌套量词,遇到一段"几乎符合但最终不匹配"的文本会指数级尝试。我们线上有一次接口 CPU 被单条输入打满,排查到一条清洗规则里的([\w\.-]+)+@,换成[\w.-]+@之后同样的输入耗时从 8 秒多降到几十毫秒。 - 没限定结尾的校验:
^[\w.-]+@[\w-]+\.\w+$会放过a@b.c这种不存在的域名。校验邮箱时我倾向于把"合法格式"和"真实可投递"当成两件事,正则只负责前者。 - 批量场景别硬写:如果只是要从一段文本里把手机号、邮箱、URL、IP 这类固定类型的东西捞出来,用文本信息提取比现写正则省事;要做批量替换时用批量查找替换,能同时挂多条规则并看到每条的替换次数。
五、常见疑问
Q:为什么我的正则在 JS 里对、在后端 Java 或 Python 里不对?
多半是方言差异。JS 和 Java 默认的 \d 只认 ASCII 数字,Python 3 默认把它扩展到了全角数字和其他文字的数字,清洗混排数据时结果就会不一样;(?<=) 这类后行断言在较旧的 Safari 上会直接抛语法错误;Python 的 re.match 只从字符串开头试,re.search 才全串扫描,而 JS 的 test/exec 不加 ^ 时不做锚定。跨语言迁移时至少跑三条样本:正常值、边界值、一段更长的脏数据。
Q:捕获组和非捕获分组 (?:) 有什么区别?
括号会占一个序号,替换时 $1 按顺序引用;只需要分组作用、不需要引用内容时写成 (?:...),能少踩"序号错位"的坑,性能也略好。
Q:怎么知道自己在匹配到的内容里第几个分组?
数左括号,从左到右 1 起。带嵌套时极易数错,所以我一般用测试工具的分组详情直接看每个组的实际内容,而不是靠心算。
Q:正则能校验密码强度、金额格式吗?
能,但别指望一条表达式写完。金额类最容易漏的是小数位数与千分位组合,写两条互相独立的断言比一条超长表达式更好维护,也更容易定位失败原因。
Q:为什么看起来一样的文本匹配不上?
常见是不可见字符:全角空格、零宽空格、Windows 换行的 \r、以及从网页复制带来的软连字符。先看清字符再怀疑语法。
那次名单清洗最后拿回来的数据是对的,但我把"锚点默认不作用于每一行"这条写进了自己的笔记首页。正则这种东西,读懂它匹配了什么只是及格,能说出它在哪些位置停下来、为什么停下,才是能被信任的表达式。