1. 模式匹配与正则表达式
你可能熟悉文本查找,即按下Ctrl-F,输入你要查找的词。 “正则表达式”更进一步,它们让你指定要查找的“模式”。 你也许不知道一家公司的准确电话号码,但如果你住在美国或加拿大, 你就知道它有3位数字,然后是一个短横线,然后是4位数字(有时候以3位区号开始)。 因此作为一个人,你看到一个电话号码就知道: 415-555-1234 是电话号码,但 4,155,551,234 不是。
正则表达式很有用,但如果不是程序员,很少会有人了解它, 尽管大多数现代文本编辑器和文字处理器(诸如微软的Word或OpenOffice/LibreOffcie), 都有查找和查找替换功能,可以根据正则表达式查找。 正则表达式可以节约大量时间,不仅适用于软件用户,也适用于程序员。 实际上,技术作家Cory Doctorow声称,甚至应该在教授编程之前,先教授正则表达式:
“知道[正则表达式]可能意味着用3步解决一个问题, 而不是用3000步。如果你是一个技术怪侠, 别忘了你用几次击键就能解决的问题, 其他人需要数天的烦琐工作才能解决, 而且他们容易犯错。”
在本章中,你将从编写一个程序开始,先不用正则表达式来寻找文本模式。 然后再看看,使用正则表达式让代码变得多么简洁。 我将展示用正则表达式进行基本匹配,然后转向一些更强大的功能,诸如字符串替换, 以及创建你自己的字符类型。
1.1. 不用正则表达式来查找文本模式
假设你希望在字符串中查找电话号码。你知道模式:3个数字, 一个短横线,3个数字,一个短横线,再是4个数字。 例如:415-555-4242。
假定我们用一个名为 isPhoneNumber() 的函数, 来检查字符串是否匹配模式,它返回 True 或 False 。 打开一个新的文件编辑器窗口,输入以下代码, 然后保存为 isPhoneNumber.py :
def isPhoneNumber(text):
if len(text) !=12:
return False
for i in range(3):
if not text[i].isdecimal():
return False
if text[3]!=‘-’:
return False
for i in range(4,7):
if not text[i].isdecimal():
return False
if text[7] != ‘-’:
return False
for i in range(8,12):
if not text[i].isdecimal():
return False
return True
print(‘415-555-4242 is a phone number:’)
print(isPhoneNumber(‘415-555-4242’))
print(‘Moshi moshi is a phone number:’)
print(isPhoneNumber(‘Moshi moshi’))
415-555-4242 is a phone number:
True
Moshi moshi is a phone number:
False
isPhoneNumber() 函数的代码进行几项检查, 看看 text 中的字符串是不是有效的电话号码。 如果其中任意一项检查失败,函数就返回 False 。 代码首先检查该字符串是否刚好有12个字符。 然后它检查区号(就是 text 中的前3个字符)是否只包含 数字。函数剩下的部分检查该字符串是否符合电话号码的模式: 号码必须在区号后出现第一个短横线), 3个数字,然后是另一个短横线,最后是4个数字。 如果程序执行通过了所有的检查,它就返回 True 。
用参数 ‘415-555-4242’ 调用 isPhoneNumber() 将返回真。 用参数 ‘Moshimoshi’ 调用 isPhoneNumber() 将返回假, 第一项测试失败了,因为不是12个字符。
必须添加更多代码,才能在更长的字符串中寻找这种文本模式。 用下面的代码, 替代 isPhoneNumber.py 中最后4个 print() 函数调用:
message=‘Call me at 415-555-1011 tomorrow.415-555-9999 is my office.’
for i in range(len(message)):
chunk=message[i:i+12]
if isPhoneNumber(chunk):
print(‘Phone number found:’ + chunk)
print(‘Done’)
Phone number found:415-555-1011
Phone number found:415-555-9999
Done
该程序运行时,输出看起来像这样:
在 for 循环的每一次迭代中, 取自 message 的一段新的12个字符被赋给变量 chunk() 。 例如,在第一次迭代, i 是 0 , chunk 被赋值为 message[0:12] (即字符串’Call me at 4’)。在下一次迭代, i 是1, chunk 被赋值为 message[1:13] (字符串 ‘all me at 41’ )。
将 chunk 传递给 isPhoneNumber(), 看看它是否符合电话号码的模式。如果符合, 就打印出这段文本。
继续遍历 message ,最终 chunk 中的12个字符会是一个电话号码。 该循环遍历了整个字符串,测试了每一段12个字符, 打印出所有满足 isPhoneNumber() 的 chunk 。 当我们遍历完 message ,就打印出 Done 。
在这个例子中,虽然 message 中的字符串很短, 但它也可能包含上百万个字符,程序运行仍然不需要一秒钟。 使用正则表达式查找电话号码的类似程序, 运行也不会超过一秒钟,但用正则表达式编写这类程序会快得多。
1.2. 用正则表达式查找文本模式
如果你想在有分机的电话号码中快速查找电话号,例如415-555-4242 x99,该怎么办呢?
正则表达式,又称规则表达式,英语简称为 regex,是文本模式的描述方法,能帮助你方便的检查一个字符串是否与某种模式匹配。
例如,\d 是一个正则表达式,表示一位数字字符, 即任何一位0到9的数字。 Python 使用正则表达式 \d\d\d-\d\d\d-\d\d\d\d , 来匹配前面 isPhoneNumber()函数匹配的同样文本: 3个数字、一个短横线、3个数字、一个短横线、4个数字。 所有其他字符串都不能匹配 \d\d\d-\d\d\d-\d\d\d\d 正则表达式。
但正则表达式可以复杂得多。例如, 在一个模式后加上花括号包围的3 ({3}), 就是说,“匹配这个模式3次”。 所以较短的正则表达式 \d{3}-\d{3}-\d{4} , 也匹配正确的电话号码格式。
1.2.1. 创建正则表达式对象
Python 中所有正则表达式的函数都在 re 模块中。在交互式环境中输入以下代码,导入该模块:
import re
re 模块的 compile() 函数根据一个模式字符串和可选的标志参数生成一个正则表达式对象。 该对象拥有一系列方法用于正则表达式匹配和替换。 例如,向 re.compile() 传入一个字符串值表示正则表达式, 它将返回一个 Regex 模式对象(简称为 Regex 对象)。
要创建一个 Regex 对象来匹配电话号码模式, 就在交互式环境中输入以下代码。
regobj = re.compile(‘\d\d\d-\d\d\d-\d\d\d\d’)
现在 regobj 变量包含了一个 Regex 对象。
1.2.2. 匹配 Regex 对象
Regex 对象的 search() 方法查找传入的字符串,寻找该正则表达式的所有匹配。 如果字符串中没有找到该正则表达式模式,search() 方法将返回None 。 如果找到了该模式,search() 方法将返回一个 Match 对象。 Match 对象有一个 group() 方法,它返回被查找字符串中实际匹配的文本。 例如,在交互式环境中输入以下代码:
mo = regobj.search(‘My number is 415-555-4242.’)
print('Phone number found: ’ + mo.group())
Phone number found: 415-555-4242
变量名 mo 是一个通用的名称,用于 Match 对象。
这里,我们将期待的模式传递给 re.compile() ,并将得到的 Regex 对象保存在 phoneNumRegex 中。 然后我们在 phoneNumRegex 上调用 search() ,向它传入想查找的字符串。查找的结果保存在变量 mo 中。 在这个例子里,我们知道模式会在这个字符串中找到,所以我们知道会返回一个 Match 对象。 知道 mo 包含一个 Match 对象,而不是空值 None ,我们就可以在 mo 变量上调用 group() ,返回匹配的结果。 将 mo.group() 写在打印语句中,显示出完整的匹配,即 415-555-4242 。
如果对象没找到,可以进行判断:
mo2 = regobj.search(‘my number is 41555433.’)
print(‘got’) if mo2 else print(‘not got’)
not got
向 re.compile() 传递原始字符串
回忆一下, Python 中转义字符使用倒斜杠()。 字符串 V 表示一个换行字符, 而不是倒斜杠加上一个小写的 n 。 你需要输入转义字符 \ ,才能打印出一个倒斜杠。 所以 ‘\n’ 表示一个倒斜杠加上一个小写的 n 。 但是,通过在字符串的第一个引号之前加上 r , 可以将该字符串标记为原始字符串,它不包括转义字符。
因为正则表达式常常使用倒斜杠, 向 re.compile() 函数传入原始字符串就很方便, 而不是输入额外得到斜杠。输入 r’\d\d\d-\d\d\d-\d\d\d\d’ , 比输入 ‘\d\d\d-\d\d\d-\d\d\d\d’ 要容易得多。
1.2.3. 正则表达式匹配复习
虽然在 Python中使用正则表达式有几个步骤, 但每一步都相当简单。
用 import re 导入正则表达式模块。
用 re.compile() 函数创建一个 Regex 对象(记得使用原始字符串)。
向 Regex 对象的 search() 方法传入想查找的字符串。它返回一个 Match 对象。
调用 Match 对象的 group() 方法,返回实际匹配文本的字符串。
下面主要介绍Python中常用的正则表达式处理函数。
1.2.4. re.match函数
re.match() 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话, match() 就返回 None 。
函数语法:
re.match(pattern, string, flags=0)
函数参数说明:
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。
我们可以使用 group(num) 或 groups() 匹配对象函数来获取匹配表达式。
group(num=0) 匹配的整个表达式的字符串, group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。
groups() 返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。
实例 1:
在起始位置匹配
print(re.match(‘www’, ‘www.runoob.com’).span())
(0, 3)
不在起始位置匹配
print(re.match(‘com’, ‘www.runoob.com’))
None
1.2.5. re.search方法
re.search 扫描整个字符串并返回第一个成功的匹配。
函数语法:
re.search(pattern, string, flags=0)
函数参数说明:
参数 | 描述
pattern | 匹配的正则表达式
string | 要匹配的字符串。
flags | 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。
匹配成功 re.search 方法返回一个匹配的对象,否则返回None。
我们可以使用 group(num) 或 groups() 匹配对象函数来获取匹配表达式。
匹配对象方法 | 描述
group(num=0) |匹配的整个表达式的字符串,group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。
groups() | 返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。
实例 1:
在起始位置匹配
print(re.search(‘www’, ‘www.runoob.com’).span())
(0, 3)
不在起始位置匹配
print(re.search(‘com’, ‘www.runoob.com’).span())
(11, 14)
实例 2:
import re
line = “Cats are smarter than dogs”;
searchObj = re.search(‘(.) are (.?) .*’, line, re.M|re.I)
if searchObj:
print ("searchObj.group() : ", searchObj.group())
print ("searchObj.group(1) : ", searchObj.group(1))
print ("searchObj.group(2) : ", searchObj.group(2))
else:
print (“Nothing found!!”)
searchObj.group() : Cats are smarter than dogs
searchObj.group(1) : Cats
searchObj.group(2) : smarter
instr = ‘/home/bk/book-rst/doculet/sphinx-tutorial/runoob-src/pt01_language_eb00kh’
re_book = re.compile(‘eb\d\d…’)
uu = re_book.search(instr)
uu.span()
(67, 73)
dir(uu)
[‘class’,
‘class_getitem’,
‘copy’,
‘deepcopy’,
‘delattr’,
‘dir’,
‘doc’,
‘eq’,
‘format’,
‘ge’,
‘getattribute’,
‘getitem’,
‘getstate’,
‘gt’,
‘hash’,
‘init’,
‘init_subclass’,
‘le’,
‘lt’,
‘module’,
‘ne’,
‘new’,
‘reduce’,
‘reduce_ex’,
‘repr’,
‘setattr’,
‘sizeof’,
‘str’,
‘subclasshook’,
‘end’,
‘endpos’,
‘expand’,
‘group’,
‘groupdict’,
‘groups’,
‘lastgroup’,
‘lastindex’,
‘pos’,
‘re’,
‘regs’,
‘span’,
‘start’,
‘string’]
uu.group()
‘eb00kh’
1.2.6. re.match 与 re.search 的区别
re.match 只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回 None ; 而 re.search 匹配整个字符串,直到找到一个匹配。
实例:
import re
line = “Cats are smarter than dogs”;
matchObj = re.match( ‘dogs’, line, re.M|re.I)
if matchObj:
print ("match --> matchObj.group() : ", matchObj.group())
else:
print (“No match!!”)
matchObj = re.search( r’dogs’, line, re.M|re.I)
if matchObj:
print ("search --> matchObj.group() : ", matchObj.group())
else:
print (“No match!!”)
No match!!
search --> matchObj.group() : dogs
1.2.7. 检索和替换
Python 的re模块提供了re.sub用于替换字符串中的匹配项。
语法:
re.sub(pattern, repl, string, count=0)
参数:
pattern : 正则中的模式字符串。
repl : 替换的字符串,也可为一个函数。
string : 要被查找替换的原始字符串。
count : 模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。
实例:
#!/usr/bin/python3
import re
phone = “2004-959-559 # 这是一个电话号码”
删除注释
num = re.sub(‘#.*$’, “”, phone)
print ("电话号码 : ", num)
移除非数字的内容
num = re.sub(‘\D’, “”, phone)
print ("电话号码 : ", num)
电话号码 : 2004-959-559
电话号码 : 2004959559
1.2.8. repl 参数是一个函数
以下实例中将字符串中的匹配的数字乘于 2:
#!/usr/bin/python
import re
将匹配的数字乘于 2
def double(matched):
value = int(matched.group(‘value’))
return str(value * 2)
s = ‘A23G4HFD567’
print(re.sub(‘(?P\d+)’, double, s))
A46G8HFD1134
1.2.9. 正则表达式修饰符 - 可选标志
正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。多个标志可以通过按位 OR(|) 它们来指定。如 re.I | re.M 被设置成 I 和 M 标志:
修饰符 | 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符。这个标志影响 \w , \W , \b , \B .
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。
1.2.10. 正则表达式模式
模式字符串使用特殊的语法来表示一个正则表达式:
字母和数字表示他们自身。一个正则表达式模式中的字母和数字匹配同样的字符串。
多数字母和数字前加一个反斜杠时会拥有不同的含义。
标点符号只有被转义时才匹配自身,否则它们表示特殊的含义。
反斜杠本身需要使用反斜杠转义。
由于正则表达式通常都包含反斜杠,所以你最好使用原始字符串来表示它们。 模式元素(如 r’/t’ ,等价于’//t’ )匹配相应的特殊字符。
下表列出了正则表达式模式语法中的特殊元素。如果你使用模式的同时提供了可选的标志参数,某些模式元素的含义会改变。
模式 | 描述
^ 匹配字符串的开头
$ 匹配字符串的末尾。
. 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
[…] 用来表示一组字符,单独列出: [amk] 匹配 ‘a’,‘m’或’k’
[^…] 不在[]中的字符: [^abc] 匹配除了a,b,c之外的字符。
re* 匹配0个或多个的表达式。
re+ 匹配1个或多个的表达式。
re? 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
re{ n}
re{ n,} 精确匹配n个前面表达式。
re{ n, m} 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a|b 匹配a或b
(re) G匹配括号内的表达式,也表示一个组
(?imx) 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。
(?-imx) 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。
(?: re) 类似 (…), 但是不表示一个组
(?imx:re) 在括号中使用i, m, 或 x 可选标志
(?-imx: re) 在括号中不使用i, m, 或 x 可选标志
(?#…) 注释.
(?= re) 前向肯定界定符。如果所含正则表达式,以 … 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。
(?! re) 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功
(?> re) 匹配的独立模式,省去回溯。
\w 匹配字母数字
\W 匹配非字母数字
\s 匹配任意空白字符,等价于 [\t\n\r\f] .
\S 匹配任意非空字符
\d 匹配任意数字,等价于 [0-9] .
\D 匹配任意非数字
\A 匹配字符串开始
\Z 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。c
\z 匹配字符串结束
\G 匹配最后匹配完成的位置。
\b 匹配一个单词边界,也就是指单词和空格间的位置。例如, er\b 可以匹配 never 中的 er , 但不能匹配 verb 中的 er 。
\B 匹配非单词边界。‘er:raw-latex:B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。
\n, \t , 等. 匹配一个换行符。匹配一个制表符。等
\1…\9 匹配第n个分组的内容。
\10 匹配第n个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式。
正则表达式符号复习
本章介绍了许多表示法,所以这里快速复习一下学到的内容:
? 匹配零次或一次前面的分组。
匹配零次或多次前面的分组。
+匹配一次或多次前面的分组。
{n}匹配 n 次前面的分组。
{n,} 匹配 n 次或更多前面的分组。
{,m} 匹配零次到 m 次前面的分组。
{n,m}匹配至少 n 次、至多 m 次前面的分组。
{n,m}? 或 *? 或 +? 对前面的分组进行非贪心匹配。
^spam 意味着字符串必须以 spam 开始。
spam$意味着字符串必须以 spam 结束。
.匹配所有字符,换行符除外。
\d 、 \w和\s 分别匹配数字、单词和空格。
\D、\W和 \S分别匹配出数字、单词和空格外的所有字符。
[abc] 匹配方括号内的任意字符(诸如a、b或c)。
[^abc] 匹配不在方括号内的任意字符。