跳转至

正则表达式

前言

正则表达式一直是困扰很多程序员的一门技术。作为程序开发者,有必要好好了解一下正则,因为很多问题都可以通过一条简单的正则表达式解决,避免写大量脚本去实现。希望这次分享能帮助大家掌握基础的正则,揭开正则表达式各种符号的神秘面纱,从此走上正则大神之路!

句子

(?!...) · (?<!...) · 零宽断言 · 不消耗字符。负向预查排除指定单词。

# 查询没有 linuxserver 这个单词的行  (?!...) 断言给定的子表达式在当前位置不能匹配,不消耗字符。
^(?!linuxserver\b).*

# (?<!...) 断言给定的子表达式在当前位置从右往左不能匹配,表达式必须为固定长度。不消耗字符。
/(?<!not )foo/    ||   not foo but foo  只匹配第二个 foo

常见正则

原子 · 范围 · 分枝 · 量词 · 贪婪 · 边界 · 分组。基础语法速查。

正则表达式基础(字符与结构)

原子是正则表达式的最基本组成单位,而且必须至少要包含一个原子。

正则 意思 说明
. 匹配除换行符以外的任意字符 /s 表示所有字符
\d 匹配一个数字字符 等价于 [0-9]
\D 匹配一个非数字字符 等价于 [^0-9]
\w 匹配包括下划线的任何单词字符 等价于 [A-Za-z0-9_]
\W 匹配任何非单词字符 等价于 [^A-Za-z0-9_]
\s 匹配任何空白字符,包括空格、制表符、换页符等等 等价于 [\f\n\r\t\v\u000B\u0020\u00A0\u2028\u2029]
\S 匹配任何非空白字符 等价于 [^ \f\n\r\t\v]
\n 匹配一个换行符 等价于 \x0a\cJ
\f 匹配一个换页符 等价于 \x0c\cL
\r 匹配一个回车符 等价于 \x0d\cM
\t 匹配一个制表符 等价于 \x09\cI
\v 匹配一个垂直制表符 等价于 \x0b\cK
\xxx 匹配八进制规定的 ASCII 编码字符 比如 [0-9] 可写成 [\48-\57]
\xdd 匹配十六进制规定的 ASCII 编码字符 比如 [0-9] 可写成 [\x30-\x39]
\uxxxx 匹配十六进制规定的 Unicode 字符 比如 [0-9] 可写成 [\u0030-\u0039]
[abc],表示 a 或者 b 或者 c 中的任意一个字符;
[a-z]、[A-Z]、[0-9],表示小写字母、大写字母、0 到 9 的数字;
[^a-z]、[^A-Z]、[^0-9],表示非小写字母、非大写字母、非 0 到 9 的数字;

更多参见基本多语言面(Basic Multilingual Plane, BMP)详细信息:基本多文种平面

分枝条件指的是有几种规则,如果满足其中任意一种规则都应该当成匹配,方法是用 | 把不同的规则分隔开。

"aababxb".replace(/a|b/g, '')
正则 说明
{m} 表示前面的原子必须出现 m 次
{m,} 表示前面的原子最少出现 m 次
{m,n} m 要小于 n,表示前面出现的原子最少 m 次、最多 n 次(包括 m 和 n 次)
? 等价 {0,1},前面的原子可以出现 0 次或 1 次,要么有一次,要么没有
+ 等价 {1,},前面的原子可以出现 1 次或多次,最少要有一个
* 等价 {0,},前面的原子可以出现 0 次、1 次或多次

当正则表达式中包含能接受重复的限定符时,通常的行为是匹配尽可能多的字符。

"aababxb".match(/a.*b/);   // 贪婪匹配 返回 aababxb
"aababxb".match(/a.*?b/);  // 懒惰匹配 返回 aab

它将会匹配整个字符串,这被称为贪婪匹配。

代码/语法 说明
{n,}? 重复 n 次以上,但尽可能少重复
{n,m}? 重复 n 到 m 次,但尽可能少重复
?? 重复 0 次或 1 次,但尽可能少重复
+? 重复 1 次或更多次,但尽可能少重复
*? 重复任意次,但尽可能少重复

正则表达式基础(位置与分组)

位置可以理解为相邻字符之间的位置。咱们可以和空字符串进行类比,字符的首尾、间隙都可以用空字符串进行连接。

'hello' === '' + 'h' + '' + 'e' + '' + 'l' + '' + 'l' + '' + 'o' + ''
符号 说明
^ 脱字符,有 m 时是行的开头,无 m 是字符串的开始
$ 美元符,有 m 时是行的末尾,无 m 是字符串的结束
\b 单词的边界,具体讲有三点规则:① \w\W 之间的位置 ② ^\w 之间的位置 ③ \w$ 之间的位置
\B 非单词的边界,与上面相反:① \w\w 之间的位置 ② \W\W 之间的位置 ③ ^\W 之间的位置 ④ \W$ 之间的位置
\A 文本开头
\Z 文本结尾

用小括号来指定 子表达式(也叫做分组)。

代码/语法 说明
(exp) 匹配 exp,并捕获文本到自动命名的组里
(?:exp) 匹配 exp,不捕获匹配的文本,也不给此分组分配组号
(?<name>exp) 匹配 exp,并捕获文本到名称为 name 的组里

特殊变量名表

$$ · $n · $& · $` · $'String.replace 中的替换变量。

变量名 说明
$$ 直接量符号,即 $ 字符
$n 第 n 个子表达式相匹配的文本,n 等于 [1-9],等于 RegExp.$n
$_ 正则搜索的字符串,等于 RegExp.input
$& 正则最后一次匹配的字符串,等于 RegExp.lastMatch
$+ 正则最后一个分组内容,等于 RegExp.lastParen
$` 正则匹配子串左侧的文本,等于 RegExp.leftContext
$' 正则匹配子串右侧的文本,等于 RegExp.rightContext

反向引用(回溯引用)

使用小括号指定一个子表达式后,默认情况下每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为 1,第二个为 2,以此类推(等于 RegExp.$n)。你可以使用 (?:exp) 这样的语法来剥夺一个分组对组号分配的参与权。

var reg = /<(\w)>.*?<\1>/  // 匹配 html 标签
var str = 'abc%45,efd%12'.replace(/(%)(\d+)/g, '$2$1')  // 互换字符

正则表达式断言

零宽断言 · 环视 · lookaround · (?=) · (?<=)。只匹配位置、不消耗字符。

当我们想要查找类似 \b^$ 那样的一个位置,这个位置应该满足一定的条件(即断言),但这个位置并不包含任何内容,我们称之为 零宽断言。又称之为 环视(lookaround)结构。

零宽断言

代码/语法 说明 名称
x(?=exp)y 断言 y 部分能匹配 exp,与 x 无关 零宽度正预测先行断言、肯定顺序环视、正向前瞻、正向预查、向前断言
x(?!exp)y 断言 y 部分不能匹配 exp,与 x 无关 零宽度负预测先行断言、否定顺序环视、负向前瞻、负向预查、向前否定断言
x(?<=exp)y 断言 x 部分能匹配 exp,且 exp 从右到左匹配,与 y 无关。ES7 支持 零宽度正回顾后行断言、肯定逆序环视、向后断言
x(?<!exp)y 断言 x 部分不能匹配 exp,且 exp 从右到左匹配,与 y 无关。ES7 支持 零宽度负回顾后发断言、否定逆序环视、向后否定断言

后行断言的特点是从右向左匹配,分组编号虽然一样从左到右分配,但引用时必须在编号的左边引用。

字符转义

\/[\](){}?+*|.^$-

作为字符来匹配必须转义,才能作为正则的原子。

性能

核心:减少「回溯」次数,尽快匹配结果:

  1. 尽量使用边界符(^$\b\B 等),限定搜索字符串位置。
  2. 使用具体的元字符(\d\w\s 等),少用 . 字符。
  3. 多使用确定的量词({n}{n,m}),少用贪婪匹配。
  4. 减少分支,减少「回溯」。

灾难性回溯

嵌套量词(如 (a+)+$)配合长文本会触发指数级回溯,导致 CPU 打满甚至卡死。改写为等价但线性安全的形式(如 a+$),或先用边界符缩小范围。1

正则辅助网站

regex101 · regulex · regexper · debuggex。在线调试与可视化。


  1. 回溯(backtracking)是正则引擎在匹配失败后回退到上一个选择点重试的机制;嵌套量词会让选择点数量相乘,从而出现指数级增长。