转载的老板请注明出处:http://blog.youkuaiyun.com/cc_xz/article/details/78694376万分感谢!
在本篇中,你将了解到:
1.re模块常用函数。
2.常见用的元字符。
3.重复匹配。
4.位置匹配。
5.子表达式。
正则表达式是一种小型的、高度专业化的字符串处理语言,在Python中,通过内嵌的re模块,可以直接使用正则表达式的匹配规则。由C语言编写的匹配引擎来执行。
re模块常用函数:
findall()遍历字符串函数:
re.findall()函数可以遍历匹配源字符串中所有的字符,然后将符合匹配规则的字符返回成一个列表。
格式:
re.findall( pattern , string ,flags = 0 )
re.findall( 正则规则 ,源字符串 ,标记 )
import re # 导入正则表达式模块
text = "这是一段字符串,aGb8cD8D1S192"
# \d是匹配所有的数字,而+是完整结果,例如不加+则为:[1,9,2]。
list01 = re.findall("\d+",text) # 首先定义正则匹配规则,然后将一个字符串作为参数。
print(list01)
输出结果为:
['8', '8', '1', '192']
compile()生成正则对象:
re.compile()函数用来编译正则匹配规则,将其返回成一个对象,这样就可以把一些常用的正则匹配规则编译成对象,多次使用。
格式:re.compile( pattern , flags = 0 )
re.compile ( 正则规则 , 编译标志位 )
其中,编译标志位用于修改正则表达式中的匹配方式,例如,是否区分大小写、匹配多行等。常用的内置flags有:
- re.S 使正则表达式匹配包含换行符在内的所有字符。
- re.I 使正则表达式匹配忽略大小写。
import re
text = "这是一段字符串,aGb8cD8D1S192"
# \D是匹配所有非数字的字符,+是将符合条件的字符集成一个元素,而不是分开的。
express = re.compile("\D+") # 将正则匹配规则通过comile()函数生成对象。
list01 = re.findall(express, text) # 在findall中应用规则。
print(list01)
输出结果为:
['这是一段字符串,aGb', 'cD', 'D', 'S']
match()匹配一次函数:
import re
text = "这是一段字符串,aGb8cD8D1S192"
express = re.compile("\D+")
# match()只要在源字符串中成功匹配一次,就直接返回匹配成功的字符串,而不继续匹配。
string = re.match(express, text).group() # group()函数会返回匹配成功的一个或多个结果。
print(string)
# 如果正则匹配规则没有正确的匹配到字符,则会报出AttributeError异常。
输出结果为:
这是一段字符串,aGb
re.match()与re.search()的区别::
import re
text = "这是一段字符串,aGb8cD8D1S192"
express = re.compile("\d+")
string01 = re.match(express,text)# 只检索源字符串起始端。
string02 = re.search(express,text).group() #在整个源字符串中检索。
print(string01,type(string01)) # 如果没有正常匹配到,则返回None
print(string02,type(string02))
输出结果为:
None <class 'NoneType'>
8 <class 'str'>
- group() 返回被 RE 匹配的字符串
- start() 返回匹配开始的位置
- end() 返回匹配结束的位置
- span() 返回一个元组包含匹配 (开始,结束) 的位置
finditer()返回迭代器 :
yield是类似return的关键字,只不过这个函数是返回一个生成器。当你调用这个函数的时候,函数内部的代码并不会立刻执行。这个函数会返回一个生成器对象。而真正执行这个函数的时候,是使用for等循环进行迭代的时候。
import re
text = "这是一段字符串,aGb8cD8D1S192"
express = re.compile("\D+")
# finditer()函数会将返回匹配到的结果作为一个迭代器返回。
iter01 = re.finditer(express, text)
for x in iter01:
print(x) # 输出迭代器对象。
print(x.group()) # 输出当前已迭代出的被正则匹配规则筛选出的结果。
print(x.span()) # 输出当前结果在源字符串中的起止位置。
输出结果为:
<_sre.SRE_Match object; span=(0, 11), match='这是一段字符串,aGb'>
这是一段字符串,aGb
(0, 11)
<_sre.SRE_Match object; span=(12, 14), match='cD'>
cD
(12, 14)
<_sre.SRE_Match object; span=(15, 16), match='D'>
D
(15, 16)
<_sre.SRE_Match object; span=(17, 18), match='S'>
S
(17, 18)
split()分割字符串函数:
import re
text = "这是一段字符串,aGb8cD8D1S192"
express = re.compile("\d+")
# split()函数会根据指定的分割表达式,对源字符串进行分割,例如上例中定义使用数字类型的字符来进行分割。
string = re.split(express,text)
print(string)
输出结果为:
['这是一段字符串,aGb', 'cD', 'D', 'S', '']
sub()替换字符函数:
import re
text = "这是一段字符串,aGb8cD8D1S192"
express = re.compile("\d+")
# sub()函数可以根据定义的正则匹配规则,将对应的字符筛选出,然后使用第二个参数(repl)对筛选出的结果进行替换操作。
string = re.sub(express,"囧",text)
输出结果为:
这是一段字符串,aGb囧cD囧D囧S囧
常见元字符:
完整的正则表达式由两种字符沟通:特殊字符,例如前面案例中的\d等,这被称为元字符,另一种是普通的文本字符。如果说正则表达式是一门语言,那么其中的普通文本字符是其中的文字,那么元字符就是这门语言的语法。根据语法,可以将不同的文字拼接起来,从而得到无穷尽含义的词语、句子。
行的起始和结束:
最容易理解的元字符就是”^”和”$”了,在检查一行文本时,”^”表示一行的开始,而”$”表示一行的结束。但要注意的是,如果匹配的文档中,存在多行(使用\n换行符),那么”^”和”$”会分别去匹配每一行的开头和结尾。
案例如下:
text = "booooooobbbbbby"
print(re.findall("bo", text)) # 在文本的任意位置中找出“bo”这两个字符。
print(re.findall("^bo", text)) # 在文本的行首位置找出“bo”这两个字符。
print(re.findall("bo$", text)) # 在文本的行尾位置找出“bo”这两个字符。
输出结果为:
['bo']
['bo']
[]
正确阅读上述正则表达式的方法是:
- “^bo” 匹配的是如果一行字符串的第一个字符是b,而b的下一个字符是o,表示匹配成功。
而不是: - “^bo” 匹配的是以cat开头的一行字符。
上述两种理解的结果并无差异,但按照第一种方式更容易明白新遇到的正则表达式的内部逻辑。”^”和”$”的特殊之处在于,它们匹配的是一个位置,而不是具体的某个文本。
字符集“[ ]”:
如果我们希望搜索一个单词,但又不确定这个单词具体的写法,那么可以使用正则表达式中的结构体。它允许使用者列出在某处希望匹配的字符。通常情况下:
“o”只能匹配字符o,而”b”只能匹配字符b。而使用字符集:[ob]则表示既可以匹配o,也可以匹配b。
text = "booooooobbbbbby"
print(re.findall("[bo]bb", text))
输出结果为:
['obb', 'bbb']
”[bo]bb”表示,先找个一个b或o,如果这个b或o后面连续跟着两个b,则匹配成立。值得注意的是,在字符集之外,所有的普通字符除了包括匹配自身的含义外,还表示“接下来要匹配X”的意思。例如obb,则表示,首先匹配一个o,接下来匹配一个b,最后再匹配一个b(如果不目标文本中不存在这样的字符串,则匹配失败返回空白)。
当然在一个字符集中可以同时列举多个字符,例如:
text = "<H1><H2><H3><H4><H5>"
print(re.findall("<H[1-5]>", text)) # "-"被称为连字符,是元字符的一种,它表示从1一直到5。即:[12345]。
text01 = "<A1><B1><c1><D1><e1><F2>"
print(re.findall("<[A-F][1-2]>", text01)) # 字母也可以使用连字符连接起来。
print(re.findall("<[A-Fa-f][1-2]>", text01)) # 一个字符集中可以拥有多个连字符。
输出结果为:
['<H1>', '<H2>', '<H3>', '<H4>', '<H5>']
['<A1>', '<B1>', '<D1>', '<F2>']
['<A1>', '<B1>', '<c1>', '<D1>', '<e1>', '<F2>']
但值得注意的是,只有在字符集中,”-“才是一个元字符,如果不在字符集中,它就是一个普通的文本字符。而且,即使是在字符集内部,它也不一定就是元字符。如果连字符”-“出现在字符集的开头,则表示它只是一个普通的字符。同样的,一些常用的元组,例如”.”和”?”,在字符集中就只是普通的文本字符。
排除字符用法:
当”^”出现在字符集的第一位时,就表示会返回不存在于字符集中文本字符的结果,例如:
text = "<H1><H2><H3><H4><H5><Ha>"
print(re.findall("<H[^3-5]>", text))
输出结果为:
['<H1>', '<H2>', '<Ha>']
如果”^”出现在字符集中,则不再表示行首如何,而是表示该字符集会匹配出所有字符集中不包含的字符,在上述案例中,字符集中包含了3、4、5,而在加上”^”后,实际输出为H+非3、4、5的字符。
也就是说,排除型字符集表示:“匹配一个未列出的字符”。
匹配任意子表达式:
”|”是一个表示”或”的元字符,使用它可以将不同的子表达式组合成一个总的表达式,而这个总表达式又能根据文本匹配到任意的子表达式。例如:”re1”和”re2”分别是两个正则表达式,那么在使用中,”re1|re2”就能够同时匹配其中任意一个正则表达式,在这样的使用方式中,子表达式被称为”多选分支”。
例如:
text = "admin test data"
print(re.findall("admin|test|data", text)) # 匹配结果全部符合。
print(re.findall("^(test|admin|data)", text)) # 首先匹配一个^,如果行首是test或admin或data,则将实际的行首返回。
输出结果为:
['admin', 'test', 'data']
['admin']
再来看下述案例:
print(re.findall("(admum|admin)", text))
print(re.findall("(ad[n|m]in)", text))
输出结果为:
['admin']
['admin']
上述案例中的结果是相同的,但值得注意的是,一个字符集组只能匹配目标文本中的单个字符,而每个多选结构自身都是完整的正则表达式。而,在字符集中”|”只是一个字符,并不代表”或”
可选项元素:
元字符”?”表示可选项,把它放在一个字符的后面,就表示允许此处出现这个字符,但它是否出现并非匹配成功的必要条件。例如:
text = "admin adminis admini"
print(re.findall("adminis", text))
print(re.findall("admini?s?", text))
输出结果为:
['adminis']
['admin', 'adminis', 'admini']
如果单纯的匹配”adminis”,那么由于admin不包含is,所以无法成功匹配,但若使用”admini?s?”,则表示,可以直接成功匹配admin,但如果admin后面有包含i或s的单词,也可以成功匹配。
匹配空白字符:
元字符大致可以分为两种,一种是用来匹配文本的,例如”.”,另外一种是正则表达式的语法要求,例如”[ ]”。而在进行正则表达式搜索的时候,可能会遇到需要将原始文本中的非打印(实际存在,但打印时并不会将其打印出来的,例如换行符\n)空白字符进行匹配。
text01 = """requirement
compile
matches
empty
target"""
print(re.findall("\n",text01))
输出结果为:
['\n', '\n', '\n', '\n', '\n']
除\n代表换行符外,其他的空白元字符为:
- \f:换页符
- \n:换行符
- \r:回车符
- \t:制表符(Tab键)
- \v:垂直制表符
而在前面看到的元字符中,”.”和”[ ]”都是元字符,但前提是没有对它们进行转义,而”f”和”n”也是元字符,但必须对他们进行转义,如果没有转义,那么他们就只是普通字符,只能匹配他们本身。
匹配特定的字符类别
字符集(匹配多个字符中的某一个)是最常见的匹配方式,而一些常用的字符集合可以用一些特殊元字符来表示(更精简),这些特殊的元字符匹配的是某一类别的字符。
匹配数字与非数字:
你可以选择使用[0-9]来表示从0到9的数字,这可以匹配任何一个数字,如果希望匹配除数字以外的其他内容,也可以使用[^0-9]来表示,而使用元字符表示如下:
text = "Hello python2,this regular expression in Python3。"
print(re.findall("[pP]ython\d", text))
print(re.findall("He\Dlo",text))
输出结果为:
['python2', 'Python3']
['Hello']
可以看到,正则表达式的语法是区分字母大小写的,\d表示匹配数字,而\D的含义与之相反。
匹配任意字母和数字与非字母和数字:
text = "aDM0i3n,cang1。"
print(re.findall("\w", text)) # \w等同于[A-Za-z0-9_]
print(re.findall("\W", text)) # \W等同于[^A-Za-z0-9_]
输出结果为:
['a', 'D', 'M', '0', 'i', '3', 'n', 'c', 'a', 'n', 'g', '1']
[',', '。']
匹配任意空白字符:
text = "\ttest\ntest01 \r test02"
print(re.findall("\s", text))
print(re.findall("\S", text))
# 值得注意的是,在正则表达式中,空格也算是空白字符。
输出结果为:
['\t', '\n', ' ', '\r', ' ']
['t', 'e', 's', 't', .....省略部分结果... 't', '0', '2']
在使用这种方式时,需要注意的是,在将各个列表组合成元组时,如果列表的长度不相等,那么元组的长度将以最短的列表的长度为基准。
重复匹配
在前边学习的元字符中,每个例子都有一个很致命的限制,也就是我们写出的正则表达式都是只能匹配一个字符,例如,如果我们希望匹配一个邮箱,那么邮箱的格式为:
cangl@qq.com
如果使用前面的知识,那么可以使用:
“\w@\w.\w”
由于\w可以匹配所有的字母和数字,所以这个正则表达式本身没有任何错误,但问题在于,它几乎没有任何实际的用户,即:它只能匹配:
x@x.x
的邮箱,导致这个结果的关键就是,\w只能匹配单个的字符,而我们无法预知某个电子邮箱会有多少个字段。
匹配一个或多个字符:
如果希望所匹配的字符或字符集重复多次,只需要在该字符或字符集的后面加上一个”+”即可,”+”用来匹配一个或多个字符(至少一个,如果该字符为0,则匹配失败)。
例如字符a只能匹配a本身,而a+将匹配一个或多个连续出现的a。同样的,”[0-9]”表示匹配单个任意数字,而”[0-9]+”表示匹配一个或多个连续的数字。
值得注意的是,在给一个字符集添加”+”为后缀时,必须把”+”放在这个字符集的外面。例如”[0-9]+”表示匹配一个或多个连续的数字,而”[0-9+]”则定义了一个由数字0至9以及一个字符+组成的字符集,所以只能匹配一个单独的数字或一个纯粹的加号。
text = "Hello,this regular expression in Python。There is a mailbox at cangL@qq.com"
print(re.findall("\w+@\w+\.\w+", text))
输出结果为:
['cangL@qq.com']
如果希望理解上述的表达式,那么就要知道,”\w”可以匹配任意的字母、汉字,但无法匹配”,”逗号以及空格。所以上述表达式可以成功匹配。
但仍然有一个问题没有解决,虽然上述这种标准的邮箱可以成功匹配,但有一些带有子域名等情况的域名,例如:cangL@test.qq.com,这种域名完全合法,但是上述表达式并不会将其成功筛选出来。
text = "cangL@qq.com cangL@test.qq.com cl.cangL@qq.com cl.cangL@test.qq.com"
print(re.findall("\w+@\w+\.\w+", text)) # 原有的正则表达式无法完全的将不常见但合法的邮箱完整的匹配出来。
print(re.findall("[\w.]+@[\w.]+\.[\w.]+", text)) # 使用字符集的方式,以"@"和一个"."为节点,而不是所有的".",来匹配不常见但合法的邮箱。
输出结果为:
['cangL@qq.com', 'cangL@test.qq', 'cangL@qq.com', 'cangL@test.qq']
['cangL@qq.com', 'cangL@test.qq.com', 'cl.cangL@qq.com', 'cl.cangL@test.qq.com']
而造成上述情况的原因,是因为在构造第一个正则表达式时,只想到在”@”后面只有一个”.”字符来分开两个字符串,而完全没有想到,在”@”之前可能会有”.”,甚至”@”后面会有多个”.”
匹配零个或多个字符:
”+”匹配一个或多个字符,但无法匹配零个字符,即必须匹配至少一个字符,否则将视为匹配失败。而元字符”“同”+”的用法一致,但”“表示前面的字符可以匹配零次或多次。
元字符”.”是用来匹配任意字符的。
text = "010/199+76"
print(re.findall("010.199.76", text)) # 这里的"."是元字符。
print(re.findall("010[+./]199[+./]76", text)) # 这里的"."不是元字符。
输出结果为:
['010/199+76']
['010/199+76']
虽然上述两个表达式都将结果正确的匹配出来了,但在字符集”[ ]”中,”.”并不是一个元字符,只是用来在文本中匹配一个.。但需要注意的是,表达式:”010.199.76”,同时也能对”010199076”匹配成功。所以”010[+./]199[+./]76”所匹配的结果更加精确,但更难读写,而”010.199.76”更容易理解,但匹配精确率不高,选择哪个表达式,取决于对需要检索的文本的了解,以及所需要的精确程度。而常见的问题是,在写正则表达式时,我们需要对需检索的文本的了解程度和检索精确性之间把握平衡。
例如,如果在检索某段文本时,使用表达式:”010.199.76”只会出现我们希望的结果,而不会匹配到其他非希望的结果,那么使用它就是合理的。反之,如果这样会匹配出我们不希望出现的结果,那么就需要修改表达式。
# 虽然"."在邮箱中是一个合法地址,但如果"."位于邮箱的首位,那就是非法非法字符了。
text = ".cangL.test@qq.com"
""" 正则表达式解析:
使用一个"\w"表示,邮箱的第一位字符必须是一个数字、字母,但不能是一些符号。
后面使用"+"表示邮箱的起始必须是至少1位的。此例中"\w+"匹配到的是cangL,而不会匹配到最前方的"."
"""
print(re.findall("\w+[\w.]*@[\w.]+\.\w+", text))
print(re.findall("[\w.]+@[\w.]+\.[\w.]+", text))
输出结果为:
['cangL.test@qq.com']
['.cangL.test@qq.com']
可以把”*”理解成是一个用来表达:在我前面的字符或字符集,是可选的,如果条件符合,则成功匹配,如果条件不符合,也没有关系。
匹配零个或一个字符:
text = "http://www.baidu.com/ https://www.baidu.com/ htss://www.baidu.com/"
print(re.findall("http://[\w.]+/", text)) # 由于在表达式中已经定义了,必须是由http开头,而http后跟着:,所以不会匹配https。
print(re.findall("http[s]?://[\w.]+/", text)) # 而在定义了http后,在s后面加"?",即表示s出现与否都可以成功匹配。
print(re.findall("[\w]*://[\w.]+/", text)) # 由于http或https是WEB协议的标识,所以可以以此为节点筛选,而不是将节点也设置为自行匹配的。
输出结果为:
['http://www.baidu.com/']
['http://www.baidu.com/', 'https://www.baidu.com/']
['http://www.baidu.com/', 'https://www.baidu.com/', 'htss://www.baidu.com/']
值得注意的是,在上述例子中,针对可选项”s”,将其定义为一个字符集,而该字符集中只有”s”这一个成员,而且”[s]”和”s”在功能上是完全相同的。这么做的好处是在于增加了表达式的可读性,使阅读者可以一眼看出哪个字符与哪个字符相关联。
但是,如果同时打算使用”[ ]”和”?”,那么必须把”?”放在字符集的外面。因为如果将”?”放到字符集中,那么它就是一个普通的?问号了。
设置重复匹配次数:
在正则表达式中,’+”、”*”和”?”可以解决很多问题,但有些问题光靠它们并不能解决,例如:
1. 要知道”+”和”*”匹配的字符个数没有上限,我们无法为它们将要匹配的字符个数设置一个最大的值。
2. 并且”+”、”*”和”?”至少匹配零个或一个字符,无法为它们将要匹配的字符的个数再设置一个最小值。
3. 如果只使用”+”和”*”,就无法将它们要匹配的字符个数设置为一个精确的数字。
但正则表达式提供了一个用来设定重复次数的语法,使用大括号:”{ }”,将数值写在它们直接。
text = "#000000 #FF0000 #00AA #FFFFFF"
print(re.findall("#[A-F0-9]{6}", text)) # {6}意味着前一个字符或字符集必须在原始文本中连续重复出现6次。
print(re.findall("#[A-F0-9]{4,6}", text)) # {4,6}意味着前一个字符或字符集,必须在原始文本中连续重复出现4次、5次、6次,才算一个完整的匹配。
print(re.findall("#[A-F0-9]{5,}", text)) # {5,}表示最少重复5次,但并没有设置重复出现的最大值。
输出结果为:
['#000000', '#FF0000', '#FFFFFF']
['#000000', '#FF0000', '#00AA', '#FFFFFF']
['#000000', '#FF0000', '#FFFFFF']
在下述数字中,超出大于100美元的值。
text = "1001:$496.80 1002:$1290.69 1003:$26.43 1004:$613.42 1005:$7.61 1006:$414.90 1007:$25.00 "
print(re.findall("\d{4}:\$\d{3,}\.\d{2}", text))
输出结果为:
['1001:$496.80', '1002:$1290.69', '1004:$613.42', '1006:$414.90']
上述案例中,第一列是订单号,第二列是订单金额。我们首先使用”\d”{4}表示匹配一个数字为4位的订单号。接着使用”:$”匹配固定格式:$,由于”$”是一个元字符,所以需要使用”\”将其转义。接着使用”\d{3,}”匹配一个最短3位,长度无上限的数字,即取出大于100美元的数值,然后使用”.”匹配固定格式.,同时也要将”.”转义。最后匹配最后两位数字。
防止过度匹配:
”?”只能匹配零个或一个字符,”{ }”也有重复次数的限制,也就是说,这几种重复次数都是存在一定限制的。这样做有时候会导致过度匹配的现象。而这种情况往往经常发生。例如:
text = "<B>tag01</B> and <B>tag02</B>"
print(re.findall("<B>[\w]+<[/B]{2}>", text)) # 这条表达式可以正确匹配到两个B标签,是因为"\w"不包括空格,所以在此项表达式中,使用空格为分界。
print(re.findall("<[Bb]>.*</[Bb]>", text)) # 而由于"."可以匹配任何字符,而配合"*"则表示匹配开头为<Bb>,结尾为</Bb>的字符串,这样将整个字符串作为一个元素匹配成功。
输出结果为:
['<B>tag01</B>', '<B>tag02</B>']
['<B>tag01</B> and <B>tag02</B>']
之所以会出现这种情况,是因为无论是”*”还是”+”都是所谓”贪婪型”的元字符,它们再进行匹配时,遵从着多多益善的原则,它们会尽可能的从一段文字的开头匹配到这段文字的末尾,而不是从这段文字的开头匹配到第一个合适的结果就停止。
在不希望出现这种”贪婪”行为时,可以使用元字符的”懒惰”版本,即,这些元字符会尽量匹配符合表达式条件较短的文本。懒惰型的元字符很简单,在贪婪型元字符后面添加一个”?”的后缀即可。
text = "<B>tag01</B> and <B>tag02</B>"
print(re.findall("<[Bb]>.*?</[Bb]>", text)) # 由于贪婪与非贪婪是针对重复匹配元字符的,所以在"*"旁添加"?"即可。
输出结果为:
['<B>tag01</B>', '<B>tag02</B>']
位置匹配
在某些场合中,只需要对某段文本的特定位置进行匹配,这就引出的位置匹配的概念:
边界:
位置匹配用来解决在什么地方进行字符串匹配操作,下例中描述了对位置匹配以及其相关的概念:
text = "book booked."
print(re.findall("book", text))
输出结果为:
['book', 'book']
上述案例中,正则表达式”book”将文本中所有的book都找了出来,即使单词booked中仅仅是包含了book也不例外。但如果我们只希望将book这个单词找出来。
如果希望解决这个问题,就需要使用”边界限定符”,也就是在正则表达式中使用特殊的元字符来表明我们希望匹配操作在什么位置(边界)发生。
单词边界:
首先来尝试使用”限定符”“\b”指定一个单词的边界,也就是使用”\b”来匹配一个单词的开头和结尾。例如:
text = "book booked testbook"
print(re.findall(r"\bbook", text)) # 定义单词的起始位置,但不定义单词的结束位置。
print(re.findall(r"book\b", text)) # 定义单词的结束位置,但不定义单词的开始位置。
print(re.findall(r"\bbook\b", text)) # 同时定义单词的开始和结束位置。
输出结果为:
['book', 'book']
['book', 'book']
['book']
”\b”是通过空格和空白字符来分割单词的。
在第一条表达式中,它对单词的开始位置进行了定义,当他匹配到第一个book时,它的起始位置是一个空白字符,结束位置是一个空格,这符合了”\bbook”的匹配标准,在匹配booked时,它的结束位置是其他字母,但不在表达式的筛选范围内(”\bbook”只关注单词的起始位置是否正确),这时再次匹配成功。但匹配testbook时,由于次单词的开头是其他字母,而”\bbook”对所筛选的book的起始位置有要求,所以匹配失败。
而由于”\bbook\b”对开头和结尾都有要求,所以只能匹配成功一个完成的单词:book。
text = "book booked testbook ccbookcc"
print(re.findall(r"\Bbook", text))
print(re.findall(r"book\B", text))
print(re.findall(r"\Bbook\B", text))
输出结果为:
['book', 'book']
['book', 'book']
['book']
正如之前所了解的,一个元字符的大写形式与它的小写形式所实现的功能是截然相反的。实际上,”\b”,是以不能构成单词的字符为节点(”\W”)。而”\B”则是以一个可以构成单词的字符为节点(”\w”)。
字符串边界:
前面了解到的”^”和”$”就是字符串边界,字符串边界与单词边界有着类似的用途,只不过是用来在字符串中的位置匹配。”^”代表字符串的开头,”$”代表字符串的结尾。
下述的text中,是百度的首页中的一部分HTML代码,有删减。
text = """<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta content="always" name="referrer">
<meta name="theme-color" content="#2932e1">
<title>百度一下,你就知道</title>"""
print(re.findall(r"^<title>", text)) # 默认的"^"只能匹配整个字符串的开头。
print(re.findall(r"(?m)^<title>", text)) # "(?m)"是可以改变其他元字符的行为的元字符序列(分行模式)。分行模式使得正则表达式引擎把行分隔符(换行符\n)也作为一个字符串分隔符来对待。即:在分行模式下,"^"不仅可以匹配正常的字符串开头,也将匹配换行符后面的开始位置。但在使用时,"(?m)"必须出现在整个模式(表达式)的最前面。
print(re.findall(r"(?m)^<meta", text)) # 但是空格在HTML语言中正确的元素,默认情况下会使用空格来划分HTML代码的代码块,但正则表达式却不会这么认为。正则表达式认为,第四行和第五行的^(开头)对应着的是空格(制表符也是由空格组成的),所以不会匹配第四行和第五行的<meta
print(re.findall(r"(?m)^\t*<meta", text)) # 而在"(?m)^"前添加一个"\t"制表符,而这个制表符是可以出现0次或多次的,这样就可以将所有的<meta匹配出来。
输出结果为:
[]
['<title>']
['<meta', '<meta']
['<meta', '\t<meta', '\t<meta', '<meta']
从上述结果中可以看出,制表符”\t”也被筛选了出来,这时就需要继续进行数据清洗工作。
子表达式:
之前学习过如何连续多次重复匹配一个字符。例如,使用”\d+”可以匹配一个或多个数字字符。但在这个例子中,用来表示重复的元字符(”?”“*”“+”等)都只作用于紧挨着它的前一个字符或字符集。但事实上,有些短语,例如Windows 2000,虽然是由一个单词、一个空格和一串数字组成的,但它们却是一个整体。
而子表达式”( )”,则可以解决这个问题。
分组(捕捉组):
在学习子表达式之前,先来了解一下分组的概念。
text = "020-82228888 010-6888883 010-6000 0755-6833024"
# 下述代码中,定义两个分组,"(\d{3,4}-)"为分组1;"(\d{7,8})"为分组2。
# findall()会将在同组中匹配成功的内容,放到同一个字典中(tuple)。
print(re.findall(r"(\d{3,4}-)(\d{7,8})",text))
输出结果为:
[('020-', '82228888'), ('010-', '6888883'), ('0755-', '6833024')]
之所以这样命名捕捉组,是因为在匹配的过程中,捕捉的子序列可以通过Back反向引用在后续的表达式中使用。但值得注意的是,这时使用的,是捕捉组中获取到的文本,而不是每个组对应的正则表达式。
非捕捉组:
# 将下述金额的整金额数、小数点、小数和单位提取出。
text = "1073.38$ 9821.20¥"
print(re.findall(r"(\d{4})\.(\d{2})[$¥]", text)) # 当创建了捕获组后,就只会显示捕获组中的内容。
print(re.findall(r"(\d{4})(\.)(\d{2})([$¥])", text)) # 如果希望将4个段落的内容全部输出,则创建4个捕获组。
print(re.findall(r"(?:\d{4})(?:\.)(?:\d{2})(?:[$¥])", text)) # 使用"?:"将捕获组设置为非捕获组,这样捕获组将仅仅根据正则表达式匹配结果,并将其返回。
输出结果为:
[('1073', '38'), ('9821', '20')]
[('1073', '.', '38', '$'), ('9821', '.', '20', '¥')]
['1073.38$', '9821.20¥']
继续看如何筛选出一个IP地址:
text = "IP address: 192.168.1.123 and 202.106.0.20"
# 如果希望找出文档中的IP地址,可以以192.以及168.为节点,即[\d.]进行匹配,然后让他们重复2至4次,这样创建3个,因为IP的第四位最后没有".",所以直接使用"\d"即可,然后再它重复1至3次。
print(re.findall(r"[\d.]{2,4}[\d.]{2,4}[\d.]{2,4}\d{1,3}", text), "------->第1个表达式结果")
# "[\d.]{2,4}"连续重复出现了3次,每次都对应着一段数字以及分割IP地址的".",实际上这种情况,重复出现3次的"[\d.]{2,4}"可以使用子表达式使其出现一次即可。
# 但如果使用子表达式,就可以把上述的表达式分为两个部分,第一个部分是使用"( )"将"([\d]{1,3}\.)"视为一个整体,然后使用"{3}"把它循环3次,得出192.168.1.;最后的"\d{1,3}"是匹配出123 。
print(re.search(r"(\d{1,3}\.){3}\d{1,3}", text).group(), "------->第2个表达式结果") # search()只匹配一次结果,成功后即将结果返回。
print(re.findall(r"(\d{1,3}\.){3}\d{1,3}", text), "------->第3个表达式结果") # 但同样的结果,使用常用的findall()则无法匹配正常。
# 通过下述3个测试可以发现,如果不给子表达式添加用于重复的元字符的话,还可以根据子表达式里面的内容匹配。但一旦给子表达式添加了用于重复的元字符,就只返回"(\d{1,3}\.)"最后一次,也就是第三次的匹配结果了。
print(re.findall(r"(\d{1,3}\.)", text), "------->第4个表达式结果")
print(re.findall(r"(\d{1,3}\.){2}", text), "------->第5个表达式结果")
print(re.findall(r"(\d{1,3}\.){3}", text), "------->第6个表达式结果")
# 实际上,这是因为当使用了"( )"后,默认的捕捉容器只有一个空间,所以就只保存最后一次捕捉到的结果。
print(re.findall(r"(?:\d{1,3}\.){3}\d{1,3}", text), "------->第7个表达式结果")
print(re.findall(r"(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)", text), "------->第8个表达式结果")
输出结果为:
['192.168.1.123', '202.106.0.20'] ------->第1个表达式结果
192.168.1.123 ------->第2个表达式结果
['1.', '0.'] ------->第3个表达式结果
['192.', '168.', '1.', '202.', '106.', '0.'] ------->第4个表达式结果
['168.', '106.'] ------->第5个表达式结果
['1.', '0.'] ------->第6个表达式结果
['192.168.1.123', '202.106.0.20'] ------->第7个表达式结果
['192.168.1.123', '202.106.0.20'] ------->第8个表达式结果
回溯引用:前后一致匹配:
在HTML语言中,经常使用标题标签,即:H1-H6,以及配对的结束标签来定义和排版WEB页面中的标题文字,通过以下案例来理解什么是回溯引用。
text = "<H1>这是H1标签</H1> <H2>这是H2标签</H2> <H3>这是H3标签</H3> <H4>这是错误的标签</H5>"
print(re.findall(r"<[Hh][1-6]>.*</[Hh][1-6]>", text)) # 使用非懒惰型匹配,将3个标签作为1个整体输出。
print(re.findall(r"<[Hh][1-6]>.*?</[Hh][1-6]>", text)) # 使用懒惰型匹配,会分别将3个标签取出。
# 在文本中,出现了一个错误的标签,但正则表达式并没有发现并处理这个问题,仍然将其匹配。
输出结果为:
['<H1>这是H1标签</H1> <H2>这是H2标签</H2> <H3>这是H3标签</H3> <H4>这是错误的标签</H5>']
['<H1>这是H1标签</H1>', '<H2>这是H2标签</H2>', '<H3>这是H3标签</H3>', '<H4>这是错误的标签</H5>']
出现这种问题就是因为:表达式中的第三部分("<[Hh][1-6]>")对表达式的第一部分("<[Hh][1-6]>")所匹配出的结果毫无所知,所以,回溯引用就是告诉后边的表达式,之前所匹配的结果是什么。