《正则表达式必知必会》读书笔记
Posted 二木成林
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《正则表达式必知必会》读书笔记相关的知识,希望对你有一定的参考价值。
注:《正则表达式必知必会》这本书很适合入门正则表达式,比网上很多教程都适合。其实正则表达式主要学习的就是各种元字符的使用,最后搭配起来就是一个完整的正则表达式。前8章重点学习掌握;第9章也需要掌握,但上讲解的不是很明白,可以搜索网上相关教程学习,最好是多写几个例子练习;第10章在实际工作都使用较少,而且较难,所以了解即可。
第1章 正则表达式入门
正则表达式是什么:正则表达式是一个字符串,由一些特殊字符组成的字符串,能用来匹配文本内容。主流编程语言如Java、python、javascript都支持正则表达式。
正则表达式有什么用:查找、替换、校验。
- 查找:在指定的文本内容找出需要的信息,例如从一段html代码中找出a标签的href属性的值。
- 替换:将查找到的内容替换成指定的内容,如将一段文本中所有的数字替换成字符串’xxxx"。
- 校验:通常在前端的表单中就需要校验用户输入的信息,如输入的手机号是否是全是数字并且是11位,邮箱的格式是否正确,使用正则表达式来进行校验非常简单。
第2章 匹配单个字符
2.1 匹配纯文本
正则表达式当然可以是纯文本,如"hello"、"123"这样的文本内容。正则表达式可以包含纯文本,甚至可以只包含纯文本。
- 注意:匹配结果可能有多个。通常在文本中被正则表达式匹配的结果可能有多个,绝大多数正则表达式引擎默认只返回第一个匹配结果,如果要返回多个匹配结果,大多数编程语言都提供了相关实现能够一次将所有的匹配结果都返回回来。例如:Java的实现。
@Test
public void test0302(){
String regex="my";
String text="Hello, my name is Ben. Please visit my website at http://www.forta.com/.";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println(matcher.group());
}
}
- 注意:字母大小写问题。正则表达式是区分大小写的,如"my"不能匹配到"My",但是大部分编程语言可以让它忽略大小写,即不区分大小写进行匹配。如JavaScript可以使用
i
修饰符来忽略大小写。Java可以在使用Pattern.compile(String regex, int flags)
方法时通过第二个参数flags
来设定修饰符从而忽略大小写。
2.2 匹配任意字符
在正则表达式中规定了.
(英文句号)可以匹配任何一个单个字符。如所有的大写字母、小写字母、数字、标点符号等。注:其中.
相当于SQL中的_
(下划线)字符。
如m.
就可以匹配"my"、“me”、“m/”、"m."等结果,因为.
表示匹配任意一个单个字符。
总结:.
字符可以匹配任何单个字符、字母、数字甚至是.
字符本身。
注意:
- 在同一个正则表达式中允许使用多个
.
字符,它们既可以连续出现(如c..
中的..
表示匹配任意两个字符,如cat
、c12
、cb1
等),也可以间隔着出现在不同位置(如h.ll.
可以匹配hello
、h1ll3
、h.ll.
等)。
2.3 匹配特殊字符
在本节中.
是特殊字符,后面还会学到*
、$
、^
等都是特殊字符,这些特殊字符可以类比于编程语言中的关键字。
如果我们要匹配特殊字符如.
本身而不是它在正则表达式中所代表的特殊含义,那么需要在特殊字符如.
前面添加一个反斜杠\\
字符来对它进行转义。
例如我们有这样一个文本"abc-log abc.log abc_log abcdlog"
,要匹配里面的"abc.log"
,如果我们使用的正则表达式是abc.log
,毫无疑问它会匹配到所有的单词,因为在正则表达式.
字符表示可以匹配任意字符,而-
、.
、_
、d
字符都包括在内,那么我们要匹配到.
字符本身就需要用到转义字符\\
,所以将正则表达式改成这样abc\\.
就可以成功只匹配到"abc.log"
了。
注意:
- 同样的,如果要匹配
\\
字符本身,也需要对\\
字符进行转义,即\\\\
。如我们有这一个文本"This path is C:\\Users\\Public."
,那么我们要匹配里面的路径通过这样的正则表达式C:\\Users\\Public
无疑是不能成功的,因为并没有匹配到\\
字符本身,所以要改成这样C:\\\\Users\\\\Public
,对\\
字符进行转义。 - 在大多数编程语言实现的正则表达式中
.
严格来说只能匹配除换行符以外的任何单个字符。
第3章 匹配一组字符
3.1 匹配多个字符中的某一个
举例:如我们要匹配一个11位的手机号,手机号的第二位只能是3或5或7或8,那么正则表达式该如何写呢?
那么只需要使用[]
来构成字符集合,里面可以存放任何字符,它们之间的单个字符是或
的关系。如[abc]表示要么是a要么是b要么是c;如[ta]om
可以匹配"tom"
或"aom"
;如[123]456
可以匹配"1456"
或"2456"
或"3456"
。
注意:
[
和]
不匹配任何字符,只负责定义一个字符集合。- 字符集合在不需要区分大小写或者只需要匹配某个特定部分的搜索操作比较常见,如
[Rr]eg[Ee]x
、1[3578]
等。
3.2 利用字符集合区间
举例:如果我们要匹配26个小写字母字符中的任何一个,那么用字符集合[]
来写就是这样:[abcdefghijklmnopqrstuvwxyz]
,或者匹配任何一个数字则字符集合的写法是:[0123456789]
。是不是太长了,每次如果都要写这么长一串,很麻烦?
所以在使用正则表达式时会用到字符集合区间,着重在区间上。正则表达式提供了一个特殊的元字符-
来定义。如[0-9]
表示任意一个数字,完全等价于[0123456789]
。合法的字符区间如下:
[A-Z]
:匹配A到Z的所有大写字母。[a-z]
:匹配a到z的所有小写字母。[A-F]
:匹配A到F的所有大写字母。这里是举例,也就是说区间的边界A和F是可以根据需要更换的。[A-z]
:匹配从ASCII字符A到ASCII码字符z的所有字符,但该字符区间不常用,因为它除了包含大写字母字符和小写字母字符之外,还包括了[
和^
等在ASCII码表中排列在z和a之间的字符。[0-9]
:匹配所有的数字。
注意:
[abcd123]
这样被[
和]
符号包裹起来并且没有-
符号的叫做字符集合;[0-9]
这样被[
和]
符号包裹起来并且有-
符号的叫做字符集合里的字符区间。- 字符区间
[]
的首、尾字符可以是ASCII码表中的任意字符,但实际工作中最常用的还是数字字符区间和字母字符区间。 - 在定义一个字符区间的适合,一定要避免这个区间的尾字符小于它的首字符(如
[3-1]
),因为这种区间是没有意义的,而且会让整个正则表达式失效。 - 连字符
-
是一个特殊的元字符,只能用在[
和]
之间。如果在字符集合之外的地方(如ab-cd
)则只表示一个普通的字符-
,只能与-
本身相匹配。因此,在正则表达式中,-
字符不需要被转义。 - 在同一个字符集合里可以有多个字符区间。如
[A-Za-z0-9]
可以匹配任何一个大写或小写字母或数字,但除此之外的其他字符(既不是字母也不是数字的字符)都不匹配,并且是[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]
的简写形式,它们完全等价。如#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]
可以匹配十六进制的RGB值(如白色#FFFFFF
、红色#FF0000
等)。
3.3 取非匹配
举例:字符集合通常用来指定一组必须匹配其中之一的字符,对应的现实中我们可以说必须是"张三"、“李四”、“王五"这三个同学任何一个去打扫卫生,而取非就是除了"张三”、“李四”、"王五"这三个同学之外的任何一个同学去打扫卫生。前者是包含在内的,后者是排除在外的。对应到正则表达式的字符集合中就是除了指定字符集合里的字符,其他字符都可以匹配。
用元字符^
就可以达到对字符集合进行取非的目的,如[^abc]
表示可以匹配除了"a"或"b"或"c"这三个字符之外的所有其他字符;如[^0-9]
表示可以匹配除了数字字符之外的任何一个字符,而[0-9]
是只匹配数字。
注意:
^
的效果是作用于给定字符集合的里的所有字符或字符区间,而不是仅限于跟在^
字符后面的那一个字符或字符区间。如[^abcd]
是指排除"a"或"b"或"c"或"d"而不只是"a";如[^0-9a-zA-Z]
表示排除所有的数字字符和字母字符而不仅仅是跟在^
字符后面的数字字符区间。
第4章 使用元字符
4.1 对特殊字符进行转义
元字符:即正则表达式中带有特殊含义的字符,如.
、[
等,类比于编程语言中的关键字,因为关键字属于编程语言所以无法作为变量名,同样正则表达式这些元字符也不能代表它们本身(即不能使用.
字符来匹配.
字符,而不是能匹配任何字符)。
如果想要元字符代表它们本身的含义,可以给元字符添加一个反斜杠\\
字符作为前缀来进行转义,如.
转义应该是\\.
、[
转义\\[
等。
如我们有这样一个文本"nums[0]=1;nums[1]=2;nums[2]=3;"
,如果要匹配里面的"nums[0]=1;"
,那么可能我们会把正则表达式写成nums[0]=1;
,但毫无疑问是无法匹配成功的,因为在正则表达式中[0]
表示匹配0
字符,而没有匹配到"["
和"]"
字符,要匹配到它们,必须进行转义,即nums\\[0\\]=1;
。如果要匹配到上面的所有结果,正则表达式应该是:nums\\[[0-9]\\]=[0-9];
。
注意:
- 反斜杠
\\
也是元字符,如果如果要匹配"\\"
字符,必须进行转义,也就是写两根反斜杠\\\\
。如一段表示路径的文本\\home\\apache\\bin\\startup.sh
如果要成功匹配则正则表达式应该是:\\\\home\\\\apache\\\\bin\\\\startup.sh
。 - 所以在一个完整的正则表达式中,字符
\\
后面永远跟着另一个字符。
4.2 匹配空白字符
空白字符:实际存在却又肉眼看不见且无法被打印出来的一些特殊字符,如换行符、制表符等。如果想要看到这些空白字符可以使用Nodepad++软件打开文本文件,菜单栏选择"视图"——>“显示符号”——>“显示所有字符”。
在进行正则表达式搜索的适合,可能需要对原始文本中的非打印空白字符进行匹配的情况,如找出所有的制表符、换行符。但实际上这类字符很难被直接输入到正则表达式中,但可以通过下表列出的特殊元字符来输入它们:
元字符 | 说明 |
---|---|
[\\b] | 回退(并删除)一个字符(Backspace键) |
\\f | 换页符 |
\\n | 换行符 |
\\r | 回车符 |
\\t | 制表符(Tab键) |
\\v | 垂直制表符 |
例如有一个文本文件中的内容如下:
abc
123
def
456
要求我们删除空白行,那么可以使用正则表达式\\r\\n\\r\\n
来进行处理。\\r\\n
匹配一个"回车+换行"组合,在许多操作系统如Windows把这个组合作为文本行的结束标签,可以通过Nodepad++清晰看到。而使用正则表达式\\r\\n\\r\\n
进行的搜索将匹配两个连续的行尾标签,那正是两条记录之间的空白行。
注意:
\\r\\n
是Windows所使用的文本行结束标签。Unix和Linux系统只使用一个换行符来结束文本行,即在Unix或Linux系统上匹配空白行只需要使用\\n\\n
即可,不需要加上\\r
。如果要同时使用Windows和Unix或Linux系统的正则表达式应该包含一个可选的\\r
和一个必须被匹配的\\n
,即\\r?\\n\\r?\\n
。- 需要注意
.
和[
如果要在正则表达式中是元字符则不能进行转义,而f
和n
也是元字符,但必须进行转义才表示是元字符而非普通字符。
4.3 匹配特定的字符类别
字符类别(类字符):一些常用的字符集合可以用特殊的元字符来替代,这些元字符匹配的是某一类别的字符,所以称之为字符类别。但这并不是必须的,可以有其他方式代替。如字符集合[0123456789]
可以被字符集合区间[0-9]
代替,而字符集合区间[0-9]
可以被数字元字符\\d
代替。
4.3.1 匹配数字与非数字
数字元字符:
元字符 | 说明 |
---|---|
\\d | 任何一个数字字符,等价于[0-9] |
\\D | 任何一个非数字字符,等价于[^0-9] |
注意:
- 在面对用正则表达式解决问题时,通常一个问题可能有多种解法,并无优劣之分,看熟悉哪种语法就使用那种语法。
- 正则表达式的语法是严格区分大小写的,
\\d
匹配数字,而\\D
匹配非数字。
4.3.2 匹配字母和数字与非字母和数字
字母和数字,A到Z(不区分大小写)、数字0到9,加上下划线字符_
是另一种比较常用的字符集合。常见于各种命名中,如文件名、子目录名、变量名、数据库对象名。下表列出了用来匹配字母数字和非字母数字的类元字符:
元字符 | 说明 |
---|---|
\\w | 任何一个字母数字字符(大小写均可)或下划线字符,等价于[a-zA-Z0-9_] |
\\W | 任何一个非字母数字或非下划线字符,等价于[^a-zA-Z0-9_] |
4.3.3 匹配空白字符与非空白字符
下表是一些用来匹配某个特定的空白字符的元字符:
元字符 | 说明 |
---|---|
\\s | 任何一个空白字符,等价于[\\f\\n\\r\\t\\v] |
\\S | 任何一个非空白字符,等价于[^\\f\\n\\r\\t\\v] |
注意:
- 用来匹配退格字符的
[\\b]
元字符是一个特例,它不在元字符\\s
的覆盖范围内,也没有被排除在类元字符\\S
的覆盖范围外。
4.3.4 匹配十六进制或八进制数值
在正则表达式中十六进制数值要用前缀\\x
来给出,如\\x0A
对应ASCII码表中的10,即换行符,等价于\\n
。
在正则表达式中八进制数值要用前缀\\0
来给出,数值本身可以是两位或三位数字,如\\011
对应ASCII码表中的9,即制表符,等价于\\t
。
注意:
- 是为了使用某个特定字符的十六进制值或八进制值去匹配这个特定字符,才能使用。如
\\x0A
匹配特定字符\\n
。但用得很少,了解即可。 - 有一些正则表达式实现还支持使用
\\c
前缀来指定匹配各种控制字符,如\\cZ
匹配Ctrl-Z。不过,实际工作也用得非常的少,了解即可。
4.4 使用POSIX字符类
POSIX字符类:是一些正则表达式实现支持的一种简写方式。是的,又是简写。如[a-zA-Z0-9]
可以简写成[:alnum:]
。
字符类 | 说明 |
---|---|
[:alnum:] | 匹配字面和数字字符。等价于[a-zA-Z0-9] |
[:alpha:] | 匹配字母字符。等价于[a-zA-Z] |
[:blank:] | 匹配空格或制表符。等价于[\\t ] ,注意字母t后面有一个空格 |
[:cntrl:] | ASCII控制字符,ASCII码值0到31,再加上ASCII码值位127的 |
[:digit:] | 任何一个数字,等价于[0-9] |
[:graph:] | 和[:print:] 一样,但不包含空格 |
[:print:] | 任何一个可打印字符 |
[:punct:] | 既不属于[:alnum:] 也不属于[:cntrl:] 的任何一个字符 |
[:lower:] | 匹配小写字母,等价于[a-z] |
[:upper:] | 匹配大写字母,等价于[A-Z] |
[:space:] | 匹配任何空白字符,包括空格,等价于[^\\f\\n\\r\\t\\v ] ,注意字母v后面有一个空格 |
[:xdigit:] | 匹配十六进制数字。等价于[a-fA-F0-9] |
注意:
- JavaScript不支持在正则表达式中使用POSIX字符类。
- 如匹配十六进制的RGB值的正则表达式
#[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]
,这里使用的正则表达式以[[
开头和以]]
结束,即两对方括号,这是使用POSIX字符类所必须的。POSIX字符类必须括在[:
和:]
之间,我们使用的POSIX字符类是 [:xdigit:]而不是
:xdigit:。外层的
[和
]`字符用来定义一个字符集合, - 字符类元字符和POSIX字符类都是为了简化正则表达式的,所以不必过分深究。
第5章 重复匹配
5.1 有多少个匹配
用来解决能够匹配多个字符的情况。
5.1.1 匹配一个或多个字符
其中+
字符匹配一个或多个字符,至少一个,不匹配零个字符的情况。即能够匹配大于等于一个字符的情况。
如a
匹配a字符本身,而a+
匹配一个或多个连续出现的a字符,如a
、aa
、aaa
等;[0-9]
匹配任意单个数字,而[0-9]+
匹配一个或多个连续的数字,如0
、23
等。
并且+
还可以用来匹配一个或多个字符集合,如[0-9a-zA-Z]+
。
注意:
- 在给一个字符集合加上
+
后缀的时候,必须将+
放在这个字符集合的外面,比如说[0-9]+
是正确的,而[0-9+]是错误的。但事实上
[0-9+]也是一个合法的正则表达式,但它匹配的不是一个或多个数字,而是定义了一个由数字0到9和
+`构成的字符集合,因而只能匹配一个单个的数字字符或加号,并不是我们需要的东西。 +
是一个元字符,如果需要匹配+
本身,则必须进行转义使用\\+
。- 当在字符集合中使用
.
和+
这样的元字符时将被解释为普通字符而不需要被转义,但转义后也没有坏处,但推荐转义便于增加可读性。如[\\w.]
和[\\w\\.]
效果是一样的,但推荐使用后者。
5.1.2 匹配零个或多个字符
+
不匹配零个字符,会至少匹配一个字符。而*
元字符表示匹配的字符应该可以出现零次或多次的情况。用法与+
完全一样,用在一个字符或一个字符集合的后面,就可以匹配该字符或字符集合连续出现零次或多次的情况。如a.*c
可以匹配ac
、a c
、abc
、abefc
等情况。如[ab]*c
可以匹配c
、ac
、bc
、abbc
、abc
这样的情况。
注意:
*
与+
的区别是,+
匹配一个或多个字符(或字符集合),最少要匹配一次;*
匹配零个或任意多个字符(或字符集合),可以没有匹配。*
是一个元字符,如果需要匹配*
本身,则需要被转义使用\\*
。
5.1.3 匹配零个或一个字符
?
只能匹配一个字符(或字符集合)的零次或一次出现,最多不会超过一次。即如果需要在一段文本中匹配某个特定的字符(或字符集合)而该字符可能出现,也可能不出现,那么?
则是最佳选择。
如https?://[\\w\\./]+
既可以匹配http
的URL,又可以匹配https
的URL。在Unix或Linux系统上匹配空白行只使用\\n\\n
即可,不需要加上\\r
,同时适用于Windows和Unix/Linux系统的正则表达式应该包含一个可选的\\r
和一个必须被匹配的\\n
,即[\\r]?\\n[\\r]?\\n
。
注意:
- 如果同时使用
[]
和?
,那么应该把?
放在字符集合的外面,如http[s]?
。 [\\r]?
与\\r?
在功能上是完全等价的,但通常放在字符集合中,避免产生歧义和增加可读性。?
是一个元字符,如果需要匹配?
本身,则需要使用到它的转义\\?
。
5.2 匹配的重复次数
+
和*
和?
都无法精确控制匹配次数,如一定要匹配3次,则通过上面的元字符无法实现。
+
和*
匹配的字符个数没有上限,即无法为它们将匹配的字符个数设定一个最大值。+
、*
和?
至少匹配零个或一个字符,即无法为它们将匹配的字符个数设定一个最小值。- 如果只使用
+
和*
,无法把它们将匹配的字符个数设定为一个精确的数字。
所以正则表达式语言提供了一个用来设定重复次数的语法,使用{
和}
字符来给出,而控制出现次数的数字写在它们之间。
注意:
{
和}
是元字符,如果要匹配它们本身,则需要使用转义\\{
和\\}
,虽然不转义也通常不会报错。
5.2.1 为重复匹配次数设定一个精确的值
如果要为重复匹配次数设定一个精确的值,把那个数字写在{
和}
之间即可。如{3}
表示正则表达式里的前一个字符或字符集合必须连续重复出现3次才算是一个匹配。
如[\\d]{11}
可以匹配11位的号码。
5.2.2 为重复匹配次数设定一个区间
{}
语法还可以用来为重复匹配次数设定一个区间,即为重复匹配次数设定一个最小值和一个最大值。如{2, 4}
表示最少重复2次、最多重复4次。
如\\d{1,2}[-\\/]\\d{1,2}[-\\/]\\d{2,4}
可以匹配日期。注意\\/
是一个\\
和一个/
,表示转义。
注意:
- 重复次数可以是0,如
{0, 3}
表示重复次数可以是0、1、2或3。而?
等价于{0, 1}
。
5.2.3 匹配至少重复多少次
{}
语法的最后一种用法是给出一个最小的重复次数,但不必给出一个最大值。语法是:{最小重复次数, }
,如{3, }
表示至少重复3次,即必须重复3次或更多次。
注意:
- 需要注意花括号中的逗号
,
,如{3, }
忽略了逗号就会变成{3}
。
5.3 防止过度匹配
?
只能匹配零个或一个字符,{n}
和{m, n}
也有一个重复次数的上限,即所定义的重复次数是有限的。但其他的重复匹配语法在重复次数上是没有上限的,这样做会导致过度匹配。
如使用<b>.*</b>
匹配There <b>are</b> some <b>apples</b>.
文本,匹配到的结果是<b>are</b> some <b>apples</b>
,只找到一个而不是两个。因为*
和+
都是贪婪型的元字符,它们在进行匹配的时候会尽可能地多匹配,而不是适可而止。它们会尽可能地从一段文本的开头一直匹配到这段文本的末尾,而不是从这段文本的开头匹配到第一个匹配时为止。
如果不想要这种“贪婪行为”,那么可以在这些贪婪型元字符的后面加上一个?
后缀变成非贪婪模式。以下列出几个常用的贪婪型元字符和它们的非贪婪型:
贪婪型元字符 | 非贪婪型元字符 |
---|---|
* | *? |
+ | +? |
{n, } | {n, }? |
如上面的例子用非贪婪模式,即<b>.*?</b>
匹配There <b>are</b> some <b>apples</b>.
文本得到的结果是<b>are</b>
和<b>apples</b>
。
注意:
- 关于贪婪模式和非贪婪模式,需要根据实际情况来进行选择。
第6章 位置匹配
6.1 边界
引入:例如我们有一段文本"The cat scattered his food all over the room."
要求匹配里面的单词"cat"
,但是当我们写一个正则表达式cat
后,发现会把文本中的所有"cat"
都找出来,包括"scattered"
中的"cat"
。那么该怎么办呢?
解决问题的方法就是使用边界限定符,即在正则表达式中使用一些特殊的元字符来让匹配操作在什么位置或边界发生。
6.2 单词边界
单词边界即使用限定符\\b
指定的边界,即使用\\b
来匹配一个单词的开始或结尾。注意,是一个单词。
单词边界是单词和符号之间的边界,这里的单词可以是中文字符、英文字母字符、数字和下划线,符号可以是中文符号、英文符号、空格、制表符、换行符等。如hello
中空格不是边界,哦那个给与单词"hello"之间的那个才是边界。
然后使用新的正则表达式\\bcat\\b
去匹配上面的文本,就只会匹配成功一个,而"scattered"
中的字符序列"cat"不能匹配,因为它的"cat"的前一个字符是"s"、后一个字符是"t",都不能与\\b
相匹配。
事实上,\\b
匹配的是一个位置,这个位置位于一个能够用来构成单词的字符(字母、数字和下划线,也就是能与\\w
匹配的字符)和一个不能用来构成单词的字符(也就是\\W
相匹配的字符)之间。
注意:
\\b
匹配且只匹配一个位置,不匹配任何字符。用\\bcat\\b
匹配到的字符的长度是3个字符(c、a、t),不是5个字符。- 如果不想匹配一个单词边界,就可以使用
\\B
,等价于[^\\b]
。 - 除了
\\b
可以用来匹配单词边界外,有些正则表达式实现还支持\\<
匹配单词开头而\\>
匹配单词结束。不过能够支持它们的正则表达式引擎较少。
6.2 字符串边界
单词边界可以用来进行单词有关的位置匹配,如单词开头、单词结束或整个单词。字符串边界可以用来进行与字符串有关的位置匹配,如字符串的开头、字符串的结束和整个字符串。用来定义字符串边界的元字符有两个:一个用来定义字符串开头的^
,另一个是用来定义字符串结尾的$
。
注意:
^
是具有多种用途的元字符。当它出现在一个字符集合中(被放到[
和]
之间)并紧跟在左方括号[
的后面时(即[^
)才能发挥"求非"的作用。如果是在一个字符集合的外面并于一个模式的开头,^
将会匹配字符串的开头,如^[abc]
表示匹配以"a"或"b"或"c"开头。^
和$
一起的时候通常是去匹配整个待匹配的字符串,要求完全匹配字符串边界条件才会匹配成功,否则就是匹配失败。如^The.*room\\.$
才能匹配文本"The cat scattered his food all over the room."
。而如果要匹配"The cat"
则正则表达式要写成这样:^The cat
或^The cat\\b
。\\b
匹配的是单个单词;^
和$
匹配的是多个单词组成的字符串。这点应该区别开来。
第7章 使用子表达式
7.1 什么是子表达式
所谓的子表达式就是我们常说的分组,即使用()
括号包裹起来的内容。子表达式的作用之一就是可以把子表达式内的内容当作一个整体来操作,例如: {2,}
只能匹配这样的 ;;;;;
文本,而无法匹配
,但使用子表达式后,就可以让子表达式内的内容当作一个整体来进行重复次数的匹配,如( ){2,}
就可以了。
7.2 子表达式
子表达式概念:子表达式就是一个更大的表达式的一部分,把一个表达式划分为一系列子表达式的目的是为了把那些子表达式当作一个独立元素来使用。子表达式必须用(
和)
括起来。总之:子表达式就是使用()
括号括起来的正则表达式。
**子表达式用途之一:重复指定正则表达式指定次数。**所谓的指定正则表达式就是子表达式,用()
括号括起来的表达式,如( ){2,}
就会让
可以匹配出现次数大于等于2的情况,如
。因为子表达式把
括起来了,把它当作一个整体来处理。
**子表达式用途之二:对操作符|
的OR条件做出准确定义。**例如电信的电话号码是以133、153、180、189开头,如我们要匹配133的电话号码,正则表达式可以这样写133\\d{8}
,那么该如何把这几个开头都匹配到呢?编程语言中有或
的处理,正则表达式也可以实现,语法即(子表达式1|子表达式2|子表达式2|...)
,即可以使用子表达式添加|
号实现或
匹配。那么正则表达式可以写成(133|153|180|189)\\d{8}
就能匹配以这四类开头的电信电话号码了。
7.3 子表达式的嵌套
所谓的嵌套就是一个子表达式里嵌套另一个子表达式,如(子表达式(子表达式(子表达式)))
。例如匹配IP地址的正则表达式:(((\\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]))
就嵌套了多层子表达式。
子表达式的嵌套并没有什么语法作用可说,多使用就会掌握。对于分析复杂的正则表达式就是要将子表达式拆开,每次只分析和理解一个子表达式,在分析各个子表达式的时候,应该按照先内后外的原则进行而不是从第一个字符开始一个字符一个字符地去尝试。
注意:
(
和)
都是元字符,如果要匹配它们本身,必须进行转义\\(
和\\)
。
第8章 回溯引用:前后一致匹配
8.1 回溯引用有什么用
回溯引用的作用就是让你能够使用前面已经匹配到的结果。如html网页有<h1>
到<h6>
各种级别的标签,要求使用正则表达式找出所有的标题文字不管它的级别,写出的正则表达式是<h1>.*?</h1>
只能匹配一级标题;如果使用字符集合[1-6]
写成的正则表达式是<h[1-6]>.*?</h[1-6]>
能匹配任何一级标题,但也会匹配到<h1></h2>
或<h3></h1>
这样的情况;因为html标签要求起始标签和结束标签中的标识一致,那么使用回溯引用就能解决这个问题,让后面能引用前面已经匹配到的结果。
8.2 回溯引用匹配
回溯引用允许正则表达式模式引用前面的匹配结果。
例如:<(h1)>.*?</\\1>
就可以匹配<h1>一级标题</h1>
完整的一级标题,而不会匹配到<h1>一四级标题</h4>
这样的情况。如果要匹配任一级标题的开始标签和与之配对的结束标签可以写成这样:<(h[1-6])>.*?</\\1>
。
注意:
- 回溯引用指的是正则表达式的后半部分引用在前半部分中定义的子表达式。如上例中
\\1
就是引用前面的子表达式(h[1-6])
中的内容。 - 不同编程语言在实现回溯引用语法方面有很大的差异,需要注意。
\\1
代表着正则表达式中的第1个子表达式(即用(
和)
括起来的表达式),\\2
代表着第2个子表达式,\\3
代表着第3个,依次类推。- 回溯引用只能用来引用正则表达式中的子表达式(即用
(
和)
括起来的正则表达式片段)。 - 回溯引用匹配通常从1开始计数(
\\1
、\\2
等)。在许多正则表达式实现中,第0个匹配(\\0
)通常用来代表整个正则表达式。 - 子表达式是通过它们的相对位置来引用的,
\\1对应着第1个子表达式,
\\5`对应着第5个子表达式。但存在的问题是,当子表达式的相对位置发生变化,整个正则表达式可能失效,如果删除或添加子表达式可能会导致更严重的后果。所以一些比较新的正则表达式实现还支持"命名捕获":给某个子表达式起一个唯一的名字,然后用这个名字而不是相对位置来引用这个子表达式。但只有一些正则表达式实现支持这一功能。
8.3 回溯引用在替换操作中
例如:要把一段文本中的所有邮箱(如10086@qq.com,当然不止这一个邮箱)替换成<a href="mailto:10086@qq.com">10086@qq.com</a>
。那么就可以使用回溯引用来实现,如邮箱的正则表达式是(\\w+[\\w\\.]*@[\\w\\.]+\\.\\w+)
。替换操作需要用到两个正则表达式:一个用来给出搜索模式,一个用来给出匹配文本的替换模式。回溯引用可以跨模式使用,在第一个模式里被匹配出来的子表达式可以用在第二个模式里。第一个模式是:(\\w+[\\w\\.]*@[\\w\\.]+\\.\\w+)
,第二个模式是:<a href="mailto:$1">$1</a>
。具体到这个例子就是10086@qq.com变成了<a href="mailto:10086@qq.com">10086@qq.com</a>
。具体Java代码实现:
public class Test01 {
public static void main(String[] args) {
String html = "我的邮箱是10086@qq.com,你的邮箱是10000@163.com";
// 我的邮箱是<a href="mailto:10086@qq.com">10086@qq.com</a>,你的邮箱是<a href="mailto:10000@163.com">10000@163.com</a>
System.out.println(html.replaceAll("(\\\\w+[\\\\w\\\\.]*@[\\\\w\\\\.]+\\\\.\\\\w+)", "<a href=\\"mailto:$1\\">$1</a>"));
}
}
除此之外,还可以利用回溯引用来调整文本的位置,对文本进行重新排版,即把文本分解成多个子表达式。如将一段文本123-456-789
调整位置变成789-456-123
,就可以使用回溯引用来完成。具体Java代码实现如下:
public class Test01 {
public static void main(String[] args) {
String html = "或致电:800-820-6666、1010-6666(免长话费),直接预订机票、酒店、旅游线路";
// 或致电:6666-800-820、1010-6666(免长话费),直接预订机票、酒店、旅游线路
System.out.println(html.replaceAll("(\\\\d{3})-(\\\\d{3})-(\\\\d{4})", "$3-$1-$2"));
}
}
有些正则表达式实现允许我们使用下表列出的元字符对字母进行大小写转换:
元字符 | 说明 |
---|---|
\\E | 结束\\L 或\\U 转换 |
\\l | 把下一个字符转换成小写 |
\\L | 把\\L 到\\E 之间的字符全部转换成小写 |
\\u | 把下一个字符转换成大写 |
\\U | 把\\U 到\\E 之间的字符全部转换成大写 |
其中l
是lower的缩写,u
是upper的缩写,E
可能是end的缩写表示结束的含义。便于理解记忆。\\l
和\\u
只能把下一个字符或子表达式转换为小写或大写。\\L
和\\U
将会把它后面的所有字符转换为小写或大写,直到遇到\\E
为止。
通常用在替换中,如$1\\U$2\\E$3
表示将第2个子表达式的内容转换成大写。
第9章 前后查找
所谓的前后查找就是先行断言和后行断言,书中讲得不是很明了,可以参考:https://www.runoob.com/w3cnote/reg-lookahead-lookbehind.html
各种前后查找操作符:
操作符 | 说明 |
---|---|
(?=) | 正向前查找 |
(?!) | 负向前查找 |
(?<=) | 正向后查找 |
(?<!) | 负向后查找 |
注意:
- 向前查找(先行断言)和向后查找(后行断言)的匹配本身是有返回结果的,只是这个结果的字节长度永远是0而已。因此查找操作有时也被称为零宽度(zero-width)匹配操作。
- 任何一个子表达式都可以转换成一个向前查找表达式,只需要给它加上一个
?=
前缀即可。在同一个正则表达式中可以使用多个向前查找表达式,它们可以出现在正则表达式中的任意位置。 - 向前查找模式的长度是可变的,它们可以包含
.
和+
之类的元字符,很灵活。而向后查找模式只能固定长度。 - 向前查找模式可以和向后查找一起使用,如
(?<=<title>).*?(?=</title>)
可以匹配这样的文本如<title>我是标题</title>
,即获取到title标签内的文本。 - 前查找指返回的是
(?=)
或(?!)
前方正则表达式所匹配的结果,而非括号中限制的正则表达式所匹配的结果,如用abc(?=[\\d+])
匹配文本"abc123"
返回的是"abc"
。 - 后查找指返回的是
(?<=)
或(?<!)
后方正则表达式所匹配的结果,而非括号中限制的正则表达式所匹配的结果,如用(?<=[\\d]+)abc
匹配文本"123abc"
返回的是"abc"
。 - 关于本节不建议看书上的内容,说得不是很清楚,自己上网搜索相关博客学习然后练习会更加清楚。
总结:
分类 | 模板 | 说明 | 举例 |
---|---|---|---|
正向先行断言 | expr1(?=expr2) | 获取expr1,但expr1后面必须匹配expr2 | 如abc(?=\\d+) 匹配文本"abc123" 获取到abc,因为abc后面的"123"能匹配\\d+ 。 |
负向先行断言 | expr1(?!expr2) | 获取expr1,但expr1后面必须不匹配expr2 | 如abc(?!\\d+) 匹配文本"abcxyz" 获取到abc,因为abc后面的"xyz"不匹配\\d+ 。 |
正向后行断言 | (?<=expr1)expr2 | 获取expr2,但expr2前面必须匹配expr1 | 如(?<=\\d+)abc 匹配文本"123abc" 获取到abc,因为abc前面的"123"能匹配\\d+ 。 |
负向后行断言 | (?<!expr1)expr2 | 获取expr2,但expr2前面必须不匹配expr1 | 如(?<!\\d+)abc 匹配文本"xyzabc" 获取到abc,因为abc前面的"xyz"不匹配\\d+ 。 |
注意:
- "先行"指的是获取
()
括号前面的内容,如expr1(?=expr2)
中的expr1就在括号的前面;"后行"指的是获取()
括号后面的内容,如(?<=expr1)expr2
中的expr2就在括号的后面。 - "正向"指的是
()
括号内的表达式必须匹配;"负向"指的是括号内的表达式必须不匹配。
第10章 嵌入条件
10.1 为什么要嵌入条件
(123)456-7890
和123-456-7890
都是可接受的北美电话号码格式,而1234567890
、(123)-456-7890
和(123-456-7890)
都是不可接受的格式。要编写一个正则表达式来匹配可接受的格式,而不匹配其他格式。
难点在于当有一个左括号(
的时候才去匹配另外一个右括号)
,如果没有左括号则去匹配-
符号。这时候,就需要使用条件处理。
注意:
- 在正则表达式的内部可以嵌入条件,但并不经常被用到。
- 并非所有的正则表达式实现都支持条件处理。
10.2 正则表达式里的条件
正则表达式里面的条件用?
来定义,前面学过的特定条件:
?
匹配前一个字符或表达式,如果它存在的话。?=
和?<=
匹配前面或后面的文本,如果它存在的话。
嵌入条件语法也使用了?
,有如下两种情况:- 根据前一个回溯引用来进行条件处理。
- 根据一个前后查找来进行条件处理。
10.2.1 回溯引用条件
所谓的回溯引用条件就是只在一个前面的子表达式搜索取得成功的情况下才允许使用一个表达式。基本语法是:(?(backreference)true-regex)
,其中?
表明这是一个条件,括号中的backreference
是一个回溯引用,true-regex
是一个只在backreference
存在时才会被执行的子表达式。
除此之外,还有否则表达式,即只在给定的回溯引用不存在(即条件没有得到满足)时才会被执行,用来定义这种条件的语法是(?(backreferece)true-regex|false-regex)
,这个语法接受一个条件和两个将分别在这个条件得到满足和没有得到满足时执行的子表达式。
上面的例子中用条件处理写出的正则表达式是(\\()?\\d{3}(?(1)\\)|-)\\d{3}-\\d{4}
。其中(\\()?
匹配一个可选的左括号,把它用括号括起来得到一个子表达式;随后的\\d{3}
匹配三位数字;(?(1)\\)|-)
是一个回溯引用条件,它将根据条件是否得到满足而去匹配)
或-
;如果(1)
存在(也就是找到一个左括号),\\)
必须被匹配;否则,-
必须被匹配。这样只有配对出现的括号才会被匹配,如果没有使用括号或括号不配对,则电话号码中的区号和取余数字之间的-
分隔符必须被匹配。
注意:
?(1)
检查第一个回溯引用是否存在,在条件中回溯引用(1、2等)编号不需要被转义(即不需要添加\\
),因此?(1)
是正确的,而?(\\1)
是不正确的。
10.2.2 前后查找条件
前后查找条件只在一个向前查找或向后查找操作取得成功的情况下才允许一个表达式被使用。定义一个前后查找条件的语法与定义一个回溯引用条件的语法大同小异,只需把回溯引用(括号里的回溯引用编号)替换为一个完整的前后查找表达式就行了。
如要匹配11111
、22222
、33333-
、44444-4444
这样的美国邮政编码,其中正确的格式是11111
、22222
和44444-4444
,而33333-
是错误的格式,使用前后查找条件来解决这个问题,写成的正则表达式是\\d{5}(?(?=-)-\\d{4})
。其中\\d{5}
匹配前5位数字;而(?(?=-)-\\d{4})是一个向前查找条件,其中
?=-匹配但不消费一个连字符
-,如果条件满足(即那个连字符存在),
-\\d{4}将匹配那个连字符和随后的四位数字。这样
33333-`被排除在最终的匹配结果之外。
注意:
- 在实际工作中,嵌入了前后查找条件的正则表达式相当少见,因为往往可以使用更简单的方法来达到同样的目的。
以上是关于《正则表达式必知必会》读书笔记的主要内容,如果未能解决你的问题,请参考以下文章