Python3 - 正则表达式

1 简介

字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的 Email 地址。虽然可以编程提取 @ 前后的子串,再分别判断是否是单词和域名,但这样做不但麻烦,而且代码难以复用。

正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。

2 正则表达式中的特殊字符

  • '^' 匹配以某个字符串开头的字符串或者行。比如:'abc'匹配'abcd',等等,'匹配以数字开头的字符串。

  • '$' 匹配以某个字符串结束的字符串或者行。比如:'abc$'匹配'dabc',等等,'\d$'匹配以数字结束的字符串。

  • '|' 匹配两个字符串中的任意一个。比如:'abc|def'匹配'abc'或者'def'。 > 注意:一旦匹配|左边的字符串,若匹配成功则停止匹配,否则,匹配|右边的字符串。

  • '[]' 匹配中括号中所示字符集里的任意一个字符。比如:'[abc]‘匹配a,或b,或c。又比如:'[a-c]'匹配a至c字符集里的任意一个字符。 > 注意:中括号中的字符的特殊字符将失去特殊意义。比如:'[]',匹配的是''或者'w'。

  • '[^]' 匹配中括号中除了字符集之外的任意一个字符。比如:'[^abc]'匹配f,等等。就是匹配a,和b,和c之外的一个字符。'[^a-c]'同样如此。 > 注意:'^'只有在中括号里时,才有这种意义。若在其他地方,则为另外的含义,将在后面介绍。

  • '*' 可以匹配前导字符出现零次或多次的字符串。比如:'abc*'匹配'ab',或者'abc',或者'abcc',等等。

  • '+' 可以匹配前导字符出现至少一次的字符串。

  • '?' 可以匹配前导字符出现0次或者1次的字符串。

  • ' 匹配一个数字,相当于'[0-9]'。比如:'00可以匹配'007',但无法匹配'00A'。

  • '' 匹配一个非数字,相当于'[^\d]'。

  • '' 可以匹配一个单词字符。由于python3中的字符默认编码为Unicode,因此可以匹配一个汉字,以及ASCII单词字符,也即'[a-zA-Z0-9_]'中的一个字符。比如:'可以匹配'py3'。

  • '' 匹配一个非单词字符。相当于'[^\w]'。

  • '' 可以匹配一个空白字符。包括ASCII中的空白字符'[ ',以及其他空白字符。

  • '' 匹配一个非空白字符。相当于'[^\s]'。

  • '.' 可以匹配任意一个字符。比如:'py.'可以匹配'pyc'、'pyo'、'py!'等等。

上面的是基本正则表达式支持的元字符。下面是扩展表达式支持的元字符:

  • '()' 把多个字符组合在一起当作一个整体。比如:'(abc){2}'匹配'abcabc'。

  • '\id' 与 '()' 连用,表示引用前面出现的括号里的内容。id 表示编号,表示前面第 id 个括号的内容,从 1 开始。

  • '{m}' 可以匹配前导字符出现m次的字符串。

  • '{m, n}' 可以匹配前导字符出现m至n次的字符串。若m省略,表示可以匹配前导字符0至n此。若n省略,表示可以匹配前导字符m至无限次。

3 正则表达式中的元字符

. ^ $ * + ? { } [ ] \ | ( )

4 字符串匹配的一般步骤

根据给定的(要匹配的)字符串(在编程时,以str类型或者bytes类型表示),首先需要知道它应该由哪些字符进行匹配(每个字符可能有多种匹配方式)。然后生成相应的正则表达式。对于元字符,需要加上额外的""转变为原义。然后在编程时通过str类型或者bytes类型进行表示。

5 如何生成一个正则表达式

比如,对于字符"\"(它在编程时表示为"\\"),那么可以通过"."匹配它,也可以通过"\"匹配它,当然还可以通过其他形式匹配。若是通过"\"匹配它,由于它是上面提到的元字符,因此对应的正则表达式不能是"\",需要用额外的"\"转变为原义,所以只能是"\"。

再比如,对于字符"."(他在编程时表示为"."),它可以通过"\s"匹配它,也可以通过"."匹配它。若是通过"."匹配它,由于它是上面提到的元字符,因此对应的正则表达式不能是".",需要用"\"转变为原义(否则它表示的语义为任意一个字符,而非特定的"."),所以只能是"\."。

再比如,对于换行符(它在编程时表示为"\n"),它可以通过"\s"匹配它,那么对应的正则表达式为"\s"。

再比如,对于字符串"\d"(它在编程时表示为"\\d"),它可以通过很多方式进行匹配。若是通过"\d"匹配它,由于"\"是元字符,因此对应的正则表达式为"\\d"。

6 正则表达式在编程时的表示

无论是一般的字符串还是正则表达式,在编程时,都是以str类型或者bytes类型表示。

当由上面的方法生成一个正则表达式后,在编程时,还要正确写成str类型或者bytes类型。

比如,对于"\"(它在编程时为"\\")的正则表达式"\\",它在编程时,若以str类型表示,由于"\"在编程时表示为"\",因此"\\"在编程时应该表示为"\\\\"。

再比如,对于"."(它在编程时为".")的正则表达式为"\.",它在编程时,若以str类型表示,由于"\"在编程时表示为"\\",因此"\."在编程时应该表示为"\\."。

再比如,对于字符串"\d"(它在编程时表示为"\d")的正则表达式"\\d",若以str类型表示,由于"\"在编程时表示为"\\",因此"\\d"在编程时应该表示为"\\\\d"。

可以发现,若以这种方式来进行表示,会写很多"\",因此,在编程时强烈建议在str类型或者bytes类型前加上r,表示原生字符串(raw string),从而无需在编程时加上额外的"\"来表达"\"。

比如,正则表达式"\\",在编程时可以表示为r"\\";正则表达式"\."在编程时可以表示为r"\."。正则表达式"\\d",在编程时可以表示为r"\\d"。

7 re模块

7.1 re.compile()

运用re.compile()对正则表达式进行编译,得到一个正则表达对象后,就可以调用这个对象的match()方法或者fullmatch()方法去匹配字符串,search()方法去搜索字符串,split()方法去分割字符串,当然还有其他操作。

1
2
3
4
5
6
import re

str1 = r"\\"

# 编译正则表达式得到一个正则表达对象。
temp = re.compile(str1)

通过这样两个步骤来进行匹配等操作的好处在于,如果要通过同一个正则表达式去操作多个字符串时,不用多次进行编译。而且,还可以在编译得到正则表达对象后指定一些特别的参数。

7.2 正则表达对象的match()方法与re.match()

如果匹配成功,则返回match对象,否则,返回None。

具体参数为:regex.match(string[, pos[, endpos]])与re.match(pattern, string, flags=0)。只有前者能够指定字符串中的起始位置与结束位置的下一个位置,默认为从字符串的首字符开始匹配,直至末尾字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

str1 = r"\\"

# 编译正则表达式得到一个正则表达对象。
temp = re.compile(str1)

# 再用这个正则表达对象去匹配字符串。
# <_sre.SRE_Match object; span=(0, 1), match='\\'>
print(temp.match("\\abcd"))

# <_sre.SRE_Match object; span=(0, 1), match='\\'>
print(re.match(str1, "\\abcd"))

注意:无论是正则表达对象的match()方法,还是re.match(),如果正则表达式匹配正确匹配结束后,字符串还有未被匹配的字符,则也视作匹配成功。若要进行完全匹配,则可以在正则表达式末尾加上$符号,也可以使用正则表达对象的match()方法或者re.fullmatch()。

7.3 正则表达对象的fullmatch()方法与re.fullmatch()

与字符串进行完全匹配。如果正则表达式成功匹配结束后,字符串中还有未被匹配的字符,则返回None。

具体参数为:regex.fullmatch(string[, pos[, endpos]])与re.fullmatch(pattern, string, flags=0)。只有前者能够指定字符串的起始位置和结束位置的下一个位置,默认为整个字符串。

1
2
3
4
5
6
7
8
9
10
import re

str1 = r"\\"

temp = re.compile(str1)
# None
print(temp.fullmatch("\\abcd"))

# <_sre.SRE_Match object; span=(0, 1), match='\\'>
print(re.fullmatch(str1, "\\"))

7.4 正则表达对象的search()方法与re.search()

用于查找字符串中是否存在可以匹配成功的子串。若存在,则返回一个match对象,否则,返回None。

具体参数为:regex.search(string[, pos[, endpos]])与re.search(pattern, string, flags=0)。只有前者能够指定字符串中的起始位置与结束位置的下一个位置,默认为整个字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

str1 = r"\\"

temp = re.compile(str1)

# None
print(re.match(str1, "abc\\abc"))

# <_sre.SRE_Match object; span=(3, 4), match='\\'>
print(temp.search("abc\\abc"))

# <_sre.SRE_Match object; span=(3, 4), match='\\'>
print(re.search(str1, "abc\\abc"))

7.5 正则表达对象的split()方法与re.split()

按照能够匹配的子串将string分割后返回列表。

具体参数为:regex.split(string, maxsplit=0)与re.split(pattern, string, maxsplit=0, flags=0)。 两者均有一个maxsplit参数,用于指定最大分割次数,默认为尽可能多次地进行分割。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

str1 = r"\s+"
temp = re.compile(str1)

# 尽可能多次地进行分割。['a', 'bc', 'def']
print(temp.split("a bc def"))
# 最多分割1次。['a', 'bc def']
print(temp.split("a bc def", 1))

# 尽可能多次地进行分割。['a', 'bc', 'def']
print(re.split(str1, "a bc def"))
# 最多分割1次。['a', 'bc def']
print(re.split(str1, "a bc def", 1))

需要注意的是,如果pattern用一个()扩起来,那么得到的结果包含了匹配成功的子串。

1
2
3
4
5
# ['Words', 'words', 'words', '']
print(re.split('\W+', 'Words, words, words.'))

# ['Words', ', ', 'words', ', ', 'words', '.', '']
print(re.split('(\W+)', 'Words, words, words.'))

7.6 正则表达对象的findall()方法与re.findall()

搜索string,以列表形式返回字符串中全部能匹配的子串。 具体参数为:regex.findall(string[, pos[, endpos]])与re.findall(pattern, string, flags=0)。只有前者能够指定所搜寻的字符串的起始与结束位置的下一个位置。

1
2
3
4
5
6
7
8
9
10
11
12
import re

str1 = r"\d"
temp = re.compile(str1)

# ['1', '2', '3']
print(temp.findall("abc 1 23 def"))
# ['1']
print(temp.findall("abc 1 23 def", 0, 5))

# ['1', '2', '3']
print(re.findall(str1, "abc 1 23 def"))

7.7 正则表达对象的finditer()方法与re.finditer()

搜索string,返回一个顺序访问字符串中的每一个匹配结果(Match对象)的迭代器。

具体参数为:regex.finditer(string[, pos[, endpos]])与re.finditer(pattern, string, flags=0)。前者能够指定字符串中的起始位置与结束位置的下一个位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import re

str1 = r"\d"
temp = re.compile(str1)

# <_sre.SRE_Match object; span=(4, 5), match='1'>
# <_sre.SRE_Match object; span=(6, 7), match='2'>
# <_sre.SRE_Match object; span=(7, 8), match='3'>
for item in temp.finditer("abc 1 23 def"):
print(item)

# <_sre.SRE_Match object; span=(4, 5), match='1'>
for item in temp.finditer("abc 1 23 def", 0, 5):
print(item)

# <_sre.SRE_Match object; span=(4, 5), match='1'>
# <_sre.SRE_Match object; span=(6, 7), match='2'>
# <_sre.SRE_Match object; span=(7, 8), match='3'>
for item in re.finditer(str1, "abc 1 23 def"):
print(item)

7.8 正则表达对象的sub()方法与re.sub()

使用给定的字符串repl替换字符串string中每一个成功匹配的子串后,返回替换后的字符串。

具体参数为:regex.sub(repl, string, count=0)与re.sub(pattern, repl, string, count=0, flags=0)。两者均有一个count参数,用于指定最多替换次数,默认时全部替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

str1 = r"\d"
temp = re.compile(str1)

# 全部替换。abc xxx xxxxxx def
print(temp.sub("xxx", "abc 1 23 def"))
# 只替换1次。abc xxx 23 def
print(temp.sub("xxx", "abc 1 23 def", 1))

# 全部替换。abc xxx xxxxxx def
print(re.sub(str1, "xxx", "abc 1 23 def"))
# 只替换1次。abc xxx 23 def
print(re.sub(str1, "xxx", "abc 1 23 def", 1))

单词去重:

1
2
3
4
5
import re

str1 = r"hello world world nju nju haha"

print(re.sub(r"(\w+) \1", r"\1", str1))

8 数量词的贪婪匹配与非贪婪匹配

所谓的数量词,就是指: * + ? {m} {m,n}

默认情况下,它们都是贪婪匹配,也就是对于数量词,尽可能多地匹配字符串中的字符。 比如:对于正则表达式"+0*",如果用于findall()查找"a0b0c0",由于""后面是"+",它会尽可能多匹配单词字符,在这里也就是将"a0b0c0"中的所有字符都匹配成功,然后"0*"将匹配0个"0",于是最后得到结果为["a0b0c0"]。

但是如果想匹配成一个字母和一个0的组合该怎么办呢?这时,就要用到非贪婪匹配。 非贪婪匹配就是说,对于数量词,尽可能少地匹配字符串中的字符。只需要在数量词后面加上"?",就可以转换成非贪婪匹配。如下:

1
2
3
4
5
6
#import re

# ['a0b0c0']
print(re.findall(r"\w+0*", "a0b0c0"))
# ['a0', 'b0', 'c0']
print(re.findall(r"\w+?0*", "a0b0c0"))

Reference