正则的简介与使用

正则表达式(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中存在一些元字符,例如 &nbsp; 表示的不换行空格,我们需要将其视为一个整体

在下述文本中
	hello, my name is Ben&nbsp;&nbsp;Forta
我们想将多次重复的空格找出,替换为单个空格

我们预期的正则是
	 &nbsp;{2,}
可以发现其无法达到预期效果

我们此时应该使用子表达式
对应正则
	(&nbsp;){2,}

子表达式的嵌套

子表达式是可以嵌套使用的,而且支持多层嵌套

注意

  • 太过复杂的子表达式嵌套,会降低正则表达式的可读性,我们应该合理使用

或字符

前面我们学习了字符组,它可以表示在字符组范围内的任意单个字符
例如正则[ab],它可以表示a、b中的任意一个,也就是字符ab

这里提到了或者的概念,在日常生活中,或者的兼容时十分重要的,所以提供了元字符|,用于表示或者的含义
元字符|的左右可以为:

  • 单个字符
  • 字符组
  • 子表达式

注意

  • |在使用时,需要使用(``)来限定匹配范围

举个🌰:

对于IP地址的匹配
IP地址简单来说就是:由四组数字组成,每组数字由13个数字组成,中间通过.连接
所以我们可以编写以下正则
	(\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值控制字符
0NUT32(space)64@96
1SOH33!65A97a
2STX34"66B98b
3ETX35#67C99c
4EOT36$68D100d
5ENQ37%69E101e
6ACK38&70F102f
7BEL39,71G103g
8BS40(72H104h
9HT41)73I105i
10LF42*74J106j
11VT43+75K107k
12FF44,76L108l
13CR45-77M109m
14SO46.78N110n
15SI47/79O111o
16DLE48080P112p
17DCI49181Q113q
18DC250282R114r
19DC351383S115s
20DC452484T116t
21NAK53585U117u
22SYN54686V118v
23TB55787W119w
24CAN56888X120x
25EM57989Y121y
26SUB58:90Z122z
27ESC59;91[123{
28FS60<92/124|
29GS61=93]125}
30RS62>94^126`
31US63?95_127DEL

特殊字符解释

NUL空VT 垂直制表SYN 空转同步
STX 正文开始CR 回车CAN 作废
ETX 正文结束SO 移位输出EM 纸尽
EOY 传输结束SI 移位输入SUB 换置
ENQ 询问字符DLE 空格ESC 换码
ACK 承认DC1 设备控制1FS 文字分隔符
BEL 报警DC2 设备控制2GS 组分隔符
BS 退一格DC3 设备控制3RS 记录分隔符
HT 横向列表DC4 设备控制4US 单元分隔符
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方法使用,有以下点需要注意:

  1. 如果正则存在位置匹配元字符,将影响其匹配结果
// 等同于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
  1. 对于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
  1. 调用有参方法,将重置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();
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值