>>> from env_helper import info; info()
页面更新时间: 2024-03-29 09:19:20
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-18-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

1.1. 正则表达式与模式匹配

1.1.1. 查找字符串文本模式

假设你希望在字符串中查找电话号码。你知道模式:3个数字, 一个短横线,3个数字,一个短横线,再是4个数字。 例如:415-555-4242。

假定我们用一个名为 isPhoneNumber() 的函数, 来检查字符串是否匹配模式,它返回 TrueFalse

运行以下代码:

>>> 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() 。 例如,在第一次迭代, i0chunk 被赋值为 message[0:12] (即字符串'Call me at 4')。在下一次迭代, i1chunk 被赋值为 message[1:13] (字符串 'all me at 41' )。

chunk 传递给 isPhoneNumber(), 看看它是否符合电话号码的模式。如果符合, 就打印出这段文本。

继续遍历 message ,最终 chunk 中的12个字符会是一个电话号码。 该循环遍历了整个字符串,测试了每一段12个字符, 打印出所有满足 isPhoneNumber()chunk 。 当我们遍历完 message ,就打印出 Done

在这个例子中,虽然 message 中的字符串很短, 但它也可能包含上百万个字符,程序运行仍然不需要一秒钟。 使用正则表达式查找电话号码的类似程序, 运行也不会超过一秒钟,但用正则表达式编写这类程序会快得多。

1.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} , 也匹配正确的电话号码格式。

>>> import re
>>> resobj = re.search('\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
>>> print('415-555-4242 is a phone number:')
>>> print(isPhoneNumber2('415-555-4242'))
>>> print('Moshi moshi is a phone number:')
>>> print(isPhoneNumber2('Moshi moshi'))
415-555-4242 is a phone number:
True
Moshi moshi is a phone number:
False

对比两个函数的写法。 不论是可读性还是代码量,使用正则表达式都要好得的。

1.1.3. 正则表达式模式

模式字符串使用特殊的语法来表示一个正则表达式:

字母和数字表示他们自身。一个正则表达式模式中的字母和数字匹配同样的字符串。

多数字母和数字前加一个反斜杠时会拥有不同的含义。

标点符号只有被转义时才匹配自身,否则它们表示特殊的含义。

反斜杠本身需要使用反斜杠转义。

由于正则表达式通常都包含反斜杠,所以你最好使用原始字符串来表示它们。 模式元素(如 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] 匹配不在方括号内的任意字符。

1.1.4. 正则表达式修饰符 - 可选标志

正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。 多个标志可以通过按位 OR ( | ) 它们来指定。如 re.I | re.M 被设置成 IM 标志:

修饰符 | 描述

  • re.I 使匹配对大小写不敏感

  • re.L 做本地化识别(locale-aware)匹配

  • re.M 多行匹配,影响 ^$

  • re.S 使 . 匹配包括换行在内的所有字符

  • re.U 根据Unicode字符集解析字符。这个标志影响 \w , \W , \b , \B .

  • re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

>>> 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