Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 正则表达式 #4

Open
bouquetrender opened this issue May 9, 2018 · 0 comments
Open

JavaScript 正则表达式 #4

bouquetrender opened this issue May 9, 2018 · 0 comments

Comments

@bouquetrender
Copy link
Owner

本篇是阅读 《JavaScript 正则表达式迷你书》《正则表达式30分钟入门教程》
的个人笔记,用于快速查阅与总结。关于正则表达式的教程还有文章网络资源非常多,这本迷你书比较全面对学习正则提供很大帮助。还有正则逻辑可视化网站对理解看起来又长又乱的正则有很大帮助。

字符匹配篇

匹配模式

横向模糊匹配:一个正则可匹配的字符串的长度不是固定的,其实现的方式是使用量词。例如 {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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant