JavaScript 正则表达式
关于正则表达式的教程还有文章网络资源非常多,这本迷你书比较全面对学习正则提供很大帮助。还有正则逻辑可视化网站对理解看起来又长又乱的正则有很大帮助。
字符匹配篇
匹配模式
横向模糊匹配:一个正则可匹配的字符串的长度不是固定的,其实现的方式是使用量词。例如 {m,n}
,表示连续出现最少 m 次,最多 n 次。/a{1,4}/
匹配A连续出现一次或四次。
纵向模糊匹配:一个正则匹配的字符串具体到某一位字符时,它可以不是某个确定的字符存在多种可能。其实现的方式是使用字符组。譬如 [abc]
,表示该字符是可以字符 "a"
、"b"
、"c"
中的任何一个。/a[123]b/
匹配"a1b
,"a2b"
或 "a3b"
。
字符组
[abc]
匹配一个字符。匹配范围使用 - 连接,[1-6]
匹配1到6范围。如果想匹配-需要用上转义符避免引擎认为是范围表示法 [1\-6]
匹配"1"
"6"
"-"
。排除使用 ^ 符号,例如 [^abc]
表示除了"a"
"b"
"c"
之外的任意一个字符。以下是简写形式列表:
组 | 具体含义 |
---|---|
\d | 表示 [0-9]。表示是一位数字。记忆方式:其英文是 digit(数字)。 |
\D | 表示 [^0-9]。表示除数字外的任意字符。 |
\w | 表示 [0-9a-zA-Z_]。表示数字、大小写字母和下划线。记忆方式:w 是 word 的简写,也称单词字符。 |
\W | 表示 [^0-9a-zA-Z_]。非单词字符。 |
\s | 表示 [ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。记忆方式:s 是 space 的首字母,空白符的单词是 white space。 |
\S | 表示 [^ \t\v\n\r\f]。 非空白符。 |
. | 表示 [^\n\r\u2028\u2029]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。记忆方式:想想省略号 … 中的每个点,都可以理解成占位符,表示任何类似的东西。 |
匹配任意字可以使用 [\d\D]、[\w\W]、[\s\S] 和 [^] 。
量词
以下是量词列表:
组 | 具体含义 |
---|---|
{m,} | 表示至少出现 m 次。 |
{m} | 等价于 {m,m},表示出现 m 次。 |
? | 等价于 {0,1},表示出现或者不出现。记忆方式:问号的意思表示,有吗 ? |
+ | 等价于 {1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。 |
* | 等价于 {0,},表示出现任意次,有可能不出现。记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来。 |
关于贪婪匹配与惰性匹配。这里有个例子:
var regex = /\d{2,4}/g var string = "11 22 333333" console.log( string.match(regex) ) // -> ["11", "22", "3333", "33"]
/\d{2,4}/
表示匹配数字0到9出现两次至四次。上面例子的结果是贪婪匹配的结果,会尽可能多的匹配到4位数字。如果在量词后面加入一个?
问号则改为惰性匹配:
var regex = /\d{2,4}?/g var string = "11 22 333333" console.log( string.match(regex) ) // -> ["11", "22", "33", "33", "33"]
/\d{2,5}?/
表示匹配数字0到9出现两次至四次,但当匹配到两次时就不再往下继续匹配。对比最上面的例子贪婪匹配 "333333"
console输出结果,惰性匹配会尽可能少的匹配。通过在量词后面加个问号就能实现惰性匹配:
惰性量词 | 贪婪量词 |
---|---|
{m,n}? | {m,n} |
{m,}? | {m,} |
?? | ? |
+? | + |
*? | * |
多选分支
一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一。
var regex = /good|goodbye/g var string = "goodbye" console.log( string.match(regex) ) // -> ["good"]
/good|goodbye/g
表示匹配"good"
和"goodbye"
其中任何之一,但多选分支是惰性的,匹配到"good"
后则不会匹配后面的字符。
位置匹配篇
^
和 $
正则位置匹配,最常用的就是 ^
与 $
两个锚,分别匹配开头和结尾。多行匹配模式(即有修饰符 m)时匹配的是每行开头与结尾:
var result = "line1\nline2".replace(/^|$/gm, '#') console.log(result) /* #line1# #line2# */
\b
和 \B
\b
是单词边界,具体就是 \w
与 \W
之间的位置,也包括 \w
与 ^
之间的位置,和 \w
与 $
之间的位置。
var result = '[JS] JS'.replace(/\b/g, "#") console.log(result) // -> "[#JS#] #JS#"
这里匹配 \b
单词边界位置并且替换成 #,结果分析:
- 第一个#,两边字符是
"["
与"J"
,是\W
与\w
之间的位置。 - 第二个#,两边字符是
"S"
与"]"
,是\w
与\W
之间的位置。 - 第三个#,两边字符是 空格 与
J
,是\W
与\w
之间的位置。 - 第四个#,两边字符是
"S"
与 结尾,是\w
与$
之间的位置。
\B
则是与 \b
相反,匹配非单词边界
var result = '[JS] JS'.replace(/\B/g, "#") console.log(result) // -> "#[J#S]# J#S"
(?=p)
(?!p)
(?:p)
(?=p)
,其中 p
是一个子模式,即 p
前面的位置。例如 (?=a)
,表示 "a"
字符前面的位置。而 (?!p)
与 (?=p)
相反。(?:p)
则表示不捕获分组。
var result = "cat".replace(/(?=a)/g, '@') console.log(result) // -> "c@at" var result = "cat".replace(/(?!a)/g, '@') console.log(result) // -> "@ca@t@"
数字的千位分隔符表示法,首先写出(?=\d{3}$)
匹配位置到最后三位前面,因为是多个三个数字一组所以可以使用量词+匹配。
'12345678911'.replace(/(?=\d{3}$)/g, ',') // -> '12345678,911' '12345678911'.replace(/(?=(\d{3})+$)/g, ',') // -> '12,345,678,911'
这样写会有一个问题是,当数字尾数是3的倍数时会出现,123,456,789
的情况。可以利用(?!^)
匹配不是开头的位置。
'123456789'.replace(/(?!^)(?=(\d{3})+$)/g, ',') // -> '123,456,789'
比较(p)
和(?:p)
,前者是捕获分组,后者不捕获,区别在于正则表达式匹配输入字符串之后所获得的匹配的(数)组当中没有(?:p)匹配的部分。
const m = "abcabc".match(/(?:a)(b)(c)/) //结果 ["abc", "b", "c"] // m[0] 是/(?:a)(b)(c)/匹配到的整个字符串,这里包括了a // m[1] 是捕获组1,即(b)匹配的子字符串substring or sub sequence // m[2] 是捕获组2,即(c)匹配到的
括号分组
如果分组后面有量词的话例如/(\d)+/
,那么分组捕获的是数据是最后一次的匹配。
var regex = /(\d)+/ var string = "12345" console.log( string.match(regex) ) // -> ["12345", "5", index: 0, input: "12345"]
分组引用
括号内的正则是一个整体,即提供子表达式。
var regex = /(ab)+/g var string = "ababa abbb ababab" console.log( string.match(regex) ) // -> ["abab", "ab", "ababab"] var regex = /^I love (milk|juice)$/ console.log( regex.test("I love milk") ) // -> true console.log( regex.test("I love juice") ) // -> true
提取数据
在匹配过程中给括号每一个分组都开辟一个空间,用来存储每一个分组匹配到的数据。下面这个例子是匹配提取年月日。可以使用字符串的 match
方法或者正则的 exec
方法。
var regex = /(\d{4})-(\d{2})-(\d{2})/ var string = "2017-06-12" console.log( string.match(regex) ) // -> ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"] var regex = /(\d{4})-(\d{2})-(\d{2})/ var string = "2017-06-12" console.log( regex.exec(string) ) // -> ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"] console.log(RegExp.$1); // "2017" console.log(RegExp.$2); // "06" console.log(RegExp.$3); // "12"
match
返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。另外,正则表达式是否有修饰符 g
,match
返回的数组格式是不一样的。同时可以用 RegExp
构造函数的全局属性 $1
至 $9
来获取匹配的内容。
替换数据
在字符串的 replace
方法第二个参数中如果是字符串则 $1
至 $9
分组其实指向的就是 RegExp
构造函数中的全局属性。如果第二个参数是一个方法那么方法里也可直接用 RegExp
访问全局属性。或者方法接收匹配到的分组内容参数作为参数。
var regex = /(\d{4})-(\d{2})-(\d{2})/; var string = "2017-06-12"; // 三种等价写法 var result = string.replace(regex, "$2/$3/$1"); var result = string.replace(regex, function () { return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1; }); var result = string.replace(regex, function (match, year, month, day) { return month + "/" + day + "/" + year; }); console.log(result); // -> "06/12/2017"
反向引用
正则表达式里的/1
,表示第一个分组,/2
和/3
等等含义以此类推。下面是一个运用到反向引用的例子,例如一个正则需要匹配 95/12/14 或 95-12-14 或 95.12.14。
const regex = /\d{2}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/ const string1 = '95/12/14' const string2 = '95-12-14' const string3 = '95.12.14' regex.test(string1); // -> true regex.test(string2); // -> true regex.test(string3); // -> true const errorString = '95/12-14' regex.test(errorString); // -> true
这个正则会出现一个问题,匹配 95/12-14 或 95.12/14 这种前后符号不一情况也测试通过。所以这里就利用 \1
代替后面的 (-|\/|\.)
匹配分组。不管前面分组匹配到什么字符,后面 \1
都匹配前面匹配到的那个相同的字符。
const regex = /\d{2}(-|\/|\.)\d{2}\1\d{2}/ const errorString = '95/12-14' regex.test(errorString); // -> false
非捕获括号
之前的分组引用与反向引用都会捕获到匹配的数据,也因此称为捕获型分组和捕获型分支。如果只想要括号最原始的功能,但不会引用它(既不在 API 里引用,也不在正则里反向引用)。此时可以使用非捕获括号 (?:p)
和 (?:p1|p2|p3)
// 原正则是 /(ab)+/g const regex = /(?:ab)+/g const string = "ababa abbb ababab" console.log( string.match(regex) ) // -> ["abab", "ab", "ababab"] // 原正则是 /^I love (JavaScript|Regular Expression)$/ const regex = /^I love (?:JavaScript|Regular Expression)$/ console.log( regex.test("I love JavaScript") ) // -> true console.log( regex.test("I love Regular Expression") ) // -> true
正则拆分
正则这门语言跟其他语言有一点不同,它通常就是一大堆字符,而没有所谓“语句”的概念。如何能正确地把一大串正则拆分成一块一块的,成为了破解“天书”的关键。
结构和操作符
在 JS 中正则的结构有字符字面量、字符组、量词、锚、分组、选择分支、反向引用。
正则结构 | 说明 |
---|---|
字面量 | 匹配一个具体字符,包括不用转义的和需要转义的。比如 a 匹配字符 "a",又比如 \n 匹配换行符,又比如 \. 匹配小数点。 |
字符组 | 匹配一个字符,可以是多种可能之一,比如 [0-9] ,表示匹配一个数字。也有 \d 的简写形式。另外还有反义字符组,表示可以是除了特定字符之外任何一个字符,比如 [^0-9] ,表示一个非数字字符,也有 \D 的简写形式。 |
量词 | 表示一个字符连续出现,比如 a{1,3} 表示 "a" 字符连续出现 3 次。另外还有常见的简写形式,比如 a+ 表示 "a" 字符连续出现至少一次。 |
锚 | 匹配一个位置,而不是字符。比如 ^ 匹配字符串的开头,又比如 \b 匹配单词边界,又比如 (?=\d) 表示数字前面的位置。 |
分组 | 用括号表示一个整体,比如 (ab)+ ,表示 "ab" 两个字符连续出现多次,也可以使用非捕获分组 (?:ab)+ 。 |
分支 | 多个子表达式多选一,比如 `abc |
其中涉及到的操作符
操作符 | 描述操作符 | 优先级 |
---|---|---|
转义符 | \ | 1 |
括号和方括号 | (…) 、(?:…) 、(?=…) 、(?!…) 、[…] | 2 |
量词限定符 | {m} 、{m,n} 、{m,} 、? 、* 、+ | 3 |
位置和序列 | ^ 、$ 、\ 元字符、一般字符 | 4 |
管道符(竖杠) | ` | ` |
所谓元字符,就是正则中有特殊含义的字符。所有结构里,用到的元字符总结如下:
^
、$
、.
、*
、+
、?
、|
、\
、/
、(
、)
、[
、]
、{
、}
、=
、!
、:
、-
var string = "^$.*+?|\\/[]{}=!:-,"; var regex = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/; console.log( regex.test(string) ); // => true
相关 API
String.search() String.split() String.match() String.replace() RegExp.test() RegExp.exec()
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论