JavaScript 正则表达式

发布于 2023-05-04 20:10:18 字数 11319 浏览 54 评论 0

关于正则表达式的教程还有文章网络资源非常多,这本迷你书比较全面对学习正则提供很大帮助。还有正则逻辑可视化网站对理解看起来又长又乱的正则有很大帮助。

字符匹配篇

匹配模式

横向模糊匹配:一个正则可匹配的字符串的长度不是固定的,其实现的方式是使用量词。例如 {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 返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。另外,正则表达式是否有修饰符 gmatch 返回的数组格式是不一样的。同时可以用 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

双马尾

暂无简介

0 文章
0 评论
21 人气
更多

推荐作者

qq_eQNo9e

文章 0 评论 0

内心旳酸楚

文章 0 评论 0

mb_BlPo2I8v

文章 0 评论 0

alipaysp_ZRaVhH1Dn

文章 0 评论 0

alipaysp_VP2a8Q4rgx

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文