# 正则 - 入门

# 遇到的问题

正则表达式主要用来做一些校验(限制和验证输入的合法性),可以改变输入字符串的顺序,也可以在搜索的时候用来秀一下,偶尔使用,每次都查资料,所以整理一份入门指南方便查阅。

# 定义

正则表达式是用语匹配字符串中字符组合的模式。在 JavaScript 中,正则表达式也是对象。这些模式被用于 RegExp 的 exec 和 test 方法,以及 String 的 match、matchAll、replace、search 和 split 方法。

# 语法

字面量,构造函数和工厂函数都是可以创建正则表达式的:

/pattern/flags
new RegExp(pattern [, flags])
RegExp(pattern [, flags])
1
2
3

参数:

pattern 正则表达式的文本

flags 修饰符。如果指定,标志可以具有以下值的任意组合

  • g 全局匹配;找到所有匹配,而不是在第一个匹配后停止
  • i 忽略大小写
  • m 多行; 将开始和结束字符(^和$)视为在多行上工作(也就是,分别匹配每一行的开始和结束(由 \n 或 \r 分割),而不只是只匹配整个输入字符串的最开始和最末尾处。
  • u Unicode; 将模式视为Unicode序列点的序列。不加的时候可以匹配小于 \uFFFF 的 unicode 字符,大于的则需要添加 u 修饰符。
/^.$/.test("\uD842\uDFB7"); //false  "\uD842\uDFB7"是一个四个字节的UTF-16编码,代表一个字符
/^.$/u.test("\uD842\uDFB7"); //true

/\u{61}/.test("a"); //false
/\u{61}/u.test("a"); //true 
// 使用u修饰符,正则表达式能够识别大括号{}表示的Unicode字符,否则无法识别,{61}也会被解读为量词,表示61 的u字符。
1
2
3
4
5
6

但是需要注意,u 修饰符是 ES6 新增,截止至2019/11/05,兼容性如下:

 修饰符兼容性

  • y 粘性匹配; 仅匹配目标字符串中此正则表达式的lastIndex属性指示的索引(并且不尝试从任何后续的索引匹配)。
  • s dotAll模式,匹配任何字符(包括终止符 '\n')。

# 特殊字符

# 字符类别(Character Classes)

字符 含义
. (点号,小数点) 匹配任意单个字符,但是行结束符除外:\n \r \u2028\u2029
在字符集中,点( . )失去其特殊含义,并匹配一个字面点( . )。
需要注意的是,m 多行(multiline)标志不会改变点号的表现。因此为了匹配多行中的字符集,可使用 [^] (当然你不是打算用在旧版本 IE 中),它将会匹配任意字符,包括换行符。
例如,/.y/ 匹配 "yes make my day" 中的 "my" 和 "ay",但是不匹配 "yes"。
\d 匹配任意阿拉伯数字。等价于 [0-9]
例如,/\d//[0-9]/ 匹配 "B2 is the suite number." 中的 '2'。
\D 匹配任意一个不是阿拉伯数字的字符。等价于[^0-9]
例如,/\D//[^0-9]/ 匹配 "B2 is the suite number." 中的 'B'。
\w 匹配任意来自基本拉丁字母表中的字母数字字符,还包括下划线。等价于 [A-Za-z0-9_]
例如,/\w/ 匹配 "apple" 中的 'a',"$5.28" 中的 '5' 和 "3D" 中的 '3'。
\W 匹配任意不是基本拉丁字母表中单词(字母数字下划线)字符的字符。等价于 [^A-Za-z0-9_]
例如,/\W//[^A-Za-z0-9_]/ 匹配 "50%" 中的 '%'。
\s 匹配一个空白符,包括空格、制表符、换页符、换行符和其他 Unicode 空格。
等价于 [ \f\n\r\t\v​\u00a0\u1680​\u180e\u2000​\u2001\u2002​\u2003\u2004​ \u2005\u2006​\u2007\u2008​\u2009\u200a​\u2028\u2029​​\u202f\u205f​ \u3000]
例如 /\s\w*/ 匹配 "foo bar" 中的 ' bar'。
\S 匹配一个非空白符。等价于 [^ \f\n\r\t\v​\u00a0\u1680​\u180e\u2000​\u2001\u2002​\u2003\u2004​ \u2005\u2006​\u2007\u2008​\u2009\u200a​\u2028\u2029​\u202f\u205f​\u3000]
例如,/\S\w*/ 匹配 "foo bar" 中的 'foo'。
\t 匹配一个水平制表符(tab)
\r 匹配一个回车符(carriage return)
\n 匹配一个换行符(linefeed)
\v 匹配一个垂直制表符(vertical tab)
\f 匹配一个换页符(form-feed)
[\b] 匹配一个退格符(backspace)(不要与 \b 混淆)
\0 匹配一个 NUL 字符。不要在此后面跟小数点。
\cX XA - Z 的一个字母。匹配字符串中的一个控制字符 (opens new window)
例如,/\cM/ 匹配字符串中的 control-M。
\xhh 匹配编码为 hh (两个十六进制数字)的字符。
\uhhhh 匹配 Unicode 值为 hhhh (四个十六进制数字)的字符。
\ 对于那些通常被认为字面意义的字符来说,表示下一个字符具有特殊用处,并且不会被按照字面意义解释。
例如 /b/ 匹配字符 'b'。在 b 前面加上一个反斜杠,即使用 /\b/,则该字符变得特殊,以为这匹配一个单词边界。

对于那些通常特殊对待的字符,表示下一个字符不具有特殊用途,会被按照字面意义解释。
例如,* 是一个特殊字符,表示匹配某个字符 0 或多次,如 /a*/ 意味着 0 或多个 "a"。 为了匹配字面意义上的 * ,在它前面加上一个反斜杠,例如,/a\*/ 匹配 'a*'。

# 字符集合(Character Sets)

字符 含义
[xyz] 一个字符集合,也叫字符组。匹配集合中的任意一个字符。你可以使用连字符'-'指定一个范围。
例如,[abcd] 等价于 [a-d],匹配"brisket"中的'b'和"chop"中的'c'。
[^xyz] 一个反义或补充字符集,也叫反义字符组。也就是说,它匹配任意不在括号内的字符。你也可以通过使用连字符 '-' 指定一个范围内的字符。
例如,[^abc] 等价于 [^a-c]。 第一个匹配的是 "bacon" 中的'o' 和 "chop" 中的 'h'。

# 边界(Boundaries)

字符 含义
^ 匹配输入开始。如果多行(multiline)标志被设为 true,该字符也会匹配一个断行(line break)符后的开始处。
例如,/^A/ 不匹配 "an A" 中的 "A",但匹配 "An A" 中的 "A"。
$ 匹配输入结尾。如果多行(multiline)标志被设为 true,该字符也会匹配一个断行(line break)符的前的结尾处。
例如,/t$/ 不匹配 "eater" 中的 "t",但匹配 "eat" 中的 "t"。
\b 匹配一个零宽单词边界(zero-width word boundary),如一个字母与一个空格之间。 (不要和 [\b] 混淆)
例如,/\bno/ 匹配 "at noon" 中的 "no",/ly\b/ 匹配 "possibly yesterday." 中的 "ly"。
\B 匹配一个零宽非单词边界(zero-width non-word boundary),如两个字母之间或两个空格之间。
例如,/\Bon/ 匹配 "at noon" 中的 "on",/ye\B/ 匹配 "possibly yesterday." 中的 "ye"。

# 分组(Grouping)与反向引用(back references)

字符 含义
(x) 匹配 x 并且捕获匹配项。 这被称为捕获括号(capturing parentheses)。
例如,/(foo)/ 匹配且捕获 "foo bar." 中的 "foo"。被匹配的子字符串可以在结果数组的元素 [1], ..., [n] 中找到,或在被定义的 RegExp 对象的属性 $1, ..., $9 中找到。捕获组(Capturing groups)有性能惩罚。如果不需再次访问被匹配的子字符串,最好使用非捕获括号(non-capturing parentheses),见下面。
\n n 是一个正整数。一个反向引用(back reference),指向正则表达式中第 n 个括号(从左开始数)中匹配的子字符串。
例如,/apple(,)\sorange\1/ 匹配 "apple, orange, cherry, peach." 中的 "apple,orange,"。一个更全面的例子在该表格下面。
(?:x) 匹配 x 不会捕获匹配项。这被称为非捕获括号(non-capturing parentheses)。匹配项不能够从结果数组的元素 [1], ..., [n] 或已被定义的 RegExp 对象的属性 $1, ..., $9 再次访问到。
(?<name>x) 分组命名,和普通的分组 (x) 相比增加了 ?<name> ,之后就可以通过名字来引用啦。
反向引用一个命名分组的语法是 \k<name>,注意命名分组同样可以通过数字索引来反向引用,比如:
/(?<foo>a)\k<foo>\1/.test("aaa") // true
在 replace() 方法的替换字符串中反向引用是用 $<name>
"abc".replace(/(?<foo>a)/, "$<foo>-") // "a-bc",同样 $1 仍然可用

# 数量词(Quantifiers)

字符 含义
x* 匹配前面的模式 x 0 或多次。
例如,/bo*/ 匹配 "A ghost booooed" 中的 "boooo","A bird warbled" 中的 "b",但是不匹配 "A goat grunted"。
x+ 匹配前面的模式 x 1 或多次。等价于 {1,}
例如,/a+/ 匹配 "candy" 中的 "a","caaaaaaandy" 中所有的 "a"。
x*?
x+?
像上面的 *+ 一样匹配前面的模式 x,然而匹配是最小可能匹配。
例如,/".*?"/ 匹配 '"foo" "bar"' 中的 '"foo"',而 * 后面没有 ? 时匹配 '"foo" "bar"'。
x? 匹配前面的模式 x 0 或 1 次。
例如,/e?le?/ 匹配 "angel" 中的 "el","angle" 中的 "le"。
如果在数量词 *+?{}, 任意一个后面紧跟该符号(?),会使数量词变为非贪婪( non-greedy) ,即匹配次数最小化。反之,默认情况下,是贪婪的(greedy),即匹配次数最大化。
在使用于向前断言(lookahead assertions)时,见该表格中 (?=)、(?!) 和 (?:) 的说明。
x(?=y) 只有当 x 后面紧跟着 y 时,才匹配 x。 例如,/Jack(?=Sprat)/ 只有在 'Jack' 后面紧跟着 'Sprat' 时,才会匹配它。/Jack(?=Sprat|)Frost)/ 只有在 'Jack' 后面紧跟着 'Sprat' 或 'Frost' 时,才会匹配它。然而,'Sprat' 或 'Frost' 都不是匹配结果的一部分。
x(?!y) 只有当 x 后面不是紧跟着 y 时,才匹配 x。例如,/\d+(?!\.)/ 只有当一个数字后面没有紧跟着一个小数点时,才会匹配该数字。/\d+(?!\.)/.exec("3.141") 匹配 141 而不是 3.141。
x|y 匹配 xy
例如,/green|red/ 匹配 "green apple" 中的 ‘green',"red apple." 中的 'red'。
x{n} n 是一个正整数。前面的模式 x 连续出现 n 次时匹配。
例如,/a{2}/ 不匹配 "candy," 中的 "a",但是匹配 "caandy," 中的两个 "a",且匹配 "caaandy." 中的前两个 "a"。
x{n,} n 是一个正整数。前面的模式 x 连续出现至少 n 次时匹配。
例如,/a{2,}/ 不匹配 "candy" 中的 "a",但是匹配 "caandy" 和 "caaaaaaandy." 中所有的 "a"。
x{n,m} n 和 m 为正整数。前面的模式 x 连续出现至少 n 次,至多 m 次时匹配。
例如,/a{1,3}/ 不匹配 "cndy",匹配 "candy," 中的 "a","caandy," 中的两个 "a",匹配 "caaaaaaandy" 中的前面三个 "a"。注意,当匹配 "caaaaaaandy" 时,即使原始字符串拥有更多的 "a",匹配项也是 "aaa"。

# 断言(Assertions)

字符 含义
x(?=y) 先行断言,仅匹配被y跟随的x。
举个例子,/Jack(?=Sprat)/,如果"Jack"后面跟着 "Sprat" ,则匹配之。
/Jack(?=Sprat|Frost)/ ,如果"Jack"后面跟着"Sprat"或者"Frost",则匹配之。但是,"Sprat" 和"Frost" 都不会在匹配结果中出现。
x(?!y) 先行否定断言,仅匹配不被y跟随的x。
举个例子,/\d+(?!\.)/ 只会匹配不被点(.)跟随的数字。/\d+(?!\.)/.exec('3.141') 匹配"141",而不是"3.141"
(?<=y)x 后行断言,仅匹配跟随在y后面的x。
举个例子,/(?<=\$)\d+/ ,只会匹配在 $ 符号后面的数字。/(?<=\$)\d+/.exec('1.There is $100 taped underneath your seat.') 匹配"100"。没有 "1"
(?<!y)x 后行否定断言,仅匹配不跟随在y后面的x。
举个例子,/(?<!\$)\d+/,只会匹配不在 $ 符号后面的数字。/(?<!\$)\d+/.exec('1.There is $100 taped underneath your seat.') 匹配"1"。没有 "100"

# 子表达式

从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式。

# 分组

分组体现在:所有以()元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。

# 回溯引用

所谓回溯引用(backreference)指的是模式的后面部分引用前面的子表达式。可以把它当成是变量,回溯引用的语法像\1,\2,....,其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。在表达式里面 \1 引用第一个子表达式,在结果里面 $1 引用匹配到的第一个子表达式对应的字符串。

假设现在要在下面这个文本里匹配两个连续相同的单词,你要怎么做呢?

let str = 'Hello what what is the first thing, and I am am scq000.'
1

利用回溯引用,我们可以很容易地写出\b(\w+)\s\1这样的正则。

回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2...来引用要被替换的字符串。下面以js代码作演示:

let str = 'abc abc 123';
str.replace(/(ab)c/g,'$1g'); 
// 得到结果 'abg abg 123'
1
2
3

如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。

let str = 'scq000';
str.replace(/(scq00)(?:0)/, '$1,$2') 
// 返回scq00,$2
// 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2
1
2
3
4

有时,我们需要限制回溯引用的适用范围。那么通过先行断言和后行断言就可以达到这个目的。

# 先行断言

先行断言(lookahead)是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程中都会用来限制前面的表达式的匹配。例如 happy happily 这两个单词,我想获得以 happ 开头的副词,那么就可以使用happ(?=ily)来匹配。如果我想过滤所有以happ开头的副词,那么也可以采用先行否定断言的正则 happ(?!ily),就会匹配到happy单词的happ前缀。

# 后行断言

后行断言(lookbehind)是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: apple和people都包含ple这个后缀,那么如果我只想找到apple的ple,该怎么做呢?我们可以通过限制app这个前缀,就能唯一确定ple这个单词了。

let reg = /(?<=app)ple/;
1

其中(?<=regex)的语法就是我们这里要介绍的后行断言。regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向后查找。另外一种限制匹配是利用 (?<!regex) 语法,这里称为后行否定断言。与后行断言不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找apple的ple也可以这么写成/(?<!peo)ple。 es2018 之后 js 才支持。所以低版本不兼容的时候,有一个思路是将字符串进行翻转,然后再使用先行断言,作完处理后再翻转回来。看一个简单的例子:

// 比如我想替换apple的ple为ply
let str = 'apple people';
str.split('').reverse().join('').replace(/elp(?=pa)/, 'ylp').split('').reverse().join('');
1
2
3
定义 正则 记忆方式
引用 \0,\1,\2 和 $0, $1, $2 转义+数字
引用 $& 最后匹配的字符
引用 $` 匹配的字符左侧内容
引用 $' 匹配的字符右侧内容
非捕获组 (?: ) 引用表达式(()), 本身不被消费(?),引用(: )
先行断言 (?=) 引用子表达式(()),本身不被消费(?), 查找(=)
先行否定断言 (?!) 引用子表达式(()),本身不被消费(?), 否定的查找(!)
后行断言 (?<=) 引用子表达式(()),本身不被消费(?), 后行的(<,开口往后),查找(=)
后行否定断言 (?<!) 引用子表达式(()),本身不被消费(?), 后行的(<,开口往后),否定的查找(!)

# 逻辑处理

编程语言中用到的三种逻辑关系,与或非

逻辑关系 正则元字符
默认
[^regex](?!regexp) / (?<!regexp)
|

# 方法

# replace

语法:

str.replace(regexp|substr, newSubStr|function);
1

参数:

  • regexp (pattern)

一个RegExp 对象或者其字面量。该正则所匹配的内容会被第二个参数的返回值替换掉。

  • substr (pattern)

一个将被 newSubStr 替换的 字符串。其被视为一整个字符串,而不是一个正则表达式。仅第一个匹配项会被替换。

  • newSubStr (replacement)

用于替换掉第一个参数在原字符串中的匹配部分的字符串。该字符串中可以内插一些特殊的变量名。参考下面的使用字符串作为参数。

  • function (replacement)

一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。参考下面的指定一个函数作为参数。

返回值:一个部分或全部匹配由替代模式所取代的新的字符串。

# 使用字符串作为参数

替换字符串可以插入下面的特殊变量名:

变量名 代表的值
$$ 插入一个 "$"。
$& 插入匹配的子串。
$` 插入当前匹配的子串左边的内容。
$' 插入当前匹配的子串右边的内容。
$n 假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。提示:索引是从1开始

# 指定一个函数作为参数

你可以指定一个函数作为第二个参数。在这种情况下,当匹配执行后,该函数就会执行。 函数的返回值作为替换字符串。(注意:上面提到的特殊替换参数在这里不能被使用。) 另外要注意的是,如果第一个参数是正则表达式,并且其为全局匹配模式,那么这个方法将被多次调用,每次匹配都会被调用。

下面是该函数的参数:

变量名 代表的值
match 匹配的子串。(对应于上述的$&。)
p1,p2, ... 假如 replace() 方法的第一个参数是一个 RegExp 对象,则代表第n个括号匹配的字符串。(对应于上述的$1,$2等。)例如,如果是用 /(\a+)(\b+)/ 这个来匹配,p1 就是匹配的 \a+,p2 就是匹配的 \b+。
offset 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是 'abcd',匹配到的子字符串是 'bc',那么这个参数将会是 1)
string 被匹配的原字符串。
NamedCaptureGroup 命名捕获组匹配的对象

(精确的参数个数依赖于 replace() 的第一个参数是否是一个正则表达式(RegExp)对象,以及这个正则表达式中指定了多少个括号子串,如果这个正则表达式里使用了命名捕获, 还会添加一个命名捕获的对象)

栗子: 使 newString 变成 'abc - 12345 - #$*%':

function replacer(match, p1, p2, p3, offset, string) {
  // p1 is nondigits, p2 digits, and p3 non-alphanumerics
  return [p1, p2, p3].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, replacer);
console.log(newString);  // abc - 12345 - #$*%
1
2
3
4
5
6

# 思考栗子🌰

# 使用正则改变数据结构

let re = /(\w+)\s(\w+)/;
let str = "John Smith";
let newstr = str.replace(re, "$2, $1");
console.log(newstr); 
// "Smith, John"
1
2
3
4
5

# 在多行中使用正则表达式

let s = "Please yes\nmake my day!";
s.match(/yes.*day/);
// Returns null
s.match(/yes[^]*day/);
// Returns 'yes\nmake my day'
1
2
3
4
5

# 使用正则表达式和 Unicode 字符

正如上面表格提到的,\w 或 \W 只会匹配基本的 ASCII 字符;如 'a' 到 'z'、 'A' 到 'Z'、 0 到 9 及 '_'。为了匹配其他语言中的字符,如西里尔(Cyrillic)或 希伯来语(Hebrew),要使用 \uhhhh,"hhhh" 表示以十六进制表示的字符的 Unicode 值。下例展示了怎样从一个单词中分离出 Unicode 字符。

let text = "Образец text на русском языке";
let regex = /[\u{0400}-\u{04FF}]+/ug;

let match = regex.exec(text);
console.log(match);  // prints "Образец"
console.log(regex.lastIndex);  // prints "7"

let match2 = regex.exec(text);
console.log(match2);  // prints "на" [did not print "text"]
console.log(regex.lastIndex);  // prints "15"
1
2
3
4
5
6
7
8
9
10

# 数字千分位

还记得有一次面试遇到数字处理千分位的问题,当时想到可以用正则的,但是想了一下没写出来。 1234 > 1,234

let num = '1234567';
num.replace(/(\d)(?=(?:\d{3})+$)/g,'$1,');

num.replace(/\d{1,3}(?=(\d{3})+$)/g,'$&,');

num.replace(/(?!^)(?=(\d{3})+$)/g,',');
1
2
3
4
5
6

# 参考

上次更新: 10/28/2021, 10:58:51 AM