python正则表达式
1 标准库模块re
python3中使用re模块支持正则表达式(Regular Expression),需要定义一个用于匹配的模式(pattern)字符串,以及一个要匹配的字符串(string)。
- 简单匹配
import re
m = re.match('My','My name is zhangsan')
print(m.group())
print(m.start(),m.end())
print(m.span())
My
0 2
(0, 2)
其中,My是正则表达式模式,最简单的,只匹配字符My本身。而My name is zhangsan是想要检查的字符串,re.match()函数用于查看字符串是不是以正则模式开头。
如果你仅仅是做一次简单的文本匹配/搜索操作的话,可以直接使用 re 模块级别的函数,比如re.match。如果你打算做大量的匹配和搜索操作的话,最好先编译正则表达式,然后再重复使用它
- 复杂匹配
import re
p = re.compile('[a-z]+') # [a-z]+ 是正则模式,表示1个或多个小写字母
print(p)
if p.match('hello123'):# p是预编译后的正则模式,它也有match等方法,只是参数不同,不需要再传入正则模式。判断字符串'hello123'是否以1个或多个小写字母开头
print('yes')
else:
print('no')
if p.match('123hi'):# 重用预编译过的正则模式
print('yes')
else:
print('no')
re.compile('[a-z]+')
yes
no
模块级别的函数会将最近编译过的模式缓存起来,因此并不会消耗太多的性能, 但是如果使用预编译模式的话,你将会减少查找和一些额外的处理损耗。
1.1 使用match()从字符串开头开始匹配
可以使用模块级别的re.match()或预编译模式的p.match(),如果字符串是以正则表达式开头,则表明匹配成功,返回匹配到的对象,比如<_sre.SRE_Match object; span=(0, 2), match='My'>,如果匹配失败,返回None
import re
m1 = re.match('zhangsan', 'zhangsan is a handsome boy.') # 模块级的match方法
print(m1) # 匹配成功,返回Match对象
print(m1.group()) # Match对象有group()、start()、end()、span()等方法
m2 = re.match('mayun', 'zhangsan is a handsome boy.') # 匹配失败
print(m2) # 返回None
p = re.compile('zhangsan') # 预编译正则模式也是可以的
p.match('zhangsan is a handsome boy.') # 调用预编译正则模式的match方法
zhangsan
None
1.2 使用search()寻找首次匹配
如果字符串中有多个地方与正则表达式匹配的话,search()方法返回第一次匹配到的结果:
import re
s = 'I wish I may, I wish I might have a dish of fish tonight.'
print(re.search('wish', s))
print(re.search('wish', s).span())
(2, 6)
1.3 使用findall()或finditer()寻找所有匹配
前面两个函数都是查找到一个匹配后就停止,如果要查找字符串中所有的匹配项,可以使用findall()和finditer()
- findall():会搜索文本并以列表形式返回所有的匹配
- finditer():以迭代方式返回匹配
import re
text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
# 以列表的方式返回
p = re.compile('\d+/\d+/\d+')
print(p.findall(text))
# 以迭代的方式返回
iters = p.finditer(text)
print(iters)
for m in iters:
print(m)
['11/27/2012', '3/13/2013']
1.4 使用split()按匹配切分
字符串的str.split()方法只适应于非常简单的字符串分割情形, 它并不允许有多个分隔符或者是分隔符周围不确定的空格。 当你需要更加灵活的切割字符串的时候,最好使用 re.split() 方法
import re
line = 'asdf fjdk; afed, fjek,asdf, foo'
print(re.split(r'[;,\s]\s*', line)) # 正则模式表示 ;或,或空白字符且它们的后面再跟0个或多个空白字符
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
1.5 使用sub()替换匹配
- 对于简单的字面模式,直接使用字符串的str.replace()方法即可;对于复杂的模式,请使用re模块中的sub()
import re
text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
print(re.sub('(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text))#sub()`函数中的第一个参数是被匹配的模式,第二个参数是替换模式。反斜杠数字比如`\3`指向前面模式的第3个捕获组,此时要加`r`指定为原始字符串,否则会被Python自动转义为`\x03
'Today is 2012-11-27. PyCon starts 2013-3-13.'
- 对于更加复杂的替换,可以传递一个替换回调函数来代替。一个替换回调函数的参数是一个Match对象,也就是match()或者find()返回的对象。使用group()方法来提取特定的匹配部分。回调函数最后返回替换字符串。
import re
from calendar import month_abbr
def change_date(m):
mon_name = month_abbr[int(m.group(1))]
return '{} {} {}'.format(m.group(2), mon_name, m.group(3))
text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
p = re.compile(r'(\d+)/(\d+)/(\d+)')
print(p.sub(change_date, text))
'Today is 27 Nov 2012. PyCon starts 13 Mar 2013.'
- 如果除了替换后的结果外,你还想知道有多少替换发生了,可以使用re.subn()。
text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
p = re.compile(r'(\d+)/(\d+)/(\d+)')
newtext, n = p.subn(r'\3-\1-\2', text)
print(newtext)
print(n)
Today is 2012-11-27. PyCon starts 2013-3-13.
2
2 正则表达式语法
2.1 基本模式
语法 | 说明 | 模式范例 | 匹配 |
---|---|---|---|
普通字符 | 普通的文本值代表自身,用于匹配非特殊字符 | ab | ab |
. | 匹配除换行符\n以外的任意一个字符。如果要匹配多行文本,可以指定re.DOTALL标志位 | ab. | 匹配abc或abC,不匹配ab,因为b后面一定要有一个字符 |
\ | 转义字符,比如要匹配点号.本身,需要转义\.,如果不转义,.将有上一行所示的特殊含义 | ab. | 匹配ab.com,不匹配abc |
[] | 匹配中括号内的一个字符 1. 中括号内的字符可以全部列出,如[abc]表示匹配字符a或b或c 2. 也可以使用-表示范围,如[a-z]表示匹配所以小写字母中的任意一个字符 3. 本文后续要介绍的如*、+等特殊字符在中括号内将失去特殊含义,如[+()]表示匹配字符或+或(或) 4. 本文后续要介绍的特殊字符集如\d、\w等也可以放入此中括号内,继续保持特殊含义 5. 如果中括号内的字符序列前面有一个^,表示不匹配中括号内的任何一个字符,如[^0-9]表示不匹配数字,a[^0-9]c不匹配a1c,但会匹配abc 6. 要匹配字符],可以转义它,或者把它放在中括号内的首位,如a[()[\]{}]c或a[]()[{}]c都可以匹配到a]c | a[0-9]b | a1b或a2b |
2.2 特殊字符集
语法 | 说明 | 模式范例 | 匹配 |
---|---|---|---|
\d | 匹配任意一个数字字符,等价于[0-9] | a\db | a1b |
\D | 匹配任意一个非数字字符,等价于[^0-9] | a\Db | aAb |
\s | 匹配任意一个空白字符,等价于[ \t\n\r\f\v] | a\sb | a b |
\S | 匹配任意一个非空白字符,等价于[^ \t\n\r\f\v] | a\Sb | aAb |
\w | 匹配任意一个 alphanumeric character,等价于[a-zA-Z0-9_] | a\wb | azb或aZb或a1b或a_b |
\W | 匹配任意一个 non-alphanumeric character,等价于[^a-zA-Z0-9_] | a\Wb | a-b或a b |
这些字符集也可以放入[]中,比如a[\d\s]b表示匹配字符a和字符b,且中间有一个数字字符或空白字符,所以它会匹配a1b或a b等
python的string模块中预先定义了一些可供我们测试用的字符串常量。我们将使用其中 的printable字符串,它包含 100 个可打印的 ASCII 字符,包括大小写字母、数字、空格 符以及标点符号
import string
printable = string.printable
print(len(printable))
print(printable[:50])
print(printable[50:])
100
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN
OPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ \t\n\r\x0b\x0c
# 查找printable中的数字
print(re.findall('\d', printable))
# 查找printable中的数字,字符,下划线
print(re.findall('\w', printable), end='')
# 查找printable中的空格符
re.findall('\s', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_']
[' ', '\t', '\n', '\r', '\x0b', '\x0c']
注意:\d和\w不仅会匹配ASCII字符,还可以匹配Unicode字符
s = 'abc' + '-/*' + '\u00ea' + '\u0115' + '\u0030' + '\u0031'
print(re.findall('\w', s))
print(re.findall('\d',s))
['a', 'b', 'c', 'ê', 'ĕ', '0', '1']
['0', '1']
2.3 数量
说明: 表格中的 prev 表示 1. 单个字符如b 2. 或者复杂的表达式如(abc)或(.|\n)(这是分组的功能,见2.5)
语法 | 说明 | 模式示例 | 匹配 |
---|---|---|---|
prev* | 匹配0个或多个 prev,尽可能多地匹配,贪婪模式,等价于{0,} | ab* | a或ab或abb或abbb,注意是匹配字符a后面跟0个或多个字符b |
prev*? | 匹配0个或多个 prev,尽可能少地匹配,非贪婪模式 | ab*? | a,非贪婪模式下匹配0个字符b |
prev+ | 匹配1个或多个 prev,尽可能多地匹配,贪婪模式,等价于{1,} | ab+ | ab或abb或abbb |
prev+? | 匹配1个或多个 prev,尽可能少地匹配,非贪婪模式 | ab+? | ab,非贪婪模式下匹配1个字符b |
prev? | 匹配0个或1个 prev,尽可能多地匹配,贪婪模式,等价于{0,1} | ab? | a或ab |
prev?? | 匹配0个或1个 prev,尽可能少地匹配,非贪婪模式 | ab?? | a,非贪婪模式下匹配0个字符b |
prev{m} | 匹配m个连续的 prev | a{3} | aaa |
prev{m,n} | 匹配m到n个连续的 prev ,尽可能多地匹配,贪婪模式。n可选,如果不指定,则表示m到无穷多个连续的 prev | a{3,5} | aaa或aaaa或aaaaa |
prev{m,n}? | 匹配m到n个连续的 prev ,尽可能少地匹配,非贪婪模式 | a{3,5}? | aaa |
可以在*或+或?的后面再添加一个?,此时表示非贪婪模式匹配,python中的正则表达式默认是贪婪模式匹配,它会在满足整个表达式要求的前提下,尽可能多地去匹配字符
2.4 边界
语法 | 说明 | 模式示例 | 匹配 |
---|---|---|---|
^prev | 匹配以 prev 开头的字符串(托字符)。多行文本中,默认^只会匹配第一行的开头位置,如果设置了re.MULTILINE标志位,则^也会匹配换行符之后的开头位置 | ^ab | abcd |
prev$ | 匹配以 prev 结尾的字符串。多行文本中,默认$只会匹配最后一行的结尾位置,如果设置了re.MULTILINE标志位,则$也会匹配换行符之前的结尾位置 | ab$ | 只匹配ab。如果是.*ab$则会匹配123ab,否则使用ab\Z |
\b | 单词边界。Matches the entire string, but only at the beginning or end of a word. A word is defined as a sequence of word characters. Note that formally, \b is defined as the boundary between a \w and a \W character (or vice versa), or between \w and the beginning/end of the string. 注意: \b在Python中默认会被转义为\x08表示退格,需要将整个正则表达式指定为原始字符串(在前面加个r),即r'\bfoo\b' | r'\bfoo\b' 请使用re.findall()测试 | 匹配foo或foo.或(foo)或bar foo baz,但不匹配foobar或foo3 |
\B | 非单词边界。Matches the entire string, but only when it is not at the beginning or end of a word. \B is just the opposite of \b. | py\B | 匹配python或py3或py2,但不匹配py或py.或py! |
\A | Matches only at the start of the string. | \Aab | abcde |
\Z | Matches only at the end of the string. | ab\Z | 123ab |
2.5 分组
语法 | 说明 | 模式示例 | 匹配 |
---|---|---|---|
(expr) | 将小括号内的表达式作为一个分组,后面可以接表示数量的特殊字符。每个分组的编号从1开始递增,后续可以使用\1、\2这样引用分组匹配到的内容 | a(bc)?d | ad或abcd,不匹配abcbcd |
expr1 │expr2 | 匹配 expr1 或 expr2 ,a│b│c等价于[abc],都表示匹配字符a或b或c。从左至右,如果匹配了某个表达式,则跳过后续表达式。expr1 │ expr2表示作用于整个待匹配的字符串,而(expr1│expr2)加入小括号内表示分组,不是整个字符串 | p(i│u)g | pig或pug |
\1 | 引用编号为1的分组匹配到的字符串,同理\2表示引用第2个分组。注意: 此时为了不让python自动将\1转义为\x01,需要将整个正则表达式指定为原始字符串(在前面加个r),即r'a(\d)b\1c' | r'a(\d)b\1c' | a3b3c |
(?P |
类似于(expr),同时给分组指定了一个别名NAME,注意是大写的字母P | r'a(?P\d)b\1c' |
a3b3c |
(?P=NAME) | 引用别名为NAME的分组,当然也可以继续使用编号的形式引用分组如\1,此时要加r指定为原始字符串 | a(?P\d)b(?P=quote)c 或 r'a(?P\d)b\1c' |
a3b3c |
- 引用命名的分组
当使用match()或search()时,所有的匹配会以m.group()的形式返回到对象m中。如果你用括号将某一模式包裹起来, 括号中模式匹配得到的结果归入自己的分组group(无名称)中,而调用 m.groups() 可以得到包含这些匹配的元组
import re
s = 'I wish I may, I wish I might have a dish of fish tonight.'
m = re.search(r'(. dish\b).*(\bfish)', s)
print(m.group())
print(m.groups())
print(m.group(0))
print(m.group(1))
print(m.group(2))
a dish of fish
('a dish', 'fish')
a dish of fish
a dish
fish
给分组指定别名:
import re
s = 'I wish I may, I wish I might have a dish of fish tonight.'
m2 = re.search(r'(?P. dish\b).*(?P\bfish)', s)
print(m2.group())
print(m2.groups())
print(m2.group(0))
print(m2.group('DISH'))
print(m2.group('FISH'))
print(m2.group(1))
print(m2.group(2))
a dish of fish
('a dish', 'fish')
a dish of fish
a dish
fish
a dish
fish
2.6 扩展语法
python3中的正则表达式,以(?开头的都是一些扩展语法,比如上面学过的(?P
语法 | 说明 | 模式示例 | 匹配 |
---|---|---|---|
(?:expr) | 非捕获组。如果分组后续要被\1这样的形式引用,使用(expr),这叫捕获组,后面可以接表示数量的特殊字符。如果不想分组被引用,就使用(?:expr),这叫非捕获组,后面可以接表示数量的特殊字符 | a(?:\d)+bc,此时不支持引用分组,像r'a(?:\d)+b\1c'这样的属于语法错误 | a333bc |
prev(?=next) | 如果后面为 next,则返回 prev | ab(?=\d) | 如果ab后面紧跟一个数字,则匹配,比如ab3,返回ab |
prev(?!next) | 如果后面不是 next,则返回 prev | ab(?!\d) | 如果ab后面不是紧跟一个数字,则匹配,比如abc,返回ab |
(?<=prev)next | 如果前面为 prev,则返回 next | (?<=\d)ab | 如果ab前面是一个数字,则匹配,比如1ab,返回ab |
(? | 如果前面不是 prev,则返回 next | (? | 如果ab前面不是一个数字,则匹配,比如Aab,返回ab |
- 非捕获组:
比如要使用多个分割符或者是分隔符周围不确定的空格时,来分割一个字符串
import re
line = 'asdf fjdk; afed, fjek,asdf, foo'
print(re.split(r'[;,\s]\s*', line))
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
这里使用了[],如果你想在正则表达式中用括号()包含一个捕获分组,那么被匹配的文本(那些分隔符)也将出现在结果列表中
print(re.split(r'(;|,|\s)\s*', line))
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo']
如果你不想保留分割字符串到结果列表中去,但仍然需要使用到括号来分组正则表达式的话, 确保你的分组是非捕获分组,形如(?:...) 。
print(re.split(r'(?:,|;|\s)\s*', line))
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
3 实例
3.1 字符串忽略大小写的搜索替换
为了在文本操作时忽略大小写,你需要在使用re模块的时候给这些操作提供re.IGNORECASE标志参数。
import re
text = 'UPPER PYTHON, lower python, Mixed Python'
print(re.findall('python', text))
print(re.findall('python', text, flags=re.IGNORECASE))
print(re.sub('python', 'snake', text, flags=re.IGNORECASE))
['python']
['PYTHON', 'python', 'Python']
UPPER snake, lower snake, Mixed snake
这样不管原字符串中的是大写、小写或首字母大写的python,全替换为小写的snake,如果你想替换字符串能自动跟被匹配字符串的大小写保持一致,需要提供一个辅助函数
import re
text = 'UPPER PYTHON, lower python, Mixed Python'
def matchcase(word):
def replace(m):
text = m.group()
if text.isupper():
return word.upper()
elif text.islower():
return word.lower()
elif text[0].isupper():
return word.capitalize()
else:
return word
return replace
print(re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE))
UPPER SNAKE, lower snake, Mixed Snake
matchcase('snake')返回了一个回调函数
3.2 python支持的其它正则表达式的标志位flags
- re.A: 或re.ASCII,使\w/\W/d/\D/\s/\S/\b/\B只匹配ASCII字符,不匹配Unicode字符。
- re.I: 或re.IGNORECASE,忽略大小写,[A-Z]会匹配小写字母。
- re.M: 或re.MULTILINE,多行模式,改变^或$的默认行为。
- re.S: 或re.DOTALL,Make the.special character match any character at all, including a newline; without this flag, .will match anything except a newline。
- re.U: 或re.UNICODE,默认使用此标志位,\w/\W/d/\D/\s/\S/\b/\B会匹配Unicode字符,如果指定了re.A标志,则re.U失效。
- re.X: 或re.VERBOSE,允许整个正则表达式写成多行,忽略空白字符,并可以添加#开头的注释,这样更美观。
import re
a = re.compile(r"""\d + # the integral part
\. # the decimal point
\d * # some fractional digits""", re.X)
b = re.compile(r"\d+\.\d*")
print(a.match('10.2'))
print(b.match('10.2'))
`可以一次指定多个标志位·
import re
text = '''UPPER PYTHON
lower python
Mixed Python'''
print(re.sub('python$', 'snake', text)) # 三个python都没被替换,$表示只能匹配最后一个python,但是大小写不匹配,它是Python,所以没有被替换
print()
print(re.sub('python$', 'snake', text, flags=re.IGNORECASE)) # 指定了re.I,忽略大小写,最一个Python被替换成了snake
print()
print(re.sub('python$', 'snake', text, flags=re.IGNORECASE|re.MULTILINE)) # 同时指定re.I和re.M,三个都被替换
print()
p = re.compile(r'python$', re.I|re.M)
print(p) # 预编译后的正则模式除了默认的re.UNICODE标志位外,又添加了re.I和re.M两个标志位
print(p.sub('snake', text))
print()
print(re.sub('(?im)python$', 'snake', text)) # re.I|re.M 等价于 (?im),注意(?im)必须位于整个正则的开头
UPPER PYTHON
lower python
Mixed PythonUPPER PYTHON
lower python
Mixed snakeUPPER snake
lower snake
Mixed snakere.compile('python$', re.IGNORECASE|re.MULTILINE)
UPPER snake
lower snake
Mixed snakeUPPER snake
lower snake
Mixed snake
3.3 最短匹配模式: 非贪婪
你正在试着用正则表达式匹配某个文本模式,但是它找到的是模式的最长可能匹配:
import re
p = re.compile(r'"(.*)"')
text1 = 'Computer says "no."'
print(p.findall(text1))
text2 = 'Computer says "no." Phone says "yes."'
print(p.findall(text2)) # 默认是贪婪模式,(.*)会尽可能多的匹配,只要后面有一个"就能满足整个正则,所以匹配到最后一个冒号之前了
['no.']
['no." Phone says "yes.']
为了修正这个问题,可以在模式中的*操作符后面加上?修饰符
p2 = re.compile(r'"(.*?)"')
pritn(p2.findall(text2))
['no.', 'yes.']
3.4 多行匹配
你正在试着使用正则表达式去匹配一大块的文本,而你需要跨越多行去匹配,当你用点.去匹配任意字符的时候,忘记了点.不能匹配换行符\n的事实。比如,假设你想试着去匹配C语言分割的注释
import re
text1 = '/* this is a comment */'
text2 = '''/* this is a
multiline comment */
'''
p = re.compile(r'/\*(.*?)\*/')
print(p.findall(text1)) # 能正确匹配单行
print(p.findall(text2)) # . 点号不能匹配多行中的换行符,所以整体匹配失败
[' this is a comment ']
[]
可以使用.|\n匹配所有字符,或者指定re.DOTALL标志位
p2 = re.compile(r'/\*((?:.|\n)*?)\*/')
print(p2.findall(text2))
p3 = re.compile(r'/\*(.*?)\*/', re.DOTALL)
print(p3)
print(p3.findall(text2))
[' this is a\n multiline comment ']
re.compile('/\(.?)\*/', re.DOTALL)
[' this is a\n multiline comment ']
re.MULTILINE
import re
text = 'This is some text -- with punctuation.\nA second line.'
pattern = r'(^\w+)|(\w+\S*$)'
single_line = re.compile(pattern)
multiline = re.compile(pattern, re.MULTILINE)
print('Single Line :')
for match in single_line.findall(text):
print(' {!r}'.format(match))# 转义字符,只能和format一起使用
print('Multline :')
for match in multiline.findall(text):
print(' {!r}'.format(match))
Single Line :
('This', '')
('', 'line.')
Multline :
('This', '')
('', 'punctuation.')
('A', '')
('', 'line.')