正则表达式使用说明书

188 阅读16分钟

正则表达式(Regular Expression,RE):描述一组字符串特征的模式,用来匹配特定的字符串。

正则表达式简单来说就是在整个字符串中找符合某种模式子字符串,而这种模式就是由正则表达式定义。

正则表达式可以验证文本格式,如 Java:

import java.util.regex.Pattern;
Pattern pattern = Pattern.compile("正则表达式");
Matcher matcher = pattern.matcher("输入的字符串文本");
if (matcher.matches()) {
    // 匹配成功
} else {
    // 匹配失败
}

3.

可以提取文本信息:

// 循环遍历匹配结果
while (matcher.find()) {
    String matchedText = matcher.group(); // 获取整个匹配到的字符串
}

可以替换文本

String replacedText = matcher.replaceAll("replacement_text"); // 将匹配到的结果替换为指定文本

...

无论如何,正则表达式就是用来做匹配的,闲话少说,直接急速入门:

在线正则验证网站 regex101

ChatGPT

单字符匹配

字面量匹配

所谓字面值,就是字面上看起来是什么就是什么。

如下,正则表达式直接使用字符字面量:a,它将匹配原始文本里中的字符:a

image.png

绝大多数正则表达式引擎的默认行为是只返回第1个匹配结果,但是绝大多数正则表达式的实现都提供了一种能够把所有的匹配结果全部找出来的机制(通常返回为一个数组)。

比如说,在 Java 里,可选的g(global,全局)标志将返回一个包含着所有匹配的结果数组:

image.png

Unicode字符集匹配:\u

计算机能处理的数据只有二进制(01),字符也不例外,至于具体某个二进制表示哪个字符,则由编码集所规范。

比如对英语的编码的编码集 ASCII ,一个字符就用一个字节(8位)长度的二进制进行编码。如:

0000 0001 (十六进制:0x01) 表示 字符 a;

0000 0010 (十六进制:0x02)表示 字符 b;

0000 0011 (十六进制:0x03) 表示 字符 A;

0000 0100 (十六进制:0x04) 表示 字符 B;

⨳ ...

英语就26个字母,算上大小写也就 52,再算上一些标点符号、运算符号,也不会太多。一个字节(8位二进制)有 256 种不同的组合方式呢,表示 这些个字母和符号绰绰有余。比如:

0x30 ~ 0x39 表示 数字 0 ~ 9

0x41 ~ 0x5A 表示 大写字母 A ~ Z

0x61 ~ 0x7A 表示 小写字母 a ~ z

事实上,ASCII 只用到了 0x00 ~ 0x7F 范围的组合,用来表示 128 个字符。

ASCII 码只能表示英文,其他国家的人怎么办?一个字节的组合太少,就多用几个字节。下面列举一下常见的字符集:

latin1(ISO 8859-1):1 字节定长编码,在ASCII字符集的基础上又扩充了128个西欧常用字符(包括德法两国的字母)

GB23121~2字节编码,收录了汉字以及拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母。其中收录汉字 6763 个,其他文字符号 682 个。兼容 ASCII 字符集,对于在 ASCII 码里面的字符还是使用 1 字节编码,其他就是 2 字节编码。

GBK1~2字节编码,在收录字符范围上对 GB2312 字符集作了扩充,编码方式上兼容 GB2312

⨳ ...

总而言之,编码集是人为规定的二进制与字符之间的映射关系,某个一字符使用什么编码集编码成计算机能看懂的二进制,就需要使用相同的编码集将二进制解码成人类能看懂的字符。编码与解码要用同一套编码集,这样信息传递才能准确无误。

如果每个国家每种语言都搞一个自己的编码集,在自己国内交流没啥问题,那国际之间的交流就乱套了。

比如你发我一份latin1编码的文档,我收到后用GBK解码,发现都是乱码,我是中国人,用的中国机器,不用外国货,解码不了怎么办。

这时,Unicode标准 横空出世,Unicode(统一码) 为全球范围内的文字提供一个统一的编码方案。Unicode24个字节编码,旨在收录地球上能想到的所有字符,而且还在不断扩充。

我们现在常用的 UTF-8 就是 Unicode 字符集的一种编码方案,使用1~4个字节编码一个字符,英文还是采用 ASCII 进行一个字节编码,汉字是三个字节。比如:

⨳ 兼容ASCII码:0x00 \~ 0x7F

⨳ 易经六十四卦符号:0x4DC0 \~ 0x4DFF

⨳ 中日朝统一表意文字:0x4E00 \~ 0x9F15

⨳ ...

闲话少说,现在我想使用正则匹配单个字符,而字符是由编码集编码的,那是不是可以直接使用这个字符原本的样子(字节形式)进行匹配呢?

答案是可以的。

比如 aUnicode 字符集中对应字节 0x0061,那我直接就可以 0x0061 的形式进行匹配:

image.png

符号 \ 被称为转义符,转义符后面的字符不再表示对应的字面量,而转成了别的意思,这里表示\u后面的十六进制不再是字面量字符而是这个十六进制对应的字符。

比如,汉字的“”在 Unicode 字符集中对应字节 0x6c49,那我直接就可以 0x6c49 的形式进行匹配:

image.png

我直接可以使用字符就能匹配的,为啥要用字节的形式进行匹配呢?

image.png

这是因为谁知道你写的文档是什么编码方法,如程序中的正则表达式是 Unicode 编码,你写的文档是 GBK编码,虽然看起来都是一个字,但本质有很大不同,这就会导致匹配不上,或错误匹配,所以在使用时,将正则表达式和匹配文本都指定 Unicode 编码是个很好得习惯。

编码统一是一方面,还有更重要的字符组匹配

任意字符匹配:字符组 [ ]

字符组:也被称为方括号表达式(bracketed expression),字符组有助于匹配特定字符或者特定的字符序列

特定的字符

image.png

如上,当字符串字面量 aeiou 用中括号 [] 括起来后,匹配的不是连续的字符串 aeiou,而是匹配单字符 a 或 单字符 e或 单字符 i... 。

注意,要将g(global,全局)标志打开,否则只会匹配一个结果

中括号[] 代表多选一,正则运算会遍历整个测试文本的每个字符,如果当前字符在中括号中,则符合匹配结果。

再比如匹配数字:

image.png

注意字符组匹配是单个字符匹配,虽然数字 18 也是一个数字,但是上述匹配结果是字符 1 和字符 8 ,共两个匹配结果。

特定的字符序列

字符序列是字符组的简写形式,用来匹配某个范围(按 Unicode 排序)的字符。如匹配数字的方括号表达式为[0123456789],可以简写成字符序列的形式:[0-9]

image.png

中括号中的 0-9 不再是匹配 字符 0、字符 - 和 字符 9

常见的字符序列如下:

[0-9] 匹配数字:0123456789

[3-6] 匹配数字:3456

[a-z] 匹配小写字母:abcd .... xyz

[A-Z] 匹配大写字母:ABCD .... XYZ

[ -~] 匹配空格 ~ 范围内的字符,这也对应 ASCII码中所有可见字符。

[\u4E00-\u9F15] 匹配 0x4E000x9F15 范围内的字符,这也是汉字所在范围。0x4E00 代表汉字“” ,0x9F15 代表汉字“” 。(这就是为什么提前介绍编码集的原因)

注意,字符序列一定要避免让这这个区间的尾字符编码数小于它的首字符编码数,如[5-2] 这种区间是没有意义的。只要首编码数大于尾编码数,汉字区间也是可以使用的:

image.png

字符组集合运算

取反

可以使用取反运算符^,匹配方括号中任意单个字符之外的单个字符。

image.png 文本中的除了数字字符 18 ,其他字符(包括字母、汉字、空格、逗号、句号...)都被匹配到了。

并集

在同一字符组中可以给出多个字符区间,紧贴着写在一起就可以了。

image.png

如上,[A-Za-z0-9] 匹配大小写字母或数字:A B C ... X Y Za b c d e ... x y z 0 1 2 ... 9

元字符简介

为了后续理解方便,这里先提一嘴元字符这个概念。

元字符是在正则表达式中有特殊含义的字符,也是保留字符。

元字符的对立面是字面量,字面量是看上去是什么就是什么,元字符是看上去是什么但不是什么,比如字符组匹配中的中括号也属于元字符,看上去 [012] 是用来匹配字符串 “[012]”的,但实际上是匹配字符 0 或字符 1 或字符 2

可以使用转义符 \ ,将有特殊功能的元字符转成普通字符字面量。

image.png

也可以使用转义符\,将普通的字面量转成有特殊含义的元字符.

元字符Unicode 码匹配内容
\t\u0009水平制表符
\n\u000A换行符 LF
\v\u000B垂直制表符 VT
\f\u000C换页符 FF
\r\u000D回车键CR

不同的系统在每行文本结束位置默认的“换行”会有区别, 比如在 Windows 里是 \r\n,在 Linux 和 MacOS 中是 \n

元字符可以代表对字符组匹配的简写形式:

元字符字符组匹配内容
\d[0-9]任意数字
\D[^0-9]任意非数字
\w[a-zA-Z0-9_]任意字母数字下换线
\W[^a-zA-Z0-9_]任意非字母数字下换线
\s[\r\n\t\f\v ]任意空白符,包括空格
\S[^\r\n\t\f\v ]任意非空白符
.[^\n]任意除了换行符以外的字符

凡是看起来不是它字面意思的字符都是元字符,如字符组 [],如字符组取反符 ^,如管道符 |,如量词 {m,n}...

学习正则表达式,大体来说就是认识元字符到底代表什么意思。

字符串匹配

字面值匹配

和字符字面量一样,字符串字面量所写即所得,写什么就匹配什么。

image.png

任意字符串匹配:管道符 |

和字符组可以匹配中括号中的任意单个字符一样,可以使用管道符号 | 来隔开多个正则(注意是隔开多个正则),表示满足其中任意一个正则就行。

image.png

管道符号 | 不仅可以隔开多个正则,还可以与分组运算符小括号配合,区分多个字符串。

image.png

用分组括号括起来表示一个整体,这是括号的一个重要功能,my (name|age) 相当于 my name|my age

image.png

重复匹配:量词 {m,n}

量词:也称为大括号表达式,用量词修饰单个字符或字符串可以实现重复字符串的效果。

{m,n} 表示出现 mn,前闭后闭,包括 m 次或 n 次;

{m} 表示出现正好 m 次,相当于 {m,m};

{m,} 表示出现至少 m 次。

注意,量词默认修饰的是前面的单个字符,如 a{2} 相当于 aaba{2} 相当于 baa,并不是 baba,如量词前面的是分组运算符小括号,那修饰的就是小括号里面的字符串。

image.png

a{3,6} 相当于aaaaaa|aaaaa|aaaa|aaa,就是先匹配 6个a6个a匹配完了再匹配5个a5个a 匹配完了再匹配 4个a4个a 匹配完了再匹配 3个a

为什么数量大的在前面,这涉及到正则的匹配行为,正则默认使用贪婪模式进行匹配,即尽量多地匹配字符,与此相对的是非贪婪匹配,尽可能少地匹配字符。这里简单了解一下即可,后续讲正则匹配原理时,再详细谈谈。

大括号表达式也有元字符的简写形式:

量词大括号表达式含义
*{0,}出现至少0次,0到多次
+{1,}出现至少1次
?{0,1}出现0到1次

你可能有这样的疑惑,量词前边的字符(串)出现 1 次,2 次,100次都很好理解,出现 0 次是什么意思? 比如说 a{0} 匹配的是什么?

image.png

可以看到对于有 26个字符的文本, a{0} 匹配的是 27 个空字符串,这些空字符串存在于字符与字符之间,文本第一个字符前,和文本最后一个字符后,这些没有实际字符存在的地方。

{0,1} 的代替元字符 ? 可以看出这是个疑问,有还是没有?存在或不存在?

那匹配空字符串有啥用呢?

单个字符(串)加上量词 {0} 当然没啥意义,但如果和其他匹配规则合在一起就有用了,如“颜色”的英语单词,在英国拼写是"colour",在美国拼写是"color",那怎么匹配这个颜色呢?

当然可以使用管道符写两个正则,colour|colorcolo(u|)r,使用量词可以更优雅的解决这个问题。

image.png

同样的,https?://将匹配 http:// https://

除此之外,有两个元字符 ^$ ,分别匹配文本第一个字符前,和文本最后一个字符后的空字符串:

image.png

重复匹配:分组引用 ()

分组引用的前提是分组,格式为用分组运算符小括号()括起来的部分。

小括号()括起来的部分也称子表达式,就是一个更大的表达式的一部分。把一个表达式划分为一系列子表达式的目的是为了把那些子表达式当作一个独立元素来使用。

前边讲过,管道符号 | 可以隔开多个正则,还可以与分组运算符小括号配合,区分多个字符串。如 my (name|age) 相当于 my name|my age

前边也讲过,默认来说量词只修饰其前边的单个字符,如果量词前面的是分组运算符小括号,那修饰的就是小括号里面的字符串。如(ba){2} 相当于 baba;

总而言之,分组运算符小括号的目的就是划分子表达式,从而将其看作是一个整体。

子表达式就是子表达式呗,小括号括起来的就是子表达式,可以看作一个整体,这我上小学的时候就知道了,干嘛将小括号称为分组运算符,徒增理解难度。

这就要讲一下子表达式最重要的一个用途了:分组引用

被括号括起来的部分“子表达式”会被保存成一个子组,以便后续正则表达式中或程序中可以使用分组编号来引用这个分组匹配的字符串。

分组编号:从左往右数,第几个左括号 ( ,那这个左括号 ( 与其对应的右括号里面的内容就是编号几。如:

正则(编号1)正则(编号1)正则(编号2)正则(编号1(编号2)(编号3(编号4)正则))正则....

当然也可以自定义编号名,格式为(?P<分组名>正则)

有了分组的编号后,就可以在后续正则中对其进行引用了:一般使用 \编号 的方式来进行引用,如 Java 和 Python,也有的语言使用 $编号 来引用,如 JavaScript。

分组引用怎么用呢?先从简单的例子开始:

(abc)\1 相当于 abcabc,靠!我还不如写 (abc){2} 呢。

分组引用在字面量的字符匹配上用处不大,看一下字符组(任意字符匹配)上的应用:

image.png

如上, (\w+) 可以匹配任意单词,后面加上 指这个单词后空格,后面再加上 \1 是对 (\w+) 的引用。这里的 \1 就不是固定的字面量了, (\w+) 匹配到什么单词 \1 就是什么单词。

整个正则合起来就是匹配两个连续相同的单词。这个单词并不是使用字面量写死的,分组匹配到啥,引用就是啥。

下面看一下在代码中,怎么使用分组引用的,以 Java 为例:

// 编译正则表达式模式,匹配日期格式 "MM-DD-YYYY"
final Pattern pattern = Pattern.compile("(\d{2})-(\d{2})-(\d{4})"); 
// 创建 Matcher 对象,匹配输入字符串
Matcher match = pattern.matcher("02-20-2020 05-21-2020"); 
// 替换匹配到的日期格式为 "YYYY年MM月DD日" 的形式并输出
System.out.println(match.replaceAll("$3年$1月$2日")); 

如果仅仅是为了将子表达式看作一个整体而不是分组引用,可以在小括号中加上 ?:,即 (?:正则)

总结一下,分组引用的可以实现重复匹配,但和量词的重复匹配不一样,量词重复的是正则本身,分组重复的是匹配结果。

位置匹配

啥叫位置匹配呢,量词的时候说过元字符 ^$ ,分别匹配文本第一个字符前,和文本最后一个字符后的空字符串,这就是位置匹配。

位置匹配也叫断言(Assertion),断言匹配到的文本只有在指定位置才符合要求。

除了字符串文本起始位置外,正则表达式还可以定位更多的位置,下面来看一下:

单词边界 \b

先看一下 \b 可以定位哪些空字符串:

image.png

可以看到连续的大小写字母、下划线和数字([A-Za-z0-9_]{1,},\w+)就算一个单词,而单词两边的空字符串就是单词的边界。

那汉字单词的边界在哪呢?

image.png

可以看到,汉字单词也是以连续的大小写字母、下划线、数字和汉字为界,并不是语义上的词语。

\B 是对 \b 的取反,即处单词边界之外的空字符串。

行的边界 ^ 和 $

^$ 不是整个字符串文本的边界吗,咋表示行的边界呢?难道是限制匹配文本字符串没有换行符?

只要开启多行模式就可以了:

image.png

注意图中,增加了一个正则修饰符 m,原来是 g,表示 global,全局匹配的意思,现在是 gm,在全局匹配的基础上增加了 multi line 模式。

有些正则表达式实现还支持使用\A 来定位一个字符串的开始,以 \Z 来定位一个字符串的结束,职责更清晰。

自定义边界 (?=自定义规则)

自定义边界,专业术语叫环视(Look Around),类似行首与行尾,单词的左右边界,环视也分左右:

⨳ 左环视,即左边边界符合自定义规则才行,语法为 (?<=自定义规则)

⨳ 右环视,即左边边界符合自定义规则才行,语法为 (?=自定义规则)

左右环视中也可以对自定义规则取反,只需要将等号 = 换成 感叹号 ! 即可。注意不要子表达式(?=自定义规则)混淆。

举个例子,原始文本是一些URL地址,先需要把它们的协议名部分提取出来:

http://www.cango.com
https://www.cango.com
ftp://ftp.cango.com

要取协议名(如 httphttpsftp),而这些协议自身并没有什么可以提出来的共同点,不好直接写正则,就用它的位置定位,协议一般都在字符串 :// 前边,那直接就拿 :// 进行定位。

协议名都是英文字母([A-Za-z]),数量大于1吧({1,}),必须在字符串 :// 前边((?=://)),就这样一个正则表达式就出来了:

image.png

单词边界也可以使用环视,如单词的正则为 [A-Za-z0-9_]{1,}\w+),那单词的边界就是 \W,合在一起就是 (?<=\W)\w+(?=\W)

总结一下,位置匹配仅仅是限制要匹配子字符串所在位置,位置本身处的字符串并不会被匹配出来,就像 [A-Za-z]{1,}(?=://) 并不会将 :// 匹配出来。

正则修饰符

修饰符可以修改匹配的行为。比如前边提到的:

g (global,全局匹配) :匹配目标字符串中所有符合条件的结果,而不是仅匹配第一个。

m (multi line,多行匹配) :使得 ^ 和 $ 分别匹配行的开头和结尾,而不仅仅是整个字符串的开头和结尾。

i (ignore case,忽略大小写) :匹配不区分大小写。

image.png

s (single line,单行模式) :改变元字符 . 的匹配字符,使得其可以匹配任何字符,包括换行符 \n

练习

知道了正则表达式怎么表示字符(串),怎么分组,那就写写练练手喽。

手机号码匹配

有些网站需要的手机号规则如下:

⨳ 第 1 位固定为数字 1;

⨳ 第 2 位可能是 3,4,5,6,7,8,9;

⨳ 第 3 位到第 11 位我们认为可能是 0-9 任意数字。

最简单的写法,一个一个字符进行匹配:

1[3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9]

使用量词简化重复的规则:

1[3,4,5,6,7,8,9][0,1,2,3,4,5,6,7,8,9]{9}

使用字符序列组简化连续的数字字符:

1[3-9][0-9]{9}

使用数字元字符代替连续的数字字符:

1[3-9]\d{9}

到此就好了吗?别忘了,正则是匹配字符串文本的子字符串,只要有一个子字符串满足条件也是满足条件:

image.png

所以这里需要加上元字符 ^$ ,用来匹配文本第一个字符前,和文本最后一个字符后的空字符串。

^1[3-9]\d{9}$

至此,手机号匹配正则表达式完成!

密码校验

有些网站需要的密码规则如下:

⨳ 密码必须包含 1 个数字

⨳ 密码必须包含 1 个小写字母

⨳ 密码必须包含 1 个大写字母

⨳ 密码必须包含 1 个除了字母和数字之外的特殊字符(non-alpha numeric number)

⨳ 密码必须 8-16 位没有空格的字符

这咋写?最简单的写法就是将根据每条规则写一个正则表达式,然后在程序中组合起来。

密码必须包含数字就是 \d,必须包含大写字母是 [A-Z],必须包含小写字母是 [a-z],除了字母、数字和空格之外的是 [^\w\d\s],8-16 没有空格的字符为 ([^\s]){8,16}

可以看到我只想要8-16 位没有空格的字符:

^([^\s]){8,16}$

其他的只是判断条件,更一步讲,就是位置限定条件。使用自定义边界,在文本起始位置右环视一下行。

^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^\w\d\s])([^\s]){8,16}$

这里的练习是在正则101中挑的简单的,更多示例可以去正则101上玩玩。