预定义术语 | 匹配内容 |
---|---|
\t | 水平制表符(tab) |
\b | 空格(blank) |
\v | 垂直制表符(vertical) |
\f | 换页符(form feed) |
\r | 回车(return) |
\n | 换行符 |
\cA:\cZ | 控制符,例如:\cM 匹配一个Control-M |
\x0000:\xFFFF | 十六进制Unicode码 |
\x00:\xFF | 十六进制ASCII码 |
. | 匹配除了换行符、回车符、行分隔符和段分隔符以外的任意字符,等价于[^\n\u\u2028\u2029] |
\d | 匹配任意数字(digital),等价于[0-9] |
\D | 匹配任意非数字,等价于[^0-9] |
\w | 匹配包括下划线的任意单词字符(word),等价于[A-Za-z0-9_] |
\W | 匹配任何非单词字符,等价于[^A-Za-z0-9_] |
\s | 匹配任何空白字符(space),包括空格、水平制表符、垂直制表符、换行符、回车符和换页符。等价于[\t\v\n\r\f] |
\S | 匹配任何非空白字符。等价于[^\t\v\n\r\f] |
\b | 匹配单词边界(boundary) |
\B | 匹配非单词边界 |
由以上可知要想匹配任意字符的话,只要结合它们的对立面即可,可以使用如下任意一个:[\d\D]
、[\w\W]
、[\s\S]
和[^]
。
3.6 分组:()
到目前为止,我们看到的操作符(如+
和*
)只能影响前面的术语,如果将操作符运用于一组术语,可以像数学表达式一样在该组上使用小括号()
,例如/(ab)+/
匹配一个或多个连续出现的子字符串'ab'
。
当正则表达式由一部分是用括号进行分组时,它具有双重责任,同时也创建了所谓的捕获(capture)。
如果只想要括号最原始的功能,但不会引用它,即,既不在 API 里引用,也不在正则里反向引用,此时可以使用非捕获括号(因为捕获分组和分支里的数据需要更多的内存来保存这些信息): (?:p)
和 (?:p1|p2|p3)
,这样就只取到分组的作用。
3.7 或操作符:|
可以使用竖线(|
)表示或者的关系。例如/a|b/
匹配'a'
字符或者'b'
字符,/(ab)+|(cd)+/
表示匹配出现一次或多次的'ab
或'cd'
3.8 反向引用
正则表达式中最复杂的术语是,在正则中所定义的捕获(capture) 的反向引用。这种术语表示法就是在反斜杆后面加一个要引用的捕获数字(捕获所在的索引),该数字从1开始,如\1
、\2
等。
举例来说,/^([dnt])a\1/
可以匹配任意一个以'd'
、'n'
、't'
开头,且后面跟着一个'a'
字符,并且再后面跟着的是和第一个捕获相关字符的字符串(这个很重要!)。比如'dad'
就可以正确匹配,而'dan'
就不行(因为最后一个'n'
字符不是第一个捕获'd'
字符)。
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。例如\2
,就匹配 '\2'
。注意 '\2'
表示对 '2'
进行了转义。
分组后面有量词的话,分组最终捕获到的数据是最后一次的匹配。例子:
let regex = /(\d)+/
let string = '12345'
console.log( string.match(regex) ) // ["12345", "5", index: 0, input: "12345", groups: undefined]
- 1
- 2
- 3
从上面看出,分组 (\d)
捕获的数据是'5'
。
反向引用在匹配HTML类型标记的时候会很有用,比如这个正则:
/<(\w+)>(.+)<\/\1>/
要匹配像<strong>whatever</strong>
这样的简单元素,不使用反向引用,是无法做到的。因为我们无法知道关闭标签和开始标签是否匹配。
3.9 位置匹配:(?=p)、(?!p)
(?=p)
,其中p
是一个子模式,即p
前面的位置,或者说,该位置后面的字符要匹配p
。例子:
let result = 'hello'.replace(/(?=l)/g, '#')
console.log(result) // 'he#l#lo'
- 1
- 2
即表示匹配'l'
字符前面的位置,然后将该位子替换为'#'
,result
的结果为:'he#l#lo'
。
(?!p)
就是 (?=p)
的反面意思。
let result = 'hello'.replace(/(?!l)/g, '#')
console.log(result) // '#h#ell#o#'
- 1
- 2
一般理解(?=p)
为:要求接下来的字符与p
匹配,但不能包括p
匹配的那些字符。
我们也可以理解为p
前面的那个位置。对于位置的理解,我们可以理解为空字符''
,如'hello'
等价于如下的形式:
'hello' == '' + 'h' + '' + 'e' + '' + 'l' + '' + 'l' + '' + 'o' + '';
- 1
4. 正则表达式的4种操作
正则表达式主要分4种操作:验证、切分、提取、替换 。
正则操作的方法共有6个,字符串方法4个,正则方法2个:
- String.prototype.match()
- String.prototype.replace()
- String.prototype.search()
- String.prototype.split()
- RegExp.prototype.exec()
- RegExp.prototype.test()
这些API不详细解释,不清楚的可以去学习一下。
4.1 验证
比如表单验证,其实就是字符串的匹配操作,如果匹配上了就是验证通过。例子,判断一个字符串中是否有数字,可以采用的验证方法如下:
- 使用
test
(这个方法最常用)let regex = /\d/ let string = 'abc123' console.log( regex.test(string) ) // true
- 1
- 2
- 3
- 使用
search
let regex = /\d/ let string = 'abc123' // 对search的结果先取反,可以排除search结果为0的情况(如string为'0abc'时) console.log( !!~string.search(regex) ) // true
- 1
- 2
- 3
- 4
- 使用
match
let regex = /\d/ let string = 'abc123' console.log( !!string.match(regex) ) // true
- 1
- 2
- 3
- 使用
exec
let regex = /\d/ let string = 'abc123' console.log( !!regex.exec(string) ) // true
- 1
- 2
- 3
4.2 切分
使用字符串的split
方法可以对字符串进行切分。
- 例1,目标字符串是
'html,css,javascript'
,按逗号来切分:let regex = /,/ let string = 'html,css,javascript' console.log( string.split(regex) ) // ["html", "css", "javascript"]
// 当然像这种简单的可以直接用字符串来作为参数
console.log( string.split(’,’) ) // [“html”, “css”, “javascript”]
- 1
- 2
- 3
- 4
- 5
- 6
- 例2,目标字符串是
'2019/08/13'
、'2019.08.13'
、'2019-08-13'
,切分出年月日:let regex = /\D/ console.log( '2019/08/13'.split(regex) ) // ["2019", "08", "13"] console.log( '2019.08.13'.split(regex) ) // ["2019", "08", "13"] console.log( '2019-08-13'.split(regex) ) // ["2019", "08", "13"]
- 1
- 2
- 3
- 4
4.3 提取
有时匹配上了,我们想要提取部分匹配的数据,这时通常要使用分组引用(分组捕获)功能,还要配合使用相关的API。这里,还是以日期为例,提取出年月日。注意下面正则中的括号:
- 使用
match
(这个方式最为常用)let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' console.log( string.match(regex) ) // => ["2019-08-13", "2019", "08", "13", index: 0, input: "2019-08-13", groups: undefined]
- 1
- 2
- 3
- 4
- 使用
exec
let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' console.log( regex.exec(string) ) // => ["2019-08-13", "2019", "08", "13", index: 0, input: "2019-08-13", groups: undefined]
- 1
- 2
- 3
- 4
- 使用
test
let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' regex.test(string) // 执行完test方法后,RegExp全局属性$1、$2、$3...的值为对应分组的匹配值 console.log( RegExp.$1, RegExp.$2, RegExp.$3 ) // "2019" "08" "13"
- 1
- 2
- 3
- 4
- 5
- 使用
search
let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' string.search(regex) // 执行完search方法后,RegExp全局属性$1、$2、$3...的值为对应分组的匹配值 console.log( RegExp.$1, RegExp.$2, RegExp.$3 ) // "2019" "08" "13"
- 1
- 2
- 3
- 4
- 5
- 使用
replace
let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/ let string = '2019-08-13' let date = [] string.replace(regex, (match, year, month, day) => { date.push(year, month, day) }); console.log(date); // ["2019", "08", "13"]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
4.4 替换
找,往往不是目的,通常下一步是为了替换。在 JavaScript 中,使用
replace
进行替换。
比如把日期格式,从yyyy-mm-dd
替换成yyyy/mm/dd
:let string = '2019-08-13' console.log( string.replace(/-/g, '/') ) // "2019/08/13"
- 1
- 2
replace
方法很强大,需要重点掌握。4.5 强大的replace
总体来说 replace 有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数。
-
当第二个参数是字符串时,如下的字符有特殊的含义:
属性 描述 $1
、$2
、····,$99
匹配第 1-99 个 分组里捕获的文本 $&
匹配到的子串文本 $`
匹配到的子串的左边文本 $'
匹配到的子串的右边文本 $$
美元符号 例如,把
'2,3,5'
,变成'5=2+3'
:let result = '2,3,5'.replace(/(\d+),(\d+),(\d+)/, '$3=$1+$2') console.log(result) // 5=2+3
- 1
- 2
又例如,把
'2,3,5'
,变成'222,333,555'
:let result = '2,3,5'.replace(/(\d+)/g, '$&$&$&') console.log(result) // 222,333,555
- 1
- 2
再例如,把
'2+3=5'
,变成'2+3=2+3=5=5'
:let result = '2+3=5'.replace(/=/, "$&$`$&$'$&") // $&匹配到的是'=',$`这个匹配到的是'2+3',$'匹配到的是'5' console.log(result) // 2+3=2+3=5=5
- 1
- 2
- 3
- 使用
-
当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:
'1234 2345 3456'.replace(/(\d)\d{2}(\d)/g, (match, $1, $2, index, input) => { console.log([match, $1, $2, index, input]) }) // ["1234", "1", "4", 0, "1234 2345 3456"] // ["2345", "2", "5", 5, "1234 2345 3456"] // ["3456", "3", "6", 10, "1234 2345 3456"]
- 1
- 2
- 3
- 4
- 5
- 6
4.6 注意点
在使用正则的过程中,以下的一些点需注意:
- 字符串 4 个方法参数都支持正则和字符串,但
search
和match
方法会把字符串转换为正则的。let string = '2019.08.13' console.log( string.search('.') ) // 0 //需要修改成下列形式之一 console.log( string.search('\\.') ) // 4 console.log( string.search(/\./) ) // 4
- 1
- 2
- 3
- 4
- 5
match
返回结果的格式,与正则对象是否有修饰符g
有关。let string = '2019.08.13' let regex1 = /\b(\d+)\b/ let regex2 = /\b(\d+)\b/g console.log( string.match(regex1) ) // ["2019", "2019", index: 0, input: "2019.08.13", groups: undefined] console.log( string.match(regex2) ) // ["2019", "08", "13"]
- 1
- 2
- 3
- 4
- 5
g
,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,最后是输入的目标字符串。有g
,返回的是所有匹配的内容。当没有匹配时,不管有无g
,都返回null
。exec
比match
更强大
当正则没有g
时,使用match
返回的信息比较多。但是有g
后,就没有关键的信息index
了。而exec
方法就能解决这个问题,它能接着上一次匹配后继续匹配:let string = '2019.08.13' let regex2 = /\b(\d+)\b/g console.log( regex2.exec(string) ) // ["2019", "2019", index: 0, input: "2019.08.13", groups: undefined] // lastIndex 属性,表示下一次匹配开始的位置 console.log( regex2.lastIndex) // 4 console.log( regex2.exec(string) ) // ["08", "08", index: 5, input: "2019.08.13", groups: undefined] console.log( regex2.lastIndex) // 7 console.log( regex2.exec(string) ) // ["13", "13", index: 8, input: "2019.08.13", groups: undefined] console.log( regex2.lastIndex) // 10 console.log( regex2.exec(string) ) // null console.log( regex2.lastIndex) // 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
exec
时,经常需要配合使用while
循环:let string = '2019.08.13' let regex2 = /\b(\d+)\b/g let result while ( result = regex2.exec(string) ) { console.log( result, regex2.lastIndex ); } // ["2019", "2019", index: 0, input: "2019.08.13", groups: undefined] 4 // ["08", "08", index: 5, input: "2019.08.13", groups: undefined] 7 // ["13", "13", index: 8, input: "2019.08.13", groups: undefined] 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 修饰符
g
,对exex
和test
的影响
上面提到了正则实例的lastIndex
属性,表示尝试匹配时,从字符串的lastIndex
位开始去匹配。字符串的四个方法,每次匹配时,都是从 0 开始的,即lastIndex
属性始终不变。
而正则实例的两个方法exec
、test
,当正则是全局匹配时,每一次匹配完成后,都会修改lastIndex
。下面
让我们以test
为例,看看你是否会迷糊:let regex = /a/g // 每次调用都会从 lastIndex 下标处开始查找 console.log( regex.test('a'), regex.lastIndex ) // true 1 console.log( regex.test('aba'), regex.lastIndex ) // true 3 console.log( regex.test('ababc'), regex.lastIndex ) // false 0
- 1
- 2
- 3
- 4
- 5
g
,自然都是从字符串第 0 个字符处开始尝试匹配:let regex = /a/ console.log( regex.test('a'), regex.lastIndex ) // true 0 console.log( regex.test('aba'), regex.lastIndex ) // true 0 console.log( regex.test('ababc'), regex.lastIndex ) // true 0
- 1
- 2
- 3
- 4
test
整体匹配时需要同时使用^
和$
test
方法只是查看目标字符串中是否有字串匹配正则,只要有部分匹配则返回true
。如果需要整体匹配,正则前后需要添加开头和结尾。console.log( /123/.test('a123b') ) // true console.log( /^123$/.test('a123b') ) // false console.log( /^123$/.test('123') ) // true
- 1
- 2
- 3
split
需要注意的两点
第一,它可以有第二个参数,表示结果数组的最大长度:let string = 'html,css,javascript' console.log( string.split(/,/, 2) ) // ["html", "css"]
- 1
- 2
let string = 'html,css,javascript' console.log( string.split(/(,)/) ) // ["html", ",", "css", ",", "javascript"]
- 1
- 2
5. 练习
5.1 匹配16进制颜色值
像:
#AAfa10
、#ABC
、#111
。
思路:- 16进制每一位的所有可能取值为0~9,A ~ F或a ~ f:
[0-9A-Fa-f]
- 位数可以为3位或者6位:
/#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})/
5.2 匹配数字时钟
像:
23:59
、01:00
、19:50
。
思路(一共四位数字,它们之间有约束):- 第一位数字为0、1或2
- 当第一位为0和1时,第二位可为0~9中的一个:
[01][0-9]
,因为[0-9]
可用\d
表示,那么也可为[01]\d
- 当第一位为2时,第二位可为0~3中的一个:
2[0-3]
- 那么第一位和第二位的组合为:
[01]\d|2[0-3]
- 第三位可为0~5中的一个:
[0-5]
- 第四位为0~9中的一个:
\d
- 最后组合一起:
/([01]\d|2[0-3]):[0-5]\d/
若也要支持时和分前面的0忽略的情况,那么可为:
/(0?\d|1\d|2[0-3]):(0?\d|[1-5]\d)/
5.3 日期的匹配
比如:
yyyy-mm-dd
的格式。
思路:- 年份的四位为数字即可:
\d{4}
- 月份分首位为0和1的情况
- 当月份首位为0,那么第二位可为1~9中的一个:
0[1-9]
- 当月份首位为1,那么第二位可为0~2中的一个:
1[0-2]
- 那月份可为:
0[1-9]|1[0-2]
- 日首位有0、1、2、3四种
- 当日首位为0时,第二位可为1~9中的一个:
0[1-9]
- 当日首位为1或2时,第二位可为0~9中的一个:
[12]\d
- 当日首位为3时,第二位只能为0和1:
[01]
- 所以日的组合为:
0[1-9]|[12]\d|3[01]
- 最终组合为:
/\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])/
这里其实有问题,2月份是个特殊的例子,大家自己想想应该如何解决。
5.4 windows操作系统的文件路径匹配
比如:
D:\study\javascript\RegExp\regular expression.pdf
、D:\study\javascript\RegExp\
、D:\study\javascript
、D:\
思路:- 匹配
D:\
,盘符不区分大小写,\
需要转义:[a-zA-Z]:\\
- 文件夹或文件名不能包含特殊字符:
[^\\:*<>|“?\r\n/]
- 并且它们不能为空,因此匹配
文件夹\
为:[^\\:*<>|“?\r\n/]+\\
文件夹\
可以出现任意次:([^\\:*<>|“?\r\n/]+\\)*
- 路径的最后一部分可以是
文件夹
,没有\
,所以要添加:([^\\:*<>|“?\r\n/]+)?
- 最终:
/[a-zA-Z]:\\([^\\:*<>|“?\r\n/]+\\)*([^\\:*<>|“?\r\n/]+)?/
5.5 将数字改成千分位符表示法
比如将
12345678
变为12,345,678
。
思路:- 需要使用位置匹配
(?=p)
,从最末尾开始计算,每三位数字前加一个','
,先弄出最后一个逗号,此时(?=p)
中的p
即为\d{3}$
:/(?=\d{3}$)/g
,此时'12345678'.replace(/(?=\d{3}$)/g, ',')
变为12345,678
- 因为逗号出现的位置,要求后面3个数字为一组,也就是
\d{3}
至少出现一次,此时可以使用量词'+'
,弄出所有逗号:/(?=(\d{3})+$)/g
, 此时'12345678'.replace(/(?=(\d{3})+$)/g, ',')
变为12,345,678
- 不知道你发现没,这里会出现一个问题,匹配的数字串位数如果刚好是3的倍数的话,那么第一个字符就会变为
','
,如'123456'.replace(/(?=(\d{3})+$)/g, ',')
变为,123,456
,这不是我们想要的。所以我们要排除掉开头的位置,可以使用(?!^)
,那么最终的匹配模式为:/(?!^)(?=(\d{3})+$)/g
,'123456'.replace(/(?!^)(?=(\d{3})+$)/g, ',')
变为123,456
应用例子,货币的格式化,比如将
1888
转化为$ 1,888.00
,代码如下:function format (num) { // $$ 即表示 $ 符号 return num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g, ',').replace(/^/, '$$ '); }; console.log(format(1888)) // '$ 1,888.00' console.log(format(1564312.62165)) // '$ 1,564,312.62'
- 1
- 2
- 3
- 4
- 5
- 6
5.6 密码验证
假设密码长度要求为6~12位,由数字、大写字母和小写字母组成,并且必须至少包括两种字符。
思路:- 首先,不考虑至少包括两种字符的话,那很好写出来:
/^[0-9A-Za-z]{6,12}$/
- 其次,更进一步,假如必须包含数字的话,那么我们可以通过
(?=.*[0-9])
来实现。则正则变为:/(?=.*[0-9])^[0-9A-Za-z]{6,12}$/
,这个正则(?=.*[0-9])^
这部分理解了就搞懂整个了。(?=.*[0-9])^
分开来看就是(?=.*[0-9])
和^
,表示开头前面还有一个位置(当然也是开头,即同一个位置,想想之前的空字符类比)。(?=.*[0-9])
表示该位置后面的字符匹配.*[0-9]
,即,有任何多个任意字符,后面再跟个数字。翻译成大白话,就是接下来的字符,必须包含个数字。 - 需同时包含数字和小写字母的话,可用
(?=.*[0-9])(?=.*[a-z])
来实现,则正则变为:/(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/
- 有了以上,我们只要组合一下即可得出最后的答案:
/((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[ 0-9A-Za-z]{6,12}$/
另一种思路,至少包含两种字符的意思即为:不能全为数字、不能全为小写字母,不能全为大写字母。简单开始,那么不能全为数字该怎么实现?需要这个
(?!p)
来匹配了:/(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/
,那么三个都不能的话,最终结果为:/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/
5.7 模拟字符串的trim方法
trim
方法用来去掉字符串首尾的空白符。
思路:找到字符串开头和结尾的所有空白符,将其替换为空字符''
即可。function trim(str) { return str.replace(/^\s+|\s+$/g, '') } console.log( trim(' javascript ') ) // 'javascript'
- 1
- 2
- 3
- 4
5.8 将每个单词的首字母转为大写
比如将
'my name is javascript'
转化为'My Name Is Javascript'
。
思路:关键点在于如何找到字符串的每个单词的首字母,使用/(?:^|\s)\w/g
即可,即开头或者空白字符后面的第一个任意单词字符。
则可以通过如下代码实现:function titleize (str) { //这里不使用非捕获匹配也是可以的:/(^|\s)\w/g return str.toLowerCase().replace(/(?:^|\s)\w/g, c => c.toUpperCase()) } console.log( titleize('my name is javascript') ) // 'My Name Is Javascript'
- 1
- 2
- 3
- 4
- 5
5.9 驼峰化
比如将
'-moz-transform'
转化为'MozTransform'
。
思路:其实主要还是怎么找到单词的首字母,将首字母变为大写,单词之间的内容去除。function camelize (str) { return str.replace(/[-_\s]+(.)?/g, (match, c) => c ? c.toUpperCase() : '') } console.log( camelize('-moz-transform') ) // 'MozTransform'
- 1
- 2
- 3
- 4
上面的代码中,
(.)
即为首字母,单词的界定是,前面的字符可以是多个连字符'-'
、下划线'_'
以及空白符。正则后面
的'?'
的目的,是为了应对 str 尾部的字符可能不是单词字符,比如 str 是'-moz-transform '
。反之,将驼峰写法中划线化:
function dasherize (str) { return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase(); } console.log( dasherize('MozTransform') ); // '-moz-transform'
- 1
- 2
- 3
- 4
5.10 HTML转义与反转义
// 将HTML特殊字符转换成等值的实体 function escapeHTML (str) { const escapeChars = { '<' : 'lt', '>' : 'gt', '"' : 'quot', '&' : 'amp', '\'' : '#39' } //'[' + Object.keys(escapeChars).join('') +']', 结果即为:[<>"&'] return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'), match => '&' + escapeChars[match] + ';') } console.log( escapeHTML('<div>Blah blah blah</div>') ); // '<div>Blah blah blah</div>'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
逆过程:
// 实体字符转换为等值的HTML。 function unescapeHTML (str) { const htmlEntities = { nbsp: ' ', lt: '<', gt: '>', quot: '"', amp: '&', apos: '\'' }; return str.replace(/\&([^;]+);/g, (match, key) => key in htmlEntities ? htmlEntities[key] : match ) } console.log( unescapeHTML('<div>Blah blah blah</div>') ); // '<div>Blah blah blah</div>'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
通过 key 获取相应的分组引用,然后作为对象的键。
写在最后:正则虽然很强大,但也没必要事事都用正则,有时字符串自带的API使用起来会更为简单且易于理解。
- 字符串 4 个方法参数都支持正则和字符串,但