您当前位置: 首页 深入 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.'
  1. 我的目标是标准化街道地址,以便 'ROAD' 始终缩写为 'RD.'。 乍一看,我认为这很简单,我只需要使用字符串方法 replace() 就可以了。 毕竟,所有数据都已经是大写,所以大小写不匹配不会成为问题。 并且搜索字符串 'ROAD' 是一个常量。 在这个看似简单的示例中,s.replace() 确实有效。
  2. 不幸的是,生活中充满了反例,我很快就发现了这个例子。 这里的问题是,'ROAD' 在地址中出现了两次,一次作为街道名称 'BROAD' 的一部分,一次作为它自己的单词。 replace() 方法看到这两个出现并盲目地替换了它们;与此同时,我看到我的地址被破坏了。
  3. 要解决地址包含多个 'ROAD' 子字符串的问题,您可以诉诸以下方法:仅在地址的最后四个字符(s[-4:])中搜索和替换 'ROAD',并将字符串保留原样(s[:-4])。 但是您可以看到这已经变得很笨拙了。 例如,模式取决于您要替换的字符串的长度。(如果您要将 'STREET' 替换为 'ST.',您需要使用 s[:-6]s[-6:].replace(...)。)您想在六个月后回来调试这个吗? 我知道我不想。
  4. 是时候升级到正则表达式了。 在 Python 中,与正则表达式相关的所有功能都包含在 re 模块中。
  5. 看一下第一个参数:'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'
  1. 真正想要的是匹配 'ROAD',当它位于字符串的末尾并且它是一个完整的单词(而不是更大单词的一部分)时。 要在正则表达式中表达这一点,您需要使用 \b,这意味着“此处必须出现一个单词边界”。 在 Python 中,由于字符串中的 '\' 字符本身必须转义,因此这很复杂。 这有时被称为反斜杠瘟疫,也是正则表达式在 Perl 中比在 Python 中更容易的原因之一。 另一方面,Perl 将正则表达式与其他语法混合在一起,因此如果您遇到错误,可能很难确定是语法错误还是正则表达式错误。
  2. 为了解决反斜杠瘟疫,您可以使用所谓的原始字符串,方法是在字符串前面加上字母 r。 这告诉 Python 字符串中的任何内容都不应该转义;'\t' 是一个制表符字符,但 r'\t' 实际上是反斜杠字符 \ 后跟字母 t。 我建议在处理正则表达式时始终使用原始字符串;否则,事情会很快变得过于混乱(正则表达式本身已经足够混乱了)。
  3. *叹气* 不幸的是,我很快发现更多的情况与我的逻辑相矛盾。 在这种情况下,街道地址包含单词 'ROAD',作为一个完整的单词,但它不在末尾,因为地址在街道名称之后有一个公寓号码。 因为 'ROAD' 不在字符串的末尾,所以它不匹配,因此对 re.sub() 的整个调用最终什么都没有替换,您得到了原始字符串,这不是您想要的。
  4. 要解决这个问题,我删除了 $ 字符,并添加了另一个 \b。 现在,正则表达式变成了“匹配 'ROAD',当它是一个完整的单词,位于字符串中的任何位置时”,无论是结尾、开头还是中间。

案例研究:罗马数字

您很可能见过罗马数字,即使您不认识它们。 您可能在旧电影和电视节目版权(“版权 MCMXLVI” 而不是“版权 1946”)或图书馆或大学的献词墙上看到过它们(“成立于 MDCCCLXXXVIII” 而不是“成立于 1888”)。 您可能还在大纲和参考书目中看到过它们。 这是一种表示数字的系统,它确实可以追溯到古罗马帝国(因此得名)。

在罗马数字中,有七个字符以各种方式重复和组合以表示数字。

以下是一些构建罗马数字的一般规则

检查千位

验证一个任意字符串是否是一个有效的罗马数字需要什么? 让我们一次处理一位。 由于罗马数字总是从最高到最低书写,让我们从最高位开始:千位。 对于 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>
  1. 这种模式有三个部分。 ^ 仅在字符串的开头匹配它后面的内容。 如果没有指定这一点,那么无论 M 字符在哪里,模式都会匹配,这不是您想要的。 您需要确保 M 字符(如果存在)位于字符串的开头。 M? 可选地匹配单个 M 字符。 由于它被重复了三次,所以您匹配了零到三个连续的 M 字符。 $ 匹配字符串的结尾。 当与开头处的 ^ 字符组合时,这意味着模式必须匹配整个字符串,在 M 字符之前或之后没有其他字符。
  2. re 模块的本质是 search() 函数,它接受一个正则表达式(pattern)和一个字符串('M')来尝试匹配正则表达式。 如果找到了匹配项,search() 会返回一个对象,该对象具有各种方法来描述匹配项;如果找不到匹配项,search() 会返回 None,即 Python 的空值。 目前,您只关心模式是否匹配,您可以通过查看 search() 的返回值来判断。 'M' 匹配这个正则表达式,因为第一个可选的 M 匹配,而第二个和第三个可选的 M 字符被忽略。
  3. 'MM' 匹配,因为第一个和第二个可选的 M 字符匹配,而第三个 M 被忽略。
  4. 'MMM' 匹配,因为所有三个 M 字符都匹配。
  5. 'MMMM' 不匹配。所有三个 M 字符都匹配,但正则表达式坚持字符串必须以 $ 字符结尾(因为 $ 字符),而字符串还没有结束(因为还有第四个 M)。所以 search() 返回 None
  6. 有趣的是,空字符串也匹配这个正则表达式,因为所有 M 字符都是可选的。

检查百位数

百位数比千位数更难,因为根据其值,有几种互斥的方式可以表达它。

所以有四种可能的模式

最后两种模式可以合并

此示例展示了如何验证罗马数字的百位数。

>>> 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>
  1. 此模式从与上一个模式相同的地方开始,检查字符串的开头 (^),然后是千位数 (M?M?M?)。然后它包含新部分,在括号中,定义了一组三个互斥模式,用竖线分隔:CMCDD?C?C?C?(即可选的 D 后跟零到三个可选的 C 字符)。正则表达式解析器按顺序(从左到右)检查这些模式中的每一个,选择第一个匹配的模式,并忽略其余模式。
  2. 'MCM' 匹配,因为第一个 M 匹配,第二个和第三个 M 字符被忽略,并且 CM 匹配(因此 CDD?C?C?C? 模式甚至没有被考虑)。MCM1900 的罗马数字表示。
  3. 'MD' 匹配,因为第一个 M 匹配,第二个和第三个 M 字符被忽略,并且 D?C?C?C? 模式匹配 D(三个 C 字符中的每一个都是可选的,并且被忽略)。MD1500 的罗马数字表示。
  4. 'MMMCCC' 匹配,因为所有三个 M 字符都匹配,并且 D?C?C?C? 模式匹配 CCCD 是可选的,被忽略)。MMMCCC3300 的罗马数字表示。
  5. 'MCMC' 不匹配。第一个 M 匹配,第二个和第三个 M 字符被忽略,并且 CM 匹配,但 $ 不匹配,因为您还没有到达字符串的结尾(您还有一个未匹配的 C 字符)。C 作为 D?C?C?C? 模式的一部分匹配,因为互斥的 CM 模式已经匹配。
  6. 有趣的是,空字符串仍然匹配此模式,因为所有 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') 
>>> 
  1. 这匹配字符串的开头,然后是第一个可选的 M,但不是第二个和第三个 M(但这没关系,因为它们是可选的),然后是字符串的结尾。
  2. 这匹配字符串的开头,然后是第一个和第二个可选的 M,但不是第三个 M(但这没关系,因为它可选),然后是字符串的结尾。
  3. 这匹配字符串的开头,然后是所有三个可选的 M,然后是字符串的结尾。
  4. 这匹配字符串的开头,然后是所有三个可选的 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') 
>>> 
  1. 此模式表示:“匹配字符串的开头,然后是零到三个 M 字符,然后是字符串的结尾。”0 和 3 可以是任何数字;如果您希望至少匹配一个,但不超过三个 M 字符,则可以写 M{1,3}
  2. 这匹配字符串的开头,然后是三个 M 字符中的一个,然后是字符串的结尾。
  3. 这匹配字符串的开头,然后是三个 M 字符中的两个,然后是字符串的结尾。
  4. 这匹配字符串的开头,然后是三个 M 字符中的三个,然后是字符串的结尾。
  5. 这匹配字符串的开头,然后是三个 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') 
>>> 
  1. 这匹配字符串的开头,然后是第一个可选的 M,然后是 CM,然后是 XL,然后是字符串的结尾。请记住,(A|B|C) 语法表示“匹配 A、B 或 C 中的任何一个”。您匹配 XL,因此您会忽略 XCL?X?X?X? 选择,然后继续到字符串的结尾。MCMXL1940 的罗马数字表示。
  2. 这匹配字符串的开头,然后是第一个可选的 M,然后是 CM,然后是 L?X?X?X?。在 L?X?X?X? 中,它匹配 L 并跳过所有三个可选的 X 字符。然后您转到字符串的结尾。MCML1950 的罗马数字表示。
  3. 这匹配字符串的开头,然后是第一个可选的 M,然后是 CM,然后是可选的 L 和第一个可选的 X,跳过第二个和第三个可选的 X,然后是字符串的结尾。MCMLX1960 的罗马数字表示。
  4. 这匹配字符串的开头,然后是第一个可选的 M,然后是 CM,然后是可选的 L 和所有三个可选的 X 字符,然后是字符串的结尾。MCMLXXX1980 的罗马数字表示。
  5. 这匹配字符串的开头,然后是第一个可选的 M,然后是 CM,然后是可选的 L 和所有三个可选的 X 字符,然后没有匹配字符串的结尾,因为仍然有一个 X 未被考虑。因此整个模式没有匹配,并返回 NoneMCMLXXXX 不是有效的罗马数字。

个位数的表达式遵循相同的模式。我将省去详细信息,并向您展示最终结果。

>>> 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>
  1. 这匹配字符串的开头,然后是三个 M 字符中的一个,然后是 D?C{0,3}。在其中,它匹配可选的 D 和三个 C 字符中的零个。继续,它通过匹配可选的 L 和三个 X 字符中的零个来匹配 L?X{0,3}。然后它通过匹配可选的 V 和三个 I 字符中的零个来匹配 V?I{0,3},最后是字符串的结尾。MDLV1555 的罗马数字表示。
  2. 这匹配字符串的开头,然后是三个 M 字符中的两个,然后是 D?C{0,3},其中包含一个 D 和三个 C 字符中的一个;然后是 L?X{0,3},其中包含一个 L 和三个 X 字符中的一个;然后是 V?I{0,3},其中包含一个 V 和三个 I 字符中的一个;然后是字符串的结尾。MMDCLXVI2666 的罗马数字表示。
  3. 这匹配字符串的开头,然后是三个 M 字符中的三个,然后是 D?C{0,3},其中包含一个 D 和三个 C 字符中的三个;然后是 L?X{0,3},其中包含一个 L 和三个 X 字符中的三个;然后是 V?I{0,3},其中包含一个 V 和三个 I 字符中的三个;然后是字符串的结尾。MMMDCCCLXXXVIII3888 的罗马数字表示,它是在没有扩展语法的情况下可以写出的最长的罗马数字。
  4. 仔细观察。(我感觉自己像个魔术师。“仔细观察,孩子们,我要从帽子中变出一只兔子。”)这匹配字符串的开头,然后是三个 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') 
  1. 使用详细正则表达式时,最重要的是要记住您需要传递一个额外的参数:re.VERBOSEre 模块中定义的一个常量,它表示模式应该被视为详细正则表达式。如您所见,此模式包含大量的空格(全部被忽略),以及多个注释(全部被忽略)。忽略空格和注释后,这与您在上一节中看到的正则表达式完全相同,但它更易读。
  2. 这与字符串的开头匹配,然后是三个可能的M中的一个,然后是CM,然后是L和三个可能的X中的三个,然后是IX,然后是字符串的结尾。
  3. 这与字符串的开头匹配,然后是三个可能的M中的三个,然后是D和三个可能的C中的三个,然后是L和三个可能的X中的三个,然后是V和三个可能的I中的三个,然后是字符串的结尾。
  4. 这并不匹配。为什么?因为它没有re.VERBOSE标志,所以re.search函数将模式视为一个紧凑的正则表达式,其中包含重要的空格和文字井号。Python无法自动检测正则表达式是详细的还是非详细的。Python假设每个正则表达式都是紧凑的,除非您明确声明它是详细的。

案例研究:解析电话号码

到目前为止,您一直专注于匹配整个模式。模式要么匹配,要么不匹配。但是正则表达式比这强大得多。当正则表达式确实匹配时,您可以提取它的特定部分。您可以找出哪些部分匹配以及匹配的位置。

这个例子来自我遇到的另一个现实世界问题,同样来自我之前的工作。问题是解析美国电话号码。客户希望能够自由输入电话号码(在一个字段中),然后将区号、主干号、号码和可选的分机号码分别存储在公司的数据库中。我浏览了网络,找到了许多据称可以实现此目的的正则表达式示例,但它们都不够宽松。

以下是需要接受的电话号码

变化很大!在所有这些情况下,我需要知道区号是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'
  1. 始终从左到右阅读正则表达式。这一个匹配字符串的开头,然后是(\d{3})\d{3}是什么?\d表示“任何数字”(0 到9)。{3}表示“匹配正好三个数字”;它是您之前看到的{n,m} 语法的变体。将所有内容括在括号中表示“匹配正好三个数字,然后将它们作为以后可以请求的组记住”。然后匹配一个文字连字符。然后匹配另一个正好三个数字的组。然后是另一个文字连字符。然后是另一个正好四个数字的组。然后匹配字符串的结尾。
  2. 要访问正则表达式解析器沿途记住的组,请对search()方法返回的对象使用groups()方法。它将返回一个元组,其中包含在正则表达式中定义的组的数量。在本例中,您定义了三个组,一个包含三个数字,一个包含三个数字,一个包含四个数字。
  3. 这个正则表达式不是最终答案,因为它没有处理结尾有分机号码的电话号码。为此,您需要扩展正则表达式。
  4. 这就是为什么您不应该在生产代码中“链接”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') 
>>> 
  1. 这个正则表达式与前一个几乎相同。与之前一样,您匹配字符串的开头,然后是三个数字的记住组,然后是连字符,然后是三个数字的记住组,然后是连字符,然后是四个数字的记住组。新增的是,您随后匹配另一个连字符,然后是记住的一或多个数字的组,然后是字符串的结尾。
  2. groups()方法现在返回一个包含四个元素的元组,因为正则表达式现在定义了四个要记住的组。
  3. 不幸的是,这个正则表达式也不是最终答案,因为它假设电话号码的不同部分由连字符分隔。如果它们由空格、逗号或点分隔怎么办?您需要一个更通用的解决方案来匹配几种不同的分隔符类型。
  4. 糟糕!这个正则表达式不仅没有完成您想要的所有事情,而且实际上还倒退了一步,因为现在您无法解析没有分机号码的电话号码。这完全不是您想要的;如果分机号码存在,您想了解它是什么,但如果它不存在,您仍然想了解主号码的不同部分是什么。

下一个例子展示了用于处理电话号码不同部分之间的分隔符的正则表达式。

>>> 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') 
>>> 
  1. 系好安全带。您正在匹配字符串的开头,然后是三个数字的组,然后是\D+。这是什么?\D匹配任何除了数字以外的字符,+表示“1 个或多个”。所以\D+匹配一个或多个非数字字符。这是您用来代替文字连字符的方法,试图匹配不同的分隔符。
  2. 使用\D+代替-意味着您现在可以匹配电话号码,其中部分由空格而不是连字符分隔。
  3. 当然,由连字符分隔的电话号码仍然有效。
  4. 不幸的是,这仍然不是最终答案,因为它假设存在分隔符。如果电话号码根本没有空格或连字符怎么办?
  5. 糟糕!这仍然没有解决需要分机号码的问题。现在您有两个问题,但您可以用相同的技术解决这两个问题。

下一个例子展示了用于处理没有分隔符的电话号码的正则表达式。

>>> 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') 
>>> 
  1. 自上一步以来,您所做的唯一更改是将所有+更改为*。您现在匹配\D*,而不是电话号码部分之间的\D+。请记住,+表示“1 个或多个”?*表示“零个或多个”。所以现在您应该能够解析电话号码,即使它根本没有分隔符字符。
  2. 瞧,它确实有效。为什么?您匹配了字符串的开头,然后是三个数字的记住组(800),然后是零个非数字字符,然后是三个数字的记住组(555),然后是零个非数字字符,然后是四个数字的记住组(1212),然后是零个非数字字符,然后是任意数量数字的记住组(1234),然后是字符串的结尾。
  3. 现在其他变体也起作用了:点代替连字符,以及分机号码前既有空格又有x
  4. 最后,您解决了另一个长期存在的问题:分机号码再次可选。如果没有找到分机号码,groups()方法仍然返回一个包含四个元素的元组,但第四个元素只是一个空字符串。
  5. 我不愿意当那个带来坏消息的人,但您还没有完成。这里的问题是什么?区号前有一个额外的字符,但正则表达式假设区号是字符串开头的第一个字符。没问题,您可以使用相同的“零个或多个非数字字符”技术来跳过区号前的引导字符。

下一个例子展示了如何处理电话号码中的引导字符。

>>> 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') 
>>> 
  1. 这与上一个例子相同,只是现在您正在匹配\D*,零个或多个非数字字符,在第一个记住的组(区号)之前。请注意,您没有记住这些非数字字符(它们不在括号中)。如果找到它们,您只需跳过它们,然后在到达区号时开始记住区号。
  2. 您能够成功解析电话号码,即使区号前有左括号。(区号后的右括号已经被处理了;它被视为非数字分隔符,并由第一个记住的组后的\D*匹配。)
  3. 只是一次健全性检查,以确保您没有破坏任何以前有效的内容。由于引导字符是完全可选的,因此这将匹配字符串的开头,然后是零个非数字字符,然后是三个数字的记住组(800),然后是一个非数字字符(连字符),然后是三个数字的记住组(555),然后是一个非数字字符(连字符),然后是四个数字的记住组(1212),然后是零个非数字字符,然后是零个数字的记住组,然后是字符串的结尾。
  4. 这就是正则表达式让我想要用钝器挖掉自己眼睛的地方。为什么这个电话号码不匹配?因为区号前有一个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')
  1. 注意此正则表达式中没有^。您不再匹配字符串的开头。没有什么规定您必须使用正则表达式匹配整个输入。正则表达式引擎将努力找出输入字符串从哪里开始匹配,并从那里开始。
  2. 现在,您可以成功解析包含引导字符和引导数字的电话号码,以及电话号码各个部分周围的任意数量的任何类型的分隔符。
  3. 健全性检查。这仍然有效。
  4. 这仍然有效。

看看正则表达式有多快就失控了?快速浏览一下前面的任何迭代。你能区分它们之间的区别吗?

虽然您仍然理解最终答案(它确实是最终答案;如果您发现它没有处理的案例,我不想知道),但让我们在您忘记为什么做出这些选择之前,将其写成一个详细的正则表达式。

>>> 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', '')
  1. 除了分布在多行上之外,这与上一步的正则表达式完全相同,因此它解析相同的输入也就不足为奇了。
  2. 最终的健全性检查。是的,这仍然有效。您完成了。

总结

这只是正则表达式可以做的事情的冰山一角。换句话说,即使您现在被它们完全压垮,相信我,您还没有看到什么。

您现在应该熟悉以下技术

正则表达式非常强大,但它们不是解决所有问题的正确方法。你应该学习足够的知识来了解何时使用它们,何时它们可以解决你的问题,以及何时它们会比解决问题带来更多问题。

© 2001–11 Mark Pilgrim