# 正则 - 入门
# 遇到的问题
正则表达式主要用来做一些校验(限制和验证输入的合法性),可以改变输入字符串的顺序,也可以在搜索的时候用来秀一下,偶尔使用,每次都查资料,所以整理一份入门指南方便查阅。
# 定义
正则表达式是用语匹配字符串中字符组合的模式。在 JavaScript 中,正则表达式也是对象。这些模式被用于 RegExp
的 exec 和 test 方法,以及 String
的 match、matchAll、replace、search 和 split 方法。
# 语法
字面量,构造函数和工厂函数都是可以创建正则表达式的:
/pattern/flags
new RegExp(pattern [, flags])
RegExp(pattern [, flags])
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字符。
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 | X 是 A - 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 | 匹配 x 或 y 例如,/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.'
利用回溯引用,我们可以很容易地写出\b(\w+)\s\1
这样的正则。
回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2...来引用要被替换的字符串。下面以js代码作演示:
let str = 'abc abc 123';
str.replace(/(ab)c/g,'$1g');
// 得到结果 'abg abg 123'
2
3
如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)
这样就可以避免浪费内存。
let str = 'scq000';
str.replace(/(scq00)(?:0)/, '$1,$2')
// 返回scq00,$2
// 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2
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/;
其中(?<=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('');
2
3
定义 | 正则 | 记忆方式 |
---|---|---|
引用 | \0,\1,\2 和 $0, $1, $2 | 转义+数字 |
引用 | $& | 最后匹配的字符 |
引用 | $` | 匹配的字符左侧内容 |
引用 | $' | 匹配的字符右侧内容 |
非捕获组 | (?: ) | 引用表达式(()), 本身不被消费(?),引用(: ) |
先行断言 | (?=) | 引用子表达式(()),本身不被消费(?), 查找(=) |
先行否定断言 | (?!) | 引用子表达式(()),本身不被消费(?), 否定的查找(!) |
后行断言 | (?<=) | 引用子表达式(()),本身不被消费(?), 后行的(<,开口往后),查找(=) |
后行否定断言 | (?<!) | 引用子表达式(()),本身不被消费(?), 后行的(<,开口往后),否定的查找(!) |
# 逻辑处理
编程语言中用到的三种逻辑关系,与或非
逻辑关系 | 正则元字符 |
---|---|
与 | 默认 |
非 | [^regex] 和 (?!regexp) / (?<!regexp) |
或 | | |
# 方法
# replace
语法:
str.replace(regexp|substr, newSubStr|function);
参数:
- 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 - #$*%
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"
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'
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"
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,',');
2
3
4
5
6