正则表达式这事,我做了十多年开发,几乎每个项目都会碰到。第一次接触的时候,我也觉得它像天书,满屏的括号反斜杠让人头皮发麻。但后来我慢慢搞明白了一件事:正则表达式匹配文本,本质就是给文本引擎写一份“查找说明书”,你描述得越精确,它找得就越准。不管是 Python、Java、JavaScript,还是 MySQL、命令行、富文本编辑器,底层逻辑全都一样。这篇内容我就把“使用正则表达式匹配文本”这件事从头到尾拆开讲,适合刚入门的朋友,也适合那些已经会用但经常踩坑的人。
1. 正则表达式到底在匹配什么:先从文本说起
1.1 你写的正则,其实是给引擎的一份“查找说明书”
很多人学正则,一上来就背符号,\d 代表数字,* 代表任意多个,^ 代表开头,但背完还是不会用。原因在于没有建立“文本在引擎眼里是什么”的模型。
正则引擎看一段文本,不会把它当成有意义的句子,而是当成一个字符数组。比如 hello world 这串文本,在引擎眼里是 h、e、l、l、o、空格、w、o、r、l、d 这 11 个字符的线性序列。正则表达式的任务,就是从这个序列中找到符合你描述的“一段连续子串”。
这里有几个关键点值得记住:
- 正则默认是“在文本里找子串”,不是必须从头匹配到尾。
- 正则匹配返回的位置通常包括“起始索引”和“结束索引”,有些语言还有“捕获组”。
- 同一个正则在一段文本中可能有多处匹配,实际取哪个,取决于引擎的遍历顺序和你的业务需求。
我经常用一个生活类比来解释:你老婆让你去超市买“某种红色包装的、500ml 的饮料”,她描述得越具体,你在货架上就越不容易拿错。正则表达式就像这句话,红色包装 对应字符类,500ml 对应固定长度,饮料 对应结尾模式。描述清楚,匹配结果就可靠。
1.2 引擎的两种脾气:NFA 与回溯
正则引擎分为两大流派:DFA(确定性有限自动机)和 NFA(非确定性有限自动机)。市面上绝大多数编程语言里的正则引擎,比如 Python 的 re、Java 的 java.util.regex、JavaScript 的 RegExp、C# 的 Regex,都是 NFA 引擎。NFA 最大的特点就是“回溯”。
什么是回溯?我举个最简单的例子。正则 a.*b 去匹配字符串 a123b456b。引擎从左往右走:
- 先匹配
a,成功。 .*是贪婪模式,会尽量吞掉所有字符,于是引擎一口气吃到字符串末尾。- 然后引擎发现还需要匹配一个
b,但已经到末尾了,于是它开始“吐”字符,往回退一格,检查是不是b。 - 回溯到倒数第二个位置时,正好是
b,匹配成功。
这个“往回退”的过程就是回溯。回溯本身不是坏事,但量词嵌套得多,文本又长又复杂时,回溯次数会指数级暴涨,这就是后面要说的“灾难性回溯”。你理解这一点,就能明白为什么写正则不能光看结果,还要考虑性能。
1.3 位置也是一种字符:锚点与边界
正则匹配的很多坑,来自“位置”和“字符”没分清楚。^ 和 $ 匹配的是位置,不是字符。\b 也是位置,它表示“单词边界”,即一侧是单词字符(字母、数字、下划线),另一侧不是单词字符的位置。
举个例子,正则 ^abc 匹配字符串 abcdef 时,它会从字符串开头这个位置开始检查,成功。但正则 abc$ 去匹配 abcdef 时,它会在末尾这个位置反向检查,失败,因为 f 不等于 c。
这个区别在实际处理中很重要。比如你要匹配一个完整的邮箱地址,如果只用 \w+@\w+\.\w+,在文本 联系我: test@example.com,谢谢 里也能匹配出 test@example.com,但也会在 xiaoming@example.com.cn 里只匹配出 xiaoming@example.com 这一部分。如果你希望严格匹配“完整单词”,就要加上 \b 边界,写成 \b\w+@\w+\.\w+\b。
我在日常处理文本时,几乎每个正则都会先问自己一句:我要的是位置精确,还是字符精确?想清楚这个,匹配逻辑就清晰了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 匹配文本的核心语法拆解
2.1 字符与字符类:精确控制“一个字”
正则最基础的单元是字符匹配。你写一个 a,它就匹配字符 a。写 1,就匹配字符 1。但在真实场景里,你往往不想匹配某个固定字符,而是想匹配“某一类字符”。这时候就需要字符类,也就是方括号 []。
字符类的常见用法:
[abc]:匹配a、b、c中任意一个字符。[a-z]:匹配小写字母中的任意一个。[0-9]:匹配数字中的任意一个。[a-zA-Z0-9_]:匹配字母、数字、下划线中的任意一个。[^abc]:匹配除a、b、c之外的任意字符,注意^在方括号里表示“取反”,这和它在开头表示“行首”是两回事。
字符类里还有一些内置的简写:
| 简写 | 含义 | 等价说明 |
|---|---|---|
\d |
数字 | 相当于 [0-9] |
\D |
非数字 | 相当于 [^0-9] |
\w |
单词字符 | 大多数语言里相当于 [A-Za-z0-9_] |
\W |
非单词字符 | 相当于 [^A-Za-z0-9_] |
\s |
空白符 | 包括空格、制表符、换行符 |
\S |
非空白符 | 相当于 [^ \t\r\n\f\v] |
很多人会忽略一个细节:\d 在 Python 3 的 re 模块里默认还能匹配 Unicode 数字,比如某些全角数字;但在 JavaScript 里,\d 通常只匹配 ASCII 数字。这个差异在处理中文文本和全球化内容时特别容易踩坑,后面我再详细说。
2.2 量词:匹配多长有讲究
正则里的量词决定了“前面的表达式要重复多少次”。最常用的三个:
*:0 次或多次。+:1 次或多次。?:0 次或 1 次。
还有更精确的花括号量词:{n} 表示恰好 n 次,{n,} 表示至少 n 次,{n,m} 表示 n 到 m 次。
这里有一个最重要的概念:贪婪与懒惰。默认情况下,量词是贪婪的,也就是“尽量多匹配”。a.*b 在文本 a123b456b 中,会匹配 a123b456b,而不是只匹配 a123b。如果你想让它“匹配到第一个 b 就停”,可以改成懒惰量词 a.*?b,这时会匹配 a123b;如果有多个符合条件的片段,它会从左边开始逐个匹配。
这个差异在实际文本提取中影响很大。比如你要从 HTML 片段中提取标签内容,如果用 <div>(.*)</div>,贪婪模式下会把从第一个 <div> 到最后一个 </div> 之间的所有内容都吞进去。你想要的可能是第一层 div 的内容,而不是整块嵌套。改成 <div>(.*?)</div> 往往更接近预期。
但懒惰量词也不是万能药。它在多次匹配时会频繁“试探性”地前进和回溯,如果文本特别长,性能会下降。所以性能敏感的场景,我更推荐尽可能用排除型字符类,比如 <div>([^<]*)</div>,而不是 .*?。
2.3 分组、捕获与非捕获
括号在正则有双重身份:一是改变优先级,二是创建捕获组。比如 (ab)+ 可以匹配 ab、abab、ababab。
捕获组的意思是,正则引擎会把括号里匹配到的内容单独存下来,供你后面提取或引用。比如用 Python 正则提取日期,r'(\d{4})-(\d{2})-(\d{2})',匹配成功后 group(1) 是年份,group(2) 是月份,group(3) 是日期。
反向引用也是一个很有用的特性。\1 引用的是第一个捕获组匹配到的内容。比如你想找文本里重复出现的单词,可以用 \b(\w+)\b\s+\1\b,这个正则在 hello hello 里能匹配成功。
但有些时候你只想用括号分组,不想捕获内容。一方面是为了减少内存占用,另一方面是为了避免分组编号混乱。这时候用非捕获组 (?:...)。比如 (?:ab)+ 匹配 abab,但不会生成捕获组。我写正则时,非捕获组用得非常多,尤其是复杂表达式里,它能让你后续的 group 编号更清晰。
2.4 零宽断言:匹配位置而不是字符
零宽断言是正则里最“高级”的玩法之一。它本身不消费字符,只检查当前位置是否符合条件。常用四类:
(?=...):正向前瞻,表示后面跟着的是...。(?!...):负向前瞻,表示后面跟着的不是...。(?<=...):正向后顾,表示前面是...。(?<!...):负向后顾,表示前面不是...。
举个例子:正则 \d+(?=元) 匹配数字,但要求这个数字后面紧跟“元”字。用它解析字符串 苹果5元 香蕉3个,匹配到的是 5,而不是 3,因为 3 后面没有“元”字。
后顾断言在 JavaScript 和 Python 的支持情况不太一样。Python 的 re 模块要求后顾断言必须是固定长度,而 regex 第三方库支持变长后顾。JavaScript 从 ES2018 开始支持后顾断言,之前版本不支持。这个兼容性问题,写跨平台脚本时一定要查清楚。
我在实际中常用负向前瞻来排除某些关键词。比如匹配所有以 .txt 结尾、但文件名里不包含 temp 的文件路径,可以用 ^((?!temp).)+\.txt$。这个写法的核心是“排除型匹配”,原理就是让每个字符都先经过 (?!temp) 的检查。很多文本过滤、敏感词排除的场景,都可以用这类结构实现。
3. 在常见语言和工具里落地正则
3.1 Python:re 模块的四个常用函数
Python 的 re 模块是我日常使用频率最高的正则工具。核心函数就四个:
re.search(pattern, text):在整个文本中查找第一处匹配,找不到返回None。re.match(pattern, text):从字符串开头匹配,注意它只匹配开头,不是全文匹配。re.findall(pattern, text):返回所有匹配的子串列表。re.sub(pattern, repl, text):替换所有匹配的子串。
很多从其他语言转 Python 的人会把 re.match 当成“判断字符串是否完全匹配”,这是个坑。比如 re.match(r'\d+', 'abc123') 返回 None,但 re.search(r'\d+', 'abc123') 能找到 123。如果你要校验整个字符串都是数字,正确写法是 re.fullmatch(r'\d+', text),或者用 re.match(r'\d+$', text)。
Python 的 re.findall 还有一个容易搞混的点:如果正则里有捕获组,它返回的是每个组的内容元组,而不是完整匹配的字符串。比如:
python复制import re
text = "2024-01-15"
result = re.findall(r'(\d{4})-(\d{2})-(\d{2})', text)
print(result) # 输出 [('2024', '01', '15')]
如果你想要完整日期字符串,需要写成 re.findall(r'\d{4}-\d{2}-\d{2}', text),不带括号。
我处理日志文本时,最喜欢 Python 的原因是,它可以用 re.sub 配合函数进行动态替换。比如要把所有数字字段加一:
python复制import re
text = "编号: 10, 数量: 3"
def increment(match):
return str(int(match.group()) + 1)
result = re.sub(r'\d+', increment, text)
print(result) # 输出 编号: 11, 数量: 4
这种“replace with function”的能力,比大多数语言的正则替换函数都方便。
3.2 Java 与 JavaScript:转义、匹配方法与差异
Java 的正则在 java.util.regex 包里,核心类是 Pattern 和 Matcher。一个常见误区是字符串里的反斜杠需要双重转义。比如匹配数字,你在正则表达式里写 \d,但在 Java 字符串里要写成 "\\d"。很多新手在 Java 里写 "\d",编译直接报错,其实就是转义没处理对。
Java 常用的匹配方法:
java复制Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher("abc123def456");
while (matcher.find()) {
System.out.println(matcher.group());
}
这里 find() 是“查找下一处匹配”,而 matches() 是“整个字符串完全匹配”,二者语义完全不同。我见过不少同事把 matches() 当成“包含匹配”,结果怎么调都不对。
JavaScript 的正则用起来比 Java 灵活,但坑也不少。RegExp 对象方法包括 test() 和 exec(),字符串方法包括 match()、replace()、search()、split()。
JavaScript 最需要注意的是全局标志 g。正则对象带 g 时,exec() 会记住上次匹配的 lastIndex,下次调用会从上次的位置继续。这意味着同一个正则对象多次 exec() 时,结果可能不是从头开始的。如果你在循环里复用带 g 的正则,很容易出现漏匹配或死循环。我建议每次调用前手动重置 lastIndex = 0,或者干脆每次重新创建正则对象。
前端开发中,还常常需要用正则处理富文本 HTML。这里我多说一句:富文本编辑器生成的内容结构复杂,标签嵌套、属性混杂,用正则去解析 HTML 标签层级非常脆弱。简单的文本清理可以,比如去掉所有标签 <[^>]+>,但要提取特定深处的节点,还是交给 DOM 解析器或者专门的 HTML 解析库靠谱。
3.3 MySQL REGEXP:数据库里的文本筛选
MySQL 的 REGEXP 运算符可以让你在 SQL 查询里使用正则匹配文本字段。基本用法:
sql复制SELECT * FROM user WHERE email REGEXP '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$';
注意 MySQL 字符串里的反斜杠也需要转义。而且 MySQL 8.0 之前,正则引擎不支持某些高级语法,比如固定长度的后顾断言。MySQL 8.0 引入了 REGEXP_LIKE() 等函数,功能更接近 ICU 正则,但依然和编程语言里的正则有些差异。
我踩过的一个坑是 MySQL 的字符集问题。默认情况下,如果你的数据库连接字符集设置不对,针对中文或生僻字的正则匹配会失效。比如要匹配某个字段中包含“喆”字的记录,正常写 WHERE name REGEXP '喆' 应该能查出来,但如果连接字符集是 latin1,汉字被转成了乱码字节,正则当然匹配不上。排查这类问题时,先 SHOW VARIABLES LIKE 'character_set_connection'; 看字符集,再检查字段的 collation。
MySQL 里还有一个容易和正则混淆的功能:LIKE。LIKE 支持 % 和 _ 通配符,但功能远不如正则强大。能用 LIKE 解决的问题尽量不要上正则,因为正则在数据量大的表上做全表扫描时,性能很难看。
3.4 命令行与编辑器:grep、sed 和 VSCode 文本处理
服务器上处理文本,grep 和 sed 是绕不开的两个工具。grep 默认使用基础正则表达式(BRE),|、+、? 这些符号在 BRE 里需要转义才能表示特殊含义。比如匹配 cat 或 cut,在扩展正则里写 grep -E 'c(a|u)t',在基础正则是写 grep 'c\(a\|u\)t'。我建议直接用 grep -E 或 grep -P,省去转义的痛苦。-P 表示使用 PCRE(Perl 兼容正则),功能最全,但并不是所有系统都默认支持。
sed 主要用来做文本替换和流式编辑。常用命令:
bash复制sed -n 's/foo/bar/p' file.txt
这行命令把每行第一次出现的 foo 替换成 bar,并且只打印发生替换的行。-n 配合 p 是 sed 里很常用的“只看修改结果”的组合。如果你要全局替换每一行所有匹配项,需要在替换命令末尾加 g,写成 s/foo/bar/g。
编辑器里的正则匹配是我平时推荐刚上手的人练习的最佳环境。VSCode 的查找替换框默认支持正则,并且它基于 JavaScript 正则语法。如果你在 VSCode 里用 \d 匹配数字,可以直接验证你的正则写法是否正确,所见即所得。Obsidian 这类笔记软件的高级查找也支持正则,用来批量整理 Markdown 文本非常好用。比如把 [[]] 语法的 wiki 链接统一改成 Markdown 链接,一条正则替换就搞定了。
4. 典型文本匹配场景与完整实操
4.1 校验需求:纯数字、手机号、邮箱
正则最广泛的用途之一就是输入校验。我整理几个最常见场景的写法:
纯数字校验:
- Python:
re.fullmatch(r'\d+', text) - Java:
text.matches("\\d+") - JavaScript:
/^\d+$/.test(text)
这里要注意 \d 的范围问题。JavaScript 的 \d 等同于 [0-9],不会匹配小数点和负号,所以如果你要校验的是整数,^\d+$ 就够。可如果要校验负数和小数,正则就要改成 ^-?\d+(\.\d+)?$。
手机号校验,就不能只靠正则,还需要结合号段规则。比如中国大陆的手机号通常以 1 开头,第二位是 3-9,后面跟 9 位数字。正则可以写作 ^1[3-9]\d{9}$。但请注意,号段是会增加的,正则写太死,反而会给将来埋坑。我的建议是:正则只做格式约束,真正是否有效依赖发送短信验证码之类的业务逻辑验证。
邮箱校验是另一个经典场景。一个常见的正则:
code复制^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
这个正则已经够覆盖 90% 的常用邮箱。但你要明白,它不能覆盖所有合法邮箱,比如某些域名后缀长度是 2 位或 3 位,但有些新顶级域名可能更长。过度严格的正则反而会拒绝真实用户。我个人的做法是,做前端格式提示,后端用真正成熟的校验库,而不是自己堆一个超长正则。
4.2 日志处理:提取关键字段
日志文本是正则匹配的最佳训练场。比如你给我一行 Nginx 访问日志:
code复制192.168.1.1 - - [15/Jan/2024:13:22:11 +0800] "GET /api/user?id=123&lang=zh HTTP/1.1" 200 5300 "https://example.com/page" "Mozilla/5.0"
要从这行文本里提取 IP、时间、请求路径、状态码、响应大小,用 Python 可以这么写:
python复制import re
log_line = '192.168.1.1 - - [15/Jan/2024:13:22:11 +0800] "GET /api/user?id=123&lang=zh HTTP/1.1" 200 5300 "https://example.com/page" "Mozilla/5.0"'
pattern = (
r'(?P<ip>\d+\.\d+\.\d+\.\d+)'
r'.*?\[(?P<time>[^\]]+)\]'
r'\s+"\w+ (?P<path>[^?\s]+)[^"]*"\s+'
r'(?P<status>\d{3})\s+'
r'(?P<size>\d+)'
)
match = re.search(pattern, log_line)
if match:
print(match.groupdict())
输出会是:
code复制{'ip': '192.168.1.1', 'time': '15/Jan/2024:13:22:11 +0800', 'path': '/api/user', 'status': '200', 'size': '5300'}
这里 (?P<name>...) 是 Python 的命名分组,比 group(1) 这种数字索引可读性高得多。我在处理复杂日志时,几乎都用命名分组,尤其是字段多的时候,后续 match.group('name') 取值不容易乱。
一行日志能拆,多行日志也能处理。很多应用异常日志是跨行的,比如 Java 的堆栈信息。你要想提取“异常类型 + 第一行消息”,可以先把整个文件读成字符串,再用 DOTALL 模式匹配。
4.3 多行文本匹配的坑:. 不等于一切
默认情况下,正则里的 . 匹配除换行符以外的任意字符。这是一个让很多人困惑的点。你用 re.search(r'start(.*)end', text, re.DOTALL) 才能让 . 匹配换行。
不同语言对“让点号匹配换行”的写法不同:
- Python:
re.DOTALL或re.S - Java:
Pattern.DOTALL - JavaScript: 用
[\s\S]、[\d\D]或者[^]代替.,因为 JS 正则没有点号全匹配模式(s修饰符是较新版本才有的)。 - MySQL: 正则默认也可以匹配换行,但需要确认具体版本行为。
我自己的习惯是,如果确实要匹配“包括换行在内的任意字符”,优先写 [\s\S],这个写法在绝大多数语言里都兼容,不用担心修饰符设置问题。比如 start([\s\S]*?)end,配合懒惰量词,可以从多行文本中精准抓取第一个结束标记之前的内容。
多行匹配还有另一个常见问题:^ 和 $ 默认只匹配整个字符串的开头结尾,而不是每行的开头结尾。Python 里用 re.MULTILINE 开启多行模式,JavaScript 里给正则加 m 修饰符。假如你要统计一个文本里以 ERROR 开头的行数,必须开启多行锚定。
4.4 文本清洗与替换:从 JSON 到富文本
“匹配文本”不光是提取和校验,替换同样重要。我经常需要清洗从别人那边拿来的脏数据,正则替换是最高效的方式。
比如要清理一段文本中的不可见字符和连续空白:
python复制import re
text = "Hello \t world\r\n 2024"
cleaned = re.sub(r'\s+', ' ', text).strip()
print(repr(cleaned)) # 'Hello world 2024'
在富文本编辑器的场景里,后台返回的 HTML 往往夹杂着一堆内联样式。如果你想干掉所有 style="..." 属性,但保留标签结构:
python复制html = '<div style="color:red; font-size:12px">hello</div>'
cleaned = re.sub(r'\s+style="[^"]*"', '', html)
print(cleaned) # <div>hello</div>
这里 [^"]* 比 .*? 更安全,因为 .*? 可能在前面的引号配对混乱时匹配到不该匹配的内容。
但我也要提醒一句:正则处理富文本和 HTML 只能做“粗活”。嵌套标签、注释、脚本内容会瞬间击穿正则方案。比如正则 <div>(.*?)</div> 遇到 <div>a<div>b</div>c</div>,只能匹配出 <div>a<div>b</div>,而不是外层完整的嵌套结构。真正需要解析 HTML,还是用 BeautifulSoup、htmlparser、DOM 解析器这类真正的解析工具。
5. 常见问题与排查技巧实录
5.1 匹配不上:先检查转义和编码
遇到正则匹配不上,我的排查顺序是固定的:
- 看转义。大部分语言里,字符串本身会处理反斜杠。Java、C#、Python 里的
\d在字符串常量中需要写"\\d"或r'\d'。先从最简单的数字匹配判断转义是否正正确。 - 看编码。处理中文文本时,如果文件是 UTF-8 编码,但你的脚本用默认 GBK 打开,那么正则匹配到的字节流就已经错了。你用中文写正则去匹配乱码,除非有 Unicode 规范化处理,否则大概率失败。
- 看模式标志。大小写不敏感用
re.I或i标志;多行用re.M或m;点号是否匹配换行,根据情况加s标志。 - 看锚点位置。
^是行首还是字符串首?\b在中文文本里经常不生效,因为汉字不是\w的一部分?这个要特别小心,不同引擎对 Unicode 单词边界的处理并不一致。
这几个点排查完,八成以上“匹配不上”的问题都能解决。
5.2 性能杀手:灾难性回溯
前面提到 NFA 引擎会出现回溯。当正则里出现多个嵌套的贪婪量词时,回溯次数可能爆炸。经典例子是匹配 HTML 注释 <!--.*-->,如果目标文本里没有 -->,引擎会把 .* 一直扩展到最后,然后逐字节回溯,性能极慢。更极端的例子是 (a+)+$ 去匹配一串没有 a 结尾的超长字符串,时间复杂度会接近 O(2^n)。
我的建议是:
- 能使用字符类排除,就不要用点号加懒惰量词。
- 避免嵌套量词,比如
(a+)+、(.*)*这种写法。 - 长文本复杂匹配优先使用非回溯引擎,比如 Python 的
regex库提供(?>...)原子组,或者 Go 的regexp使用 RE2 语法,从根源上避免灾难性回溯。 - 对高并发、大流量的文本校验场景,要设置正则超时,或者限制输入文本长度。
5.3 中文和生僻字的匹配
很多人处理中文时直接写 [\u4e00-\u9fa5]。这个范围能覆盖大部分常用汉字,但它并不完整。Unicode 中 CJK 统一表意文字的范围还包括扩展 A、扩展 B 等区域,生僻字如“𠀀”“𠮷”不在 \u4e00-\u9fa5 内。如果你要匹配“任意汉字”,更稳妥的方式是使用 Unicode 属性转义。
Python 的 regex 库支持 \p{Han},JavaScript ES2018 之后支持 \p{Script=Han}(需要加 u 标志)。Java 也支持 \p{Script=Han}。MySQL 8.0 的正则基于 ICU,应该也能用类似写法。
实际业务里,比如你有个字段存客户姓名,里面有生僻字,你用 WHERE name REGEXP '[\\u4e00-\\u9fa5]+' 可能漏数据。保险的做法是先确认数据库字符集为 utf8mb4,再配合 \p{Han} 或者直接在应用层用专门的 Unicode 处理库判断。
5.4 为什么不要用正则去解析 HTML/富文本
这个问题我几乎每年都要讲一遍。正则适合处理“平坦文本”,不适合解析“树形结构”。HTML、XML、富文本内容是树形的,标签有嵌套关系。用正则匹配 <div>(.*?)</div> 这种模式,在有嵌套标签时必然出错。
举一个实际例子:你要在富文本编辑器内容中找出所有链接的 URL。简易正则 href="(.*?)" 确实能拿到大多数链接,但如果属性顺序是 <a class="link" href="https://example.com">,那么 href 前面的其他属性里有 " 或空格,正则写法稍不注意就会匹配错。更麻烦的是有些编辑器会生成单引号属性,或者省略引号,甚至链接里有转义字符。
正确的做法是:
- HTML: 用
BeautifulSoup、jsoup、DOMDocument。 - 富文本 JSON: 很多富文本编辑器(如 Tiptap、Quill)保存的是带
type、children字段的 JSON 结构,直接遍历 JSON 树比正则可靠得多。
我见过不少人为了省事,硬用正则把富文本里的图片地址抠出来,结果经常把用户粘贴的文本里的 URL 一起抠出来,误处理一堆脏数据。踩过几次坑之后,我再也不在富文本场景里用正则替代解析器了。
5.5 证书不匹配、Excel 匹配与正则的边界
除了前面提到的大量文本场景,正则的“匹配”概念还经常被其他领域借用,容易混淆。比如网络请求时报错 服务器证书与任何预期值都不匹配,这其实是 TLS 证书校验问题,和正则表达式没有任何关系。你不可能用正则去解决证书信任链问题,它属于“身份验证”的匹配,不是文本子串的匹配。
Excel 里的 VLOOKUP 跨表匹配,本质是单元格值的精确匹配,同样不是正则匹配。不过有一点相通:VLOOKUP 匹配不了时,先查数据格式是否一致、有没有隐藏空格、数字是不是文本格式,这和正则匹配中文文本时的编码排查思路一模一样。都是“你以为数据是一样的,但底层表示不同”。
明白正则的边界,比学会更多语法更重要。正则不是万能的文本处理工具,它擅长的是模式查找、校验、替换;不擅长的是语义理解、层级嵌套、上下文判断。该用解析器就用解析器,该用算法匹配就用算法匹配,工具用对了,效率才能上来。
最后再分享一个我的个人习惯。遇到稍微复杂的正则,我不会一次性写完,而是先拆成小块,用在线测试工具或者本地脚本逐个验证,确认每一段的行为符合预期后,再拼接成最终表达式。每写一个正则,还会顺手补上几个边界测试用例,比如空字符串、超长字符串、特殊字符、中文、不带匹配项的情况。这套习惯帮我省掉了大量线上故障,正则这东西,写出来只是第一步,测透了才算真的会用了。
