正则表达式(Regular Expression)是强大的文本处理工具。
我们通常可以使用正则表达式来对文本进行搜索、替换的处理
参考书籍:正则表达式必知必会
元字符
元字符
表示在正则表达式中含有特殊含义的字符(字符组合),其代表的不是其本身所表示的纯文本字符
匹配单个字符
我们可以使用元字符.
来匹配任意的单个字符
匹配一组字符
.
能匹配单个字符,但是无法确定匹配到什么字符,如果我们有预期的候选范围,则无法使用.
进行匹配。
此时可以使用字符组,字符组通过元字符[``]
组成,它们负责定义一个候选的字符集合,可以匹配字符集合中的任意一个字符
举个🌰:
正则表达式:
[ab]c[de]
匹配范围:
acd、ace、bcd、bce
字符区间
上述通过[``]
可以定义字符组,如果我们候选的字符较多时,此时需要在字符组中定义所有的候选字符是十分繁琐的,此时可以使用元字符-
在字符组中定义一个字符区间
举个🌰:
候选字符有:
abcdefg123456789
不使用字符区间的正则表达式:
[abcdefg123456789]
使用字符区间的正则表达式:
[a-g1-9]
注意点:
- 字符组中,元字符
-
在两个字符中间时才表示从字符 至 字符 的字符区间
正则表达式:
[a-c1-]
匹配范围:
a、b、c、1、- 中任意一个
-
字符区间是以ASCII字符集的顺序定义区间的,首字符应在尾字符的前面才表示一个区间
- 例:
[3-1]
这种是无效的,使用时将会报错
- 例:
-
常用的字符区间有:
[0-9]
表示数字字符集合
[A-Z]
表示大写字母
[a-z]
表示小写字母
[A-z]
表示所有大小写字母,还有在Z和a之间的 [ / ] ^ _ 、 等字符
取非匹配
上述字符组用于匹配预期的字符集合,有时我们需要反过来,也就是匹配除列举外的字符的其他所有字符
我们可以使用元字符^
来对字符组进行取非操作
举个🌰:
预期字符集合:除数字以外的所有字符
正则表达式:
[^0-9]
注意:
^
字符只有在字符组中才表示为元字符,在字符组外表示普通字符,匹配其本身^
字符只有出现在字符组的首位才被视为元字符,达到字符组取非的效果,否则匹配其本身
[0-9^]
匹配 数字字符 和 ^
- 字符组中只有
^
将会匹配所有字符
[^]
将匹配所有字符,类似于 元字符 .
其他元字符
转义字符
上述中,元字符具有特殊含义,不再表示其本身字符,那么如果我们需要表示元字符本身字符,需要使用转义字符\
转义字符\
的左右是取消紧跟其后的单个字符的特殊含义
举个🌰:
\.
匹配 . 字符本身
\\
匹配 \ 字符本身
注意:
- 在字符串中,
\
本身具有特殊含义,所以在字符串中表示单个\
,需要使用\\
表示- 可以在正则模式下先定义好正则表达式,在将
\
替换为\\
放入字符串中表示正则
- 可以在正则模式下先定义好正则表达式,在将
\
转义字符自身也是一个元字符,所以需要匹配\
本身时,需要对其自身进行转义,即\\
字符组中的转义
在字符组中,元字符仅表示其自身字符,不具有特殊含义
举个🌰:
[.]
匹配 . 字符本身
[[]
匹配 [ 字符本身
不过在字符组中,也存在具有特殊含义的字符
^
-
]
[
不是
\
如果想表示其自身字符,也需要使用\
进行转义
[\^]
匹配 ^ 字符本身
[\-]
匹配 - 字符本身
[\]]
匹配 ] 字符本身
[\\]
匹配 \ 字符本身
匹配空白字符
在文本中,存在许多非打印的空白字符,有时我们需要对空白字符进行匹配,可以用以下元字符:
元字符 | 说明 |
---|---|
\f | 换页符 |
\n | 换行符 |
\r | 回车符 |
\t | 制表符(Tab键) |
\v | 垂直制表符 |
举个🌰:
需求:匹配空白行
\r\n\r\n
注:
windows下使用 \r\n 表示文本行的结束标签,使用连续的两个\r\n,将匹配两个连续的行尾标签,表示一个空白行
unix/linux 使用 \n 表示文本行的结束标签,使用 \n\n 即可匹配空白行
匹配特定的字符类别
对于一些常用的字符匹配,提供元字符来简化正则
数字匹配
数字在正则中使用的频率较高,所以提供元字符,便于正则编写
元字符 | 说明 | 等价于 |
---|---|---|
\d | 任意单个数字字符 | [0-9] |
\D | 任意单个非数字字符 | [^0-9] |
数字/字母匹配
字母、数字和下划线_
是常用的,提供下列元字符:
元字符 | 说明 | 等价于 |
---|---|---|
\w | 任意单个数字/字母字符,或下划线 _ | [0-9A-Za-z_] |
\W | 任意单个非数字/字母字符,或下划线 _ | [^0-9A-Za-z_] |
空白字符匹配
元字符 | 说明 | 等价于 |
---|---|---|
\s | 任意单个空白字符 | [\f\n\r\t\v] |
\S | 任意单个非空白字符 | [^\f\n\r\t\v] |
进制匹配
通过特定的进制值来匹配特定字符
十六进制
正则中表示 十六进制值 需要使用\x
前缀来表示,后接 两位 表示的 十六进制值
举个🌰:
期望匹配换行符: \n
其对应ASCII编码表为: 10
对应十六进制为: 0A
正则表示为:\x0A
八进制
正则中表示 八进制值 需要使用\0
前缀来表示,后接 两/三位 表示的 八进制值
举个🌰:
期望匹配换行符: \t
其对应ASCII编码表为: 9
对应八进制为: 11
正则表示为:\011
重复匹配
前面我们学习的是匹配单个字符,下面我们将学习匹配多个重复字符(字符集合)
非固定次数匹配
元字符 | 说明 | 正则 | 匹配项 |
---|---|---|---|
+ | 匹配一个或多个 | ab+ | ab、abb、abbb、… |
* | 匹配任意个数,包括零个 | ab* | a、ab、abb、… |
? | 匹配零个/一个 | ab? | a、ab |
注意:
- 对于元字符,我们可以使用
[]
包裹来增加可读性
对于 \f \r \n之类的元字符
例如我想匹配 \f
正则 [\f]? 等价于 \f?
通过 [] 包裹来表示单个字符的字符组,其作用是增加了正则的可读性
- 对于匹配字符组的出现次数,上述元字符需要紧跟字符组的
]
符号后面
固定次数匹配
上述的三个元字符可以满足大部分的需求,但是如果我们想精确的限定匹配字符(字符集合)的次数/次数范围,它们就难以满足
此时我们可以通过 元字符{``}
来精确控制出现次数
元字符 | 说明 | 正则 | 匹配项 |
---|---|---|---|
{n} | 固定出现n次 | a{2} | aa |
{min,max} | 出现次数在 min - max 之间 | a{2,4} | aa、aa、aaaa |
{min,} | 至少出现min次,max无限制 | a{2,} | aa、aa、aaaa、… |
防止过度匹配
贪婪模式
元字符*
和+
都是所谓的贪婪型
元字符,它们进行匹配时是多多益善而不是适可而止。
举个🌰:
示例文本
living in <b>AK</b> and <b>HI</b>
正则
<[Bb]>.*</[Bb]>
原意是想分别匹配到
<b>AK</b>
.* 匹配 AK
<b>HI</b>
.* 匹配 HI
结果其匹配到了
<b>AK</b> and <b>HI</b>
.* 匹配 AK</b> and <b>HI
懒惰型元字符
由上述示例可知,有时贪婪型
的元字符无法满足我们的需求,我们需要使用其对应的懒惰版本来定义正则表达式
懒惰型元字符写法就是在贪婪型的元字符后添加 ?
贪婪型元字符 | 懒惰型元字符 |
---|---|
* | *? |
+ | +? |
{min,} | {min,}? |
位置匹配
有时我们需要匹配的不是一个具体的字符,而是表示一个位置/锚点
举个🌰:
对于文本
the cat scattered his food all over the room
我们需要匹配其中的cat
定义正则表达式
cat
我们预期匹配结果为
cat
而实际匹配结果为
cat、scattered
因为scattered中包含了cat
单词边界
元字符 | 说明 |
---|---|
\b | 单词边界,匹配一个单词的开始或结尾 |
\B | 非单词边界 |
那么 \b
具体匹配的是什么呢?
其实际是匹配一个 \w(字母、数字或_)
和一个 \W(非字母、数字或_)
的两个字符之间的位置
举个🌰:
1. 在上述的示例中,我们可以使用正则
\bcat\b
来匹配单独的 cat 单词
注意:
- 必须是
\w
与\W
之间才是\b
,否则都是\B
正则
\B-\B
可以匹配文本:'a - a'
不可以匹配文本:'a-a'
空格属于\W,- 也属于\W,所以其中间位置也是 \B
特:
- 部分
egrep
程序支持以下元字符用于匹配单词边界
| 元字符 | 说明 |
| — | — |
| \< | 匹配单词的开头 |
| \> | 匹配单词的结尾 |
字符串边界
上述我们的匹配内容是一个字符串中的部分内容,如果我们想以整个字符串为匹配对象时,就需要使用字符串边界元字符
元字符 | 说明 |
---|---|
^ | 字符串开头 |
$ | 字符串结尾 |
举个🌰:
正则
abc
可以匹配内容包含 abc 文本的字符串
正则
^abc$
只能匹配字符串为 'abc'
子表达式
有时我们可能对于一个短语,其虽然是由多个字符组成,但是我们将其当做一个整体
在正则表达式中,我们想对于匹配这种短语的表达式单独组成个体,此时就可以使用元字符(``)
,将其包裹内容视为一个独立整体,称为子表达式
举个🌰:
在HTML中存在一些元字符,例如 表示的不换行空格,我们需要将其视为一个整体
在下述文本中
hello, my name is Ben Forta
我们想将多次重复的空格找出,替换为单个空格
我们预期的正则是
{2,}
可以发现其无法达到预期效果
我们此时应该使用子表达式
对应正则
( ){2,}
子表达式的嵌套
子表达式是可以嵌套使用的,而且支持多层嵌套
注意:
- 太过复杂的子表达式嵌套,会降低正则表达式的可读性,我们应该合理使用
或字符
前面我们学习了字符组,它可以表示在字符组范围内的任意单个字符
例如正则[ab]
,它可以表示a、b
中的任意一个,也就是字符a
或b
这里提到了或者的概念,在日常生活中,或者的兼容时十分重要的,所以提供了元字符|
,用于表示或者的含义
元字符|
的左右可以为:
- 单个字符
- 字符组
- 子表达式
注意:
|
在使用时,需要使用(``)
来限定匹配范围
举个🌰:
对于IP地址的匹配
IP地址简单来说就是:由四组数字组成,每组数字由1至3个数字组成,中间通过.连接
所以我们可以编写以下正则
(\d{1,3}\.){3}\d{1,3}
但是实质上正则表达式还有许多取值的限制,不能由上述简单的正则表达式来匹配
- 任何一个1位或2位数字
- 任何一个以1开头的3位数字
- 任何一个以2开头、第2位数字在 0~4 之间的3位数字
- 任何一个以25开头、第3位数字在 0~5 之间的3位数字
由此编写的单组数字正则为
(\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5])
整体正则为
((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5])\.){3}((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))
命名捕获组
在一些正则匹配函数中,可以捕获子表达式内容,称为一个捕获组,之后可以通过索引的方式来获取捕获组的匹配内容
对于这种普通的子表达式对应的捕获组,它没有对应的标志,被称为匿名捕获组
如果我们想对一个捕获组添加一个标志,以便后续引用,则可以使用命名捕获组,其常用的格式如下:(?<name>子表达式内容)
注意,对应于不同语法:
- 其定义命名捕获组的方式可能不同
- 其引用命名捕获组的方式可能不同
所以使用命名捕获组之前,需要查看对应语法
回溯引用
回溯引用的作用
我们先使用一个例子来体现回溯引用的概念
举个🌰:
在开发过程中,我们通常使用标题标签(<h1>到<h6>)
如果我们要匹配一个标题,可以使用如下正则
<[hH][1-6]>.*?</[hH][1-6]>
我们可以发现其可以匹配如下内容
<h1>welcome to my homepage</h1>
<h2>coldfusion</h2>
<h3>wireless</h3>
此时我们觉得这个正则挺好用,那么当我们匹配到下面的内容时,就不会这样想了
<h3>no body</h4>
我们会发现 <h3> 与 </h4> 不是符合规则的标签组合,但是它确实符合我们上述的正则表达式
这时我们就应该使用回溯引用了
回溯引用使用
回溯引用就是通过元字符,引用前面定义的子表达式匹配的内容,以此来匹配期望的重复内容
回溯引用:
- 通过
\n
来引用第n个子表达式匹配的内容 - 数字
n
计数通常从1
开始 - 通常第
0
个匹配可以表示整个正则表达式的匹配内容 - 对于嵌套的子表达式,数字
n
表示的子表达式,是子表达式(
从左至右顺序
举个🌰:
在上述示例中,此时我们可以修改正则为
<[hH]([1-6])>.*?<\/[hH]\1>
此时可以固定匹配成对的标签
借助回溯引用进行替换
就是在替换使用时,借助回溯引用,来引用匹配的子表达式内容
举个🌰:
对于文本
my name is lisi, my email is lisi@163.com
我们需要匹配其中的邮箱,可以使用下面的正则表达式
\w+[\w\.]*@[\w\.]+\.\w+
如果此时我们想将邮箱替换为一个可点击的a标签
以javascript语法为例
'my name is lisi, my email is lisi@163.com'
.replace(/(\w+[\w\.]*@[\w\.]+\.\w+)/, '<a href="mailto:$1">$1</a>')
结果:
"my name is lisi, my email is <a href=\"mailto:lisi@163.com\">lisi@163.com</a>"
在上述示例中,可以通过$n
来回溯引用子表达式匹配的内容,进行文本替换
注意:
- 对于不同的语言,回溯引用的语法是不同的
- 例如
JavaScript
中,替换使用$n
- 例如
- 回溯引用是可以被引用任意次数(不只是能引用一次)
前后查找
向前查找
向前查找的语法为:?=
举个🌰:
对于文本:
http://www.hhttpp.com/
我们需要匹配后面存在 : 的http内容,此时按上述学习内容,编写以下正则
.*:
可以发现 .* 正确匹配到了 http,但是连带一起匹配到了后面的 : ,其匹配内容为
http:
可以发现通过前面学习的知识就无法满足,此时我们可以使用向前查找,编写正则为
.*(?=:)
此时 .* 匹配的内容为 http,而结果中并不包含 :
结论:
- 向前查找就是以其为查找内容进行内容匹配,但是其不包括在最终的匹配结果中
向后查找
向前查找是匹配预期内容前面的文本内容,而向后查找则相反,其是匹配预期内容后面的文本内容
向后查找的语法是:?<=
举个🌰:
对于文本:
banana: $30
Number: 20
我们需要匹配价格而不是数量,此时编写正则为
\$[1-9]\d*
其实际匹配内容为:
$30
此时我们需要进行处理,去除前面的 $ 才是真正的价格,比较麻烦,所以我们使用向后查找
(?<=\$)[1-9]\d*
其实际匹配内容为:
30
符合预期
前后查找总结
- 前后查找被称为 零宽度匹配
- 前后查找与普通查找一致,只是对于
?=``?<=
标记的内容,不会出现在匹配结果中 - 任何一个子表达式都可以转换为前后查找表达式,只要添加
?=``?<=
前缀
注意:
- 向前查找模式的子表达式的长度是可变的,所以可以使用
.``+
之类的元字符,而向后查找模式的子表达式只能是固定长度
举个🌰:
以JS代码为例
将 http:: 替换为 https::
正则替换:
'http::'.replace(/[^:]*(?=:+)/, 'https')
结果:
https::
正则:
/(?<=\$+)[1-9]\d*/
报错:
Invalid regular expression: /(?<=$+)[1-9]\d*/: Nothing to repeat
替换后正则为
'$30'.replace(/(?<=\$)[1-9]\d*/, '20')
结果:
$20
对前后查找取非
上述我们通过前后查找来匹配文本,通常目的是为了确定匹配内容的文本位置(通过指定前后查找表达式来确定匹配内容前后必须是什么文本),这些被称为正向的前后查找
与之相反的是匹配内容前后不能是什么文本,这就是负向的前后查找
其对应表达式为:
操作符 | 说明 |
---|---|
(?=) | 正向前查找 |
(?!) | 负向前查找 |
(?<=) | 正向后查找 |
(?<!) | 负向后查找 |
举个🌰:
对于上述文本
banana: $30
Number: 20
此时我们需要匹配数量,而不是价格,此时正则应为:
/(?<!\$)[1-9]\d*/
嵌入条件
有时在匹配时需要进行条件判断来适配复杂的场景,此时就可以使用嵌入条件
嵌入条件是通过?
来实现,其具体有两种使用场景:
- 回溯引用条件
- 前后查找条件
注意:
- 不是所有正则都支持嵌入条件
回溯引用条件
回溯引用条件的语法是:(?(backrefrence)true-regex|false-regex)
语法解释:
?
- 表示条件判断
backrefrence
- 表示回溯引用的子表达式的编号
- 注意,此处编号无需转义
true-regex
- 回溯引用子表达式存在时适配的正则表达式
false-regex
- 回溯引用子表达式不存在时适配的正则表达式
举个🌰:
假定我们匹配电话号码
123-456-7890
(123)456-7890
如果存在 ( ,就匹配 ) ,否则匹配 -
普通正则
(\()?\d{3}(\))?-?\d{3}-\d{4}
解释
(\()? 匹配可有可无的 (
(\))? 匹配可有可无的 )
-? 匹配可有可无的 -
但是上述正则还会匹配如下字符串:
(123)-456-7890
所以此时我们使用嵌入条件
(\()?\d{3}(?(1)\)|-)\d{3}-\d{4}
解释
(\()? 匹配可有可无的 (
(?(1)\)|-) 中
?(1) 判断第一个子表达式是否匹配成功
匹配成功时,正则匹配 \)
否则匹配失败时,正则匹配 -
前后查找条件
前后查找的语法是:(?(前后查找表达式)true-regex)
举个🌰:
例如我们匹配文本
44444-4444
22222
使用正则为:
\d{5}(-\d{4})?
此时可以符合,但是 - 将会存在于匹配结果中,如果我们想剔除,此时使用前后查找条件
\d{5}(?(?=-)-\d{4})
ASCII编码表
ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 |
---|---|---|---|---|---|---|---|
0 | NUT | 32 | (space) | 64 | @ | 96 | 、 |
1 | SOH | 33 | ! | 65 | A | 97 | a |
2 | STX | 34 | " | 66 | B | 98 | b |
3 | ETX | 35 | # | 67 | C | 99 | c |
4 | EOT | 36 | $ | 68 | D | 100 | d |
5 | ENQ | 37 | % | 69 | E | 101 | e |
6 | ACK | 38 | & | 70 | F | 102 | f |
7 | BEL | 39 | , | 71 | G | 103 | g |
8 | BS | 40 | ( | 72 | H | 104 | h |
9 | HT | 41 | ) | 73 | I | 105 | i |
10 | LF | 42 | * | 74 | J | 106 | j |
11 | VT | 43 | + | 75 | K | 107 | k |
12 | FF | 44 | , | 76 | L | 108 | l |
13 | CR | 45 | - | 77 | M | 109 | m |
14 | SO | 46 | . | 78 | N | 110 | n |
15 | SI | 47 | / | 79 | O | 111 | o |
16 | DLE | 48 | 0 | 80 | P | 112 | p |
17 | DCI | 49 | 1 | 81 | Q | 113 | q |
18 | DC2 | 50 | 2 | 82 | R | 114 | r |
19 | DC3 | 51 | 3 | 83 | S | 115 | s |
20 | DC4 | 52 | 4 | 84 | T | 116 | t |
21 | NAK | 53 | 5 | 85 | U | 117 | u |
22 | SYN | 54 | 6 | 86 | V | 118 | v |
23 | TB | 55 | 7 | 87 | W | 119 | w |
24 | CAN | 56 | 8 | 88 | X | 120 | x |
25 | EM | 57 | 9 | 89 | Y | 121 | y |
26 | SUB | 58 | : | 90 | Z | 122 | z |
27 | ESC | 59 | ; | 91 | [ | 123 | { |
28 | FS | 60 | < | 92 | / | 124 | | |
29 | GS | 61 | = | 93 | ] | 125 | } |
30 | RS | 62 | > | 94 | ^ | 126 | ` |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
特殊字符解释
NUL空 | VT 垂直制表 | SYN 空转同步 |
---|---|---|
STX 正文开始 | CR 回车 | CAN 作废 |
ETX 正文结束 | SO 移位输出 | EM 纸尽 |
EOY 传输结束 | SI 移位输入 | SUB 换置 |
ENQ 询问字符 | DLE 空格 | ESC 换码 |
ACK 承认 | DC1 设备控制1 | FS 文字分隔符 |
BEL 报警 | DC2 设备控制2 | GS 组分隔符 |
BS 退一格 | DC3 设备控制3 | RS 记录分隔符 |
HT 横向列表 | DC4 设备控制4 | US 单元分隔符 |
LF 换行 | NAK 否定 | DEL 删除 |
JS中正则使用
RegExp类
JS中使用RegExp
类来表示正则表达式,其具体语法为 new RegExp('正则字符', '修饰符')
特殊语法
除了通过 new
关键字创建RegExp
对象,还有一种更简便的语法,也同样可以构建RegExp
对象
语法为:/正则表达式主体/修饰符(可选)
属性
对于构建好的RegExp
对象,其存在5个属性来描述其性质
source
source
是一个只读的文本字符串,包含正则表达式的文本表示
举个🌰:
var reg = new RegExp('^asd$', 'g');
reg.source; // "^asd$"
global
global
是一个只读的布尔值,表示正则是否带有修饰符g
修饰符g
表示全局匹配,检索文本中所有的匹配项
举个🌰:
var reg = new RegExp('^asd$', 'g');
reg.global; // true
var reg = new RegExp('^asd$');
reg.global; // false
ignoreCase
ignoreCase
是一个只读的布尔值,表示正则是否带有修饰符i
修饰符i
表示检索文本时忽略大小写
举个🌰:
var reg = new RegExp('^asd$');
reg.ignoreCase; // false
var reg = new RegExp('^asd$', 'i');
reg.ignoreCase; // true
multiline
multiline
是一个只读的布尔值,表示正则是否带有修饰符m
修饰符m
表示正则是否多行匹配模式,如果是多行匹配模式,则元字符^``&
不仅匹配整个字符串的开始和结尾,还能匹配每行的开始和结尾
举个🌰:
var reg = new RegExp('^asd$');
reg.multiline; // false
var reg = new RegExp('^asd$', 'm');
reg.multiline; // true
lastIndex
lastIndex
是一个可读/写的整数,用于在全局匹配模式中,存储在整个字符串中下一次检索的开始位置
如果是非全局匹配模式,则无需关系此属性
此属性将在exec``test
方法中使用到
举个🌰:
var reg = new RegExp('^asd$', 'g');
console.log(reg.lastIndex);
console.log(reg.exec('asd asd'));
console.log(reg.lastIndex);
console.log(reg.exec('asd asd'));
console.log(reg.lastIndex);
console.log(reg.exec('asd asd'));
console.log(reg.lastIndex);
输出:
0
["asd", index: 0, input: "asd asd asd", groups: undefined]
3
["asd", index: 4, input: "asd asd asd", groups: undefined]
7
如果exec方法没有发现匹配结果,它将重置lastIndex
null
0
方法
test(str)
test
方法用于测试给定的字符串是否满足正则规则,返回true/false
举个🌰:
var reg = new RegExp('a{1,4}');
reg.test('a'); // true
在全局匹配模式中,调用后将修改lastIndex
属性
举个🌰:
var reg = new RegExp('a{1,2}');
var reg2 = new RegExp('a{1,2}', 'g');
console.log(reg.test('a aa')); // true
console.log(reg.lastIndex); // 0
console.log(reg2.test('a aa')); // true
console.log(reg2.lastIndex); // 1
console.log(reg.test('a aa')); // true
console.log(reg.lastIndex); // 0
console.log(reg2.test('a aa')); // true
console.log(reg2.lastIndex); // 4
console.log(reg.test('a aa')); // true
console.log(reg.lastIndex); // 0
console.log(reg2.test('a aa')); // false
console.log(reg2.lastIndex); // 0
exec(str)
test
方法只能简单测试是否匹配,无法获取更多的信息。exec
方法可以找到匹配的文本,并且返回一个结果数组。如果匹配不上,则返回null
对于结果数组:
- 第0个元素是与正则表达式匹配的整个文本内容
- 后续元素是顺序对应的子表达式内容
除了上述的元素,结果数组中还有两个额外属性:
index
:此次匹配文本的第一个字符的位置input
:被检索的整个字符串内容
同时,与test
方法一样,在全局模式时,exec
方法也将修改lastIndex
属性
举个🌰:
var reg1 = /a{1,2}/;
var reg2 = /a{1,2}/g;
var str = 'a aa';
console.log(reg1.exec(str)); // ["a", index: 0, input: "a aa", groups: undefined]
console.log(reg1.lastIndex); // 0
console.log(reg2.exec(str)); // ["a", index: 0, input: "a aa", groups: undefined]
console.log(reg2.lastIndex); // 1
console.log(reg1.exec(str)); // ["a", index: 0, input: "a aa", groups: undefined]
console.log(reg1.lastIndex); // 0
console.log(reg2.exec(str)); // ["aa", index: 2, input: "a aa", groups: undefined]
console.log(reg2.lastIndex); // 4
console.log(reg1.exec(str)); // ["a", index: 0, input: "a aa", groups: undefined]
console.log(reg1.lastIndex); // 0
console.log(reg2.exec(str)); // null
console.log(reg2.lastIndex); // 0
字符串方法
在JS中,字符串对象存在一些与正则相关的方法,我们一一分析
search(regexp|str)
search
方法用于检索字符串,具体匹配根据传入参数而定:
- 目标子字符串
- 检索字符串中是否存在目标子字符串
- 正则对象
- 检索字符串中是否存在于正则匹配的子字符串
如果匹配成功则返回0
,匹配失败返回-1
注意:
- 全局模式不适用于
search
方法,它只查询第一个匹配项
下面我们测试上述两种情况:
- 目标子字符串
'hello world'.search('hello'); // 0
'hello world'.search('helloo'); // -1
- 正则对象
'(0734)23432'.search(/\(\d{4}\)/); // 0
match(regexp|str)
match
方法,用于检索字符串中与正则/文本参数匹配的文本内容
- 非全局模式
- 与
exec
方法类似 - 执行一次匹配
- 匹配成功,将返回一个数组存储匹配结果
- 子表达式的匹配结果将按顺序存储
- 与
'(0734)23432'.match(/\((\d{4})\)/);
结果:
["(0734)", "0734", index: 0, input: "(0734)23432", groups: undefined]
'(0734)23432'.match(/\((\d{5})\)/);
结果:
null
- 全局模式
- 将执行多次匹配
- 匹配成功,将返回一个数组存储匹配结果
- 并不会包括子表达式的匹配结果
'(0734)2(3432)'.match(/\((\d{4})\)/g);
结果:
["(0734)", "(3432)"]
split(regexp|str, limt)
split
方法用于进行字符串的分割,以入参为分隔符
- 可传入正则与普通字符串分割符,默认是将普通字符串分割符转为正则
limit
用于限制结果数组的长度
注意:
- 如果正则表达式中包括子表达式,则每次分割匹配时,子表达式的匹配结果将拼接到结果数组中
- 并不是所有浏览器都支持
举个🌰:
'Hello 1 word. Sentence number 2.'.split(/\d/);
结果:
["Hello ", " word. Sentence number ", "."]
'Hello 1 word. Sentence number 2.'.split(/(\d)/);
结果:
["Hello ", "1", " word. Sentence number ", "2", "."]
'Hello 1 word. Sentence number 2.'.split(/(\d)/, 3);
结果:
["Hello ", "1", " word. Sentence number "]
replace(regexp|str, str|func)
replace
方法用于进行字符串的内容替换
对于replace
方法,我们需要关注一下几个点:
- 对于正则参数,如果为全局模式,则替换所有匹配项,否则只替换第一个匹配项
举个🌰:
字符串参数:
'12-34-56'.replace('-', ':'); // "12:34-56"
正则参数(非全局):
'12-34-56'.replace(/-/, ':'); // "12:34-56"
正则参数(全局):
'12-34-56'.replace(/-/g, ':'); // "12:34:56"
- 第二个参数为替代字符串时,可以在其中使用特殊字符
特殊字符如下:
符号 | 替换字符串中的操作 |
---|---|
$& | 表示整个匹配项 |
$` | 表示在匹配项之前字符串内容 |
$’ | 表示在匹配项之后字符串内容 |
$n | 表示第n个子表达式内容, n 是一个 1 到 2 位的数字,实际就是回溯引用 |
$ | 表示带有给定 name 的子表达式的内容 |
$$ | 表示字符 $ |
举个🌰:
1. $&:表示匹配项
'99999'.replace(/\d{1,3}(?=(\d{3})$)/, '$&,');
正则解析:
\d{1,3} :匹配1~3个数字
(?=(\d{3}) :表示多个向前查找的 3个数字 ,且不在匹配结果中
$& 表示匹配项,值为:99
所以替换结果为:"99,999"
使用示例:数字千分位
'99999999999'.replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,')
结果:"99,999,999,999"
2. $`:表示匹配项之前的字符串内容
'123456789'.replace('4', '$`')
正则解析:
匹配项为 '4',在匹配项之前内容为 '123'
所以替换结果为:"12312356789"
3. $':表示匹配项之后的字符串内容
'123456789'.replace('5', "$'")
正则解析:
匹配项为 '5',在匹配项之前内容为 '6789'
所以替换结果为:"123467896789"
4. $n:表示第n个子表达式内容
'(0734)132465479'.replace(/\((\d{4})\)(\d+)/, '$1-$2')
正则解析:
\((\d{4})\):表示由括号包裹的四位数字,且四位数字内容为子表达式
(\d+):表示至少一位的数字
所以 $1 表示:0734
$2 表示:132465479
替换结果为:"0734-132465479"
- 第二个参数为函数时,可以自定义替换规则
该函数的定义为func(match, p1, p2, ..., pn, offset, input, groups)
,参数解释如下:
match
- 整个匹配项
p1, p2, ..., pn
- 从1开始的子表达式内容
- 如果正则中没有子表达式,则这些参数将省略
offset
- 匹配项的位置
input
- 源字符串
groups
- 所指定分组的对象
举个🌰:
'my phone is (0734)23423423'.replace(/\((\d{4})\)(\d+)/,
(match, p1, p2, offset, input, groups) => {
console.log(match);
console.log(p1);
console.log(p2);
console.log(offset);
console.log(input);
console.log(groups);
return p1 + '-' + p2;
});
输出:
(0734)23423423
0734
23423423
12
my phone is (0734)23423423
undefined
"my phone is 0734-23423423"
groups属性
在exec/match
方法使用时,如果正则中使用了命名捕获组,则匹配结果数组将多出一个groups
属性,其内容对应命名捕获组的匹配内容
"04-25-2017".match(/(?<month>\d{2})-(?<day>\d{2})-(?<year>\d{4})/)['groups']
{month: "04", day: "25", year: "2017"}
在replace
方法中还可以反向引用,其语法为:$<name>
举个🌰:
"abc".replace(/(?<foo>a)/, "$<foo>-")
结果:"a-bc"
注意:
- 对于命名捕获组,也可以如匿名捕获组一样,通过索引来进行引用
Java中正则使用
java中正则使用是通过java.util.regex
包下的两个类:
Pattern
Matcher
Pattern
Pattern
是表示一个正则模式,它的构造是私有的,所以无法通过构造创建
// java.util.regex.Pattern
public final class Pattern implements java.io.Serializable {
// 源正则表达式字符串
private String pattern;
// 模式标志
private int flags;
private Pattern(String p, int f) {...}
}
并且其持有flags
来标志正则的不同模式,通用的有:
// 忽略大小写,等同于 i
public static final int CASE_INSENSITIVE = 0x02;
// 启动多行模式,等同于 m
public static final int MULTILINE = 0x08;
下面我们来分析其方法:
compile
虽然构造是私有的,但是提供了静态方法compile
来创建实例
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
public static Pattern compile(String regex, int flags) {
return new Pattern(regex, flags);
}
举个🌰:
Pattern compile = Pattern.compile("[A-Z]\\d{2}", Pattern.CASE_INSENSITIVE);
注意:
- 对于Java字符串来说,
\
是特殊符号,需要使用\
来转义,所以\
在字符串中应用\\
来表示- 可以先编写好正则,再将
\
替换为\\
后使用
- 可以先编写好正则,再将
split
split
方法是用于字符串分割的方法
public String[] split(CharSequence input) {
return split(input, 0);
}
public String[] split(CharSequence input, int limit) {...}
split
方法有两个参数:
input
- 要处理的字符数据
limit
- 限制的结果数量
第一个参数容易理解,那么第二个参数是如何用的?
对于limit
参数的理解,可以分为以下情况:
- 当参数为
0
时,就是正常的字符串分割 - 当参数为
n
时- 如果
n
大于字符串分割结果数量时,则按分割结果数量 - 如果
n
小于字符串分割结果数量时,则按n - 1
分割后,剩余整体做为一份
- 如果
举个🌰:
public static void main(String[] args) {
Pattern compile = Pattern.compile(",");
String[] split = compile.split("张三,李四,王五,王麻子", limit);
for (String s : split) {
System.out.println("分割结果:" + s);
}
}
如上述代码:
1. 当limit = 0,输出结果:
分割结果:张三
分割结果:李四
分割结果:王五
分割结果:王麻子
2. 当limit = 3,输出结果:
分割结果:张三
分割结果:李四
分割结果:王五,王麻子
3. 当limit = 5,输出结果:
分割结果:张三
分割结果:李四
分割结果:王五
分割结果:王麻子
所以使用带limit
参数存在不确定性,常用为无limit
参数,也就是默认limit = 0
matches
查看字符串是否符合该正则
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
其实质是通过Matcher#matches
进行匹配,但是它只返回匹配结果,如果想重复匹配,不建议使用此方法
对于Matcher#matches
,我们下面讲述Matcher
再研究
matcher
matcher
方法是针对一个字符串,创建一个Matcher
对象实例
public Matcher matcher(CharSequence input) {
if (!compiled) {
synchronized(this) {
if (!compiled)
compile();
}
}
Matcher m = new Matcher(this, input);
return m;
}
Matcher
Pattern
对象表示正则模式,而Matcher
对象就表示匹配结果Matcher
类的构造时default
修饰的,无法被外界访问,所以我们需要通过Pattern#matcher(input)
的方式,来创建Matcher
实例
并且Matcher
持有其对应的Pattern
实例
// java.util.regex.Matcher
public final class Matcher implements MatchResult {
// 持有pattern实例
Pattern parentPattern;
// 保存源字符串
CharSequence text;
Matcher() {
}
Matcher(Pattern parent, CharSequence text) {
this.parentPattern = parent;
this.text = text;
// Allocate state storage
int parentGroupCount = Math.max(parent.capturingGroupCount, 10);
groups = new int[parentGroupCount * 2];
locals = new int[parent.localCount];
// Put fields into initial states
reset();
}
}
匹配方法
matches
用于将整个源字符串与正则进行匹配,相当于在正则添加了^正则$
public boolean matches() {
return match(from, ENDANCHOR);
}
举个🌰:
Pattern.compile("\\d{3}").matcher("234分钟后").matches(); // false
Pattern.compile("\\d{3}").matcher("234").matches(); // true
lookingAt
用于匹配字符串开头是否匹配正则,相当于正则添加了^正则
public boolean lookingAt() {
return match(from, NOANCHOR);
}
举个🌰:
Pattern.compile("\\d{3}").matcher("234分钟后").lookingAt(); // true
Pattern.compile("\\d{3}").matcher("在234分钟后").lookingAt(); // false
查找方法
find
用于查看字符串中是否存在与正则匹配内容,不关注匹配项位置
public boolean find() {...}
public boolean find(int start) {...}
对于find
方法使用,有以下点需要注意:
- 如果正则存在位置匹配元字符,将影响其匹配结果
// 等同于lookingAt()
Pattern.compile("\\d{3}").matcher("在234分钟后"); // true
Pattern.compile("^\\d{3}").matcher("在234分钟后"); // false
// 等同于matches()
Pattern.compile("\\d{3}").matcher("234"); // true
Pattern.compile("^\\d{3}$").matcher("234分钟后"); // false
- 对于
find
方法,它修改了Matcher
实例的first、last
public final class Matcher implements MatchResult {
int first = -1, last = 0;
}
解释:
first、last
是最近一次匹配的字符串的所在范围last
类似于JS中的lastIndex
属性,是下一次进行匹配的开始索引
所以对于find
方法而言,它进行匹配的过程类似于JS中的全局模式匹配,所以通常可以使用while
循序来获取匹配结果
举个🌰:
public static void main(String[] args) {
Matcher matcher = Pattern.compile("\\d{2}[a-z]").matcher("12a-34b-56c");
while (matcher.find()) {
System.out.println("匹配结果:" + matcher.group());
}
}
输出:
匹配结果:12a
匹配结果:34b
匹配结果:56c
- 调用有参方法,将重置
first/last
信息,从入参start
位置开始进行匹配
public static void main(String[] args) {
Matcher matcher = Pattern.compile("\\d{2}[a-z]").matcher("12a-34b-56c");
matcher.find();
System.out.println("匹配结果:" + matcher.group());
matcher.find();
System.out.println("匹配结果:" + matcher.group());
matcher.find(0);
System.out.println("匹配结果:" + matcher.group());
}
输出:
匹配结果:12a
匹配结果:34b
匹配结果:12a
匹配结果
对于Matcher
实例而言,其在调用了上述匹配/查找方法后,将保留其对应的匹配信息,可以通过方法来获取对应的匹配信息
start/end
start()、end()
方法将返回最近一次匹配结果对应first/last
属性信息
并且,如果正则中存在子表达式,它还可以针对匿名捕获组、命名捕获组,查找对应匹配位置信息
// 获取整个匹配文本的位置信息
public int start() {
if (first < 0)
throw new IllegalStateException("No match available");
return first;
}
public int end() {
if (first < 0)
throw new IllegalStateException("No match available");
return last;
}
// 按捕获组的序号进行信息获取
public int start(int group) {...}
public int end(int group) {...}
// 按命名捕获组的名称进行信息获取
public int start(String name) {...}
public int end(String name) {...}
group
Matcher
对象对应正则的匹配结果,将保留其匹配结果的信息,包括捕获组的信息,我们可以通过group
方法来获取其对应的匹配文本
// 返回捕获组的数量
public int groupCount() {
return parentPattern.capturingGroupCount - 1;
}
// 获取整个正则匹配的文本内容
public String group() {
return group(0);
}
// 根据捕获组序号,获取捕获组匹配的文本内容
public String group(int group) {
if (first < 0)
throw new IllegalStateException("No match found");
if (group < 0 || group > groupCount())
throw new IndexOutOfBoundsException("No group " + group);
if ((groups[group*2] == -1) || (groups[group*2+1] == -1))
return null;
return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
}
// 针对于命名捕获组,通过捕获组名称,获取捕获组匹配的文本内容
public String group(String name) {
int group = getMatchedGroupIndex(name);
if ((groups[group*2] == -1) || (groups[group*2+1] == -1))
return null;
return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
}
注意:
- 没有子表达式的正则,
groupCount
结果为0
group() 或 group(0)
,都是返回整个正则匹配的文本内容- 其实质是通过属性
int[] groups
来存储捕获组匹配内容在源字符串的范围,再进行获取- 并没有存储每个捕获组真实的文本内容
示例可以查看下面的<命名捕获组使用>示例
替换方法
appendReplacement
public Matcher appendReplacement(StringBuffer sb, String replacement) {...}
此方法将字符串替换结果输出到一个StringBuffer
中,其使用具体示例如下:
Pattern p = Pattern.compile("cat");
Matcher m = p.matcher("one cat two cats in the yard");
StringBuffer sb = new StringBuffer();
while (m.find()) {
m.appendReplacement(sb, "dog");
System.out.println(sb.toString());
}
输出:
one dog
one dog two dog
由上例可知其行为:
- 将字符串的检索起点
last
到此次匹配结果的start()
结果存入sb
中 - 将替换的字符串填入
sb
中 - 将匹配结果的
end()
作为下次匹配的起点
注意:
- 对于匹配替换过程中,
$
符号是有特殊含义的,如果需要将其处理,可以配合quoteReplacement
方法使用
appendTail
public StringBuffer appendTail(StringBuffer sb) {
sb.append(text, lastAppendPosition, getTextLength());
return sb;
}
有前面示例发现,调用appendReplacement
替换后,StringBuffer
中只有前段输出,对于匹配结果之后的字符串信息并没有保存,所以需要配合appendTail
使用appendTail
方法的作用是将上次匹配结果的end()
后的源字符串信息全部拼接到后面
改造上🌰:
public static void main(String[] args) {
Pattern p = Pattern.compile("cat");
Matcher m = p.matcher("one cat two cats in the yard");
StringBuffer sb = new StringBuffer();
while (m.find()) {
m.appendReplacement(sb, "dog");
System.out.println(sb.toString());
}
m.appendTail(sb);
System.out.println("最终结果:" + sb.toString());
}
输出:
one dog
one dog two dog
最终结果:one dog two dogs in the yard
replaceFirst/replaceAll
public String replaceFirst(String replacement) {
if (replacement == null)
throw new NullPointerException("replacement");
reset();
if (!find())
return text.toString();
StringBuffer sb = new StringBuffer();
appendReplacement(sb, replacement);
appendTail(sb);
return sb.toString();
}
public String replaceAll(String replacement) {
reset();
boolean result = find();
if (result) {
StringBuffer sb = new StringBuffer();
do {
appendReplacement(sb, replacement);
result = find();
} while (result);
appendTail(sb);
return sb.toString();
}
return text.toString();
}
replaceFirst/replaceAll
用于替换匹配项,由其源码可知,其实质还是使用appendReplacement、appendTail
实现
注意:
- 对于通用匹配,我们可以使用
replaceFirst/replaceAll
- 如果比较特殊的处理,还是建议使用底层的
appendReplacement、appendTail
实现,可以提高效率
其他方法
quoteReplacement
quoteReplacement
方法用于将源字符串中的\``$
这种特殊字符转为普通字符,去除源字符串中的特殊符号,实质是对其添加\\
转义
public static String quoteReplacement(String s) {
if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1))
return s;
StringBuilder sb = new StringBuilder();
for (int i=0; i<s.length(); i++) {
char c = s.charAt(i);
if (c == '\\' || c == '$') {
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}
特殊场景示例
命名捕获组使用
public static void main(String[] args) {
// String content = "2022年3月21日上午9:00";
// String content = "3月21日8:30";
String content = "2021年12月29日";
// String content = "22日9:00";
// String content = "23日";
String regex = "((?<year>\\d{4})年)?((?<month>\\d{1,2})月)?((?<day>\\d{1,2})日)?([^0-9]*)?(?<time>\\d{1,2}[::]\\d{1,2})?";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content);
matcher.find();
System.out.println("year = " + matcher.group("year"));
System.out.println("month = " + matcher.group("month"));
System.out.println("day = " + matcher.group("day"));
System.out.println("time = " + matcher.group("time"));
}
通用字符串替换
@Slf4j
public class StringFormat {
public static final Pattern PATTERN = Pattern.compile("\\{\\}");
/**
* 字符串格式化,使用args参数替换format中 {}
* 使用Matcher#appendReplacement、appendTail替代普通replace方式,提升效率
* 注意使用quoteReplacement避免$符号影响
*/
public static String format(String format, Object... args) {
if (args == null || args.length == 0) {
return format;
}
StringBuffer sb = new StringBuffer();
Matcher matcher = PATTERN.matcher(format);
int length = args.length;
int i = 0;
while (matcher.find() && i < length) {
String info = args[i] == null ? "" : args[i].toString();
matcher.appendReplacement(sb, Matcher.quoteReplacement(info));
i++;
}
matcher.appendTail(sb);
return sb.toString();
}
}