17.2. plural.py, 第 1 阶段

你所针对的单词 (至少在英语中) 是字符串和字符。你还需要规则来找出不同的字符 (字母) 组合,并对它们进行不同的操作。这听起来像是正则表达式的工作。

例 17.1. plural1.py


import re

def plural(noun):                            
    if re.search('[sxz]$', noun):             1
        return re.sub('$', 'es', noun)        2
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)       
    elif re.search('[^aeiou]y$', noun):      
        return re.sub('y$', 'ies', noun)     
    else:                                    
        return noun + 's'                    
1 好啦,这是一个正则表达式,但是它使用了你在 第 7 章 正则表达式 中未曾见过的语法。方括号的意思是 “完全匹配这些字符中的一个”。也就是说,[sxz] 意味着 “s,或者 x,再或者 z”,但只是其中的一个。$ 应该不陌生,它意味着匹配字符串的结尾。也就是说,检查 noun 是否以 sx,或者 z 结尾。
2 re.sub 函数进行以正则表达式为基础的替换工作。让我们更具体地看看它。

例 17.2. re.sub 介绍

>>> import re
>>> re.search('[abc]', 'Mark')   1
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub('[abc]', 'o', 'Mark') 2
'Mork'
>>> re.sub('[abc]', 'o', 'rock') 3
'rook'
>>> re.sub('[abc]', 'o', 'caps') 4
'oops'
1 Mark 包含 ab,或者 c吗?是的,含有 a
2 好的,现在找出 ab,或者 c 并以 o 取代之。Mark 就变成 Mork 了。
3 同一方法可以将 rock 变成 rook
4 你可能认为它可以将 caps 变成 oaps,但事实并非如此。re.sub 替换所有 的匹配项,并不只是第一个匹配项。因此正则表达式将会把 caps 变成 oops,因为 ca 都被转换为 o了。

例 17.3. 回到 plural1.py


import re

def plural(noun):                            
    if re.search('[sxz]$', noun):            
        return re.sub('$', 'es', noun)        1
    elif re.search('[^aeioudgkprt]h$', noun): 2
        return re.sub('$', 'es', noun)        3
    elif re.search('[^aeiou]y$', noun):      
        return re.sub('y$', 'ies', noun)     
    else:                                    
        return noun + 's'                    
1 回到 plural 函数。你在做什么?你在以 es 取代字符串的结尾。换句话说,追加 es 到字符串。你可以通过字符串拼合做到相同的事,例如 noun + 'es',但是我使用正则表达式做这一切,既是为了保持一致,也是为了本章稍后你会明白的其它原因。
2 仔细看看,这是另一个新的内容。^ 是方括号里面的第一个字符,这有特别的含义:否定。[^abc] 意味着 “ab、 和 c 以外的 任意单字符”。所以,[^aeioudgkprt] 意味着除 aeioudgkprt 以外的任意字符。这个字符之后应该跟着一个 h,然后是字符串的结尾。你在寻找的是以发音的 H 结尾的单词。
3 这是一个相似的表达:匹配 Y 前面不是 aeiou,并以这个 Y 结尾的单词。你在查找的是以发 I 音的 Y 结尾的单词。

例 17.4. 正则表达式中否定的更多应用

>>> import re
>>> re.search('[^aeiou]y$', 'vacancy') 1
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.search('[^aeiou]y$', 'boy')     2
>>> 
>>> re.search('[^aeiou]y$', 'day')
>>> 
>>> re.search('[^aeiou]y$', 'pita')    3
>>> 
1 vacancy 匹配这个正则表达式,因为它以 cy 结尾,并且 c 不在 aeiou 之列。
2 boy 不能匹配,因为它以 oy 结尾,并且你特别指出 y 之前的字符不可以是 oday 不能匹配是因为以 ay 结尾。
3 pita 不匹配是因为不以 y 结尾。

例 17.5. 更多的 re.sub

>>> re.sub('y$', 'ies', 'vacancy')              1
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy') 2
'vacancies'
1 正则表达式把 vacancy 变为 vacancies,把 agency 变为 agencies,这正是你想要的。注意,将 boy 变成 boies 是可行的,但是永远不会发生,因为 re.search 首先确定是否应该应用 re.sub
2 顺便提一下,可以将两个正则表达式 (一个确定规则适用与否,一个应用规则) 合并在一起成为一个正则表达式。这便是合并后的样子。它的大部分已经很熟悉:你应用的是在 第 7.6 节 “个案研究:解析电话号码” 学过的记忆组 (remembered group) 记住 y 之前的字符。然后再替换字符串,你使用一个新的语法 \1,这意味着:“嘿!记得前面的第一个组吗?把它放这儿”。就此而言,记住了 y 之前的 c ,然后你做替换工作,你将 c 替换到 c 的位置,并将 ies 替换到 y 的位置。(如果你有不止一个组则可以使用 \2 或者 \3 等等。)

正则表达式替换非常强大,并且 \1 语法使之更加强大。但是将整个操作放在一个正则表达式中仍然晦涩难懂,也不能与前面描述的复数规则直接呼应。你原来列出的规则,比如 “如果单词以 S,X 或者 Z 结尾,结尾追加 ES”。如果你在函数中看到两行代码描述 “如果单词以 S,X 或者 Z 结尾,结尾追加 ES”,更加直观些。