正则表达式
正则表达式用于1.匹配字符,或者2.匹配位置
1.字符匹配攻略
- 横向与纵向匹配
{m,n}
匹配m到n长度的字符/ab{2,4}c/
匹配 a 和 c 中间2到4个b
[abc]
匹配a
b
c
中任一字符/ab[123]c/
匹配 ab 和 c 中间出现1、2或者3
- 纵向匹配的扩展:字符组——一个字符的集合
-
表示范围/[a-d2-5D-I]/
是[abcd2345DHI]
的省略简写模式- 转义问题:
\-
用来匹配-
字符本身
^
表示取反/[^abc]/
可以匹配x
但不匹配a
、b
、c
- Q:^ 只能放在纵向匹配方括号中吗?
- 常见缩写——大小写互为补集
\d
=[0-9]
::Digit::\D
=[^0-9]
\w
=[0-9a-zA-Z_]
数字、大小写字母和下划线 ::Word::\W
=[^0-9a-zA-Z]
\s
=[\t\v\n\r\f]
表示空白符,包括空格、水平制表符- 换行符、回车符、换页符::Space::
\S
.
=[^\n\r\u2028\u2029]
通配符,几乎匹配任何字符,换行符、回车符、行分隔符和段分隔符除外[\d\D] [\w\W] [\s\S] [^]
可以表示匹配任何字符
- 横向匹配的扩展:量词
- 简写
{m,}
最少 m 次{m}
精确的 m 次?
={0,1}
出现一次或0次+
={1,}
最少出现一次*
={0,}
可以不出现或出现不限次数
- 贪婪匹配和惰性匹配
- 在量词后边加
?
可以改为惰性匹配 - Q:差别不是特别清楚 贪婪匹配会匹配出尽可能长的字符串,惰性则是尽可能短的,符合匹配就返回
/\d{2,4}/g
匹配123 1234 12345 123456
结果:123 1234 1234 1234 56
/\d{2,4}?/g
匹配123 1234 12345 123456
结果:12 12 34 12 34 12 34 56
/\d+/
匹配123 2
结果:123 2
/\d+?/
匹配123 2
结果:1 2 3 2
- 在量词后边加
- 简写
- 多选分支
|
管道符可以在多个模式中任选其一匹配/good|goodbye/
匹配到good
就会返回,而不会匹配到goodbye
,所以管道符号会短路,有匹配到就不会继续匹配
2.位置匹配攻略
理解「位置」:可以认为位置是字符串中字符与字符之间的“空字符”
例如
1 | "hello" === "" + "hello" + "" |
匹配位置就是匹配这些空字符串,这些空字符串可以做替换
^
$
匹配开头结尾- 把字符串的开头/结尾替换成#
'hello'.replace(/^|$/g, '#') // '#hello#'
- 多行匹配时,两个字符是行的开头结尾
1
2
3
4
5
6// g: 全局匹配,m: 多行匹配
'hello\nworld'.replace(/^|$/gm, '#')
/*
* #hello#
* #world#
*/
- 把字符串的开头/结尾替换成#
\b
匹配单词边界,\w
与\W
或^
或$
的边界 ::Boundary::- 虽然被称为“单词”,但
\w
代表数字字母下划线,不仅仅是字母 - 一个文件名为”[JS] Lesson_01.mp4”,其单词边界为
1
2'[JS] Lesson_01.mp4'.replace(/\b/g, '#')
// [#JS#] #Lesson_01#.#mp4#
- 虽然被称为“单词”,但
\B
取反1
2'[JS] Lesson_01.mp4'.replace(/\B/g, '#')
// #[J#S]# L#e#s#s#o#n#_#0#1.m#p#4(?=p)
匹配p
左边的位置,p
表示一个子模式- positive lookahead,正向先行断言
- 匹配 l 之前的位置
1
2'hello'.replace(/(?=l)/g, '#')
// he#l#lo
(?!p)
匹配与(?=p)
取反- negative lookahead,负向先行断言
ES6 还支持
(?<=p)
和(?<!p)
表示匹配右边的位置
案例:数字的千分位分隔符匹配法
要求把 ”1234567” 变成 “1,234,567”
'1234567'.replace(reg, ',')
思路是匹配连续三个数字\d{3}
,在它们前边加逗号。有两个问题:
- 如何从后往前匹配
/(?=\d{3})/
这样会替换成,1,2,3,4,567
,原因是从前向后依次匹配到了123 234 345 456
, 所以1 2 3 4 5前边都加上了逗号- 增加结尾匹配
/(?=\d{3}$)/
,这样会在结尾替换出1234,567
- 如果把
$
写在括号外边:/(?=\d{3})$/
则表示匹配连续三个数字之前的位置,但是这个位置之后就是结尾。这应该是一种无效匹配
- 如果把
- 再给
\d{3}
整体加上量词/(?=(\d{3})+$)/
,就能匹配多组数字了1,234,567
- 如何避免开头有逗号
- 前边的表达式替换
123456789
会出现,123,456,789
,需要避免匹配到开头^
-
(?!^)
可以匹配非开头(还是没明白为啥…),个人觉得应该有一个类似匹配字符的 ^ 来表示取反,而不是用位置来表示“开头”。开头和结尾这两个位置的定义还是很奇妙。 /(?!^)(?=(\d{3})+$)/
可以替换出123,456,789
- ::位置匹配如何做到「非」?:: ::两个位置连用 代表对同一个位置的「且」?::看上去两个位置连用的确表示一起描述同一个位置
- 前边的表达式替换
扩展:要求匹配 123456 12345678
/\B(?=(\d{3})+\b)/g
学习正则匹配有点像是学自然语言而不是编程语言——先学会实践怎么用,而不是学习如何实现(语法结构)
括号的作用
分组
要匹配连续的字符a
可以写 /a+/
,如果想匹配连续的ab
,需要括号/(ab)+/
,括号提供分组功能
分支结构
用 |
表示分支结构时,可选项的范围需要由括号包围。/(JS|Java) is the best lang/
匹配 JS is the best lang
或者 Java is the best lang
如果没有括号 /JS|Java is the best lang/
匹配 JS
或者 Java is the best lang
,整个表达式都是分支
引用分组——JS实现
括号可以进行数据提取,必须配合使用实现环境的 API。下边以 JS 为例
简单来说就是可以将表达式中的匹配项以 JS 变量的方式获取到。
RegExp.prototype.exec
、String.prototype.match
、String.prototype.replace
三个函数都会得到一个数组(前两个是返回值,后一个会传给 lambda 的入参)
1 | /(\d{4})-(\d{2})-(\d{2})/.exec('2021-04-29') |
其中1~3个元素就是括号提取出的数据,第0个表示正则表达式匹配到的字符串。
另外提一句,返回值是一个添加了自定义属性的数组。Array 是 Object 的实例,自带
length
属性,也可以像这里的返回值一样增加index
、input
、groups
属性。
前边的例子中,调用 replace
之后也可以通过 RegExp.$1
RegExp.$2
RegExp.$3
拿到对应三个数据,不过这种方式已经废弃。这种实例能改变对象本身的变量,设计也很奇怪,不再深究。
反向引用
指表达式前边声明过的模式,后边有一份一模一样的引用
1 | // 匹配 2021-04-29 2021/04/29 2021.04.29 |
其中的 \1
就表示第一个分组(-|\/|\.)
,不论第一个分组匹配到什么,\1
的内容都一模一样。注意这里和复制粘贴一份 (-|\/|\.)
到后边有本质区别,反向引用使得两个模式有了关联。::为啥这里写([-/.])不行?::
随着分组数量增加,\1 \2 \3 后边的数字也增加
- 括号嵌套时,左括号的次序代表\后边的数字
1
2/^((\d)(\d(\d)))\1\2\3\4$/.exec("4564564566")
// [4564564566, 456(\1), 4(\2), 56(\3), 6(\4) ...] \10
表示的是第10个分组,而不是\1
和0
- 引用不存在的分组时,
\1
保持字面意思,代表一个字符
再另外提一句,在 JS 的字符串中,反斜杠
\
有转义的作用
- 转义字符:
\0 \b \f \n \r \t \v \' \" \\
\HHH
后边跟三个000
到377
的八进制数代表一个字符,HHH
代表对应 Unicode 码点(code point),一共能输出256种字符。\1 === \001
遵循这个规则\xHH
后边跟00
到FF
的十六进制数,同样代表对应 Unicode 码点,能输出256种字符\uXXXX
后边也跟0000
到FFFF
的十六进制数,XXXX
代表对应码点- 如果反斜杠后边不是特殊字符,则反斜杠会被省略
'\a' === 'a'
1 | '\172' === 'z' // true |
非捕获分组
可以用(?:p)
来避免分组引用,括号内的匹配不会被提取,而只是作为分组或者分支结构用 /(?:a|b)(\d+)/
不会引用a或b,只会引用到后边的数字