正则表达式这事,我做了十多年开发,几乎每个项目都会碰到。第一次接触的时候,我也觉得它像天书,满屏的括号反斜杠让人头皮发麻。但后来我慢慢搞明白了一件事:正则表达式匹配文本,本质就是给文本引擎写一份“查找说明书”,你描述得越精确,它找得就越准。不管是 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 次。
