您当前位置: 首页 ‣ 深入 Python 3 ‣
难度等级: ♦♦♦♢♢
❝ 有些人遇到问题,会想:“我知道了,我用正则表达式。” 现在他们有两个问题了。 ❞
— Jamie Zawinski
从一大块文本中提取一小段文本是一个挑战。 在 Python 中,字符串有用于搜索和替换的方法:index()
、find()
、split()
、count()
、replace()
、&c。 但是这些方法仅限于最简单的情况。 例如,index()
方法查找单个硬编码的子字符串,并且搜索始终区分大小写。 要对字符串 s 进行不区分大小写的搜索,您必须调用 s.lower()
或 s.upper()
确保您的搜索字符串具有匹配的大小写。 replace()
和 split()
方法具有相同的限制。
如果您的目标可以通过字符串方法完成,您应该使用它们。 它们快速、简单且易于阅读,对于快速、简单、可读的代码来说,还有很多话要说。 但是,如果您发现自己使用了很多不同的字符串函数,并使用 if
语句来处理特殊情况,或者您正在将对 split()
和 join()
的调用链接起来以对字符串进行切片和切块,那么您可能需要升级到正则表达式。
正则表达式是一种强大且(大多)标准化的方式,用于使用复杂的字符模式搜索、替换和解析文本。 尽管正则表达式的语法很紧凑,与普通代码不同,但结果最终可能比使用长串字符串函数的自制解决方案更易读。 甚至还有在正则表达式中嵌入注释的方法,这样您可以在其中包含细粒度的文档。
☞如果您在其他语言(如 Perl、JavaScript 或 PHP)中使用过正则表达式,那么 Python 的语法将非常熟悉。 阅读
re
模块的摘要,以概述可用的函数及其参数。
⁂
这一系列示例的灵感来自几年前我在日常工作中遇到的一个实际问题,当时我需要在将地址导入到较新的系统之前,清理和标准化从旧系统导出的街道地址。(请看,我不是凭空编造这些东西;它实际上很有用。)这个例子展示了我的处理方法。
>>> s = '100 NORTH MAIN ROAD' >>> s.replace('ROAD', 'RD.') ① '100 NORTH MAIN RD.' >>> s = '100 NORTH BROAD ROAD' >>> s.replace('ROAD', 'RD.') ② '100 NORTH BRD. RD.' >>> s[:-4] + s[-4:].replace('ROAD', 'RD.') ③ '100 NORTH BROAD RD.' >>> import re ④ >>> re.sub('ROAD$', 'RD.', s) ⑤ '100 NORTH BROAD RD.'
'ROAD'
始终缩写为 'RD.'
。 乍一看,我认为这很简单,我只需要使用字符串方法 replace()
就可以了。 毕竟,所有数据都已经是大写,所以大小写不匹配不会成为问题。 并且搜索字符串 'ROAD'
是一个常量。 在这个看似简单的示例中,s.replace()
确实有效。'ROAD'
在地址中出现了两次,一次作为街道名称 'BROAD'
的一部分,一次作为它自己的单词。 replace()
方法看到这两个出现并盲目地替换了它们;与此同时,我看到我的地址被破坏了。'ROAD'
子字符串的问题,您可以诉诸以下方法:仅在地址的最后四个字符(s[-4:]
)中搜索和替换 'ROAD'
,并将字符串保留原样(s[:-4]
)。 但是您可以看到这已经变得很笨拙了。 例如,模式取决于您要替换的字符串的长度。(如果您要将 'STREET'
替换为 'ST.'
,您需要使用 s[:-6]
和 s[-6:].replace(...)
。)您想在六个月后回来调试这个吗? 我知道我不想。re
模块中。'ROAD$'
。 这是一个简单的正则表达式,仅当 'ROAD'
出现在字符串末尾时才匹配。 $
表示“字符串的结尾”。(有一个对应的字符,脱字符 ^
,表示“字符串的开头”。)使用 re.sub()
函数,您可以在字符串 s 中搜索正则表达式 'ROAD$'
并将其替换为 'RD.'
。 这匹配字符串 s 末尾的 ROAD
,但不匹配作为单词 BROAD
的一部分的 ROAD
,因为它是 s 的中间部分。继续讲我的清理地址的故事,我很快发现之前的示例(匹配地址末尾的 'ROAD'
)还不够好,因为并非所有地址都包含街道名称。 一些地址只是以街道名称结尾。 我大多数时候都侥幸成功,但是如果街道名称是 'BROAD'
,那么正则表达式将匹配字符串末尾的 'ROAD'
,作为单词 'BROAD'
的一部分,这不是我想要的。
>>> s = '100 BROAD' >>> re.sub('ROAD$', 'RD.', s) '100 BRD.' >>> re.sub('\\bROAD$', 'RD.', s) ① '100 BROAD' >>> re.sub(r'\bROAD$', 'RD.', s) ② '100 BROAD' >>> s = '100 BROAD ROAD APT. 3' >>> re.sub(r'\bROAD$', 'RD.', s) ③ '100 BROAD ROAD APT. 3' >>> re.sub(r'\bROAD\b', 'RD.', s) ④ '100 BROAD RD. APT 3'
'ROAD'
,当它位于字符串的末尾并且它是一个完整的单词(而不是更大单词的一部分)时。 要在正则表达式中表达这一点,您需要使用 \b
,这意味着“此处必须出现一个单词边界”。 在 Python 中,由于字符串中的 '\'
字符本身必须转义,因此这很复杂。 这有时被称为反斜杠瘟疫,也是正则表达式在 Perl 中比在 Python 中更容易的原因之一。 另一方面,Perl 将正则表达式与其他语法混合在一起,因此如果您遇到错误,可能很难确定是语法错误还是正则表达式错误。r
。 这告诉 Python 字符串中的任何内容都不应该转义;'\t'
是一个制表符字符,但 r'\t'
实际上是反斜杠字符 \
后跟字母 t
。 我建议在处理正则表达式时始终使用原始字符串;否则,事情会很快变得过于混乱(正则表达式本身已经足够混乱了)。'ROAD'
,作为一个完整的单词,但它不在末尾,因为地址在街道名称之后有一个公寓号码。 因为 'ROAD'
不在字符串的末尾,所以它不匹配,因此对 re.sub()
的整个调用最终什么都没有替换,您得到了原始字符串,这不是您想要的。$
字符,并添加了另一个 \b
。 现在,正则表达式变成了“匹配 'ROAD'
,当它是一个完整的单词,位于字符串中的任何位置时”,无论是结尾、开头还是中间。⁂
您很可能见过罗马数字,即使您不认识它们。 您可能在旧电影和电视节目版权(“版权 MCMXLVI
” 而不是“版权 1946
”)或图书馆或大学的献词墙上看到过它们(“成立于 MDCCCLXXXVIII
” 而不是“成立于 1888
”)。 您可能还在大纲和参考书目中看到过它们。 这是一种表示数字的系统,它确实可以追溯到古罗马帝国(因此得名)。
在罗马数字中,有七个字符以各种方式重复和组合以表示数字。
I = 1
V = 5
X = 10
L = 50
C = 100
D = 500
M = 1000
以下是一些构建罗马数字的一般规则
I
是 1
,II
是 2
,III
是 3
。 VI
是 6
(实际上是“5
和 1
”),VII
是 7
,VIII
是 8
。I
、X
、C
和 M
)最多可以重复三次。 在 4
处,您需要从下一个最高的五进制字符中减去。 您不能将 4
表示为 IIII
;相反,它表示为 IV
(“比 5
小 1
”)。 40
写为 XL
(“比 50
小 10
”),41
写为 XLI
,42
写为 XLII
,43
写为 XLIII
,然后 44
写为 XLIV
(“比 50
小 10
,然后比 5
小 1
”)。9
处,您需要从下一个最高的十进制字符中减去:8
是 VIII
,但 9
是 IX
(“比 10
小 1
”),而不是 VIIII
(因为 I
字符不能重复四次)。 90
是 XC
,900
是 CM
。10
始终表示为 X
,而不是 VV
。 100
始终是 C
,而不是 LL
。DC
是 600
;CD
是一个完全不同的数字(400
,“比 500
小 100
”)。 CI
是 101
;IC
甚至不是有效的罗马数字(因为您不能直接从 100
中减去 1
;您需要将其写为 XCIX
,“比 100
小 10
,然后比 10
小 1
”)。验证一个任意字符串是否是一个有效的罗马数字需要什么? 让我们一次处理一位。 由于罗马数字总是从最高到最低书写,让我们从最高位开始:千位。 对于 1000 及以上的数字,千位由一系列 M
字符表示。
>>> import re >>> pattern = '^M?M?M?$' ① >>> re.search(pattern, 'M') ② <_sre.SRE_Match object at 0106FB58> >>> re.search(pattern, 'MM') ③ <_sre.SRE_Match object at 0106C290> >>> re.search(pattern, 'MMM') ④ <_sre.SRE_Match object at 0106AA38> >>> re.search(pattern, 'MMMM') ⑤ >>> re.search(pattern, '') ⑥ <_sre.SRE_Match object at 0106F4A8>
^
仅在字符串的开头匹配它后面的内容。 如果没有指定这一点,那么无论 M
字符在哪里,模式都会匹配,这不是您想要的。 您需要确保 M
字符(如果存在)位于字符串的开头。 M?
可选地匹配单个 M
字符。 由于它被重复了三次,所以您匹配了零到三个连续的 M
字符。 $
匹配字符串的结尾。 当与开头处的 ^
字符组合时,这意味着模式必须匹配整个字符串,在 M
字符之前或之后没有其他字符。re
模块的本质是 search()
函数,它接受一个正则表达式(pattern)和一个字符串('M'
)来尝试匹配正则表达式。 如果找到了匹配项,search()
会返回一个对象,该对象具有各种方法来描述匹配项;如果找不到匹配项,search()
会返回 None
,即 Python 的空值。 目前,您只关心模式是否匹配,您可以通过查看 search()
的返回值来判断。 'M'
匹配这个正则表达式,因为第一个可选的 M
匹配,而第二个和第三个可选的 M
字符被忽略。'MM'
匹配,因为第一个和第二个可选的 M
字符匹配,而第三个 M
被忽略。'MMM'
匹配,因为所有三个 M
字符都匹配。'MMMM'
不匹配。所有三个 M
字符都匹配,但正则表达式坚持字符串必须以 $
字符结尾(因为 $
字符),而字符串还没有结束(因为还有第四个 M
)。所以 search()
返回 None
。M
字符都是可选的。百位数比千位数更难,因为根据其值,有几种互斥的方式可以表达它。
100 = C
200 = CC
300 = CCC
400 = CD
500 = D
600 = DC
700 = DCC
800 = DCCC
900 = CM
所以有四种可能的模式
CM
CD
C
字符(如果百位数为 0,则为零)D
,后跟零到三个 C
字符最后两种模式可以合并
D
,后跟零到三个 C
字符此示例展示了如何验证罗马数字的百位数。
>>> import re >>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$' ① >>> re.search(pattern, 'MCM') ② <_sre.SRE_Match object at 01070390> >>> re.search(pattern, 'MD') ③ <_sre.SRE_Match object at 01073A50> >>> re.search(pattern, 'MMMCCC') ④ <_sre.SRE_Match object at 010748A8> >>> re.search(pattern, 'MCMC') ⑤ >>> re.search(pattern, '') ⑥ <_sre.SRE_Match object at 01071D98>
^
),然后是千位数 (M?M?M?
)。然后它包含新部分,在括号中,定义了一组三个互斥模式,用竖线分隔:CM
、CD
和 D?C?C?C?
(即可选的 D
后跟零到三个可选的 C
字符)。正则表达式解析器按顺序(从左到右)检查这些模式中的每一个,选择第一个匹配的模式,并忽略其余模式。'MCM'
匹配,因为第一个 M
匹配,第二个和第三个 M
字符被忽略,并且 CM
匹配(因此 CD
和 D?C?C?C?
模式甚至没有被考虑)。MCM
是 1900
的罗马数字表示。'MD'
匹配,因为第一个 M
匹配,第二个和第三个 M
字符被忽略,并且 D?C?C?C?
模式匹配 D
(三个 C
字符中的每一个都是可选的,并且被忽略)。MD
是 1500
的罗马数字表示。'MMMCCC'
匹配,因为所有三个 M
字符都匹配,并且 D?C?C?C?
模式匹配 CCC
(D
是可选的,被忽略)。MMMCCC
是 3300
的罗马数字表示。'MCMC'
不匹配。第一个 M
匹配,第二个和第三个 M
字符被忽略,并且 CM
匹配,但 $
不匹配,因为您还没有到达字符串的结尾(您还有一个未匹配的 C
字符)。C
不作为 D?C?C?C?
模式的一部分匹配,因为互斥的 CM
模式已经匹配。M
字符都是可选的,被忽略,并且空字符串匹配 D?C?C?C?
模式,其中所有字符都是可选的,被忽略。呼!看看正则表达式能有多快地变得难以处理?而且您只涵盖了罗马数字的千位数和百位数。但是,如果您理解了所有这些内容,那么十位数和个位数就很简单了,因为它们的模式完全相同。但是,让我们看看另一种表达模式的方式。
⁂
{n,m}
语法在上一节中,您处理的是一个模式,其中相同的字符可以重复最多三次。在正则表达式中还有另一种表达此模式的方式,有些人觉得它更易读。首先,看看我们在上一示例中已经使用的方法。
>>> import re >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'M') ① <_sre.SRE_Match object at 0x008EE090> >>> re.search(pattern, 'MM') ② <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMM') ③ <_sre.SRE_Match object at 0x008EE090> >>> re.search(pattern, 'MMMM') ④ >>>
M
,但不是第二个和第三个 M
(但这没关系,因为它们是可选的),然后是字符串的结尾。M
,但不是第三个 M
(但这没关系,因为它可选),然后是字符串的结尾。M
,然后是字符串的结尾。M
,但没有匹配字符串的结尾(因为仍然有一个未匹配的 M
),所以模式不匹配,并返回 None
。>>> pattern = '^M{0,3}$' ① >>> re.search(pattern, 'M') ② <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MM') ③ <_sre.SRE_Match object at 0x008EE090> >>> re.search(pattern, 'MMM') ④ <_sre.SRE_Match object at 0x008EEDA8> >>> re.search(pattern, 'MMMM') ⑤ >>>
M
字符,然后是字符串的结尾。”0 和 3 可以是任何数字;如果您希望至少匹配一个,但不超过三个 M
字符,则可以写 M{1,3}
。M
字符中的一个,然后是字符串的结尾。M
字符中的两个,然后是字符串的结尾。M
字符中的三个,然后是字符串的结尾。M
字符中的三个,但没有匹配字符串的结尾。正则表达式允许在字符串结尾之前最多只有三个 M
字符,但您有四个,因此模式不匹配,并返回 None
。现在,让我们扩展罗马数字正则表达式以涵盖十位数和个位数。此示例展示了对十位数的检查。
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$' >>> re.search(pattern, 'MCMXL') ① <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCML') ② <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLX') ③ <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXX') ④ <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXXX') ⑤ >>>
M
,然后是 CM
,然后是 XL
,然后是字符串的结尾。请记住,(A|B|C)
语法表示“匹配 A、B 或 C 中的任何一个”。您匹配 XL
,因此您会忽略 XC
和 L?X?X?X?
选择,然后继续到字符串的结尾。MCMXL
是 1940
的罗马数字表示。M
,然后是 CM
,然后是 L?X?X?X?
。在 L?X?X?X?
中,它匹配 L
并跳过所有三个可选的 X
字符。然后您转到字符串的结尾。MCML
是 1950
的罗马数字表示。M
,然后是 CM
,然后是可选的 L
和第一个可选的 X
,跳过第二个和第三个可选的 X
,然后是字符串的结尾。MCMLX
是 1960
的罗马数字表示。M
,然后是 CM
,然后是可选的 L
和所有三个可选的 X
字符,然后是字符串的结尾。MCMLXXX
是 1980
的罗马数字表示。M
,然后是 CM
,然后是可选的 L
和所有三个可选的 X
字符,然后没有匹配字符串的结尾,因为仍然有一个 X
未被考虑。因此整个模式没有匹配,并返回 None
。MCMLXXXX
不是有效的罗马数字。个位数的表达式遵循相同的模式。我将省去详细信息,并向您展示最终结果。
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
那么,使用这种备用 {n,m}
语法看起来如何?此示例展示了新的语法。
>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$' >>> re.search(pattern, 'MDLV') ① <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMDCLXVI') ② <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMMDCCCLXXXVIII') ③ <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'I') ④ <_sre.SRE_Match object at 0x008EEB48>
M
字符中的一个,然后是 D?C{0,3}
。在其中,它匹配可选的 D
和三个 C
字符中的零个。继续,它通过匹配可选的 L
和三个 X
字符中的零个来匹配 L?X{0,3}
。然后它通过匹配可选的 V
和三个 I
字符中的零个来匹配 V?I{0,3}
,最后是字符串的结尾。MDLV
是 1555
的罗马数字表示。M
字符中的两个,然后是 D?C{0,3}
,其中包含一个 D
和三个 C
字符中的一个;然后是 L?X{0,3}
,其中包含一个 L
和三个 X
字符中的一个;然后是 V?I{0,3}
,其中包含一个 V
和三个 I
字符中的一个;然后是字符串的结尾。MMDCLXVI
是 2666
的罗马数字表示。M
字符中的三个,然后是 D?C{0,3}
,其中包含一个 D
和三个 C
字符中的三个;然后是 L?X{0,3}
,其中包含一个 L
和三个 X
字符中的三个;然后是 V?I{0,3}
,其中包含一个 V
和三个 I
字符中的三个;然后是字符串的结尾。MMMDCCCLXXXVIII
是 3888
的罗马数字表示,它是在没有扩展语法的情况下可以写出的最长的罗马数字。M
字符中的零个,然后通过跳过可选的 D
并匹配三个 C
字符中的零个来匹配 D?C{0,3}
,然后通过跳过可选的 L
并匹配三个 X
字符中的零个来匹配 L?X{0,3}
,然后通过跳过可选的 V
并匹配三个 I
字符中的一个来匹配 V?I{0,3}
。然后是字符串的结尾。哇。如果您理解了所有这些内容,并在第一次尝试时就理解了,那么您的理解力比我强。现在,想象一下尝试理解其他人的正则表达式,尤其是当它出现在大型程序的关键函数中间时。或者,想象一下几个月后重新审视您自己的正则表达式。我做过,它看起来并不漂亮。
现在,让我们探索一种可以帮助保持表达式可维护的备用语法。
⁂
到目前为止,您一直在处理我将称之为“紧凑型”的正则表达式。正如您所见,它们很难阅读,即使您弄清楚了一个表达式的作用,也无法保证您几个月后还能理解它。您真正需要的是内联文档。
Python 允许您使用称为详细正则表达式的东西来实现这一点。详细正则表达式在两个方面不同于紧凑型正则表达式
#
字符开头,一直持续到行尾。在这种情况下,它是在多行字符串中而不是在源代码中进行注释,但其工作方式相同。用一个示例来解释会更清楚。让我们重新审视您一直在使用的紧凑型正则表达式,并将其转换为详细正则表达式。此示例展示了如何转换。
>>> pattern = ''' ^ # beginning of string M{0,3} # thousands - 0 to 3 Ms (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs), # or 500-800 (D, followed by 0 to 3 Cs) (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs), # or 50-80 (L, followed by 0 to 3 Xs) (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is), # or 5-8 (V, followed by 0 to 3 Is) $ # end of string ''' >>> re.search(pattern, 'M', re.VERBOSE) ① <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE) ② <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE) ③ <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'M') ④
re.VERBOSE
是 re
模块中定义的一个常量,它表示模式应该被视为详细正则表达式。如您所见,此模式包含大量的空格(全部被忽略),以及多个注释(全部被忽略)。忽略空格和注释后,这与您在上一节中看到的正则表达式完全相同,但它更易读。M
中的一个,然后是CM
,然后是L
和三个可能的X
中的三个,然后是IX
,然后是字符串的结尾。M
中的三个,然后是D
和三个可能的C
中的三个,然后是L
和三个可能的X
中的三个,然后是V
和三个可能的I
中的三个,然后是字符串的结尾。re.VERBOSE
标志,所以re.search
函数将模式视为一个紧凑的正则表达式,其中包含重要的空格和文字井号。Python无法自动检测正则表达式是详细的还是非详细的。Python假设每个正则表达式都是紧凑的,除非您明确声明它是详细的。⁂
到目前为止,您一直专注于匹配整个模式。模式要么匹配,要么不匹配。但是正则表达式比这强大得多。当正则表达式确实匹配时,您可以提取它的特定部分。您可以找出哪些部分匹配以及匹配的位置。
这个例子来自我遇到的另一个现实世界问题,同样来自我之前的工作。问题是解析美国电话号码。客户希望能够自由输入电话号码(在一个字段中),然后将区号、主干号、号码和可选的分机号码分别存储在公司的数据库中。我浏览了网络,找到了许多据称可以实现此目的的正则表达式示例,但它们都不够宽松。
以下是需要接受的电话号码
800-555-1212
800 555 1212
800.555.1212
(800) 555-1212
1-800-555-1212
800-555-1212-1234
800-555-1212x1234
800-555-1212 ext. 1234
work 1-(800) 555.1212 #1234
变化很大!在所有这些情况下,我需要知道区号是800
,主干号是555
,电话号码的其余部分是1212
。对于那些有分机号码的,我需要知道分机号码是1234
。
让我们一起研究开发电话号码解析的解决方案。这个例子展示了第一步。
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$') ① >>> phonePattern.search('800-555-1212').groups() ② ('800', '555', '1212') >>> phonePattern.search('800-555-1212-1234') ③ >>> phonePattern.search('800-555-1212-1234').groups() ④ Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'NoneType' object has no attribute 'groups'
(\d{3})
。\d{3}
是什么?\d
表示“任何数字”(0 到9
)。{3}
表示“匹配正好三个数字”;它是您之前看到的{n,m} 语法
的变体。将所有内容括在括号中表示“匹配正好三个数字,然后将它们作为以后可以请求的组记住”。然后匹配一个文字连字符。然后匹配另一个正好三个数字的组。然后是另一个文字连字符。然后是另一个正好四个数字的组。然后匹配字符串的结尾。search()
方法返回的对象使用groups()
方法。它将返回一个元组,其中包含在正则表达式中定义的组的数量。在本例中,您定义了三个组,一个包含三个数字,一个包含三个数字,一个包含四个数字。search()
和groups()
方法的原因。如果search()
方法没有匹配项,它将返回None
,而不是正则表达式匹配对象。调用None.groups()
会引发一个非常明显的异常:None
没有groups()
方法。(当然,当您从代码深处获得此异常时,它就不那么明显了。是的,我从经验中吸取教训。)>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$') ① >>> phonePattern.search('800-555-1212-1234').groups() ② ('800', '555', '1212', '1234') >>> phonePattern.search('800 555 1212 1234') ③ >>> >>> phonePattern.search('800-555-1212') ④ >>>
groups()
方法现在返回一个包含四个元素的元组,因为正则表达式现在定义了四个要记住的组。下一个例子展示了用于处理电话号码不同部分之间的分隔符的正则表达式。
>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$') ① >>> phonePattern.search('800 555 1212 1234').groups() ② ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212-1234').groups() ③ ('800', '555', '1212', '1234') >>> phonePattern.search('80055512121234') ④ >>> >>> phonePattern.search('800-555-1212') ⑤ >>>
\D+
。这是什么?\D
匹配任何除了数字以外的字符,+
表示“1 个或多个”。所以\D+
匹配一个或多个非数字字符。这是您用来代替文字连字符的方法,试图匹配不同的分隔符。\D+
代替-
意味着您现在可以匹配电话号码,其中部分由空格而不是连字符分隔。下一个例子展示了用于处理没有分隔符的电话号码的正则表达式。
>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') ① >>> phonePattern.search('80055512121234').groups() ② ('800', '555', '1212', '1234') >>> phonePattern.search('800.555.1212 x1234').groups() ③ ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() ④ ('800', '555', '1212', '') >>> phonePattern.search('(800)5551212 x1234') ⑤ >>>
+
更改为*
。您现在匹配\D*
,而不是电话号码部分之间的\D+
。请记住,+
表示“1 个或多个”?*
表示“零个或多个”。所以现在您应该能够解析电话号码,即使它根本没有分隔符字符。800
),然后是零个非数字字符,然后是三个数字的记住组(555
),然后是零个非数字字符,然后是四个数字的记住组(1212
),然后是零个非数字字符,然后是任意数量数字的记住组(1234
),然后是字符串的结尾。x
。groups()
方法仍然返回一个包含四个元素的元组,但第四个元素只是一个空字符串。下一个例子展示了如何处理电话号码中的引导字符。
>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') ① >>> phonePattern.search('(800)5551212 ext. 1234').groups() ② ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() ③ ('800', '555', '1212', '') >>> phonePattern.search('work 1-(800) 555.1212 #1234') ④ >>>
\D*
,零个或多个非数字字符,在第一个记住的组(区号)之前。请注意,您没有记住这些非数字字符(它们不在括号中)。如果找到它们,您只需跳过它们,然后在到达区号时开始记住区号。\D*
匹配。)800
),然后是一个非数字字符(连字符),然后是三个数字的记住组(555
),然后是一个非数字字符(连字符),然后是四个数字的记住组(1212
),然后是零个非数字字符,然后是零个数字的记住组,然后是字符串的结尾。1
,但您假设区号前的所有引导字符都是非数字字符(\D*
)。啊。让我们退一步。到目前为止,所有正则表达式都从字符串的开头开始匹配。但现在您看到字符串开头可能存在不确定的数量的您想要忽略的内容。与其尝试匹配所有这些内容只是为了跳过它们,不如采用另一种方法:根本不要明确匹配字符串的开头。下一个例子展示了这种方法。
>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') ① >>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() ② ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() ③ ('800', '555', '1212', '') >>> phonePattern.search('80055512121234').groups() ④ ('800', '555', '1212', '1234')
^
。您不再匹配字符串的开头。没有什么规定您必须使用正则表达式匹配整个输入。正则表达式引擎将努力找出输入字符串从哪里开始匹配,并从那里开始。看看正则表达式有多快就失控了?快速浏览一下前面的任何迭代。你能区分它们之间的区别吗?
虽然您仍然理解最终答案(它确实是最终答案;如果您发现它没有处理的案例,我不想知道),但让我们在您忘记为什么做出这些选择之前,将其写成一个详细的正则表达式。
>>> phonePattern = re.compile(r''' # don't match beginning of string, number can start anywhere (\d{3}) # area code is 3 digits (e.g. '800') \D* # optional separator is any number of non-digits (\d{3}) # trunk is 3 digits (e.g. '555') \D* # optional separator (\d{4}) # rest of number is 4 digits (e.g. '1212') \D* # optional separator (\d*) # extension is optional and can be any number of digits $ # end of string ''', re.VERBOSE) >>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() ① ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212') ② ('800', '555', '1212', '')
⁂
这只是正则表达式可以做的事情的冰山一角。换句话说,即使您现在被它们完全压垮,相信我,您还没有看到什么。
您现在应该熟悉以下技术
^
匹配字符串的开头。$
匹配字符串的结尾。\b
匹配一个单词边界。\d
匹配任何数字字符。\D
匹配任何非数字字符。x?
匹配一个可选的 x
字符(换句话说,它匹配 x
零次或一次)。x*
匹配 x
零次或多次。x+
匹配 x
一次或多次。x{n,m}
匹配 x
字符至少 n
次,但不超过 m
次。(a|b|c)
匹配 a
、b
或 c
中的任何一个。(x)
通常是一个记忆组。你可以通过使用 re.search
返回的对象的 groups()
方法来获取匹配的值。正则表达式非常强大,但它们不是解决所有问题的正确方法。你应该学习足够的知识来了解何时使用它们,何时它们可以解决你的问题,以及何时它们会比解决问题带来更多问题。
© 2001–11 Mark Pilgrim