Python 16 标准库-re模块

Python 16 标准库-re模块

旧博客Python系列

正则表达式 Regular Expression

正则表达式是一系列有规律的描述方式,用来模糊检索和替换具有某种特点的文本.
之前简单的接触过正则表达式,相信写过正则表达式的朋友一定有如下的感觉:
需要写正则表达式-->根据需求查看文档并编写-->完成-->回来看自己写的什么玩意已经忘记了.
正则感觉比较复杂但使用的场景很多,但这一次来好好学习一下看是否能够化繁为简.
另外还有一个高手朋友提供的PythonVerbalExpressions库的使用方法,也准备总结在这里.

正则表达式在python内通过re模块(re模块官方文档地址)引入.
这里还有一个来自ubuntu的Python正则表达式操作指南
正则表达式是一门小型和高度专业化,在 支持正则表达式的语言和程序中,正则表达式的语法是一样的.
所以学习正则分为两部分,一是正则本身的语法,而是正则模块在python里的使用.

正则表达式语法

元字符

正则表达式是一串字符串,既然是字符串,也是由各个字符组成,这些字符分为元字符与普通字符:

. ^ $ * + ? { } [ ] | ( ) \是元字符,元字符就是实现模糊匹配功能的字符,搭配普通字符实现各种匹配功能.

.通配符
.代替任意一位字符,数字,字母,特殊字符全部都可以代替,但是'\n'不能匹配.注意仅有一位,如果要匹配多位,就要用多个.

res= re.findall('j...y','auoiuaoiujwaefjwejnenyfdsaoiujeenyfds') #注意findall方法是返回所有匹配的内容.
print(res)
# ['jneny', 'jeeny']

开头是
后边跟的表达式表示从字符串开头匹配,如果开头匹配失败,后边部分不匹配,仅用于判断字符串开头.

res= re.findall('^a...n','auoinyfds')
print(res)
# ['auoin']

$ 结尾是
把$放在正则表达式的结尾,表示以之前的表达式去匹配字符串尾,如果匹配失败,不再匹配其他位置.

res= re.findall('.y..s$','aauoinyfds')
print(res)
# ['nyfds']

* 按照前边紧挨的字符进行重复0-无穷次匹配
普通字符+*,表示按照前边的普通字符重复0-无穷次.

res= re.findall('c*','ccfccccdfcc')
print(res)
# ['cc', '', 'cccc', '', '', 'cc', '']

这个结果出现的原因,是因为*的情况下,匹配0次也算是匹配到,所以会先匹配到cc,然后匹配f,由于是0次,也算匹配成功,所以返回空字符串,之后匹配cccc,在之后对于d和f字符,又返回两个空字符串,最后匹配cc成功,然后结束的时候又是一个空字符串.

+ 按照前边紧挨的字符进行1-无穷次匹配
注意,+与*有区别,*可以匹配0次,而+至少为1次,看以下示例:

res= re.findall('ccf+','ccfccccffcc')
print(res)
# ['ccf', 'ccff']
res= re.findall('ccf*','ccfccccffcc')
print(res)
# ['ccf', 'cc', 'ccff', 'cc']

*和+都是贪婪匹配,也就是如果一串字符串满足匹配1-n个,总是按照最大的n来返回匹配结果.

? 按照前边紧挨的字符进行0-1次匹配
?也是贪婪匹配,如果0个匹配和1个匹配同时成功,返回匹配1个的结果.

res= re.findall('ccf?','ccfccccffcc')
print(res)
# ['ccf', 'cc', 'ccf', 'cc']

大括号{}是自定义任意匹配
{}也是贪婪匹配.
{0,1}=?
{0,}=*
{1,}=+
如果只写一个参数n,表示重复n次.注意不是0-n次,表示区间则大括号内必须写两个参数.

res= re.findall('va.u{0,5}','kbvasuubkvauuufvjkvafuuuuudfffdvkk')
print(res)
#['vasuu', 'vauuu', 'vafuuuuu']
惰性匹配与贪婪匹配

之前所有的查找都是贪婪匹配,如果要改贪婪匹配为惰性匹配(只匹配最小的部分),就在匹配表达式后边加上?

res= re.findall('va.u{0,5}?','kbvasuubkvauuufvjkvafuuuuudfffdvkk')
print(res)
# ['vas', 'vau', 'vaf']

中括号[]是字符集
一个中括号里边的内容,是一个or条件,一对中括号只和一个字符匹配.
中括号实际上就是将字符区分开,单独使用条件,中括号还可以嵌套使用.
注意,元字符内,除了\ - ^之外,字符集之内的部分没有特殊符号.

# 元字符内部无特殊符号
res= re.findall('va[s*]','kbvasuu')
print(res)
# ['vas']
# 字符集内的 - 代表范围,可以是字母
res= re.findall('q[a-z]{1,3}','qfsdaquvqvbioqjkvqbjkvqbkj')
print(res)
print(res)
# ['qfsd', 'quvq', 'qjkv', 'qbjk', 'qbkj']
# 字符集内的 - 还可以指定数字范围
res= re.findall('q[0-9]+?','q7932987q98731897q98743q98732')
print(res)
# ['q7', 'q9', 'q9', 'q9']
# 字符集内-还可以连用
res= re.findall('q[A-Z0-9]{1,3}','q32cqCVq44q#cqufd89qxv')
print(res)
# ['q32', 'qCV', 'q44']
# 字符集内^表示非
res= re.findall('q[^a-z]+','fsLq3UFIDUq1qIORkdqvuiIDOqFO3q98732')
print(res)
# ['q3UFIDU', 'q1', 'qIOR', 'qFO3', 'q98732']

Hint:用正则表达式制作计算器的套路,是不断的匹配最内层的括号,将其中的部分取出来求值然后替换掉,然后继续采用这个方法.中间去括号的方法就是可以灵活运用字符集内的^,表达式是""([^()]*)""

反斜杠 \ 代表转义

反斜杠加上特殊的字符可以表示特定功能.加上元字符,可以取消元字符的特定功能,恢复成普通字符.

\d 匹配任何一个十进制数,相当于[0-9]
\D 匹配任何一个非数字字符,相当于[^0-9]
\s 匹配任何空白字符,相当于[\t\n\r\f\v](转义符是在字符集内也有功能的三个符号之一)
\S 匹配任何非空白字符,相当于[^\t\n\r\f\v]
\w 匹配任何字母和数字,相当于[a-za-z0-9],那些控制字符和不可打印字符无法匹配
\W 匹配任何非字母和数字字符,相当于[^a-zA-Z0-9]
\b 匹配一个特殊字符边界,如空格,& #等,一般是不属于字母和数字的非控制可打印字符

\b表示一个边界,比如可以标记出单词的边界,由于边界对比需要表示文字必须符合指定的边界条件,也就是定位点,所以\b以及相关的一类表示边界的在正则表达式里成为锚点.前边说过的^ $ 也是锚点的一种.

边界对比 说明
^ 一行开头
$ 一行结尾
\b 单词边界
\B 非单词边界
\A 输入开头
\G 前一个符合项的结尾
\Z 非最后终端(final terminator)的输入结尾
\z 输入结尾
目前来说\b先知道可以从字符串里拿单词就可以了.

re模块由于会处理一层转义,而python在处理字符串的时候,自己也会处理一层转义,所以re模块的转义使用起来必须特别注意.一般最好是用r来给re传表达式,这样只需要按照re的格式写转义即可,否则很可能需要考虑二重转义.

转义字符在python里解释的时候,会先当成字符串,转义成应该有的字符串,再传给re解释器.所以转义的时候,如果只是直接在字符串里写转义来当成正则表达式,则其中的字符串是按照转义后传递,而不是传给正则解释器能够解释的部分.如果用底层描述来解释,看如下示例:

# 错误的转义使用
ret=re.findall('c\b','abc\be')
print(ret) # ['c\x08']

这是因为python先要解释字符串'c\b',这时候,\b是一个ascii里的控制字符,所以python处理之后传给re.findall方法的字符串,内存里并不是'c'+''+'b'三个字符(外加一个'\0'),而是'c'+'\b'(外加一个'\0')两个字符,这个时候re模块接受到这个字符串就无法很好的去匹配到想要的结果.所以要反向思维,如果要匹配'c\b',则传给re的字符串必须是'c\b'(re会再解释一层转义,所以c\b对于re来说就是'c'+''+'b'),而要传'c\b',则要求其中的每个\都是普通字符而不是控制字符,所以还需要对每个\都要转义,所以正确的写法是'c\\b'

# 错误的转义使用之二
ret=re.findall('c\\b','abc\be')
print(ret) # ['c']
# python将c\\b转义成'c'+'\'(普通字符)+'b'传给re,但是re会将\b进行转义,所以只匹配出来c.但是最终的目的就是要把'c\\b'传给re,所以还可以使用r字符串,r字符串内部不存在任何转义.
# 正确的写法应该是ret=re.findall(r'c\\b','abc\be')

管道符 | 表示或
管道符和字符集不同,管道符将给出的条件分成两部分来对整个字符串进行匹配,匹配的结果全部都返回.

ret=re.findall('ka+|bd','fdsakaaadffdbdaka')
print(ret)
# ['kaaa', 'bd', 'ka']

管道符单独使用的不多,一般配合小括号分组使用

小括号 () 用于分组
小括号用于规定某一个整体,就是把某几个字符当成一个整体来处理,后边可以跟其他元字符进行控制.之前一些元字符是用来重复单个字符,分组可以用来方便的重复多个字符.
分组的目的除了匹配更方便之外,关键是可以对匹配部分进行索引或者命名引用,极大的方便了处理.

按索引分组:
根据<a href=""http://tool.oschina.net/regex"" rel=""noopener"" target=""_blank"">http://tool.oschina.net/regex 这里的测试,'abab213434duivcdcdccsdauifdfdeefeddfdui'用(\w\w)\1能够匹配到的结果是:['abab','3434','cdcd','fdfd']
\w\w表示任意2个字符,然后后边跟的\1表示将前边的再重复一次,也就是必须满足两个字符一起重复2次的部分,这和直接写(\w\w)(\w\w)是不同的,(\w\w)(\w\w)只是表示匹配任意4个字符.

按名称分组:
在Python的re模块里,名称用 ?P表示,需要写在小括号内表示该分组的名称,其中name可以自行定义,相当于一个变量名,通过re.search()方法返回的结果.group('name')可以来获得该结果.

res = re.search(r'(?P\d{3})(?P[a-z]+)','bungie343makeshalo555of3500days')
print(res.group('alpha'))  # makeshalo
print(res.group('number'))  # 343

re模块使用方法

re模块有两种使用方法,一种是直接调用re的方法进行操作,还有一种是基于正则解析式对象和match对象来操作.
先来看直接调用re模块的方法:

re.findall(pattern,string) 用re正则表达式匹配后边的字符串,将所有返回结果放到一个字符串列表里,如果没有结果,则列表为空.注意,findall函数如果表达式中有小括号的时候,会返回小括号部分匹配的内容,如果需要返回正确结果,涉及到去优先级操作,看文末的补充部分.
re.finditer(pattern, string, flags=0) 和findall类似,但是结果是放在一个迭代器里,每个元素是一个match对象.
re.search(pattern, string, flags=0) search只返回第一个匹配的结果,返回一个match对象,针对这个对象用.group()方法取得结果,还可以用分组名称来取得结果,如果找不到,返回None
re.match(pattern, string, flags=0) 只在字符串的开头寻找一次,其他和search方法的调用和返回对象都一样,如果找不到,返回None
re.split(pattern,'string) re里的split是迭代来分割,会先解析re表达式,如果有多个条件,会先按照第一个条件分割,再对分割的结果用第二个条件分割,直到无法分割为止,所以比字符串split功能更强大,一般用在按照多个条件分割字符串.
re.sub(pattern, repl, string, count=0, flags=0) pattern是正则表达式;repl可以是字符串,表示替换成什么,注意也可以是一个调用对象,这个对象能够接受match对象(指re里由search match等方法得到的那个对象)并返回一个字符串.string是要进行替换操作的字符串.count表示替换几次.sub方法的作用就是用正则匹配字符串,然后将匹配的部分替换成repl
re.sub(pattern, repl, string, count=0, flags=0) 与sub类似,但是会返回一个两个元素的元组,第一个元素是替换后的结果,第二个元素是替换了几次

直接采用re模块的方法,遇到大批量的数据的时候,需要反复写正则表达式,这个时候可以用面向对象的思想,将一个正则表达式通过re.compile()编译成一个正则对象,再用正则对象的方法得到match对象,再对match对象操作.
操作步骤:

1 编译出正则对象:

# 编译正则表达式对象
re_obj = re.compile('(abc)+')

2 对正则对象采取各种方法操作,这些方法与re模块的方法是一样的,但是无需再传入pattern参数.

# 操作正则对象并获取结果
re_obj = re.compile(r'(\w\w\w)\1+')
res = re_obj.finditer('abcabcabc is cdecdecde is equal to doremidoremidoremi')
for i in res:
    print(i.group())

正则对象匹配成功的对象就是上边一直在提的match对象,match对象也有一些方法:

  • group()返回匹配成功的字符串
  • start()返回匹配开始的位置索引,从0开始的索引
  • end()返回匹配结束的位置索引,从0开始的索引
  • span()返回一个元组包含匹配(开始索引,结束索引)的位置

正则表达式的作用非常大,文件处理,HTTP前后端大量使用到需要匹配字符串的工作,对于单纯的字符串处理,正则也提供了比字符串方法更灵活强大的功能,不过正则有时候写起来确实比较难认,于是有人开发出了VerbalExpressions系列插件,用按照人类语言描述的方法来生成正则表达式.对应Python语言的VerbalExpressions就是PythonVerbalExpressions模块

PythonVerbalExpressions的使用方法

PythonVerbalExpressions是一个第三方模块,可以通过pip install VerbalExpressions安装;或者在Pycharm - File -Settings - Project:ProjectName - Project Interpreter 下边查找和添加该模块,模块名为VerbalExpressions.
VerbalExpressions是一个开源项目,PythonVerbalExpressions的Github地址是https://github.com/VerbalExpressions/PythonVerbalExpressions

使用方法:

导入模块:
导入VerEx,这个是一个这个模块规定的正则对象,通过VerEx()实例化一个正则表达式对象.

from verbalexpressions import VerEx

用正则对象的方法生成一个正则匹配对象.

verbal_expression = VerEx()    # 生成一个空的VerEx对象,可以理解是一个类似字符串的对象
tester = (verbal_expression.   # 通过VerEx的各种方法向字符串中写入正则规则,最后用括号包起来,返回一个正则测试的字符串.
            start_of_line().
            find('http').
            maybe('s').
            find('://').
            maybe('www.').
            anything_but(' ').
            end_of_line()
)

用括号括住,列出来的就是可以使用的各种方法,每一种方法就是给正则按顺序添加一个识别规则注意参数的字符串里,没有元字符,都是当做普通字符来操作的.有这么几种方法:

anything() 无参数,表示任意多个字符或者没有字符
anything_but(string) 任何不属于string条件里的内容,源代码是self.add('([^%s]*)' % value),可见string里的全部子集都不会被包含
end_of_line() 表示匹配尾部,相当于$
maybe(value) 表示可能有,可能没有,代表value的内容出现0-1次
start_of_line() 表示从行首开始匹配,相当于^
find(value) 需要匹配的内容,源代码是self.add('(%s)' % value),所以就是需要找到value的内容.
regex() 返回一个正则表达式对象,就是re模块里的正则对象.
source() 返回tester对应的正则表达式源码
any(value) 表示value里的任意一个或者多个字符匹配
line_break() 无参数,表示匹配\n或者\r\n
range(*args) 表示范围,类似于字符集内的[a-z]
tab() 匹配制表符
word() 匹配任意字符,相当于增加(\w+)
OR() 或者,增加|到正则表达式
通过这些方法,可以比较简易的按照自然语言来生成正则表达式,还可以很容易的通过source方法返回写好后的正则表达式.

虽然有第三方库帮助,但是第三方库的正则不支持分组索引等复杂操作,仅适合比较简单的如经常使用到的URL地址解析.

补充知识

1 findall方法默认优先返回分组匹配内容,如果需要得到正常结果,需要去优先级.

# 默认优先返回分组匹配内容:
res = re.findall(r'(abc)+','afdsaababcabcabccabababa')
print(res) # ['abc']
# 分组开头加上?:去优先级,返回普通匹配结果
res = re.findall(r'(?:abc)+','afdsaababcabcabccabababa')
print(res) # ['abcabcabc']

2 取一段里边HTML一对标签的内容

com = re.compile(r'(?:<[^>]*>)[^<]*(?:<[^>]*>)')
LICENSED UNDER CC BY-NC-SA 4.0
Comment