正则表达式是提供了一种在文本中进行搜索和替换的强大的方式的模式。它由模式(Patterns)和修饰符(flags)组成。
基本使用
在 JavaScript 中,我们可以通过 RegExp 对象使用它们,也可以与字符串方法结合使用。
创建正则的方式:创建出的正则都是 RegExp 内建类的实例。
/.../
和引号包裹识别为字符串一样regexp = new RegExp("pattern", "flags");
这两种语法之间的主要区别在于,使用斜线 /…/ 的模式不允许插入表达式(如带有 ${…} 的字符串模板)。它是完全静态的。
修饰符
正则表达式可能会有的会影响搜索结果的修饰符。
在 JavaScript 中,有 6 个修饰符:
i
使用此修饰符后,搜索时不区分大小写:A 和 a 之间没有区别(请参见下面的示例)。
g
使用此修饰符后,搜索时会寻找所有的匹配项 —— 没有它,则仅返回第一个匹配项。
m
多行模式(详见 锚点 ^ $ 的多行模式,修饰符 “m”)。
s
启用 “dotall” 模式,允许点 . 匹配换行符 \n(在 字符类 中有详细介绍)。
u
开启完整的 Unicode 支持。该修饰符能够正确处理代理对。详见 Unicode:修饰符 “u” 和类 \p{…}。
y
粘滞模式,在文本中的确切位置搜索(详见 粘性修饰符 “y”,在位置处搜索)
和字符串方法搭配使用
搜索:str.match
匹配成功以数组形式返回,匹配失败则返回 null。
如果带有修饰符 g,则会返回所有匹配项,否则只会返回第一个匹配项。
替换:str.replace
str.replace(regexp, replacement) 方法使用 replacement 替换 regexp 的匹配项:如果带有修饰符 g,则会替换所有匹配项,否则只会替换第一个匹配项。
测试:regexp.test
regexp.test(str) 方法用于测试,如果找到至少一个匹配项则返回 true,否则返回 false。
字符类
let str = "+7(903)-123-45-67";
let regexp = /\d/g;
alert( str.match(regexp) ); // 匹配项构成的数组:[7,9,0,3,1,2,3,4,5,6,7]
// 让我们将其输出为纯数字构成的电话号码:
alert( str.match(regexp).join('') ); // 79031234567
这是数字的字符类。还有其他字符类。
最常用的是:
\d
(“d” 来自 “digit”)
数字:从 0 到 9 的字符。
\s
(“s” 来自 “space”)
空格符号:包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,例如 \v、\f 和 \r。
\w
(“w” 来自 “word”)
“单字”字符:拉丁字母或数字或**下划线 **_。非拉丁字母(如西里尔字母或印地文)不属于 \w。
alert( "I love HTML5!".match(/\s\w\w\w\w\d/) ); // ' HTML5'
反向类
对于每个字符类,都有一个“反向类”,用相同的字母表示,但是大写的。
“反向”表示它与所有其他字符匹配,例如:
- \D
非数字:除 \d 以外的任何字符,例如字母。
- \S
非空格符号:除 \s 以外的任何字符,例如字母。
- \W
非单字字符:除 \w 以外的任何字符,例如非拉丁字母或空格。
上面匹配电话的例子,另一种快捷的替代方式是查找非数字 \D 并将其从字符串中删除:
let str = "+7(903)-123-45-67";
alert( str.replace(/\D/g, "") ); // 79031234567
(.)匹配任何字符
点 . 是一种特殊字符类,它与“除换行符之外的任何字符”匹配。
let regexp = /CS.4/;
alert( "CSS4".match(regexp) ); // CSS4
alert( "CS-4".match(regexp) ); // CS-4
alert( "CS 4".match(regexp) ); // CS 4(空格也是一个字符)
注意:空格也是一个字符,它和abc这些字符等价。
另外,正则匹配是一个一个字符对应匹配的,如下。
alert( "CS4".match(/CS.4/) ); // null,没有匹配项,因为这里没有与点匹配的字符
// /CS.4/ 匹配的是4个字符长度的串
带有修饰符 “s” 时点字符类匹配任何字符
默认情况下,点与换行符 \n 不匹配。
alert( "A\nB".match(/A.B/) ); // null(无匹配项)
// 带上 s 修饰符
alert( "A\nB".match(/A.B/s) ); // A\nB(匹配了!)
IE 浏览器不支持 s 修饰符,我们可以使用两个字符类互补来表示全集,比如[/s/S]
,匹配“空格字符或非空格字符”,就相当于匹配所有字符。
alert( "A\nB".match(/A[\s\S]B/) ); // A\nB(匹配了!)
Unicode:修饰符 “u” 和类 \p{…}
早期 Unicode 表中大多数字符使用 2 个字节编码,这种方式只能编码最多 65536 个字符。但是后续出现了很多罕见字符,比如emoji😊 ,这时 2 个字节编码不够了,就开始使用 4 个字节编码。
JavaScript 对字符串使用的就是 Unicode 编码。但是,当 JavaScript 被发明出来的时候,Unicode 编码要更加简单:当时没有 4 个字节的字符。这就给 js 带来了一些历史遗留问题,导致 js 有些语言功能现在仍无法正确处理 4 个字节编码的字符。
- js 中 length 属性依旧是把 2 个字节编码识别为一个字符,所以会把 4 个字节编码的单个字符当成了 2 个 2 字节长的字符。
alert('😄'.length); // 2
alert('𝒳'.length); // 2
正则表达式也存在这个问题。与字符串有所不同的是,正则表达式有一个修饰符 u 被用以解决此类问题。当一个正则表达式带有这个修饰符后,4 个字节长的字符将被正确地处理。同时也能够使用 Unicode 属性进行查找。
Unicode 属性 \p{…}
Unicode 中的每个字符都有很多属性。它们描述了字符所属的“类别”,包含了关于字符的各种信息。
例如,如果一个字符具有 Letter 属性,这意味着这个字符归属于字母表,是一个字母,任意语言的字符。而 Number 属性则表示这是一个数字:也许是阿拉伯数字,亦或是中文数字,等等。
- 我们可以通过某种属性来查找字符,写作 \p{…}。为了使用 \p{…},一个正则表达式必须使用修饰符 u。
举个例子,\p{Letter} 表示任何语言中的一个字母。我们也可以使用 \p{L},因为 L 是 Letter 的一个别名。对于每种属性而言,几乎都存在对应的缩写别名。
在下面的例子中会找出来 3 个字母,它们属于不同语言,但是都属于 Unicode 编码表中认定的字母,所以都有 Letter 属性。英语、格鲁吉亚语和韩语。
let str = "A ბ ㄱ";
alert( str.match(/\p{L}/gu) ); // A,ბ,ㄱ
alert( str.match(/\p{L}/g) ); // null(没有匹配项,因为没有修饰符 "u")
还有其他字符,比如货币,中文等。
货币
表示货币的字符,例如 $、€ 和 ¥,具有 Unicode 属性 \p{Currency_Symbol},缩写为 \p{Sc}。
让我们用它来查找格式为“货币,接着是一个数字”的价格:
let regexp = /\p{Sc}\d/gu;
let str = `Prices: $2, €1, ¥9`;
alert( str.match(regexp) ); // $2,€1,¥9
中文字符
有一个 Unicode 属性 Script(一个书写系统),这个属性可能有一个值:Cyrillic、Greek、Arabic、Han(中文)等等。
要在给定的书写系统中查找字符,我们需要使用 Script=<value>
,例如对于西里尔字母:\p{sc=Cyrillic},中文象形文字:\p{sc=Han},等等。
let regexp = /\p{sc=Han}/gu; // 返回中文象形文字
let str = `Hello Привет 你好 123_456`;
alert( str.match(regexp) ); // 你,好
锚点:字符串开始 ^ 和末尾 $
插入符号 ^ 和美元符号 $ 在正则表达式中具有特殊的含义。它们被称为“锚点”。
插入符号 ^ 匹配文本开头,而美元符号 $ 则匹配文本末尾。
该模式 ^Mary 表示:字符串开始,紧接着就是 “Mary”,与此类似,我们可以用 snow$ 来测试文本是否以 snow 结尾。
let str1 = "Mary had a little lamb";
alert( /^Mary/.test(str1) ); // true
let str1 = "it's fleece was white as snow";
alert( /snow$/.test(str1) ); // true
完全匹配
这两个锚点 ^…$ 放在一起通常被用于测试一个字符串是否完全匹配一个模式。例如,检查用户输入的格式是否正确。
比如匹配时间/\d\d:\d\d/
,12:23 和 12:234 都会被匹配成功。这显然不行,就需要使用锚点进行完全限定匹配:/^\d\d:\d\d$/
锚点宽度为0,也就是正则一个一个字符匹配,它不算长度。
锚点 ^ $ 的多行模式,修饰符 “m”
多行的情况,比如段落换行 \n。
多行模式由修饰符 m 启用,它只影响 ^ 和 $ 的行为。
默认情况下,锚点 ^ 仅匹配文本的开头,在多行模式下,它匹配行的开头。所以多行是匹配每一行的开始与末尾。
let str = `1st place: Winnie
2nd place: Piglet
3rd place: Eeyore`;
console.log( str.match(/^\d/gm) ); // 1, 2, 3
词边界:\b
词边界 \b 是一种检查,就像 ^ 和 $ 一样。
当正则表达式引擎(实现正则表达式搜索的程序模块)遇到 \b 时,它会检查字符串中的位置是否是词边界。
边界就是字符类型不同的地方,比如字符串"hello world!",字母 o 和后面的空格就是边界,字母 d 和感叹号也是边界;数字 12:23, 2 和冒号之间也是一个边界。
alert( "Hello, Java!".match(/\bJava\b/) ); // Java
alert( "Hello, JavaScript!".match(/\bJava\b/) ); // null
alert( "12,34,56".match(/\b\d\d\b/g) ); // 12,34,56
转义,特殊字符
在/.../
的形式中,我们需要对特殊字符使用反斜杠\
进行转义。
在new RegExp("")
的形式中,我们不仅要对特殊字符转义,还需要额外对反斜杠进行转义\ --> \\
。
因为正则构造函数中传入的是字符串,而字符串本身具有自己的特殊符号转义,比如 \n 转义成换行符,\n 在 RegExp() 函数中,首先会被字符串转义成换行符,而不是正则中的修饰符,所以反斜杠被消耗了。对于 \d 这种字符串无法识别的转义,会直接把 \ 删掉,正则再来转义就没有 \ 存在了。
alert("\d\.\d"); // d.d 反斜杠被删除了
let reg = new RegExp("\d\.\d");
alert( "Chapter 5.1".match(reg) ); // null
// 相当于 "Chapter 5.1".match(/d.d/)
集合和范围 […]
集合
在方括号 […] 中的几个字符或者字符类表示“搜索给定字符中的任意一个”。
// 查找 [t 或 m],然后匹配 "op"
alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top"
范围
方括号也可以包含 字符范围。[0-9A-F]
中有两个范围:它搜索一个字符,该字符要么是在 0 到 9 范围内的数字,要么是从 A 到 F 的字母。
我们也可以在 […] 中使用字符类。例如,如果我们想查找单词字符 \w 或连字符 -,则集合可以写为 [\w-]
字符类是某些字符集合的简写
例如:
- \d —— 和 [0-9] 相同,
- \w —— 和 [a-zA-Z0-9_] 相同,
- \s —— 和 [\t\n\v\f\r ] 外加少量罕见的 Unicode 空格字符相同。
多语言 \w
由于字符类 \w 是简写的 [a-zA-Z0-9_],因此无法找到中文象形文字,西里尔字母等。
我们可以编写一个更通用的模式,该模式可以查找任何语言中的单词字符。借助 Unicode 属性很容易实现:[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]
。
- Alphabetic (Alpha) —— 字母,
- Mark (M) —— 音调,
- Decimal_Number (Nd) —— 数字,
- Connector_Punctuation (Pc) —— 下划线 ‘_’ 和类似的字符,
- Join_Control (Join_C) —— 两个特殊代码 200c 和 200d,用于连字,例如阿拉伯语。
let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;
let str = `Hi 你好 12`;
// 找出所有字母和数字:
alert( str.match(regexp) ); // H,i,你,好,1,2
由于字符类其实是官方给的范围的简写,这就相当于我们自己在设计一个字符类。
排除范围
通过在开头添加插入符号 ^ 来表示匹配所有 除了给定的字符 之外的任意字符。
[^aeyo]
—— 匹配除了 ‘a’、‘e’、‘y’ 或 ‘o’ 之外的任何字符。[^0-9]
—— 匹配除了数字之外的任何字符,与 \D 作用相同。[^\s]
—— 匹配任何非空格字符,与 \S 作用相同。
[…] 中的转义
除了在方括号中有特殊含义的字符外,其它所有特殊字符都是允许不转义的。
// 不需要转义
let reg = /[-().^+]/g;
alert( "1 + 2 - 3".match(reg) ); // 匹配 +,-
转义了也能正常使用:
// 转义其中的所有字符
let reg = /[\-\(\)\.\^\+]/g;
alert( "1 + 2 - 3".match(reg) ); // 仍能正常工作:+,-
范围和修饰符 “u”
如果集合中有代理对(surrogate pairs)(4 个字节编码的字符),则需要标志 u 才能使它们正常工作。
alert( '𝒳'.match(/[𝒳𝒴]/) ); // 显示了一个奇怪的字符,像 [?]
//(搜索执行不正确,返回了半个字符)
// 添加了修饰符 u,那么行为就正常了:
alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳
量词 +, *, ? 和 {n}
数量 {n}
最简单的量词便是大括号中的数字:{n}。
在一个字符(或一个字符类,或 […] 等)后附加一个量词,用来指出我们具体需要的数量。
表现形式:
- 确切的位数:{5}
\d{5} 表示 5 位数,与 \d\d\d\d\d 相同。我们可以添加 \b 来排除位数更多的数字:\b\d{5}\b。
alert( "I'm 12345 years old".match(/\d{5}/) ); // "12345"
- 范围:{3,5},匹配 3-5 个
要查找 3-5 位的数字,我们可以将限制写在花括号中:\d{3,5}
我们可以省略上限。那么正则表达式 \d{3,} 就会查找位数大于等于 3 的数字:
alert( "I'm not 12, but 1234 years old".match(/\d{3,5}/) ); // "1234"
alert( "I'm not 12, but 345678 years old".match(/\d{3,}/) ); // "345678"
量词缩写
+:代表“一个或多个”
与 {1,} 相同。
例如,\d+ 用来查找所有数字:
let str = "+7(903)-123-45-67";
alert( str.match(/\d+/g) ); // 7,903,123,45,67
?:代表“零个或一个”
与 {0,1} 相同。换句话说,它使得符号变得可选。
例如,模式 ou?r 查找 o,后跟零个或一个 u,然后是 r。
所以 colou?r 会找到 color 和 colour:
et str = "Should I write color or colour?";
alert( str.match(/colou?r/g) ); // color, colour
*:代表“零个及以上”
与 {0,} 相同。也就是说,字符可以出现任何次数或者不出现。
例如,\d0* 查找一个数字后面跟着任意数量的零(可能有很多或没有)的数字:
alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1
将其与 +(一个或更多)做比较:
alert( "100 10 1".match(/\d0+/g) ); // 100, 10
// 1 没有被匹配出来,因为 0+ 要求至少有一个 0
贪婪量词和惰性量词
捕获组 ( )
模式的一部分可以用括号括起来 (…)。这被称为“捕获组(capturing group)”。
不带括号,模式 go+ 表示 g 字符,其后 o 重复一次或多次。例如 goooo 或 gooooooooo。
括号将字符组合,所以 (go)+ 匹配 go,gogo,gogogo等。
let regexp = /([\w-]+\.)+\w+/g;
alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com-
匹配中的括号的内容
括号被从左到右编号。正则引擎会记住它们各自匹配的内容,并允许在匹配的结果中获取括号中匹配上的内容。
方法 str.match(regexp)
,如果 regexp 没有修饰符 g,将查找第一个匹配项,并将它作为数组返回:
- 在索引 0 处:完整匹配上的内容。
- 在索引 1 处:第一个括号的内容。
- 在索引 2 处:第二个括号的内容。
例如,我们想找到 HTML 标签 <.?> 并处理它们。将标签内容(尖括号内的内容)放在单独的变量中保存。
就可以将正则表达式改造一下,像这样:<(.?)>。
现在,我们在结果数组中得到了标签的整体
及其内容 h1:
let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);
alert( tag[0] ); // <h1>
alert( tag[1] ); // h1
嵌套组
括号可以嵌套。在这种情况下,括号编号依旧是从左到右,不是整体由外到内的顺序。
let str = '<span class="my">';
let regexp = /<(([a-z]+)\s*([^>]*))>/;
let result = str.match(regexp);
alert(result[0]); // <span class="my">
alert(result[1]); // span class="my"
alert(result[2]); // span
alert(result[3]); // class="my"
可选组
即使组是可选的并且在匹配项中不存在(例如,具有量词 (…)?)。如果没匹配上,结果数组中也会有对应的索引,只是结果为undefined。
let match = 'ac'.match(/a(z)?(c)?/)
alert( match.length ); // 3
alert( match[0] ); // ac(完整的匹配项)
alert( match[1] ); // undefined, 因为没有 (z)? 的匹配项
alert( match[2] ); // c
带有组搜索所有匹配项:matchAll
match 方法只有在正则没有g
修饰符时,才会单独返回组,也就是括号中的内容。
let str = '<h1> <h2>';
let tags = str.match(/<(.*?)>/g);
alert( tags ); // <h1>,<h2>
当表达式带有g
修饰符的时候,match 返回的数组,数组元素是一个一个完整的匹配结果,没有括号内匹配的内容。
要获取括号里匹配的内容,我们应该使用方法 str.matchAll(regexp)
进行搜索。
matchAll 和 match 有 3 个区别:
- 它返回的不是数组,而是一个可迭代的对象。
- 当存在修饰符 g 时,它会匹配上多个结果,每个结果都是以数组的形式返回。
- 如果没有匹配项,则返回的不是 null,而是一个空的可迭代对象。
例如:
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
// results —— 不是数组,而是一个迭代对象
alert(results); // [object RegExp String Iterator]
alert(results[0]); // undefined (*)
results = Array.from(results); // 让我们将其转换为数组,变成:[[],[]] 的形式
alert(results[0]); // <h1>,h1(第一个标签)
alert(results[1]); // <h2>,h2(第二个标签)
命名组
用数字记录组很困难。对于简单的模式,它是可行的,但对于更复杂的模式,计算括号很不方便。我们有一个更好的选择:给括号命名。
在左括号后紧跟着放置 ? 即可完成对括号的命名。命名后,所有的捕获组内容都在 groups 属性中。
例如,让我们查找 “year-month-day” 格式的日期:
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";
let groups = str.match(dateRegexp).groups;
alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30
加上 g,使用 matchAll 匹配多个结果:
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30 2020-01-01";
let results = str.matchAll(dateRegexp);
for(let result of results) {
let {year, month, day} = result.groups; // 直接解构命名来使用组的内容
alert(`${day}.${month}.${year}`);
// 第一个 alert:30.10.2019
// 第二个:01.01.2020
}
使用捕获组的内容替换
str.replace(regexp, replacement)
方法中允许我们在 replacement 字符串中使用捕获组括号中的内容。
方式:使用 $n 来完成,其中 n 是组号。
let str = "John Bull";
let regexp = /(\w+) (\w+)/;
alert( str.replace(regexp, '$2, $1') ); // Bull, John
对于命名的括号,引用为 $。
例如,让我们将日期格式从 “year-month-day” 更改为 “day.month.year”:
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30, 2020-01-01";
alert( str.replace(regexp, '$<day>.$<month>.$<year>') );
// 30.10.2019, 01.01.2020
排除捕获组 ?:
有时我们需要用括号才能正确应用量词,但我们不希望它们的内容出现在结果中。
可以通过在开头添加 ?: 来排除组。
例如,如果我们要查找 (go)+,但不希望括号内容(go)作为一个单独的数组项,则可以编写:(?:go)+。
let str = "Gogogo John!";
// ?: 从捕获组中排除 'go'
let regexp = /(?:go)+ (\w+)/i;
let result = str.match(regexp);
alert( result[0] ); // Gogogo John(完整的匹配项)
alert( result[1] ); // John
alert( result.length ); // 2(在数组中没有其他数组项)
模式中的反向引用:\N 和 \k
我们不仅可以在结果或替换字符串中使用捕获组 (…) 的内容,还可以在模式本身中使用它们。
选择 (OR) |
选择是正则表达式中的一个术语,实际上是一个简单的“或”。
在正则表达式中,它用竖线 |
表示。
例如,我们想要找出编程语言:HTML、PHP、Java 或 JavaScript。对应的正则表达式为:html|php|java(script)?。
方括号也有或的功能,但是方括号只允许字符或字符类。选择|
允许任何表达式。比如正则表达式 A|B|C 表示表达式 A、B 或 C 其一均可。
选择的处理层级比方括号更大。
要将选择应用于模式中一部分内容的选择,我们可以将其括在括号中:
I love HTML|CSS
匹配 I love HTML 或 CSS。I love (HTML|CSS)
匹配 I love HTML 或 I love CSS。