正则表达式(Regular Expression,简称regex)是一种强大的文本处理工具,用于在字符串中查找、匹配和替换特定模式的文本。 Python 通过re模块提供完整的正则表达式支持,包含以下核心功能:
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:')
415-555-4242 is a phone number:
print(isPhoneNumber('415-555-4242'))
True
print('Moshi moshi is a phone number:')
Moshi moshi is a phone number:
print(isPhoneNumber('Moshi moshi'))
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 中的字符串很短, 但它也可能包含上百万个字符,程序运行仍然不需要一秒钟。 使用正则表达式查找电话号码的类似程序, 运行也不会超过一秒钟,但用正则表达式编写这类程序会快得多。
使用正则表达式查找
如果想在有分机的电话号码中快速查找电话号,例如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} , 也匹配正确的电话号码格式。
import re
resobj = re.search(r'\d\d\d-\d\d\d-\d\d\d\d','My number is 415-555-4242.')
resobj.span()
(13, 25)
resobj.group()
'415-555-4242'
使用正则表达式修改上面的函数,得到第2个版本的函数 isPhoneNumber2() 。
运行如下:
def isPhoneNumber2(text):
if len(text) !=12:
return False
res_ob = re.search('\d\d\d-\d\d\d-\d\d\d\d',text)
if res_ob:
return True
return False
<>:4: SyntaxWarning: invalid escape sequence '\d'
<>:4: SyntaxWarning: invalid escape sequence '\d'
/tmp/ipykernel_4356/381155936.py:4: SyntaxWarning: invalid escape sequence '\d'
res_ob = re.search('\d\d\d-\d\d\d-\d\d\d\d',text)
print('415-555-4242 is a phone number:')
415-555-4242 is a phone number:
print(isPhoneNumber2('415-555-4242'))
True
print('Moshi moshi is a phone number:')
Moshi moshi is a phone number:
print(isPhoneNumber2('Moshi moshi'))
False
对比两个函数的写法。 不论是可读性还是代码量,使用正则表达式都要好得的。
模式 | 描述
^匹配字符串的开头。$匹配字符串的末尾。.匹配任意字符,除了换行符,当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匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。\z匹配字符串结束。\G匹配最后匹配完成的位置。\b匹配一个单词边界,也就是指单词和空格间的位置。例如,er\b可以匹配never中的er, 但不能匹配verb中的er。\B匹配非单词边界。'er\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]匹配不在方括号内的任意字符。
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