day5-正则表达式和re模块
产生背景
正则表达式为高级的文本模式匹配、抽取、或文本形式的搜索和替换功能提供了基础。简单的说,正则表达式是一些由字符和特殊符号组成的字符串,他们描述了模式的重复或者表述多个字符,于是,正则表达式能按照某种模式匹配一系列有相似特征的字符串。
搜索和匹配的比较:两种模式匹配
- 搜索:在字符串任意部分中搜索匹配的模式,搜索通过search()函数或方法实现
- 匹配:是指判断一个字符串能否从起始处全部或者部分匹配某个模式,匹配通过match()函数或方法实现
我们先看几个例子:
正则表达式模式
>>>import re >>>search("foo","foo") #该模式没有使用任何特殊符号去匹配其他符号,而只匹配所描述的内容。所以,能够匹配这个模式的只有包含"foo"的字符串。 foo >>>import re >>>search("d","dick") #同上 d
而正则表达式的强大之处在于引入特殊字符来定义字符集、匹配子组和重复模式。
特殊符号和字符
常见的符号和字符
注意:
1.择一匹配:符号"|"表示从多个模式中选择一个,它能够增强正则表达式的灵活性,使得正则表达式能够匹配多个字符串而不仅仅只是一个字符串。
2.如果要匹配像.这样的字符本身,必须使用反斜线转义句点符号的功能,例如:"\."
从字符串起始或者结尾或者单词边界匹配
- 匹配字符串的开始位置:使用^字符或者特殊字符\A
- 匹配字符串的末尾位置:使用$或者\Z
>>>import re >>>re.search("\Ad.+y\Z","dick jacky") <_sre.SRE_Match object; span=(0, 12), match='dick jacky'> >>>import re >>>re.search("\Ad.+a\Z","dick jacky") None #'^' 匹配字符开头,若指定flags MULTILINE,多行匹配 >>> re.search(r"^a","\nabc\neee",flags=re.MULTILINE).group() >>> re.search("^dic","dickabc").group() 'dic' >>> re.search("^ic","dickabc").group() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'NoneType' object has no attribute 'group' #'\A'只从字符开头匹配,re.search("\Aabc","alexabc") 是匹配不到的 >>> re.search("\Adic","dickabc").group() 'dic' >>> re.search("\Aic","dickabc").group() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'NoneType' object has no attribute 'group' #'$'匹配字符结尾,也可以使用'\Z' >>> re.search("foo$","bfoo\nsdfsf",flags=re.MULTILINE).group() >>> re.search("78$","dickabcdefghijk12345678").group() '78' >>> re.search("678\Z","dickabcdefghijk12345678").group() '678'
创建字符集
对于单个字符的正则表达式,使用择一匹配| 和方括号[]表示使用字符集是等效的,要么a,要么e,要么i,因为此时a,e,i都是相互独立的字符串。
限定范围和否定
除了上面的单字符外,字符集还支持匹配指定的字符范围。方括号中间使用"-"连接,用于指定一个字符的范围。
使用*、+和?实现存在性和频数匹配
#'*'匹配*号前的字符0次或多次 >>> re.findall("ab*","cabb3abcbbac") ['abb', 'ab', 'a'] >>> re.search("ab*","dickabbcdefghijk12345678").group() 'abb' #'+'匹配前一个字符1次或多次 >>>re.findall("ab+","ab+cd+abb+bba") ['ab', 'abb'] >>> re.search("ab+","dickabbbbbcdefghijk12345678").group() 'abbbbb' #'?'匹配前一个字符0次或1次 >>> re.search("ab?","dickabbbbbcdefghijk12345678").group() 'ab' #'{m}' 匹配前一个字符m次 >>> re.search("ab{3}","dickabbbbbcdefghijk12345678").group() 'abbb' #'{n,m}' 匹配前一个字符n到m次 >>> re.findall("ab{1,3}","abb abc abbcbbb") ['abb', 'ab', 'abb']
表示字符集的特殊字符
一些特殊字符能够表示字符集。
\d:表示匹配任何十进制数字
\w:表示全部字母和数字的字符集,相当于[A-Za-z0-9]
\s:表示空格字符
\D:表示任何非十进制数,相当于[^0-9]
#'\d' 匹配数字0-9 >>> re.search("\w*\d*","dickabcdefghijk12345678").group() 'dickabcdefghijk12345678' #'\D' 匹配非数字 >>> re.search("\D*","dickabcdefghijk12345678").group() 'dickabcdefghijk' #'\w' 匹配[A-Za-z0-9] >>> re.search("\w*\d*","dickabcdefghijk12345678").group() 'dickabcdefghijk12345678' #'\W' 匹配非[A-Za-z0-9] >>> re.search("\W","dickabcdefghijk12345678").group() '' #'s' 匹配空白字符、\t、\n、\r >>> re.search("\s+","ab\tc1\n3").group() '\t'
到现在,我们已经可以实现匹配某个字符串以及丢弃不匹配的字符串,但是有些时候,我们可能会对之前匹配成功的数据(特定字符串或子字符串)进行提取
答:答案是可以的
使用圆括号指定分组
用法:只要用一对圆括号包裹任何一个正则表达式即可
功能:
• 对正则表达式进行分组
• 匹配子组
应用场景:
1.当有两个不同的正则表达式而且想用他们来比较同一个字符串时。
2.对正则表达式进行分组可以在整个正则表达式中使用重复操作符(而不是一个单独的字符或者字符集)
#'(?P<name>...)' 分组匹配 >>> re.search("(?P<province>[0-9]{4})(?P<city>[0-9]{2})(?P<birthday>[0-9]{4})","320122199010152839").groupdict("city") {'birthday': '1990', 'province': '3201', 'city': '22'}
使用圆括号进行分组的一个副作用就是,匹配模式的子字符串可以保存起来供后续使用。这些子组能够被同一次的匹配或者搜索重复调用,或者提取出来用于后续处理。
而使用Python的re模块,实现如下示例:
>>> import re 导入re模块 >>> m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123') >>> m.group() # 完整匹配 'abc-123' >>> m.group(1) # 子组 1 'abc' >>> m.group(2) # 子组 2 '123' >>> m.groups() # 全部子组 ('abc', '123')
解析:如果我们更倾向于使用子组,那么就可以使用Python的re模块进行实现,因为它只是re模块的一个特性
正则表达式和Python语言
re模块:核心函数和方法
功能:正则表达式是一种功能强大的工具,它可以用来进行模式匹配、提取、查找和替换
匹配对象以及group()和groups()方法
匹配对象的定义:当处理正则表达式时,除了正则表达式对象之外,还有另一个对象类型:匹配对象。这些是成功调用match()或者search()返回的对象。
匹配对象有两个主要方法:group()和groups()
- group():返回整个匹配对象或根据要求返回特定子组,如果没有子组要求,返回整个匹配对象
- groups():仅返回一个包含唯一或者全部子组的元组。如果没有子组要求,返回一个空元组
使用match()方法匹配字符串
用法:match()函数试图从字符串的起始部分对模式进行匹配。如果匹配成功,就返回一个匹配对象,如果匹配失败,就返回None。
可用方法:匹配对象的group()方法能够用于显示那个成功的匹配
>>>import re >>>m = re.match('foo', 'foo') #模式匹配字符串 <_sre.SRE_Match object; span=(0, 3), match='foo'> #返回的是匹配对象 >>>if m is not None: print(m.group()) #如果匹配成功 foo # 输出匹配内容
解析:模式"foo"完全匹配字符串"foo",我们也能够确认m是交互式解释器中匹配对象
import re m = re.match('foo', 'bar') # 模式并不能匹配字符串 if m is not None: print(m.group()) else: print(m) #输出 None # 这个为一个失败的例子,它返回None。 import re m = re.match('foo', 'food on the table') print(m.group()) #输出 foo #尽管字符串比匹配模式长,但从字符串的起始部分开始匹配就会成功。
使用search()在一个字符串中查找模式(搜索与匹配的对比)
用法:search()的工作方式和match()完全一样,不同之处在于search()会用它的字符串参数,在任意位置对给定的正则表达式模式搜索第一次出现的匹配情况。如果搜索到成功的匹配,就会返回一个匹配对象;否则,返回None
import re m = re.match('foo', 'seafood') # 匹配失败 if m is not None: print(m.group()) else: print(m) #输出 None >>> re.search("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}","inet 192.168.1.123 netmask 0xffffff00 broadcast 192.168.1.255").group() '192.168.1.123' >>> re.search("(\d{1,3}\.){1,3}\d{1,3}","inet 192.168.1.123 netmask 0xffffff00 broadcast 192.168.1.255").group() '192.168.1.123' >>> re.search("(\d{2})(\d{2})(\d{2})(\d{4})","32012219901015283X name:dick").group() '3201221990' >>> re.search("(\d{2})(\d{2})(\d{2})(\d{4})","32012219901015283X name:dick").groups() ('32', '01', '22', '1990') >>> re.search("(\d{2})(\d{2})(\d{2})(\d{4})","32012219901015283X name:dick").group(1) '32' >>> re.search("(\d{2})(\d{2})(\d{2})(\d{4})","32012219901015283X name:dick").group(4) '1990'
解析:可以看到,此处匹配失败。match()试图从字符串的起始部分开始匹配模式;也就是说,模式中的“f”将匹配到字符串的首字母“s”上,这样的匹配肯定是失败的。然而,字符串“foo”确实出现在“seafood”之中(某个位置),所以,我们该如何让Python 得出肯定的结果呢?答案是使用search()函数,而不是尝试匹配。search()函数不但会搜索模式在字符串中第一次出现的位置,而且严格地对字符串从左到右搜索。
import re m = re.search('foo', 'seafood') # 使用search()代替 if m is not None: print(m.group()) else: print(m) #输出 foo #搜索成功 import re m = re.search('ab', 'food on the table') print(m.group()) #输出 ab
匹配多个字符串
在一开始,我们已经知道bat|bet|bit|bot使用了正则表达式|择一匹配,在Python中也可以使用正则表达式的方法:
import re bt = 'bat|bet|bit' # 正则表达式模式: bat、bet、bit m = re.match(bt, 'bat') # 'bat' 是一个匹配 if m is not None: print(m.group()) else: print(m) #输出 'bat' import re m = re.match(bt, 'blt') # 对于 'blt' 没有匹配 if m is not None: print(m.group()) else: print(m) #输出 None import re m = re.match(bt, 'He bit me!') # 不能匹配字符串 if m is not None: print(m.group()) else: print(m) #输出 None import re m = re.search(bt, 'He bit me!') # 通过搜索查找 'bit' if m is not None: print(m.group()) else: print(m) #输出 'bit' #'|'匹配|左或|右的字符 >>> re.search("abc|ABC","ABCBabcCD").group() 'ABC'
匹配任何单个字符
我们也知道了点号.不能匹配一个换行符\n或者一个空字符串
anyend = '.end' m = re.match(anyend, 'bend') # 点号匹配 'b' if m is not None: print(m.group()) else: print(m) #输出 'bend' m = re.match(anyend, 'end') # 不匹配任何字符 if m is not None: print(m.group()) else: print(m) #输出 None m = re.match(anyend, '\nend') # 除了 \n 之外的任何字符 if m is not None: print(m.group()) else: print(m) #输出 None m = re.search('.end', 'The end.') # 在搜索中匹配 ' ' if m is not None: print(m.group()) else: print(m) #输出 ' end' #'.'默认匹配除\n之外的任意一个字符,若指定flag DOTALL,则匹配任意字符,包括换行 >>> re.search(".","dickabcdefghijk12345678").group() 'd'
下面我们也可以在正则表达式中搜索一个真正的句点(小数点),而我们通过使用一个反斜线对句点的功能进行转义:
patt314 = '3.14' # 表示正则表达式的点号 pi_patt = '3\.14' # 表示字面量的点号 (dec. point) m = re.match(pi_patt, '3.14') # 精确匹配 if m is not None: print(m.group()) else: print(m) #输出 '3.14' m = re.match(patt314, '3014') # 点号匹配'0' if m is not None: print(m.group()) else: print(m) #输出 '3014' m = re.match(patt314, '3.14') # 点号匹配 '.' if m is not None: print(m.group()) else: print(m) #输出 '3 .14'
创建字符集([ ])
前面详细讨论了[cr][23][dp][o2],以及它们与r2d2|c3po 之间的差别。下面的示例将说明对于r2d2|c3po 的限制将比[cr][23][dp][o2]更为严格。
m = re.match('[cr][23][dp][o2]', 'c3po')# 匹配 'c3po' if m is not None: print(m.group()) else: print(m) #输出 'c3po' m = re.match('[cr][23][dp][o2]', 'c2do')# 匹配 'c2do' if m is not None: print(m.group()) else: print(m) #输出 'c2do' >>> m = re.match('r2d2|c3po', 'c2do')# 不匹配 'c2do' if m is not None: print(m.group()) else: print(m) #输出 None >>> m = re.match('r2d2|c3po', 'r2d2')# 匹配 'r2d2' if m is not None: print(m.group()) else: print(m) #输出 'r2d2'
重复、特殊字符以及分组
正则表达式中最常见的情况包括特殊字符的使用、正则表达式模式的重复出现,以及使用圆括号对匹配模式的各部分进行分组和提取操作。我们曾看到过一个关于简单电子邮件地址的正则表达式(\w+@\w+\.com)。或许我们想要匹配比这个正则表达式所允许的更多邮件地址。为了在域名前添加主机名称支持,例如www.xxx.com,仅仅允许xxx.com 作为整个域名,必须修改现有的正则表达式。为了表示主机名是可选的,需要创建一个模式来匹配主机名(后面跟着一个句点),使用“?”操作符来表示该模式出现零次或者一次,然后按照如下所示的方式,插入可选的正则表达式到之前的正则表达式中:\w+@(\w+\.)?\w+\.com。从下面的示例中可见,该表达式允许.com 前面有一个或者两个名称:
>>>import re >>>patt = '\w+@(\w+\.)?\w+\.com' >>>re.match(patt, 'nobody@xxx.com').group() 'nobody@xxx.com' >>>re.match(patt, 'nobody@www.xxx.com').group() 'nobody@www.xxx.com'
接下来,用以下模式来进一步扩展该示例,允许任意数量的中间子域名存在。请特别注意细节的变化,将“?”改为“*. : \w+@(\w+\.)*\w+\.com”。
>>> patt = '\w+@(\w+\.)*\w+\.com' >>> re.match(patt, 'nobody@www.xxx.yyy.zzz.com').group() 'nobody@www.xxx.yyy.zzz.com'
但是,我们必须要注意,即仅仅使用字母数字字符并不能匹配组成电子邮件地址的全部可能字符。上述正则表达式不能匹配诸如xxx-yyy.com 的域名或者使用非单词\W 字符组成的域名。
之前讨论过使用圆括号来匹配和保存子组,以便于后续处理,而不是确定一个正则表达式匹配之后,在一个单独的子程序里面手动编码来解析字符串。此前还特别讨论过一个简单的正则表达式模式\w+-\d+,它由连字符号分隔的字母数字字符串和数字组成,还讨论了如何添加一个子组来构造一个新的正则表达式 (\w+)-(\d+)来完成这项工作。下面是初始版本的正则表达式的执行情况。
import re patt = '\w+@(\w+\.)*\w*-(\w+\.)*com' x =re.match(patt, 'nobody@www.xxx-yyy.zzz.com').group() #完整匹配 print(x) #输出 nobody@www.xxx-yyy.zzz.com
请注意如何使用group()方法访问每个独立的子组以及groups()方法以获取一个包含所有匹配子组的元组。
import re patt = '\w+@(\w+\.)*\w*-(\w+\.)*com' x =re.search(patt, 'nobody@www.xxx-yyy.zzz.com').groups() #全部子组 print(x) #输出 ('www.', 'zzz.') x =re.search(patt, 'nobody@www.xxx-yyy.zzz.com').group(1) #子组 1 print(x) #输出 www. x =re.search(patt, 'nobody@www.xxx-yyy.zzz.com').group(2) #子组 2 print(x) #输出 zzz.
小结
1.group()通常用于以普通方式显示所有的匹配部分,但也能用于获取各个匹配的子组。
2.使用groups()方法来获取一个包含所有匹配子字符串的元组。
匹配字符串的起始和结尾以及单词边界
m = re.search('^The', 'The end.') # 匹配 if m is not None: print(m.group()) else: print(m) #输出 'The' m = re.search('^The', 'end. The') # 不作为起始 if m is not None: print(m.group()) else: print(m) #输出 None m = re.search(r'\bthe', 'bite the dog') # 在边界 if m is not None: print(m.group()) else: print(m) #输出 'the' m = re.search(r'\bthe', 'bite the dog') # 有边界 if m is not None: print(m.group()) else: print(m) #输出 None m = re.search(r'\Bthe', 'bite the dog') # 没有边界 if m is not None: print(m.group()) else: print(m) #输出 'the'
小结
1.特殊字符\b和\B可以用来匹配字符边界。\b用于匹配一个单词的边界,这意味着,如果一个模式必须位于单词的起始部分,就不管该单词前面(单词位于字符串中间)是否有任何字符(单词位于行首)
2.\B将匹配出现在一个单词中间的模式(即,不是单词边界)
使用findall()函数
findall()查询字符串中某个正则表达式模式全部的出现情况。这与search()在执行字符串搜索时类似
与match()和search()不同点:
findall()总是返回一个列表。如果findall()没有找到匹配部分,就返回一个空列表,如果匹配成功,列表将包含所有成功匹配部分(从左到右按出现顺序排列)。
#re.findall 把所有匹配到的字符放到以列表中的元素返回 >>> re.findall("\d+","ab3joiwdpqwdij0812312kl-0idw-i-diwd=01231231") ['3', '0812312', '0', '01231231'] >>> re.findall("[A-Za-z]+","ab3joiwdpqwdij0812312kl-0idw-i-diwd=01231231") ['ab', 'joiwdpqwdij', 'kl', 'idw', 'i', 'diwd'] >>> re.findall('car', 'car') ['car'] >>> re.findall('car', 'scary') ['car'] >>> re.findall('car', 'carry the barcardi to the car') ['car', 'car', 'car']
使用sub()和subn()搜索与替换
有两个函数用于实现搜索和替换功能:sub()和subn()
用法:sub()和subn()将某字符串中所有匹配正则表达式的部分进行某种形式的替换。用来替换的部分通常是一个字符串,也可能是一个函数(该函数返回一个用来替换的字符串)
区别:subn()啊好i返回一个表示替换的总数,替换后的字符串和表示替换总数的数字一起作为一个拥有两个元素的元组返回。
#re.sub 匹配字符并替换 sub(pattern, repl, string, count=0, flags=0) >>> re.sub("[A-Za-z]+","|","ab3joiwdpqwdij0812312kl0idwidiwd01231231") '|3|0812312|0|01231231' >>> re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n') 'attn: Mr. Smith\012\012Dear Mr. Smith,\012' >>> re.subn('X', 'Mr. Smith', 'attn: X\n\nDear X,\n') ('attn: Mr. Smith\012\012Dear Mr. Smith,\012', 2) >>> print(re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')) attn: Mr. Smith Dear Mr. Smith, >>> re.sub('[ae]', 'X', 'abcdef') 'XbcdXf' >>> re.subn('[ae]', 'X', 'abcdef') (' XbcdXf', 2)
在限定模式上使用split()分隔字符串
re模块和正则表达式的对象方法split()对于相对应字符串的工作方式是类似的。
与字符串split方法的区别:re模块的split()基于正则表达式的模式分隔字符串,为字符串分隔功能添加一些额外的威力。如果你不想为每次模式的出现都分割字符串,就可以通过为max参数设定一个值(非0)来指定最大分割数。
>>> import re >>> DATA = ( ... 'Mountain View, CA 94040', ... 'Sunnyvale, CA', ... 'Los Altos, 94023', ... 'Cupertino 95014', ... 'Palo Alto CA', ... ) >>> for datum in DATA: ... print re.split(', |(?= (?:\d{5}|[A-Z]{2})) ', datum) ... ['Mountain View', 'CA', '94040'] ['Sunnyvale', 'CA'] ['Los Altos', '94023'] ['Cupertino', '95014'] [' Palo Alto', 'CA'] #re.split以匹配到的字符当做列表分隔符 >>> re.split("[A-Za-z]+","ab3joiwdpqwdij0812312kl-0idw-i-diwd=01231231") ['', '3', '0812312', '-0', '-', '-', '=01231231']
与字符串split的相同点:如果给定分隔符不是使用特殊符号来匹配多重模式的正则表达式,那么re.split()与str.split()的工作方式相同,如:
>>> re.split(':', 'str1:str2:str3') ['str1', 'str2', 'str3']
仅需要知道的几个匹配模式
#re.I(re.IGNORECASE): 忽略大小写 >>> re.search("a",r"ABC",flags=re.I) <_sre.SRE_Match object; span=(0, 1), match='A'> #M(MULTILINE): 多行模式,改变'^'和'$'的行为 >>> re.search(r"^a","\nabc\neee",flags=re.MULTILINE) <_sre.SRE_Match object; span=(1, 2), match='a'> >>> re.search(r"^a","\nabc\neee") #S(DOTALL): 点任意匹配模式,改变'.'的行为 >>> re.search(r".+","\nabc\neee") <_sre.SRE_Match object; span=(1, 4), match='abc'> >>> re.search(r".+","\nabc\neee",flags=re.S) <_sre.SRE_Match object; span=(0, 8), match='\nabc\neee'>
反斜杠的烦恼
与大多数编程语言相同,正则表达式里使用"\"作为转义字符,这就可能造成反斜杠困扰。假如你需要匹配文本中的字符"\",那么使用编程语言表示的正则表达式里将需要4个反斜杠"\\\\":前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。Python里的原生字符串很好地解决了这个问题,这个例子中的正则表达式可以使用r"\\"表示。同样,匹配一个数字的"\\d"可以写成r"\d"。有了原生字符串,你再也不用担心是不是漏写了反斜杠,写出来的表达式也更直观。
>>> re.split("\\\\",r"c:\users\data\python3.5") ['c:', 'users', 'data', 'python3.5'] >>> re.split("\\\\",r"c:\\users\data\python3.5") ['c:', '', 'users', 'data', 'python3.5']