【正则表达式】2、深入了解与应用

1、关于分组与引用

        假设我们现在要去查找 15 位或 18 位数字。根据前面学习的知识,使用量词可以表示出现次数,使用管道符号可以表示多个选择,你应该很快就能写出\d{15}|\d{18}。但经过测试,你会发现,这个正则并不能很好地完成任务,因为 18 位数字也会匹配上前 15 位,具体如下图所示。

        为了解决这个问题,你灵机一动,很快就想到了办法,就是把 15 和 18 调换顺序,即写成 \d{18}|\d{15}。你发现,这回符合要求了。

        为什么会出现这种情况呢?因为在大多数正则实现中,多分支选择都是左边的优先。类似地,你可以使用 “北京市|北京” 来查找 “北京” 和 “北京市”。另外我们前面学习过,问号可以表示出现 0 次或 1 次,你发现可以使用“北京市?” 来实现来查找 “北京” 和 “北京市”。

        同样,针对 15 或 18 位数字这个问题,可以看成是 15 位数字,后面 3 位数据有或者没有,你应该很快写出了 \d{15}\d{3}? 。但这样写对不对呢?我们来看一下。

示例一:

        \d{15}\d{3}? 由于 \d{3} 表示三次,加问号非贪婪还是 3 次

示例二:

        \d{15}(\d{3})? 在 \d{3} 整体后加问号,表示后面三位有或无

        这时候,必须使用括号将来把表示“三个数字”的\d{3}这一部分括起来,也就是表示成\d{15}(\d{3})?这样。现在就比较清楚了:括号在正则中的功能就是用于分组。简单来理解就是,由多个元字符组成某个部分,应该被看成一个整体的时候,可以用括号括起来表示一个整体,这是括号的一个重要功能。其实用括号括起来还有另外一个作用,那就是“复用”,我接下来会给你讲讲这个作用。

1.1、分组与编号

        括号在正则中可以用于分组,被括号括起来的部分“子表达式”会被保存成一个子组。那分组和编号的规则是怎样的呢?其实很简单,用一句话来说就是,第几个括号就是第几个分组。这么说可能不好理解,我们来举一个例子看一下。

        这里有个时间格式 2020-05-10 20:23:05。假设我们想要使用正则提取出里面的日期和时间。

        我们可以写出如图所示的正则,将日期和时间都括号括起来。这个正则中一共有两个分组,日期是第 1 个,时间是第 2 个。

1.2、不保存子组

        在括号里面的会保存成子组,但有些情况下,你可能只想用括号将某些部分看成一个整体,后续不用再用它,类似这种情况,在实际使用时,是没必要保存子组的。这时我们可以在括号里面使用 ?: 不保存子组

        如果正则中出现了括号,那么我们就认为,这个子表达式在后续可能会再次被引用,所以不保存子组可以提高正则的性能。除此之外呢,这么做还有一些好处,由于子组变少了,正则性能会更好,在子组计数时也更不容易出错。

        那到底啥是不保存子组呢?我们可以理解成,括号只用于归组,把某个部分当成“单个元素”,不分配编号,后面不会再进行这部分的引用。

1.3、括号嵌套

        前面讲完了子组和编号,但有些情况会比较复杂,比如在括号嵌套的情况里,我们要看某个括号里面的内容是第几个分组怎么办?不要担心,其实方法很简单,我们只需要数左括号(开括号)是第几个,就可以确定是第几个子组

        在阿里云简单日志系统中,我们可以使用正则来匹配一行日志的行首。假设时间格式是 2020-05-10 20:23:05 。

        日期分组编号是 1,时间分组编号是 5,年月日对应的分组编号分别是 2,3,4,时分秒的分组编号分别是 6,7,8。

1.4、命名分组

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

        这里拿上一个例子做示例说明如下:

(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})

        需要注意的是,刚刚提到的方式命名分组和前面一样,给这个分组分配一个编号,不过你可以使用名称,不用编号,实际上命名分组的编号已经分配好了。不过命名分组并不是所有语言都支持的,在使用时,你需要查阅所用语言正则说明文档,如果支持,那你才可以使用。

1.5、分组引用

        在知道了分组引用的编号 (number)后,大部分情况下,我们就可以使用 “反斜扛 + 编号”,即 \number 的方式来进行引用,而 JavaScript 中是通过$编号来引用,如$1

1.5.1、在查找中的使用

        前面介绍了子组和引用的基本知识,现在我们来看下在正则查找时如何使用分组引用。比如我们要找重复出现的单词,我们使用正则可以很方便地使“前面出现的单词再次出现”,具体要怎么操作呢?我们可以使用 \w+ 来表示一个单词,针对刚刚的问题,我们就可以很容易写出 (\w+) \1 这个正则表达式了。

如下示例:

1.5.2、在替换中使用

        和查找类似,我们可以使用反向引用,在得到的结果中,去拼出来我们想要的结果。还是使用刚刚日期时间的例子,我们可以很方便地将它替换成, 2020 年 05 月 10 日这样的格式。

        完整例子如下所示,这里需要注意使用替换引用时的 PCRE 版本。如果版本是 >= 7.3的,则替换引用无法使用。

2、匹配模式

        所谓匹配模式,指的是正则中一些改变元字符匹配行为的方式,比如匹配时不区分英文字母大小写。常见的匹配模式有 4 种,分别是不区分大小写模式、点号通配模式、多行模式和注释模式。

2.1、不区分大小写模式(Case-Insensitive)

        下面我来举个例子说明一下。在进行文本匹配时,我们要关心单词本身的意义。比如要查找单词 cat,我们并不需要关心单词是 CAT、Cat,还是 cat。根据之前我们学到的知识,你可能会把正则写成这样:[Cc][Aa][Tt],这样写虽然可以达到目的,但不够直观,如果单词比较长,写起来容易出错,阅读起来也比较困难。

        我们前面说了,不区分大小写是匹配模式的一种。当我们把模式修饰符放在整个正则前面时,就表示整个正则表达式都是不区分大小写的。模式修饰符是通过 (? 模式标识) 的方式来表示的。 我们只需要把模式修饰符放在对应的正则前,就可以使用指定的模式了。在不区分大小写模式中,由于不分大小写的英文是 Case-Insensitive,那么对应的模式标识就是 I 的小写字母 i,所以不区分大小写的 cat 就可以写成 (?i)cat

        我们也可以用它来尝试匹配两个连续出现的 cat,如下图所示,你会发现,即便是第一个 cat 和第二个 cat 大小写不一致,也可以匹配上。

        如果我们想要前面匹配上的结果,和第二次重复时的大小写一致,那该怎么做呢?我们只需要用括号把修饰符和正则 cat 部分括起来,加括号相当于作用范围的限定,让不区分大小写只作用于这个括号里的内容

        需要注意的是,这里正则写成了 ((?i)cat) \1,而不是 ((?i)(cat)) \1。也就是说,我们给修饰符和 cat 整体加了个括号,而原来 cat 部分的括号去掉了。如果 cat 保留原来的括号,即 ((?i)(cat)) \1,这样正则中就会有两个子组,虽然结果也是对的,但这其实没必要。

        到这里,我们再进阶一下。如果用正则匹配,实现部分区分大小写,另一部分不区分大小写,这该如何操作呢?就比如说我现在想要,the cat 中的 the 不区分大小写,cat 区分大小写。通过上面的学习,你应该能很快写出相应的正则,也就是 ((?i)the) cat。实现的效果如下:

        有一点需要你注意一下,上面讲到的通过修饰符指定匹配模式的方式,在大部分编程语言中都是可以直接使用的,但在 JS 中我们需要使用 /regex/i 来指定匹配模式。在编程语言中通常会提供一些预定义的常量,来进行匹配模式的指定。比如 Python 中可以使用 re.IGNORECASE 或 re.I ,来传入正则函数中来表示不区分大小写。我下面给出了你一个示例,你可以看一下。


>>> import re
>>> re.findall(r"cat", "CAT Cat cat", re.IGNORECASE)
['CAT', 'Cat', 'cat']

到这里我简单总结一下不区分大小写模式的要点:

  1. 不区分大小写模式的指定方式,使用模式修饰符 (?i);
  2. 修饰符如果在括号内,作用范围是这个括号内的正则,而不是整个正则;
  3. 使用编程语言时可以使用预定义好的常量来指定匹配模式。

2.2、点号通配模式(Dot All)

        (.)它可以匹配上任何符号,但不能匹配换行。当我们需要匹配真正的“任意”符号的时候,可以使用 [\s\S] 或 [\d\D] 或 [\w\W] 等。

        但是这么写不够简洁自然,所以正则中提供了一种模式,让英文的点(.)可以匹配上包括换行的任何字符。这个模式就是点号通配模式,有很多地方把它称作单行匹配模式,但这么说容易造成误解,毕竟它与多行匹配模式没有联系,因此在本文中我们统一用更容易理解的“点号通配模式”。

        单行的英文表示是 Single Line,单行模式对应的修饰符是 (?s),我还是选择用 the cat 来给你举一个点号通配模式的例子。如下图所示:

        需要注意的是,JavasScript 不支持此模式,那么我们就可以使用前面说的[\s\S]等方式替代。在 Ruby 中则是用 Multiline,来表示点号通配模式(单行匹配模式),我猜测设计者的意图是把点(.)号理解成“能匹配多行”。

2.3、多行匹配模式(Multiline)

        讲完了点号通配模式,我们再来看看多行匹配模式。通常情况下,^ 匹配整个字符串的开头,$ 匹配整个字符串的结尾。多行匹配模式改变的就是 和 的匹配行为。如下示例

        多行模式的作用在于,使 ^$ 能匹配上每行的开头或结尾,我们可以使用模式修饰符号 (?m) 来指定这个模式。

        值得一提的是,正则中还有 \A 和 \z(Python 中是 \Z) 这两个元字符容易混淆,\A 仅匹配整个字符串的开始,\z 仅匹配整个字符串的结束,在多行匹配模式下,它们的匹配行为不会改变,如果只想匹配整个字符串,而不是匹配每一行,用这个更严谨一些。

2.4、注释模式(Comment)

        在实际工作中,正则可能会很复杂,这就导致编写、阅读和维护正则都会很困难。我们在写代码的时候,通常会在一些关键的地方加上注释,让代码更易于理解。很多语言也支持在正则中添加注释,让正则更容易阅读和维护,这就是正则的注释模式。正则中注释模式是使用 (?#comment) 来表示

        比如我们可以把单词重复出现一次的正则 (\w+) \1 写成下面这样,这样的话,就算不是很懂正则的人也可以通过注释看懂正则的意思。

        在很多编程语言中也提供了 x 模式来书写正则,也可以起到注释的作用。我用 Python3 给你举了一个例子,你可以参考一下。

import re
regex = r'''(?mx)  # 使用多行模式和x模式
^          # 开头
(\d{4})    # 年
(\d{2})    # 月
$          # 结尾
'''
re.findall(regex, '202006\n202007')
# 输出结果 [('2020', '06'), ('2020', '07')]

        需要注意的是在 x 模式下,所有的换行和空格都会被忽略。为了换行和空格的正确使用,我们可以通过把空格放入字符组中,或将空格转义来解决换行和空格的忽略问题。我下面给了你一个示例,你可以看看。

regex = r'''(?mx)
^          # 开头
(\d{4})    # 年
[ ]        # 空格
(\d{2})    # 月
$          # 结尾
'''
re.findall(regex, '2020 06\n2020 07')
# 输出结果 [('2020', '06'), ('2020', '07')]

3、正则断言使用

        什么是断言呢?简单来说,断言是指对匹配到的文本位置有要求。这么说你可能还是没理解,我通过一些例子来给你讲解。你应该知道 \d{11} 能匹配上 11 位数字,但这 11 位数字可能是 18 位身份证号中的一部分。再比如,去查找一个单词,我们要查找 tom,但其它的单词,比如 tomorrow 中也包含了 tom。

        也就是说,在有些情况下,我们对要匹配的文本的位置也有一定的要求。为了解决这个问题,正则中提供了一些结构,只用于匹配位置,而不是文本内容本身,这种结构就是断言。常见的断言有三种:单词边界、行的开始或结束以及环视。

3.1、单词边界(Word Boundary)

        比如我们想要把下面文本中的 tom 替换成 jerry。注意一下,在文本中出现了 tomorrow 这个单词,tomorrow 也是以 tom 开头的。

tom asked me if I would go fishing with him tomorrow.

中文翻译:Tom 问我明天能否和他一同去钓鱼。

        利用前面学到的知识,我们如果直接替换,会出现下面这种结果。


替换前:tom asked me if I would go fishing with him tomorrow.
替换后:jerry asked me if I would go fishing with him jerryorrow.

        这显然是错误的,因为明天这个英语单词里面的 tom 也被替换了。

        那正则是如何解决这个问题的呢?单词的组成一般可以用元字符 \w+ 来表示,\w 包括了大小写字母、下划线和数字(即 [A-Za-z0-9_])。那如果我们能找出单词的边界,也就是当出现了\w 表示的范围以外的字符,比如引号、空格、标点、换行等这些符号,我们就可以在正则中使用\b 来表示单词的边界。 \b 中的 b 可以理解为是边界(Boundary)这个单词的首字母。

        根据刚刚学到的内容,在准确匹配单词时,我们使用 \b\w+\b 就可以实现了。下面我们以 Python3 语言为例子,为你实现上面提到的 “tom 替换成 jerry”:

>>> import re
>>> test_str = "tom asked me if I would go fishing with him tomorrow."
>>> re.sub('\btom\b', 'jerry', test_str)
'tom asked me if I would go fishing with him tomorrow.'

比如在 notepad++ 中的示例:

3.2、行的开始或结束

        和单词的边界类似,在正则中还有文本每行的开始和结束,如果我们要求匹配的内容要出现在一行文本开头或结尾,就可以使用 ^ 和 $ 来进行位置界定。

        我们先说一下行的结尾是如何判断的。你应该知道换行符号。在计算机中,回车(\r)和换行(\n)其实是两个概念,并且在不同的平台上,换行的表示也是不一样的。我在这里列出了 Windows、Linux、macOS 平台上换行的表示方式。

3.3、日志起始行判断

        最常见的例子就是日志收集,我们在收集日志的时候,通常可以指定日志行的开始规则,比如以时间开头,那些不是以时间开头的可能就是打印的堆栈信息。我来给你一个以日期开头,下面每一行都属于同一篇日志的例子。


[2020-05-24 12:13:10] "/home/tu/demo.py"
Traceback (most recent call last):
  File "demo.py", line 1, in <module>
    1/0
ZeroDivisionError: integer division or modulo by zero

        在这种情况下,我们就通过日期时间开头来判断哪一行是日志的第一行,在日期时间后面的日志都属于同一条日志。除非我们看见下一个日期时间的出现,才是下一条日志的开始。

3.4、输入数据校验

        在 Web 服务中,我们常常需要对输入的内容进行校验,比如要求输入 6 位数字,我们可以使用 \d{6} 来校验。但你需要注意到,如果用户输入的是 6 位以上的数字呢?在这种情况下,如果不去要求用户录入的 6 位数字必须是行的开头或结尾,就算验证通过了,结果也可能不对。比如下面的示例,在不加行开始和结束符号时,用户输入了 7 位数字,也是能校验通过的:

>>> import re
>>> re.search('\d{6}', "1234567") is not None
True    <-- 能匹配上 (包含6位数字)
>>> re.search('^\d{6}', "1234567") is not None
True    <-- 能匹配上 (以6位数字开头)
>>> re.search('\d{6}$', "1234567") is not None
True    <-- 能匹配上 (以6位数字结尾)
>>> re.search('^\d{6}$', "1234567") is not None
False   <-- 不能匹配上 (只能是6位数字)
>>> re.search('^\d{6}$', "123456") is not None
True    <-- 能匹配上 (只能是6位数字)

        在前面的匹配模式章节中,我们学习过,在多行模式下,^ 和 $ 符号可以匹配每一行的开头或结尾。大部分实现默认不是多行匹配模式,但也有例外,比如 Ruby 中默认是多行模式。所以对于校验输入数据来说,一种更严谨的做法是,使用 \A 和 \z (Python 中使用 \Z) 来匹配整个文本的开头或结尾。

        解决这个问题还有一种做法,我们可以在使用正则校验前,先判断一下字符串的长度,如果不满足长度要求,那就不需要再用正则去判断了。相当于你用正则解决主要的问题,而不是所有问题,这也是前面说的使用正则要克制。

3.5、环视( Look Around)

        在正则中我们有时候也需要瞻前顾后,找准定位。环视就是要求匹配部分的前面或后面要满足(或不满足)某种规则,有些地方也称环视为零宽断言。

        那具体什么时候我们会用到环视呢?我来举个例子。邮政编码的规则是第一位是 1-9,一共有 6 位数字组成。现在要求你写出一个正则,提取文本中的邮政编码。根据规则,我们很容易就可以写出邮编的组成 [1-9]\d{5}。我们可以使用下面的文本进行测试

012300  不满足第一位是 1-9
130400  满足要求
465441  满足要求
4654000 长度过长
138001380002 长度过长

        我们发现,7 位数的前 6 位也能匹配上,12 位数匹配上了两次,这显然是不符合要求的。

        也就是说,除了文本本身组成符合这 6 位数的规则外,这 6 位数左边或右边都不能是数字。正则是通过环视来解决这个问题的。解决这个问题的正则有四种。我给你总结了一个表。

        你可能觉得名称比较难记住,没关系,我给你一个小口诀,你只要记住了它的功能和写法就行。这个小口诀你可以在心里默念几遍:左尖括号代表看左边,没有尖括号是看右边,感叹号是非的意思

        因此,针对刚刚邮编的问题,就可以写成左边不是数字,右边也不是数字的 6 位数的正则。即 (?<!\d)[1-9]\d{5}(?!\d)。这样就能够符合要求了。如下示例

3.6、单词边界用环视表示

学习到这里,你可以思考一下,表示单词边界的 \b 如果用环视的方式来写,应该是怎么写呢?

        这个问题其实比较简单,单词可以用 \w+ 来表示,单词的边界其实就是那些不能组成单词的字符,即左边和右边都不能是组成单词的字符。比如下面这句话:

the little cat is in the hat

        the 左侧是行首,右侧是空格,hat 右侧是行尾,左侧是空格,其它单词左右都是空格。所有单词左右都不是 \w。(?<!\w) 表示左边不能是单词组成字符(?!\w) 右边不能是单词组成字符,即 \b\w+\b 也可以写成 (?<!\w)\w+(?!\w)。

        另外,根据前面学到的知识,非\w 也可以用\W 来表示。那单词的正则可以写成 (?<=\W)\w+(?=\W)。

4、正则中转义的注意事项

4.1、转义字符

        首先我们说一下什么是转义字符(Escape Character)。它在维基百科中是这么解释的:

在计算机科学与远程通信中,当转义字符放在字符序列中,它将对它后续的几个字符进行替代并解释。通常,判定某字符是否为转义字符由上下文确定。转义字符即标志着转义序列开始的那个字符。

        这么说可能有点不好理解,我再来给你通俗地解释一下。转义序列通常有两种功能。第一种功能是编码无法用字母表直接表示的特殊数据。第二种功能是用于表示无法直接键盘录入的字符(如回车符)。

        我们这说的就是第二种情况,转义字符自身和后面的字符看成一个整体,用来表示某种含义。最常见的例子是,C 语言中用反斜线字符“\”作为转义字符,来表示那些不可打印的 ASCII 控制符。另外,在 URI 协议中,请求串中的一些符号有特殊含义,也需要转义,转义字符用的是百分号“%”。之所以把这个字符称为转义字符,是因为它后面的字符,不是原来的意思了。

        在日常工作中经常会遇到转义字符,比如我们在 shell 中删除文件,如果文件名中有 * 号,我们就需要转义,此时我们能看出,使用了转义字符后,* 号就能放进文件名里了。


rm access_log*    # 删除当前目录下 access_log 开头的文件
rm access_log\*   # 删除当前目录下名字叫 access_log* 的文件

        再比如我们在双引号中又出现了双引号,这时候就需要转义了,转义之后才能正常表示双引号,否则会报语法错误。比如下面的示例,引号中的 Hello World! 也是含有引号的。

print "tom said \"Hello World!\" to the crowd."

4.2、字符串转义和正则转义

        说完了转义字符,我们再来看一下正则中的转义。正则中也是使用反斜杠进行转义的。一般来说,正则中 \d 代表的是单个数字,但如果我们想表示成 反斜杠和字母 d,这时候就需要进行转义,写成 \d,这个就表示反斜杠后面紧跟着一个字母 d。

        刚刚的反斜杠和 d 是连续出现的两个字符,如果你想表示成反斜杠或 d,可以用管道符号或中括号来实现,比如 \|d 或 [\d]。

        需要注意的是,如果你想用代码来测试这个,在程序中表示普通字符串的时候,我们如果要表示反斜杠,通常需要写成两个反斜杠,因为只写一个会被理解成“转义符号”,而不是反斜杠本身。

4.3、正则中元字符的转义

        在前面的内容中,我们讲了很多元字符,相信你一定都还记得。如果现在我们要查找比如星号(*)、加号(+)、问号(?)本身,而不是元字符的功能,这时候就需要对其进行转义,直接在前面加上反斜杠就可以了。这个转义就比较简单了,下面是一个示例。


>>> import re
>>> re.findall('\+', '+')
['+']

4.4、括号的转义

        在正则中方括号 [] 和 花括号 {} 只需转义开括号,但圆括号 () 两个都要转义。我在下面给了你一个比较详细的例子。

>>> import re
>>> re.findall('\(\)\[]\{}', '()[]{}')
['()[]{}']
>>> re.findall('\(\)\[\]\{\}', '()[]{}')  # 方括号和花括号都转义也可以
['()[]{}']

        在正则中,圆括号通常用于分组,或者将某个部分看成一个整体,如果只转义开括号或闭括号,正则会认为少了另外一半,所以会报错。

4.5、使用函数消除元字符特殊含义

        我们也可以使用编程语言自带的转义函数来实现转义。下面我给出了一个在 Python 里转义的例子,你可以看一下。


>>> import re
>>> re.escape('\d')  # 反斜杠和字母d转义
'\\\\d'
>>> re.findall(re.escape('\d'), '\d')
['\\d']
>>> re.escape('[+]')  # 中括号和加号
'\\[\\+\\]'
>>> re.findall(re.escape('[+]'), '[+]')
['[+]']

        这个转义函数可以将整个文本转义,一般用于转义用户输入的内容,即把这些内容看成普通字符串去匹配,但你还是得好好注意一下,如果使用普通字符串查找能满足要求,就不要使用正则,因为它简单不容易出问题。下面是一些其他编程语言对应的转义函数,供你参考。

4.6、字符组中的转义

        讲完了元字符的转义,我们现在来看看字符组中的转义。书写正则的时候,在字符组中,如果有过多的转义会导致代码可读性差。在字符组里只有三种情况需要转义,下面我来给你讲讲具体是哪三种情况。

4.6.1、脱字符在中括号中,且在第一个位置需要转义


>>> import re
>>> re.findall(r'[^ab]', '^ab')  # 转义前代表"非"
['^']
>>> re.findall(r'[\^ab]', '^ab')  # 转义后代表普通字符
['^', 'a', 'b']

4.6.2、中划线在中括号中,且不在首尾位置

>>> import re
>>> re.findall(r'[a-c]', 'abc-')  # 中划线在中间,代表"范围"
['a', 'b', 'c']
>>> re.findall(r'[a\-c]', 'abc-')  # 中划线在中间,转义后的
['a', 'c', '-']
>>> re.findall(r'[-ac]', 'abc-')  # 在开头,不需要转义
['a', 'c', '-']
>>> re.findall(r'[ac-]', 'abc-')  # 在结尾,不需要转义
['a', 'c', '-']

4.6.3、右括号在中括号中,且不在首位


>>> import re
>>> re.findall(r'[]ab]', ']ab')  # 右括号不转义,在首位
[']', 'a', 'b']
>>> re.findall(r'[a]b]', ']ab')  # 右括号不转义,不在首位
[]  # 匹配不上,因为含义是 a后面跟上b]
>>> re.findall(r'[a\]b]', ']ab')  # 转义后代表普通字符
[']', 'a', 'b']

4.7、字符组中其它的元字符

        一般来说如果我们要想将元字符(.+?() 之类)表示成它字面上本来的意思,是需要对其进行转义的,但如果它们出现在字符组中括号里,可以不转义。这种情况,一般都是单个长度的元字符,比如点号(.)、星号()、加号(+)、问号(?)、左右圆括号等。它们都不再具有特殊含义,而是代表字符本身。但如果在中括号中出现 \d 或 \w 等符号时,他们还是元字符本身的含义。

>>> import re
>>> re.findall(r'[.*+?()]', '[.*+?()]')  # 单个长度的元字符 
['.', '*', '+', '?', '(', ')']
>>> re.findall(r'[\d]', 'd12\\')  # \w,\d等在中括号中还是元字符的功能
['1', '2']  # 匹配上了数字,而不是反斜杠\和字母d

5、关于正则表达式的流派与特性

5.1、POSIX 流派

        这里我们先简要介绍一下 POSIX 流派。POSIX 规范定义了正则表达式的两种标准:

  • BRE 标准(Basic Regular Expression 基本正则表达式);
  • ERE 标准(Extended Regular Expression 扩展正则表达式)。

5.1.1、BRE 标准 和 ERE 标准

        早期 BRE 与 ERE 标准的区别主要在于BRE 标准不支持量词问号和加号,也不支持多选分支结构管道符。BRE 标准在使用花括号,圆括号时要转义才能表示特殊含义。BRE 标准用起来这么不爽,于是有了 ERE 标准,在使用花括号,圆括号时不需要转义了,还支持了问号、加号 和 多选分支。

        我们现在使用的 Linux 发行版,大多都集成了 GNU 套件。GNU 在实现 POSIX 标准时,做了一定的扩展,主要有以下三点扩展。

  1. GNU BRE 支持了 +、?,但转义了才表示特殊含义,即需要用\+、\?表示。
  2. GNU BRE 支持管道符多选分支结构,同样需要转义,即用 \|表示。
  3. GNU ERE 也支持使用反引用,和 BRE 一样,使用 \1、\2…\9 表示。

        BRE 标准和 ERE 标准的详细区别,我给了你一个参考图,你可以看一下,浅黄色背景是 BRE 和 ERE 不同的地方,三处天蓝色字体是 GNU 扩展。

        总之,GNU BRE 和 GNU ERE 它们的功能特性并没有太大区别,区别是在于部分语法层面上,主要是一些字符要不要转义。

5.1.2、POSIX 字符组

        POSIX 流派还有一个特殊的地方,就是有自己的字符组,叫 POSIX 字符组。这个类似于我们之前学习的 \d 表示数字,\s 表示空白符等,POSIX 中也定义了一系列的字符组。具体的清单和解释如下所示:

5.2、PCRE 流派

        除了 POSIX 标准外,还有一个 Perl 分支,也就是我们现在熟知的 PCRE。随着 Perl 语言的发展,Perl 语言中的正则表达式功能越来越强悍,为了把 Perl 语言中正则的功能移植到其他语言中,PCRE 就诞生了。

        目前大部分常用编程语言都是源于 PCRE 标准,这个流派显著特征是有\d、\w、\s 这类字符组简记方式。

        不过,虽然 PCRE 流派是从 Perl 语言中衍生出来的,但与 Perl 语言中的正则表达式在语法上还是有一些细微差异,比如 PHP 的 preg 正则表达式 (Perl Regular Expression) 与 Perl 正则表达式的差异

PCRE 流派的兼容问题

        虽然 PCRE 流派是与 Perl 正则表达式相兼容的流派,但这种兼容在各种语言和工具中还存在程度上的差别,这包括了直接兼容与间接兼容两种情况。

        而且,即便是直接兼容,也并非完全兼容,还是存在部分不兼容的情况。原因也很简单,Perl 语言中的正则表达式在不断改进和升级之中,其他语言和工具不可能完全做到实时跟进与更新。

  • 直接兼容,PCRE 流派中与 Perl 正则表达式直接兼容的语言或工具。比如 Perl、PHP preg、PCRE 库等,一般称之为 Perl 系。
  • 间接兼容,比如 Java 系(包括 Java、Groovy、Scala 等)、Python 系(包括 Python2 和 Python3)、JavaScript 系(包括原生 JavaScript 和扩展库 XRegExp)、.Net 系(包括 C#、VB.Net 等)等。

5.3、在 Linux 中使用正则

        在遵循 POSIX 规范的 UNIX/LINUX 系统上,按照 BRE 标准 实现的有 grep、sed 和 vi/vim 等,而按照 ERE 标准 实现的有 egrep、awk 等。在 UNIX/LINUX 系统里 PCRE 流派与 POSIX 流派的对比:

        刚刚我们能提到了工具对应的实现标准,其实有一些工具实现同时兼容多种正则标准,比如前面我们讲到的 grep 和 sed。如果在使用时加上 -E 选项,就是使用 ERE 标准;如果加上 -P 选项,就是使用 PCRE 标准。

#使用 ERE 标准
grep -E '[[:digit:]]+' access.log

#使用 PCRE 标准
grep -P '\d+' access.log

        在使用具体命令时,如何知道属于哪个流派呢?你不用担心太多了记不住。在 Linux 系统中有个 man 命令可以帮助我们。比如,我在 macOS 上执行 man grep ,可以看到选项 -G 是指定使用 BRE 标准(默认),-E 是 ERE 标准,-P 是 PCRE 标准。所以,在使用具体工具时,你通过这个方法查一下命令的说明就好了。

        在 grep 中使用 \d+ 查找不到结果,是因为 grep 属于 BRE 流派,不支持 \d 来表示数字,加号也要转义才能表示量词的一到多次,所以无法找出数字那一行。如果你一定要用 BRE 流派,可以通过使用POSIX 字符组 和 转义加号 来实现。而 egrep 属于 ERE 流派,也不支持 \d,\d 相当于字母 d,所以找到了字母那一行。

        在 grep 命令中,你可以指定参数 -P 来使用 PCRE 流派,这样就和我们之前学习到的是一致的了。知道了原因之后,你应该能写出相应的解决方法。下图是一些能工作的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值