这一系列示例的灵感来自于几年前我在日常工作中遇到的一个实际问题,当时我需要在将街道地址从旧系统导入到新系统之前对其进行清理和标准化。(你看,我可不是凭空捏造这些东西;它们真的很有用。)这个例子展示了我如何解决这个问题。
示例 7.1. 匹配字符串结尾
>>> 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(...))。您想在六个月后回来调试这个吗?我知道我不想。
|
|
是时候升级到正则表达式了。在 Python 中,所有与正则表达式相关的功能都包含在 re 模块中。
|
|
看一下第一个参数:'ROAD$'。这是一个简单的正则表达式,仅当 'ROAD' 出现在字符串末尾时才匹配。 $ 表示“字符串结尾”。(有一个对应的字符,插入符号 ^,表示“字符串开头”。)
|
|
使用 re.sub 函数,您可以在字符串 s 中搜索正则表达式 'ROAD$' 并将其替换为 'RD.'。这将匹配字符串 s 末尾的 ROAD,但不会匹配作为单词 BROAD 一部分的 ROAD,因为它位于 s 的中间。
|
继续我清理地址的故事,我很快发现前面的例子,即匹配地址末尾的 'ROAD',还不够好,因为并非所有地址都包含街道类型;有些地址只以街道名称结尾。大多数情况下,我都能侥幸成功,但如果街道名称是 'BROAD',那么正则表达式就会将字符串末尾的 'ROAD' 作为单词 'BROAD' 的一部分进行匹配,这不是我想要的。
示例 7.2. 匹配整个单词
>>> 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' 本身就是一个完整的单词时,无论是在字符串的末尾、开头还是中间的某个位置,都匹配它。”
|