正则表达式
1.速查表
多个字符
| 匹配区间 | 正则表达式 | 记忆方式 |
|---|---|---|
| 除了换行符之外的任何字符 | . | |
| 单个数字, [0-9] | \d | digit |
| 除了[0-9] | \D | not digit |
| 包括下划线在内的单个字符,[A-Za-z0-9_] | \w | word |
| 非字母数字下划线 | \W | not word |
| 匹配空白字符,包括空格、制表符、换页符和换行符 | \s | space |
| 匹配非空白字符 | \S | not space |
| ^ | 表示匹配字符串的开始位置 (中括号中[] 时表示取反) | |
|---|---|---|
| $ | 表示匹配字符串的结束位置 | |
| * | 表示匹配 零次到多次 | |
| ? | 表示匹配零次或一次 | |
| + | 表示匹配 一次到多次 (至少有一次) | |
| 表示为或者,两项中取一项 | ||
| () | 小括号表示匹配括号中全部字符 | |
| [] | 中括号表示匹配括号中一个字符 范围描述 如[0-9 a-z A-Z] | |
| {} | 大括号用于限定匹配次数 如 {n}表示匹配n个字符 {n,}表示至少匹配n个字符 {n,m}表示至少n,最多m | |
| \ | 转义字符 如上基本符号匹配都需要转义字符 如 * 表示匹配*号 |
常用字符
| 正则表达式 | 含义 |
|---|---|
| [A-Za-z0-9] | 字母和数字 |
特殊字符
| 特殊字符 | 正则表达式 | 记忆方式 |
|---|---|---|
| 换行符 | \n | new line |
| 换页符 | \f | form feed |
| 回车符 | \r | return |
| 空白符 | \s | space |
| 制表符 | \t | tab |
| 垂直制表符 | \v | vertical tab |
| 回退符 | [\b] | backspace,之所以使用[]符号是避免和\b重复 |
2.循环与重复
要实现多个字符的匹配我们只要多次循环,重复使用我们的之前的正则规则就可以了。那么根据循环次数的多与少,我们可以分为0次,1次,多次,特定次。
0 | 1
元字符?代表了匹配一个字符或0个字符。设想一下,如果你要匹配color和colour这两个单词,就需要同时保证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/这个正则,就会同时匹配到cat和scattered这两处文本。这时候我们就需要使用边界正则表达式\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。前者的意思是忽略大小写,后者的意思是找到所有符合的匹配。
最后,总结一下:
| 边界和标志 | 正则表达式 | 记忆方式 |
|---|---|---|
| 单词边界 | \b | boundary |
| 非单词边界 | \B | not 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)是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: apple和people都包含ple这个后缀,那么如果我只想找到apple的ple,该怎么做呢?我们可以通过限制app这个前缀,就能唯一确定ple这个单词了。
/(?<=ap)ple/
其中(?<=regex)的语法就是我们这里要介绍的后向查找。regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向后查找。另外一种限制匹配是利用(?<!regex) 语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找apple的ple也可以这么写成/(?<!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 和 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 次 | 0 | 3 | 到第一个字母b发现不满足,输出 aaa | aaa |
| 第 2 次 | 3 | 3 | 匹配剩下b发现匹配不上,输出空字符串 | 空字符串 |
| 第 3 次 | 4 | 4 | 匹配剩下b发现匹配不上,输出空字符串 | 空字符串 |
| 第 4 次 | 5 | 5 | 匹配剩下空字符串,输出空字符串 | 空字符串 |
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'||''' , '''||'张飞'||''' , '''||'男'||''' )