7.4. 使用 {n,m} 语法

上一节中,您处理了一个模式,其中同一个字符可以重复最多三次。在正则表达式中还有另一种表达方式,有些人认为这种方式更易读。首先看看我们在前面的例子中已经使用过的方法。

例 7.5. 旧方法: 每个字符都是可选的

>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')    1
<_sre.SRE_Match object at 0x008EE090>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MM')   2
<_sre.SRE_Match object at 0x008EEB48>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MMM')  3
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM') 4
>>> 
1 这将匹配字符串的开头,然后是第一个可选的 M,但不匹配第二个和第三个 M(但这没关系,因为它们是可选的),然后是字符串的结尾。
2 这将匹配字符串的开头,然后是第一个和第二个可选的 M,但不匹配第三个 M(但这没关系,因为它是可选的),然后是字符串的结尾。
3 这将匹配字符串的开头,然后是所有三个可选的 M,然后是字符串的结尾。
4 这将匹配字符串的开头,然后是所有三个可选的 M,但随后不匹配字符串的结尾(因为还有一个未匹配的 M),所以该模式不匹配并返回 None

例 7.6. 新方法: 从 nm

>>> pattern = '^M{0,3}$'       1
>>> re.search(pattern, 'M')    2
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')   3
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')  4
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM') 5
>>> 
1 此模式表示:“匹配字符串的开头,然后是零到三个 M 字符,然后是字符串的结尾。” 0 和 3 可以是任何数字;如果您想匹配至少一个但不多于三个 M 字符,您可以使用 M{1,3}
2 这将匹配字符串的开头,然后是三个可能的 M 中的一个,然后是字符串的结尾。
3 这将匹配字符串的开头,然后是三个可能的 M 中的两个,然后是字符串的结尾。
4 这将匹配字符串的开头,然后是三个可能的 M 中的三个,然后是字符串的结尾。
5 这将匹配字符串的开头,然后是三个可能的 M 中的三个,但随后不匹配字符串的结尾。正则表达式只允许在字符串结尾之前最多出现三个 M 字符,但您有四个,所以该模式不匹配并返回 None
Note
没有办法以编程方式确定两个正则表达式是否等效。您能做的最好的事情就是编写大量的测试用例,以确保它们在所有相关输入上的行为相同。我们将在本书后面详细讨论如何编写测试用例。

7.4.1. 检查十位和个位

现在让我们扩展罗马数字正则表达式,使其涵盖十位和个位。此示例显示了对十位的检查。

例 7.7. 检查十位

>>> pattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')    1
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')     2
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')    3
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')  4
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX') 5
>>> 
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?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

那么使用这种备选的 {n,m} 语法是什么样子呢?此示例显示了新语法。

例 7.8. 使用 {n,m} 验证罗马数字

>>> pattern = '^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')             1
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')         2
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMMDCCCLXXXVIII') 3
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')                4
<_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 字符中的三个;然后是字符串的结尾。 MMMMDCCCLXXXVIII3888 的罗马数字表示形式,它是不使用扩展语法可以编写的最长的罗马数字。
4 仔细看。(我觉得自己像个魔术师。“孩子们,仔细看,我要从帽子里变出一只兔子。”)这将匹配字符串的开头,然后是四个 M 中的零个,然后通过跳过可选的 D 并匹配三个 C 中的零个来匹配 D?C{0,3},然后通过跳过可选的 L 并匹配三个 X 中的零个来匹配 L?X{0,3},然后通过跳过可选的 V 并匹配三个 I 中的一个来匹配 V?I{0,3}。然后是字符串的结尾。哇。

如果您按照所有这些步骤操作并在第一次尝试时就理解了,那么您做得比我当时好。现在想象一下,在大型程序的关键函数中,试图理解别人的正则表达式。或者甚至想象一下,几个月后回到您自己的正则表达式。我经历过,那可不是什么好事。

在下一节中,您将探索一种可以帮助您保持表达式可维护性的备选语法。