正则表达式

179 阅读3分钟

正则表达式

1.速查表

多个字符

匹配区间正则表达式记忆方式
除了换行符之外的任何字符.
单个数字, [0-9]\ddigit
除了[0-9]\Dnot digit
包括下划线在内的单个字符,[A-Za-z0-9_]\wword
非字母数字下划线\Wnot word
匹配空白字符,包括空格、制表符、换页符和换行符\sspace
匹配非空白字符\Snot space
^表示匹配字符串的开始位置 (中括号中[] 时表示取反)
$表示匹配字符串的结束位置
*表示匹配 零次到多次
?表示匹配零次或一次
+表示匹配 一次到多次 (至少有一次)
表示为或者,两项中取一项
()小括号表示匹配括号中全部字符
[]中括号表示匹配括号中一个字符 范围描述 如[0-9 a-z A-Z]
{}大括号用于限定匹配次数 如 {n}表示匹配n个字符 {n,}表示至少匹配n个字符 {n,m}表示至少n,最多m
\转义字符 如上基本符号匹配都需要转义字符 如 * 表示匹配*号

常用字符

正则表达式含义
[A-Za-z0-9]字母和数字

特殊字符

特殊字符正则表达式记忆方式
换行符\nnew line
换页符\fform feed
回车符\rreturn
空白符\sspace
制表符\ttab
垂直制表符\vvertical tab
回退符[\b]backspace,之所以使用[]符号是避免和\b重复

2.循环与重复

要实现多个字符的匹配我们只要多次循环,重复使用我们的之前的正则规则就可以了。那么根据循环次数的多与少,我们可以分为0次,1次,多次,特定次。

0 | 1

元字符?代表了匹配一个字符或0个字符。设想一下,如果你要匹配colorcolour这两个单词,就需要同时保证u这个字符是否出现都能被匹配到。所以你的正则表达式应该是这样的:/colou?r/

>= 0

元字符*用来表示匹配0个字符或无数个字符。通常用来过滤某些可有可无的字符串。

>= 1

元字符+适用于要匹配同个字符出现1次或多次的情况。

特定次数

在某些情况下,我们需要匹配特定的重复次数,元字符{}用来给重复匹配设置精确的区间范围。如'a'我想匹配3次,那么我就使用/a{3}/这个正则,或者说'a'我想匹配至少两次就是用/a{2,}/这个正则。

用法:

 - {x}: x次
 ​
 - {min, max}: 介于min次到max次之间
 ​
 - {min, }: 至少min次
 ​
 - {0, max}: 至多max次

3.位置边界

3.1单词边界

单词是构成句子和文章的基本单位,一个常见的使用场景是把文章或句子中的特定单词找出来。如:

 The cat scattered his food all over the room.

我想找到cat这个单词,但是如果只是使用/cat/这个正则,就会同时匹配到catscattered这两处文本。这时候我们就需要使用边界正则表达式\b,其中b是boundary的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。

上面的例子改写成/\bcat\b/这样就能匹配到cat这个单词了。

3.2字符串边界

匹配完单词,我们再来看一下一整个字符串的边界怎么匹配。元字符^用来匹配字符串的开头。而元字符$用来匹配字符串的末尾。注意的是在长文本里,如果要排除换行符的干扰,我们要使用多行模式。试着匹配I am scq000这个句子:

 I am scq000.
 I am scq000.
 I am scq000.

我们可以使用/^I am scq000.$/m这样的正则表达式,其实m是multiple line的首字母。正则里面的模式除了m外比较常用的还有i和g。前者的意思是忽略大小写,后者的意思是找到所有符合的匹配。

最后,总结一下:

边界和标志正则表达式记忆方式
单词边界\bboundary
非单词边界\Bnot boundary
字符串开头头尖尖那么大个
字符串结尾$终结者,美国科幻电影,美元符$
多行模式m标志multiple of lines
忽略大小写i标志ignore case, case-insensitive
全局模式g标志global
Unicode匹配u表示启用Unicode匹配模式
粘性匹配y表示从目标字符串的指定位置开始匹配

当我们使用正则表达式\bcat\b时,它将匹配单词"cat",但不会匹配"catch"或"category"等包含"cat"的单词。这是因为\b确保"cat"是一个独立的单词,而不是其他单词的一部分。

4.子表达式

4.1分组

其中分组体现在:所有以()元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。第几个括号就是第几个分组。那如果括号存在嵌套括号呢,也很简单,可以数左括号计数。

默认分组都是捕获分组

4.2回溯引用

所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1,\2,....,其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。

假设现在要在下面这个文本里匹配两个连续相同的单词,你要怎么做呢?

 Hello what what is the first thing, and I am am scq000.

利用回溯引用,我们可以很容易地写出\b(\w+)\s\1这样的正则。

回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2...来引用要被替换的字符串。$0代表整个正则表达式匹配的文本。下面以js代码作演示:

 var str = 'abc abc 123';
 str.replace(/(ab)c/g,'$1d');
 // 得到结果 'abd abd 123'

4.3非捕获组(?:regex)

在正则表达式中,捕获组用于将匹配的文本片段捕获或提取出来,以便稍后可以通过引用捕获组来使用这些匹配结果。然而,有时候我们希望对子表达式进行分组,但不希望将其捕获到匹配结果中,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。

 var str = 'scq000'.
 str.replace(/(scq00)(?:0)/, '$1,$2')
 // 返回scq00,$2
 // 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2

有时,我们需要限制回溯引用的适用范围。那么通过前向查找和后向查找就可以达到这个目的。

4.4命名捕获分组(?<name>regex)

前面我们讲了分组编号,但由于编号得数在第几个位置,后续如果发现正则有问题,改动了括号的个数,可能导致编号发生变化,因此提供了 命名分组(named grouping) ,这样和数字相比更容易辨识,不容易出错。命名分组的格式为 (?<分组名>...)

还是这个日期例子 2023-09-06 12:00:01,我们使用正则匹配日期和时间来验证下命名分组。

 const date = '2023-09-06 12:00:01'
 const regex = /(?<date>(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})) (?<time>(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2}))/
 ​
 ​
 date.replace(regex, '$<year>年$<month>月$<day>日 $<hour>点$<minute>分$<second>秒')
 // result: 
 //   '2023年09月06日 12点00分01秒'

spring cloud gateway

Pattern pattern = Pattern.compile("/api-gateway(?<segment>/?.*)");
Matcher matcher = pattern.matcher("/api-gateway/product/1");
if (matcher.matches()) {
    String segment = matcher.group("segment"); 
    System.out.println(segment); //输出/product/1
}

4.5命名捕获引用

那如果是命名分组匹配 (?<name>), 那么在正则表达式内部通过 \k<name> 引用,详见如下

 const reg = /^<(?<tag>\w+)>.+(</\k<tag>>)$/
 const html = `<html><head></head><body></body></html>`
 ​
 html.replace(reg, '$<tag>') // 'html'
 html.replace(reg, '$2') // '</html>'

4.6前向查找(?=regex)

前向查找(lookahead)是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程中都会用来限制前面的表达式的匹配。例如happy happily这两个单词,我想获得以happ开头的副词,那么就可以使用happ(?=ily)来匹配。如果我想过滤所有以happ开头的副词,那么也可以采用负前向查找的正则happ(?!ily),就会匹配到happy单词的happ前缀。

4.7后向查找(?<=regex)

介绍完前向查找,接着我们再来介绍一下它的反向操作:后向查找(lookbehind)。后向查找(lookbehind)是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: applepeople都包含ple这个后缀,那么如果我只想找到appleple,该怎么做呢?我们可以通过限制app这个前缀,就能唯一确定ple这个单词了。

 /(?<=ap)ple/

其中(?<=regex)的语法就是我们这里要介绍的后向查找。regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向查找。另外一种限制匹配是利用(?<!regex) 语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找appleple也可以这么写成/(?<!peo)ple

需要注意的,不是每种正则实现都支持后向查找。在javascript中是不支持的,所以如果有用到后向查找的情况,有一个思路是将字符串进行翻转,然后再使用前向查找,作完处理后再翻转回来。看一个简单的例子:

 // 比如我想替换apple的ple为ply
 var str = 'apple people';
 str.split('').reverse().join('').replace(/elp(?=pa)/, 'ylp').split('').reverse().join('');

ps: 感谢评论区提醒,从es2018之后,chrome中的正则表达式也支持反向查找了。不过,在实际项目中还需要注意对旧浏览器的支持,以防线上出现Bug。详情请查看kangax.github.io/compat-tabl…

最后回顾一下这部分内容:

回溯查找正则释义和例子
引用\0,\1,\2 和 0,0, 1, $2转义+数字
非捕获组(?:)(?:1[0-2]0?[1-9]):[0-5]\d:[0-5]\d
前向查找(?=)引用子表达式(()),本身不被消费(?), 正向的查找(=),如:happ(?=ily)
前向负查找(?!)引用子表达式(()),本身不被消费(?), 负向的查找(!)
后向查找(?<=)引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),正的查找(=) ,如:(?<=ap)ple
后向负查找(?<!)引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),负的查找(!)

4.8逻辑处理

计算机科学就是一门包含逻辑的科学。让我们回忆一下编程语言当中用到的三种逻辑关系,与或非。

在正则里面,默认的正则规则都是的关系所以这里不讨论。

关系,分为两种情况:一种是字符匹配,另一种是子表达式匹配。在字符匹配的时候,需要使用^这个元字符。在这里要着重记忆一下:只有在[]内部使用的^才表示非的关系。子表达式匹配的非关系就要用到前面介绍的前向负查找子表达式(?!regex)或后向负查找子表达式(?<!regex)

或关系,通常给子表达式进行归类使用。比如,我同时匹配a,b两种情况就可以使用(a|b)这样的子表达式。

逻辑关系正则元字符
[^regex]和!

5.匹配模式

这一节我们讲一下正则中的三种模式,贪婪匹配、非贪婪匹配和独占模式(JS不支持)。

这些模式会改变正则中量词的匹配行为,比如匹配一到多次;在匹配的时候,匹配长度是尽可能长还是要尽可能短呢? 如果不知道贪婪和非贪婪匹配模式,我们写的正则很可能是错误的,这样匹配就达不到期望的效果。

5.1贪婪匹配(Greedy)

我们来看一下贪婪匹配。在正则中,表示次数的量词 默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配。

首先,我们来看一下在字符串 aaabb 中使用正则 a* 的匹配过程。

字符串aaabb
下标012345
匹配开始结束说明匹配内容
第 1 次03到第一个字母b发现不满足,输出 aaaaaa
第 2 次33匹配剩下b发现匹配不上,输出空字符串空字符串
第 3 次44匹配剩下b发现匹配不上,输出空字符串空字符串
第 4 次55匹配剩下空字符串,输出空字符串空字符串

5.2非贪婪匹配(Lazy)

正则表达式默认是贪婪的,那么如何将贪婪模式变成非贪婪模式呢?我们可以 在量词后面加上英文的问号 (?) 即可,正则就变成了 a*?

对比 /'.+'//'.+?'/ 的区别

 ("'Hello Tom', 'Welcome to China'").match(/'.+'/) // ["'Hello Tom', 'Welcome to China'"]
 ("'Hello Tom', 'Welcome to China'").match(/'.+?'/) // ["'Hello Tom'"]

5.3独占模式匹配(JS不支持

独占模式虽然在 Javascript 中不支持,但对理解贪婪模式和非贪婪模式匹配却很有帮助,所以也在这里进行讲解。

不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。但是在一些场景下,我们不需要回溯,匹配不上返回失败就好了,因此正则中还有另外一种模式,独占模式,它类似贪婪匹配,但匹配过程不会发生回溯,因此在一些场合下性能会更好。

你可能会问,那什么是回溯呢?我们来看一些例子

  • 例如下面贪婪模式下的正则:

     regex = /xy{1,3}z/
     text = "xyyz"
    

    在匹配时,y{1,3} 会尽可能长地去匹配,当匹配完 xyy 后,由于 y 要尽可能匹配最长,即三个,但字符串中后面是个 z 就会导致匹配不上,这时候正则就会 向前回溯,吐出当前字符 z,接着用正则中的 z 去匹配。

  • 如果我们把这个正则改成 非贪婪模式

     regex = /xy{1,3}?z/
     text = "xyyz"
    

    由于 y{1,3}? 代表匹配 1 到 3 个 y,尽可能少地匹配。匹配上一个 y 之后,也就是在匹配上 text 中的 xy 后,正则会使用 z 和 text 中的 xy 后面的 y 比较,发现正则 z 和 y 不匹配,这时正则就会 向前回溯,重新查看 y 匹配两个的情况,匹配上正则中的 xyy,然后再用 z 去匹配 text 中的 z,匹配成功。

我们比对理解了贪婪模式非贪婪模式,那么如何理解独占模式呢?它和贪婪模式很像,独占模式会尽可能多地去匹配,但如果匹配失败就结束,不会进行回溯,这样的话就比较节省时间。

 // 贪婪模式
 (/xy{1,3}z/).test('xyyz') // true
 ​
 // 非贪婪模式
 (/xy{1,3}?z/).test('xyyz') // true
 ​
 // 独占模式, JS不支持, 下面是伪代码 (实际运行会报语法错误)
 (/xy{1,3}+z/).test('xyyz') // false

面试题

请写出一个正则来处理数字千分位,如12345替换为12,345

 '123456789'.replace(/\B(?=(\d{3})+$)/g, ',')

应用

//前单引号,左边是空格,限制前缀
String preSingleQuotation = "(?<=\s)'";
//后单引号,右边是空格,限制后缀
String postSingleQuotation = "'(?=\s)";
String insertSql = "insert into table_name  values ( '1001' , '张飞' , '男' )";
String str = Pattern.compile(preSingleQuotation).matcher(insertSql).replaceAll("'''||'");
String result = Pattern.compile(postSingleQuotation).matcher(str).replaceAll("'||'''");
System.out.println(result);
//转换后:insert into table_name  values ( '''||'1001'||''' , '''||'张飞'||''' , '''||''||''' )