7.3. 案例研究:罗马数字

您很可能见过罗马数字,即使您可能没有意识到。您可能在老电影和电视节目的版权声明中看到过它们(“版权所有 MCMXLVI”而不是“版权所有 1946”),或者在图书馆或大学的落成纪念墙上看到过它们(“建于 MDCCCLXXXVIII”而不是“建于 1888”)。您可能还在提纲和参考文献中看到过它们。这是一种表示数字的系统,它的历史可以追溯到古罗马帝国(因此得名)。

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

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

7.3.1. 检查千位

如何验证任意字符串是否是有效的罗马数字?让我们逐位分析。由于罗马数字总是从高位写到低位,让我们从最高位开始:千位。对于 1000 及以上的数字,千位由一系列 M 字符表示。

示例 7.3. 检查千位

>>> import re
>>> pattern = '^M?M?M?$'       1
>>> re.search(pattern, 'M')    2
<SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')   3
<SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')  4
<SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM') 5
>>> re.search(pattern, '')     6
<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 字符都是可选的。

7.3.2. 检查百位

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

  • 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 字符

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

示例 7.4. 检查百位

>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$' 1
>>> re.search(pattern, 'MCM')            2
<SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')             3
<SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')         4
<SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')           5
>>> re.search(pattern, '')               6
<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? 模式,其中所有字符都是可选的并且被忽略。

呼!看到了吗?正则表达式可以很快变得多么复杂?而您只学习了罗马数字的千位和百位。但是,如果您理解了所有这些,那么十位和个位就很简单了,因为它们是完全相同的模式。但是,让我们看看另一种表达模式的方法。