为什么值得学

正则表达式(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>。记不清就查文档,或在测试器里直接验证替换结果。

记不住怎么办?

不需要背。把速查表收藏起来,常用表达式存成片段库,配合测试器随用随查。用得多了,核心元字符自然就熟了。

行动小结

  1. 记住字符类、量词、锚点三大件,加上分组与断言,就覆盖了绝大多数需求;
  2. 把本文六个例子逐一在正则测试工具里跑一遍,改一改、坏一坏、修一修,比看十遍教程都管用;
  3. 结果奇怪先怀疑贪婪,页面卡死先怀疑嵌套量词;
  4. 把常用表达式存成片段,下次直接复用。

正则不是背出来的,是用出来的。现在就去测试器里贴第一条表达式吧。