为什么值得学
正则表达式(Regular Expression,简称 regex)不是某一门语言的私有功能,而是一套独立的文本匹配语言:用一串字符描述“我要找什么样的字符串”,再由程序按这个描述去搜索、校验、提取或替换。它几乎出现在所有主流编程语言(JavaScript、Python、Java、Go)、编辑器(VS Code、Vim)、命令行工具(grep、sed、awk)和数据库(MySQL、PostgreSQL)里。
正因为覆盖面如此之广,它成了少有的“一次投入、终身受益”的技能:写表单校验、搜日志、清洗数据、批量改配置、解析接口返回值……这些日常任务中,会正则的人几分钟搞定,不会的人往往要写几十行 if/else 慢慢拼。更重要的是,正则让你学会用“模式”而非“规则”思考文本:同一段表达式,从这个语言搬到那个语言,几乎原样可用。
也有人会说:“字符串函数不是也能做吗?”确实,indexOf、split、startsWith 能解决一部分问题,但它们描述的是“固定写法”和“单点判断”。一旦规则变成“11 位数字且第二位在 3–9 之间”“取两个冒号之间的内容”,字符串函数就要拆成好几行,还要处理各种边界情况;而正则用一行模式就能表达,校验和提取一次完成。
很多人觉得正则像天书,其实它只由几类零件组成:字符类、量词、位置锚点,再加上分组和断言。下面这张速查表覆盖了日常九成的需求,配合本站的正则表达式测试工具边写边看,一两个小时就能上手。
元字符速查表
字符类:匹配“什么样的字符”
| 写法 | 含义 |
|---|---|
. | 任意一个字符(默认不含换行) |
\d \D | 数字 / 非数字 |
\w \W | 字母、数字、下划线 / 取反 |
\s \S | 空白字符(空格、制表符、换行)/ 非空白 |
[abc] | 字符集,匹配其中任意一个 |
[^abc] | 排除集,匹配不在其中的任意字符 |
[a-z] [0-9] | 字符范围 |
量词:匹配“多少次”
| 写法 | 含义 |
|---|---|
* | 0 次或更多 |
+ | 1 次或更多 |
? | 0 次或 1 次(可选) |
{n} | 恰好 n 次 |
{n,} | 至少 n 次 |
{n,m} | n 到 m 次 |
锚点与边界:匹配“什么位置”
| 写法 | 含义 |
|---|---|
^ | 行首 |
$ | 行尾 |
\b | 单词边界,如 \bcat\b 不会匹配 catalog |
\B | 非单词边界 |
分组与引用:把匹配结果存下来
| 写法 | 含义 |
|---|---|
(...) | 捕获组,结果可按编号单独取出 |
(?:...) | 非捕获组,只分组不保存结果 |
(?<name>...) | 命名捕获组(部分语言支持) |
\1 \2 | 反向引用,引用第 1、2 组匹配到的内容 |
| | 或,如 cat|dog 匹配 cat 或 dog |
断言:向前看、向后看,但不消费字符
| 写法 | 含义 |
|---|---|
(?=...) | 正向先行断言,后面必须跟 … |
(?!...) | 负向先行断言,后面不能跟 … |
(?<=...) | 正向后行断言,前面必须是 …(部分语言支持) |
(?<!...) | 负向后行断言,前面不能是 …(部分语言支持) |
最后是修饰符:i 忽略大小写、g 全局匹配、m 多行模式(此时 ^ $ 按行生效)、s 让 . 也能匹配换行。写法因语言而异:JavaScript 用 /pattern/gi,Python 用 re.findall(pattern, text, re.I)。
五个实战例子
1. 手机号(中国大陆)
^1[3-9]\d{9}$
1 开头,第二位取 3–9(覆盖 13x–19x 号段),后跟 9 位数字,共 11 位。^ $ 保证整串匹配,否则 12345678901abc 也会“部分通过”。
2. 邮箱(入门版)
^[\w.+-]+@[\w-]+\.[\w.-]+$
[\w.+-]+ 匹配用户名(字母数字、点、加号、减号),然后是 @、域名、点和后缀。这是够用的初筛版;生产环境如果要更严格,还要考虑域名长度等细节。
3. 日期(YYYY-MM-DD)
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
月份用 (0[1-9]|1[0-2]) 限制在 01–12,日限制在 01–31,| 配合分组收窄可选范围。注意它校验的是格式,不校验 2 月 30 日这类日历错误——那属于业务逻辑,交给日期库处理更稳妥。
4. IPv4 地址
^((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d)$
IPv4 每段是 0–255,拆成三种情况:250–255、200–249、0–199(1?\d?\d 覆盖 0–199)。把“一段加一个点”作为分组重复 3 次,最后再来一段。这段把“数字大小范围”翻译成“字符模式”的思路很典型,值得多读几遍。
5. 从 URL 里提取参数
[?&]page=(\d+)
匹配问号或 & 后跟 page=,捕获组 (\d+) 把页码单独存下来,代码里取第 1 组即可。想同时抓多个参数,用非捕获组:[?&](?:page|size)=(\d+),分组不会增加编号,(\d+) 永远是第 1 组。
6. 提取 HTML 标签内容(反向引用实战)
<(\w+)>.*?</\1>
用 (\w+) 捕获开始标签的标签名,后面的 \1 反向引用保证闭合标签与它同名;配合非贪婪 .*? 只取一对标签之间的内容,再加全局标志就能提取出全部段落。这个例子把分组、反向引用、非贪婪串在了一起。
贪婪与非贪婪
量词默认是贪婪的:能多匹配就多匹配。比如用 <.+> 匹配 <p>你好</p>,你可能以为它先匹配到 <p>,实际上它一路吃到最后的 >,把整串都吞了。在量词后加 ? 变成非贪婪:<.+?> 只匹配 <p>。同理还有 +?、??、{n,m}?。
两者执行逻辑不同:贪婪是“先吃光,不行再往回吐”(回溯),非贪婪是“先吃一口,不够再补”。用同样的输入对照一下:
| 表达式 | 模式 | 对 <p>甲</p><p>乙</p> 的匹配 |
|---|---|---|
<.+> | 贪婪 | 整串 <p>甲</p><p>乙</p> 一次吞掉 |
<.+?> | 非贪婪 | 只匹配第一个 <p> |
实战建议:
- 提取两个标记之间的内容,优先用字符集排除,如
<p>([^<]*)</p>,比<p>(.*?)</p>更稳; - 非贪婪不是万能药,某些场景反而产生更多回溯(见下一节);
- 把
<.+>和<.+?>分别贴进测试器,高亮结果一眼看出差别。
性能陷阱:灾难性回溯
这是正则最阴险的坑:表达式没写错,运行却慢到卡死。根源在于回溯型引擎(JavaScript、Python、Java 大多如此)匹配失败时要不断回退重试,而某些模式会让重试次数随输入长度指数级增长,这就是灾难性回溯(Catastrophic Backtracking),也是 ReDoS 攻击的常见入口。
典型元凶是嵌套量词匹配重叠内容:
^(a+)+$
匹配 aaaaaaaaaa 很快,换成 aaaaaaaaab(末尾多个 b)时,引擎要尝试把 a 分配给内层和外层量词的每一种切法——每多一个 a 尝试次数翻倍,几十个字符就能卡死页面。类似的还有 (\d+)*$、(a|aa)+$。
- 避免
(a+)+这类嵌套量词; - 能用字符集排除(
[^<]*)就别用.*; - 先限制用户输入的长度;
- 需要更可控的引擎时,了解原子组
(?>...)、占有量词*+(PCRE、Java 等支持); - 上线前用测试器喂一段超长字符串,观察耗时是否暴涨。
常见转义坑
正则里 . * + ? ( ) [ ] { } ^ $ | \ 都是元字符,想匹配它们的字面意思必须加反斜杠。例如匹配“价格 9.9 元”要写 \d+\.\d+;漏掉反斜杠的 \d+.\d+ 里,点会匹配任意字符。
- 双重转义:在字符串里写正则,反斜杠本身要先转义。JavaScript 中
"\\d"才等于/\d/;Python 强烈建议用原始字符串r"\d";Java 里写"\\d"。 - 字符集内的特例:
[a-z]里-表示范围,匹配字面减号要放开头或结尾([-a-z]);]、^在集内也有位置规则。 - 中文匹配:默认
\w不含中文,匹配中文字符用[\u4e00-\u9fa5],或支持 Unicode 属性时用\p{Script=Han}。 - 斜杠:正斜杠本身不是元字符,但 JavaScript 用
/.../包裹正则,内部遇到/要写成\/。 - 字符集内反而简单:
[.]、[*]里的点、星号都是字面意思,不需要转义;部分引擎对[\.]会给出“多余转义”的警告。
各语言的正则差异
- JavaScript:默认
\d只匹配 ASCII 数字;现代引擎已支持后行断言;通过g等标志位控制行为。 - Python:建议一律用原始字符串
r"...";命名组写法是(?P<name>...);re模块默认返回第一个匹配。 - Java:默认
\d只匹配 ASCII,匹配 Unicode 数字需开启UNICODE_CHARACTER_CLASS标志。 - PCRE(PHP、Nginx 等):功能最全,支持原子组、条件子表达式等高级特性。
- grep / sed:默认基础正则(BRE)里
+、?、|要写成\+、\?、\|;用grep -E切到扩展正则(ERE)才不用转义。
结论:写之前查对应语言的文档,写完用测试器验证一遍,别凭记忆硬背。
常用正则模板:直接抄
与其每次重新写,不如把高频表达式存成模板。下面这些经过验证,可直接使用:
| 用途 | 表达式 | 说明 |
|---|---|---|
| 中国大陆手机号 | ^1[3-9]\d{9}$ | 1 开头、第二位 3–9、共 11 位 |
| 邮箱(入门版) | ^[\w.+-]+@[\w-]+\.[\w.-]+$ | 覆盖绝大多数常见邮箱 |
| 日期 YYYY-MM-DD | ^\d{4}-\d{2}-\d{2}$ | 只校验格式,不校验月份合法性 |
| IPv4 地址 | ^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$ | 每段 0–255 |
| URL(http/https) | ^https?://[\w-]+(\.[\w-]+)+([/\w-]*)*$ | 简化版,够日常校验用 |
注意:模板能省时间,但永远要在你的真实数据上验证一遍——比如"日期"模板不校验 2026-13-99,IPv4 模板的边界值也建议在正则测试工具里跑一跑,确认符合你的预期再上生产。
常见问题 FAQ
明明能匹配,为什么结果不对?
先查三件事:有没有 ^ $ 限定整串;大小写是否被忽略;量词是不是贪婪吃多了。八成问题出在这三处。
\d 能匹配中文数字吗?
默认不能。\d 匹配 0–9(部分语言开启 Unicode 标志后除外),“一二三”要用字符集或 Unicode 属性匹配。
非贪婪一定更好吗?
不一定。非贪婪在需要“最短匹配”时好用,但同样会回溯;能用字符集排除时,[^...] 往往比两者都干净。
正则能解析 HTML 吗?
取简单的标签内容可以,但 HTML 是嵌套结构,复杂页面请用真正的解析器(Python 的 BeautifulSoup、JS 的 DOMParser),否则迟早被奇怪的嵌套和注释搞崩。
替换时 $1 和 \1 有什么区别?
\1 是模式内部的反向引用;$1 是替换文本里的占位符。替换时各语言写法不一:JavaScript 和 Java 用 $1,Python 的 re.sub 用 \1 或 \g<1>。记不清就查文档,或在测试器里直接验证替换结果。
记不住怎么办?
不需要背。把速查表收藏起来,常用表达式存成片段库,配合测试器随用随查。用得多了,核心元字符自然就熟了。
行动小结
- 记住字符类、量词、锚点三大件,加上分组与断言,就覆盖了绝大多数需求;
- 把本文六个例子逐一在正则测试工具里跑一遍,改一改、坏一坏、修一修,比看十遍教程都管用;
- 结果奇怪先怀疑贪婪,页面卡死先怀疑嵌套量词;
- 把常用表达式存成片段,下次直接复用。
正则不是背出来的,是用出来的。现在就去测试器里贴第一条表达式吧。