正则表达式作为一种有特殊含义的字符串,一方面它必须遵循python字符串的规则,另一方面它又有自己的一套规则。和字符串一样,用一些特殊的字符表达特定的含义。比如用点号.来表示任意字符,用*来表示0个或者多个重复。那既然这些字符都代表了特殊的含义,那么如何表示这些字符本身呢?我们都知道,字符串中有转义字符,例如用\n表示换行,同样\n在正则表达式中也是换行的意思,这在正则表达式中叫预定义字符,所以我们之前在讲预定义字符的时候,举了一个例子\n和\\n在正则表达式中都是换行的意思。如果你忘记了,请参照之前的章节预定义字符。
像这些在正则表达式中有特殊含义的字符,我们称之为元字符。元字符是组成正则表达式的基本语法基石,我们要写好正则表达式,这些字符的含义必须牢记于心。元这个字在汉语中有根本,本源的含义,是构成事物的基础。我们现在有元素周期表,元数据等,但大体的意思没变,所以元字符就是构成正则表达式基础语法的字符。既然这些字符在正则表达式中是元字符,那么如果想表示字符串中的该字符的本来含义,该怎么办呢?我们借用了字符串中的概念,转义。我们通过转义,让元字符表示字符串中的含义,转义和字符串中的形式是一样的,例如要表示字符串中的点号.,我们只需要在前面加上反斜杠就行,形式为:\. 。正则表达式中的元字符有不少,接下来我们来详细介绍。
10元字符
元字符包含如下字符:点号.,星号*,加号+,问号?,左圆括号(,右圆括号),左方括号,右方括号,左画括号{,右花括号},管道符|,脱字符^,美元符号$,反斜杠\。这些元字符,我们在讲之前章节的语法中都提到了,本章只是一个总结,让大家明白如果字符串有这些字符,如何用正则表达式匹配它们。其实答案很简单,就是在这些字符前面加反斜杠\,不论在什么场景下,加反斜杠都不会错,但是某些不引起歧义的场景下,我们也可以不加。那么接下来我们来详细说明。
10.1点号.,星号*,加号+,问号?
这四个符号放在一起说,因为它们都代表不确定性。点号.代表任何一个非换行的字符,星号*代表前面的字符出现0次或者多次,加号+代表前面的字符出现1次或者多次,问号代表前面的字符出现0次或者1次。接下来我们看代码:
python
import re
a = "1.25*3.14=?\n5.37+4.18=?"
b = re.findall(r'\d\.\d{2}\*\d\.\d{2}=\?', a)
c = re.findall(r'\d\.\d{2}\+\d\.\d{2}=\?', a)
d = re.findall(r'\d[.]\d{2}[*+]\d[.]\d{2}=[?]', a)
e = re.findall(r'\d[\.]\d{2}[\*\+]\d[\.]\d{2}=[\?]', a)
print("b = {}, c = {}, d = {}, e = {}".format(b, c, d, e))
# 输出结果:b = ['1.25*3.14=?'], c = ['5.37+4.18=?'],
# d = ['1.25*3.14=?', '5.37+4.18=?'],
# e = ['1.25*3.14=?', '5.37+4.18=?']
字符串a包含了这四个元字符,因为在字符串中这几个字符就是普通字符,所以我们直接写就可以了。b中我们用\d代表数字,\. 代表点号,\*代表星号,用\?代表问号,可以看到匹配到了字符串a的第一行,c和b类似,只是用\+代表加号,匹配到了a字符串的第二行。
之前我们说过,如果不会引起歧义,这么元字符可以不转义而表示字符本身,在字符类中的这四个元字符可以直接使用,而不需要转义。如d所示,我们将这四个字符方括号中表示字符类,最后结果是匹配了a的两行。
当然,你仍然可以在字符类中使用转义,如e所示,但是效果和d一样,只是显得复杂点。
10.2左圆括号(,右圆括号),左方括号,右方括号,左画括号{,右花括号}
这三种括号圆括号代表分组,方括号代表字符类以及花括号代表重复次数。如果需要匹配这些括号本身,需要在前面加\。看一下下面的代码:
python
import re
a = "[apple]{pear}(peach)"
b = re.findall(r'\[\w+\]', a)
c = re.findall(r'\{\w+\}', a)
d = re.findall(r'\(\w+\)', a)
e = re.findall(r'[{[(]\w+[])}]', a)
f = re.findall(r'[\{\[\(]\w+[\]\}\)]', a)
g = re.findall(r'[{(})]\w+[(){}]', a)
h = re.findall(r'[{([)]]\w+[(){}]', a)
i = re.findall(r'[{(){]}]\w+[(){}]', a)
#unbalanced parenthesis at position 5
#j = re.findall(r'[{[(])]]\w+[(){}]', a)
k = re.findall(r'\[\w+]', a)
k1 = re.findall(r'\{\w+}', a)
#k2 = re.findall(r'\(\w+)', a)
print("b = {}, c = {}, d = {}, e = {}, f = {}, g = {}, h = {}, \
i = {}, k = {}, k1 = {} ".format(b, c, d, e, f, g, h, i, k, k1))
# 输出结果:b = ['[apple]'], c = ['{pear}'],
# d = ['(peach)'], e = ['[apple]', '{pear}', '(peach)'],
# f = ['[apple]', '{pear}', '(peach)'], g = ['{pear}', '(peach)'],
# h = [], i = [], k = ['[apple]'], k1 = ['{pear}']
上面字符串a包含了方括号,花括号以及圆括号。
b通过\来匹配左方括号,\\来匹配右方括号,\w来匹配单词,所以可以得到apple这样的结果。c和d的道理是一样的。
e把左括号和右括号分别放在了字符类中,仍然可以达到我们想要的效果,可以看到e匹配到了三个结果。原因我们在10.1已经说了,在字符类中这些都表示字符本身,只要不引起歧义,所以不需要转义也可以。在f中我们用了转义,达到的效果是一样的。
但是,方括号是需要特别关注的,因为字符类本身就是在方括号开始和结束的,我们来看看e为什么可以成功。首先e中正则出现的第一个字符类是{\[(,我们可以看到字符类中包含的是左方括号,这个在字符类中不会引起歧义,原因是我们第一个左方括号代表字符类的开始,然后遇到第二个左方括号,因为要寻找字符类的边界右方括号,所以这个第二个左方括号会当作是普通字符,然后继续寻找,寻找到了第一个右方括号,因为字符类不允许嵌套,立马结束,所以此时第一个字符类中的左方括号会当作是普通字符,不会产生歧义。第二个字符类和第一个又有不同,因为第二个字符类第一个左方括号开始,接下来就碰到了右方括号,python会把紧跟随左方括号的右方括号当作一个普通字符。所以后面的})都会被当成普通字符直到碰到新的右方括号。
g是字符类中没有方括号的情况,结果也是符合预期的。
h的问题是第一个字符类遇到第一个右中括号就结束,然后剩下],a中没有两个括号连续的情况,所以结果为空。
i的问题和h类似,第一个字符类中的右中括号和字符类开始的左中括号配成了一对,导致剩下的字符}]被当作普通字符对待,字符串a中没有匹配}]这两个连续字符的子串,所以结果为空。
j会产生错误,根源是第一个字符类中遇到第一个右中括号会认为是字符类的边界,此时后面紧跟右圆括号,这个圆阔号是分组的标志,在字符类外会当成元字符,而不是普通字符,最后曝出的错误是右圆括号)不匹配,相比较,h没有报错,是因为h中圆括号都被包含在了字符类中。
对于左括号,肯定都是字符类,分组,量词的开始,所以在字符类外必须转义,而右括号的情况却不同,如果右括号出现之前没有元字符左括号,那么会被当作普通字符,但是分组是一个例外,k,k1,k2说明了这个问题,k2的错误和j是一样的,所以在字符类外面右圆括号也必须转义,虽然我们认为不会引起歧义,但是这确实是一个例外。
当然说了这么多,并不是为了让大家都熟记所有这些特殊场景,如果你自己写正则,对元字符进行显示转义是优先的写法,但是你遇到其他人可能会有这种不转义的写法,当你读到代码的时候,知道这是怎么回事就行了。
10.3管道符|,脱字符^,美元符号$,反斜杠\
管道符号|用来表示逻辑与,脱字符^,在字符类外表示字符串或者行的开始,在字符类内表示非,美元符号$表示字符串或者行的结束,反斜杠\用来表示转义,有面必修跟符合转义的字符。这四个元字符脱字符^和反斜杠\在字符类内和字符类外都是元字符,所以必须都要用显示转义,其他两个字符在字符串内如果没有引起歧义可以不转义,我们看下下面的代码:
python
import re
a ="^apple$pear|peach\\\n"
b = re.findall(r'\^\w+\$', a)
c = re.findall(r'\|\w+\\', a)
d = re.findall(r'[^]\w+[$]', a)
e = re.findall(r'[|]\w+[\\]', a)
f = re.findall(r'[\^]\w+[\$]', a)
g = re.findall(r'[\|]\w+[\\]', a)
h = re.findall(r'[^^\\$|]\w+[^^\\$|]', a)
i = re.findall(r'[\^\\$|]\w+[\^\\$|]', a)
#unterminated character set at position 6
#j = re.findall(r'[|]\w+[\]', a)
#unterminated character set at position 0
#k = re.findall(r'[^]\w+\$', a)
#bad escape \g at position 1
#l = re.findall(r'[\g]\w+\\', a)
print("b = {}, c = {}, d = {}, e = {}, f = {}, g = {}, h = {}, \
i = {} ".format(b, c, d, e, f, g, h, i))
# 输出结果:b = ['^apple$'], c = ['|peach\\'],
# d = ['^', '|', '\\', '\n'], e = ['|peach\\'],
# f = ['^apple$'], g = ['|peach\\'],
# h = ['apple', 'pear', 'peach'], i = ['^apple$', '|peach\\']
b和c都是用了转义,将这四个元字符转义成普通字符,所以从结果来看都匹配到了,符合预期。
d的结果其实很奇怪,我们将^放到字符集中,感觉应该和b的结果一样才对,之前我们讲了,^在字符类内也是元字符,所以它将本来应该是字符类结束的右中括号]变成了普通字符,直到遇到了下一个右中括号,所以d的含义变成了要匹配单个字符,这个字符不应该是:],\w,+,[,$中的一个。所以我们得到了:^, |, \, \n。
e中字符类中的|成了普通字符,我们在字符集中用\\代表\,所以匹配到了正确的结果。g和e效果是一样的,只是用反斜杠\对管道符|进行了转义。
f在字符集中对脱字符^和美元符进行了转义,所以匹配到了\^apple。
h并没有对脱字符转义,但是通过两个脱字符达到了转义的效果,只是正则的意思变为匹配的字符串前后不能包括这四个字符。
i的意思刚好和h相反,匹配的字符串前后必须出现这四个字符中的一个。
j会报错,因为字符集中直接写入反斜杠是作为元字符,他转义了右中括号,导致字符类找不到闭合的右中括号,从而报错。
k是因为脱字符将紧随其后的右中括号变成了普通字符,到导致字符类找不到闭合的右中括号包括。这其实是d的出错版,d没有出错是因为恰好找到了一个右中括号,但是改变了正则表达式的本意。
l会报错事因为\在字符类中是元字符,它必然需要转义紧挨着它字符,但是\g在正则表达式中没有定义,所以会出错。
讲到这里,这一章就讲完了,恭喜你,已经完全学会了正则表达式的所有语法。但是关于正则表达式还没有结束。之前的章节我们只是粗略的讲了pythonn的正则函数,下一章我们讲详细讲几个重要的函数,并且总结一下内联标志和内联模式的对应。