这这个教程中,我尝试使用一种故事场景来讲解正则表达式,以期能够为大家更愉快、更容易地理解。


有一天,小明在用Python做文本清洗的时候,突然觉得“233”太多了,觉得像是在嘲讽自己,怎么都看不顺眼,下定决心要把文本里所有的“233”全部干掉,于是一场属于小明和233的战争就这样拉开了帷幕。

一开始,小明直接使用制式武器​字符串替换​,通过将“233”替换为空,处理掉了所有的“233”。

str = str.replace("233", "", str)

(以上代码的打击面是字符串中包含的所有“233”子字符串)

当他码放南山、刀枪入库准备收工的时候,发现“233”居然还有无计其数的同盟,他们有“2333”、“23333”、“233333”……于是,小明取出了无差别平A碾压攻击武器​正则表达式​,准备再次向“233”的所有同盟开火,将这一类的“233”的同盟全部消灭。

str = re.sub("23+", "", str)

(以上代码的打击面是字符串中的“23”、“233”、“2333”、“23333”……)


通过这个应用,我们可以简单地将正则表达式理解为一种用来描述字符串的字符串(文本模式)。一个正则表达式可以同时对应很多个字符串,例如在上例中,正则表达式"23+"就可以同时对应“233”、“2333“、”23333”……等等。

我们将检查字符串中是否有可以对应正则表达式的子字符串的过程,称之为​匹配​。

在上例所使用的正则表达式中,“+”表示将前一个字符匹配一次或多次,因此“23+”可以匹配“23”、“233”、“2333”……。在正则表达式中,我们将类似“+”的用来描述匹配要求的字符,称之为​元字符​。其中,“+”这类描述匹配次数的元字符,称为​限制符​。

类似的,在正则表达式中还有“*”、“+”也用来表示对前一个字符的匹配次数。这类元字符的具体描述如下:

元字符

描述

*

匹配前面的子表达式任意次;例如,“23*”可以匹配“2”、“23”、“233”……

+

匹配前面的子表达式一次或多次;例如,“23+”不能匹配“2”,但可以匹配“23”、“233”……

?

匹配前面的子表达式零次或一次。例如,“23?”只能匹配“2”和“23”


但是小明突然发现,刚才的无差别平A碾压攻击如果执行,将会误伤了己方卧底“23”。本着不放弃任何一个兄弟的原则,小明修改了无差别平A碾压攻击武器,将”23”从目标范围中剔除。

str = re.sub("23{2,}", "", str)

(以上代码的打击面是字符串中的“233”、“2333”、“23333”……,但不包括“23”)


在上例所使用的正则表达式中,“{2,}”表示至少匹配2次,因此“23{2,}”可以匹配“233”、“2333“但不能匹配”23“。类似的元字符还有”{n}“、”{n,m}“。这类元字符的具体描述如下:

元字符

描述

{n}

匹配前面的子表达式确定的n次;例如,“23{2}”只能匹配“233”……

{n,}

匹配前面的子表达式至少n次;例如,“23{2,}”可以匹配“233”、“2333”……

{n,m}

匹配前面的子表达式n次到m次(其中n<=m)。例如,“23{2,3}”只能匹配“233”和“2333”


就在小明即将按下“攻击”按钮时,卧底“23”发来消息,通过他潜伏字符串中间对“233”们的劝说,位于字符串中间的“233”的立场已经动摇了,现在可以确定的仍然是坏数只有在字符串开头和结尾的“233”,希望先将这些“233”消灭杀鸡儆猴。于是小明第一次启动了无差别平A碾压攻击武器,消灭了字符串开头和结尾的“233”族群。

str = re.sub("^23{2,}|23{2,}$", "", str)

(以上代码的打击面是位于字符串开头或结尾位置的“233”、“2333”、“23333”……)


在上例所使用的正则表达式中,”^“表示匹配字符串开头、”$“表示匹配字符串结尾,”|“表示或的意思,因此可以同时匹配在字符串开头和结尾处的“233”。这类元字符的具体描述如下(请特别注意”|“的影响范围):

元字符

描述

^

匹配字符串开头,例如“^2”在“232”中就只能匹配开头处的2

$

匹配字符串结尾,例如“2$”在“232”中就只能匹配结尾处的2

x|y

匹配x或y。例如“2|33”将会匹配“2”和“33”,但不会匹配“23“或“33”;若需要匹配”23“和”33“,需要使用”[2|3]3“


在这一轮的攻击之后,”233“族群虽然受到重创,但是其中的一些“233”却选择了反击。公然打出”小明2333“、”233小明“等标语嘲讽小明,小明不堪其扰;但是因为卧底“23”仍然在劝降,不希望打击面过广。因此,小明决定针对这些点名嘲讽小明的“233”采取定点打击,同时在打击过程中不能误伤到自己的名字。

str = re.sub("(?<=小明)23{2,}|23{2,}(?=小明)", "", str)

(以上代码的打击面是:“小明233”中的“233“,”233小明“中的”233“……)


在上例使用的正则表达式中:

“(?<=小明)”表示在匹配到的“233”应该是紧接着”小明“之后的,不要前面没有”小明“的”233“;但是匹配的结果只是”233“自己,前面的“小明”只是判断条件,并不匹配到结果中。

而“(?=小明)”则表示在已经匹配到的“233”之后应该紧跟着“小明”,不要后面没有“小明”的“233”;但是匹配的结果也只有“233”自己,后面的“小明”也只是判断条件,并不匹配到结果中。

这种不匹配到结果,而是用作判断条件的匹配我们称之为​非获取匹配​。非获取匹配的具体描述如下:

元字符

描述

(?=pattern)

正向肯定预查,在任何匹配pattern的字符串开始处匹配查询字符串。例如,“233(?=小明)”可以匹配在"233小明“中的”233“,但不能匹配”233小红“中的”233“

(?<=pattern)

反向肯定预查,与正向肯定预查的方向相反。例如”(?<=小明)233“可以匹配”小明233“中的”233“,但不能匹配”小红233“中的”233“

(?!pattern)

正向否定预查,在任何不匹配pattern的字符串开始出匹配查询字符串。例如”233(?!小明)“可以匹配”233小红“中的”233“,但不能匹配”233小明“中的”233“

(?<!pattern)

反向否定预查,与反向肯定预查的方向相反。例如”(?<!小明)233“可以匹配”小红233“中的”233“,但不能匹配”小明233“中的”233“


在这一次的定点打击后,剩余的大部分”233“都成功被卧底“23”劝降了,改成”666“不再嘲讽小明了。但是其中还是有一些”233“开始隐藏自己,有的改成了”2A333“,有的改成了”2c333“,为了进一步消灭这些”233“,小明再一次调整 了无差别平A碾压攻击武器,对这类”233“进行打击。

str = re.sub("2[A-Za-z]?3{2,}", "", str)

(以上代码的打击面包括“2A33”、“2a33”、“2B33”、“2b33”……)


在上例使用的正则表达式中,“[A-za-z]”表示匹配任意大写或小写的英文字母,因此可以匹配在“2”和“33”之间添加了英文字母的”233“。类似的元字符具体描述如下:

元字符

描述

[xyz]

匹配”x“、”y“、”z“中的任意一个字符,称为字符集合

[^xyz]

匹配除了”x“、”y“、”z“以外的任意一个字符,称为负值字符集合

[a-z]

匹配“a“到”z“范围内的任意一个字符,即所有英文小写字符,称为字符范围

[^a-z]

匹配除了“a“到”z“范围内的字符以外的任意一个字符,称为字符范围

其中同一个方括号中可以包含多个字符范围,例如“[A-Za-z]”,其中的每一个字符范围都能起作用。


经过这一次的打击,仍然隐藏着的“233”非常恐惧,他们选择进一步隐藏自己,将制表符、换行符之类不可见的字符加到了2和3之间,形成了诸如“2\t33”、“2\n33”之类深度变形的“233”。但是小明作为不懂英文还可以和外国笔友交流的优秀学生,决定对这些利用不可见字符深度隐藏的“233”进行打击。

str = re.sub("2\s?3{2,}", "", str)

(以上代码的打击面包括“2\t33”、“2\n33”、“2\r33”……)


在正则表达式中,“\”表示转义,即多种编程语言中转移字符的概念。例如“\\”匹配“\”;“\\n”匹配“\n”,而“\n”则匹配换行符。转义符及其他通过转义符表示的字符的详细描述如下:

元字符

描述

\

转义标志符,将下一个字符转义。

\f

匹配换页符

\n

匹配换行符

\r

匹配回车符

\t

匹配制表符

\v

匹配垂直制表符

在上例使用的表达式中,“\s”表示匹配任意不可见字符,包括空格、换行符、制表符等;这是一个通配符,等价于”[ \f\r\n\t\v]“。其他通配符的详细描述如下:

元字符

描述

.

匹配除“\r”和“\n”以外的任意单个字符

\b

匹配单词边界的位置,即单词和空格间的位置

\B

匹配非单词边界的位置

\d

匹配数字字符

\D

匹配非数字字符

\s

匹配任意不可见的字符,等价于”[ \f\r\n\t\v]“

\S

匹配任意可见的字符

\w

匹配包含下划线的任意单词字符

\W

匹配非单词字符


经过多次对“233”的打击,“233"向小明表示无条件投降,并将所有“233”均改为“666”,小明与“233”的战争终于结束。

至此,绝大多数的正则表达式元字符都已经被介绍过了,这些元字符在学习之初并不需要全部都记下来,可以在使用的时候查表,多用用慢慢就熟了。