Chapter 2 正则表达式,文本标准化,编辑距离

  • User: I am unhappy.
    ELIZA: DO YOU THINK COMING HERE WILL HELP YOU NOT TO BE UNHAPPY
    User: I need some help, that much seems certain.
    ELIZA: WHAT WOULD IT MEAN TO YOU IF YOU GOT SOME HELP
    User: Perhaps I could learn to get along with my mother.
    ELIZA: TELL ME MORE ABOUT YOUR FAMILY
    User: My mother takes care of me.
    ELIZA: WHO ELSE IN YOU FAMILY TAKES CARE OF YOU
    User: My father.
    ELIZA: YOUR FATHER
    User: You are like my father in some ways.
    Weizenbaum (1966)

上述对话出自ELIZA,一个早期自然语言处理系统。他可以通过模仿罗氏心理治疗师 [1] 和用户执行有限的对话。ELIZA是一个惊人简单的程序。他用模式匹配去识别像“I need X”这样的短句,并将他们翻译成像“What would it mean to you if you got X?”这样合适的输出。这个简单的技巧在这个领域得到成功,因为要模仿一个罗氏心理治疗师,实际上ELIZA不需要知道任何东西。就像Weizenbaum记录的那样,这是一种少有的对话形式,倾听者可以表现得就像他们对这个世界一无所知一样。ELIZA对于人类对话的模仿行为是非常成功的:很多和ELIZA互动过的人都开始相信,ELIZA确实可以理解他们和他们的问题。很多人甚至在被解释了程序的操作后,依然相信ELIZA的能力。甚至现在,这样的对话机器人还是一种有趣的消遣。

当然,现代的对话代理远不止是一种消遣。他们可以回答问题,预定航班,或者找餐厅。他们依靠这些功能对用户的意图有更复杂的了解。我们将在26章对此进行进一步说明。尽管如此,这个简单的基于模式的方法驱动了在自然语言处理领域扮演重要角色的ELIZA和其他对话机器人。

我们将从最重要的描述文本模式的工具正则表达式开始。正则表达式可以被用于指定字符串。这些我们可能想将从一个文件中,从ELIZA在上文的转义“I need X”中提取的字符串转变为定义的字符串,比如199美元或24.99美元,以提取文件中的标价表格。

我们之后会转向一系列任务,统称为文本标准化,在这之中正则化扮演了重要的角色。标准化的文本意味着将其转化为一个更加便捷标准的形式。举个例子,我们要对语言进行的大多数处理都依赖于文本中第一次的分词或者标记化单词,即标记化任务。英文单词往往被空格分离,但有时空格总是不充分。 New Yorkrock ’n’ roll 总是被认为是长单词,尽管他们包含空格。与此同时,我们需要把 I’m 分成 Iam 两个单词。为了处理推特文本,我们将会需要标记化表情符号比如 :) 或者标签比如 #nlproc 。有些语言,比如日语,没有单词见的空格,因此单词标签化变得更加困难。

文本标准化的另一部分就是词的形态变化,即确定两个单词是否拥有相同的词源,尽管他们表面看上去不一样。举个例子,单词 sangsungsings 都是动词 sing 的变形。形态变化对于处理语言形态复杂的语言,比如阿拉伯语,是非常必要的。提取词干指的是一种简单版本的形态变化,在这种变化中我们通常只是去掉词尾的后缀。文本标准化也包括文本分割:将一段文本分为独立的句子,使用句号或者惊叹号作为提示。

最后,我们将需要比较单词和其它字符串。我们将介绍一个度量标准称作编辑距离,它将基于把一个字符串改为另一个所需要的编辑次数(插入,删除、替换)测定两个字符串的相似程度。编辑距离是一个广泛用于语言处理的算法,从拼写纠正,语音识别到同义词解析。

[1] https://en.wikipedia.org/wiki/Carl_Rogers

2.1 正则表达式

一个没有被承认的在计算机科学标准化中的成功就是正则表达式,这是一个指定文本搜索字符串的语言。这个实用的语言用于每个计算机语言,单词处理器,以及文本处理工具比如Unix工具grep或者Emacs。正式而言,正则表达式是一种描述字符串集的代数标记。正则表达式在文本搜索中尤其有用,比如当我们有一个模式需要搜索匹配,或者有一个文本语料库需要遍历的时候。一个正则表达式搜索功能将会搜索遍历整个语料库,返回所有匹配模式的文本。这个语料库可以是单一文件或者一个集合。举个例子,Unix命令行工具grep使用正则表达式并返回匹配表达式的每一行输出文件。

如果匹配数超过一个,一次搜索可以被设计成在一行中返回所有匹配文本,也可以被设计成只返回第一个。之后的例子我们通常会强调匹配正则表达式的模式部分,并且只显示第一个匹配结果。我们将展示被斜杠限制的正则表达式,但请注意斜杠并不是正则表达式的一部分。

正则表达式有很多变种。我们将描述延伸正则表达式,不同的正则表达式解析器可能只能识别上述表达式的子集,或者处理一些表达式时会有些许区别。用一个线上正则表达式测试器是一个测试你的正则表达式以及探索这些变化的便捷方法。

2.1.1 基本正则表达式的一些模式

最简单的一种正则表达式就是简单字符的序列。为了检索 woodchunk 我们键入 /woodchuck/ 。这个表达式 /Buttercup/ 会匹配任何包含子字符串 Buttercap 的字符串。在grep中使用那个表达式将会返回 I’m called little Buttercup 。这个检索字符串可以包含单个字符(比如 /!/ )或者一个字符序列(比如 /urgl/)。

正则表达式 示例匹配pattern
/woodchucks/ “interesting links to woodchucks and lemurs“
/a/ “Mary Ann stopped by Mona’s“
/!/ “You’ve left the burglar behind again!“ said Nori
图 2.1 一些简单的正则表达式搜索

正则表达式是大小写敏感的,小写的 /s/ 和大写的 /S/ 是不一样的( /s/ 会匹配小写的 s 而不是大写的 S )。这意味着pattern woodchunks 将不会匹配字符串 Woodchunks 。我们可以用方括号 [and] 解决这个问题。在括号中的字符串指定了其中字符多选一的匹配。举个例子,图2.2展示了pattern /[wW]/ 匹配了包含 w 或者 W 的pattern。[1]

[1] 一直把pattern译为模式真是太烦了,这段之后我就直接用pattern了。

正则表达式 匹配 示例pattern
/[wW]oodchuck/ Woodchuck or woodchuck Woodchuck
/[abc]/ ‘a’, ‘b’, or ‘c’ “In uomini, in soldati”
/[1234567890]/ any digit “plenty of 7 to 5”
图 2.2 方括号 [] 用于指定字符的或

正则表达式 [1234567890] 指定了任意单个十进制数。尽管像十进制数或者字母这样类型的字符在表达中是重要的构建基块,但他们会变得笨拙。(比如,指定 [ABCDEFGHIJKLMNOPQRSTUVWXYZ] 就十分不方便)。如果有一个定义正确的序列和字符集关联,那么方括号就可以与横杠(-)连用去指定范围内任何一个字符。pattern [2-5] 指定了2345中的任意一个字符。pattern [b-g] 指定了bcdefg 中的任意一个字符。一些其他的示例如图2.3中所示。

RE Match Example Patterns Matched
/[A-Z]/ an upper case letter “we should call it ‘Drenched Blossoms’ ”
/[a-z]/ a lower case letter my beans were impatient to be hoed!”
/[0-9]/ a single digit “Chapter 1: Down the Rabbit Hole”
图 2.3 方括号 [] 和横杠 - 连用指定范围

方括号也可以和插入符号 ^ 连用来指定一个不满足某些条件的字符。如果插入符号 ^ 是左方括号后的第一个符号,那么结果pattern就需要被取反。举个例子,pattern /[ˆa]/ 匹配任意一个除了a以外的单字符(包括特殊字符)。这只有在插入符号是左方括号后的第一个符号时成立。如果他出现在了其他地方,它通常只是代表一个插入符。图2.4展示了一些例子。

RE Match(single characters) Example Patterns Matched
/[ˆA-Z]/ not an upper case letter “Oyfn pripetchik”
/[ˆSs]/ neither ‘S’ nor ‘s’ I have no exquisite reason for’t”
/[ˆ.]/ not a period our resident Djinn”
/[eˆ]/ either ‘e’ or ‘ˆ’ “look up ˆ now”
/aˆb/ the pattern ‘aˆb’ “look up aˆ b now”
图 2.4 插入符号用于表示取反或者指示代表插入符号本身

*请看后面的正则表达式,用反斜杠跳出这个状态。

我们如何描述可选元素,比如在 woodchunkwoodchunks 中的可选元素 s 。我们不能用方括号,因为当他们允许我们描述”s or S“的时候,他们不会允许我们描述”s or nothing“。为此我们用问号/?/来描述”the preceding character or nothing“,如图2.5所示。

RE Match Example Patterns Matched
/woodchucks?/ woodchuck or woodchucks woodchuck
/colou?r/ color or colour color
图 2.5 问号 ? 标志了前面表达式 [2] 的可选性

[2] 此处实际上是标志了问号前一个字符的可选性。

我们可以把问号看作是“zero or one instances of the previous character”的意思。也就是说,这是一个指定我们想要多少某个东西,这在正则表达中是一个非常重要的事情。举个例子,考虑某些绵羊的语言包括一些和下面类似的字符串:

  • baa! baaa! baaaa! baaaaa! …

这个语言包含字符串 a b ,后面跟着至少两个 a ,最后还有一个惊叹号。允许我们描述一些类似“some number of a s”的运算符集合基于星号或者说是 * ,通常被称为 Kleene * (generally pronounced“cleanystar”)。星号 * 意味着“zero or more occurrences of the immediately previous character or regular expression”。因此, /a*/ 表示“any string of zero or more a s”。这会匹配 a 或者aaa ,但是着也会匹配 Off Minor 因为 字符串Off Minor 有0个a。因此,正则表达式为了匹配一个以及以上的 a 用 /aa*/ 表示,表示a后面可以跟着0个及以上的 a 。更复杂的表达也可以被重复。因此, /[ab]*/ 表示“zero or more a’s or b’s”(不是“zero or more right square braces”)。这会匹配像 aaaaabababbbbb 这样的字符串。

为了指定多个十进制数(对寻找价钱很有用),我们俩可以将正则表达式 /[0-9]/ 拓展一位。因此,整数(十进制数的字符串)就是 /[0-9][0-9]*/ 。(为什么不是 /[0-9]*/ 呢)

有些时候,不得不为了十进制数而写两次正则表达式,这十分让人讨厌。因此,有一种指定某个字符出现至少一次的更简单方法。Kleene + 表示“one or more occurrences of the immediately precedingKleene+ character or regular expression”。因此,表达式 /[0-9]+/ 是指定一个十进制数序列最一般的方法。综上,指定绵羊语言有两种方法: /baaa*!/ 或者 /baa+!/ 。

一个非常重要的特殊符号是句号 /./ ,一个可以匹配任何单个字符的任意替代表达(除去回车),如图2.6所示。

RE Match Example Matches
/beg.n/ any character between beg and n begin, beg’n, begun
图 2.6 使用句号 . 指定任意字符

任意替代符号经常和星号合用表示“any string of characters”。举个例子,假设我们想要找到含有特定单词的一行,比如 aardvark 出现两次。我们可以通过正则表达式 /aardvark.*aardvark/ 指定上述情况。

锚符号是将正则表达式抛锚在字符串特定位置的一个特殊符号。最常见的锚符号就是插入符号 ^ 以及金钱符号 $ 。插入符号 ^ 匹配每句话的开始。pattern /ˆThe/ 只匹配位于每句话开始的单词 The 。因此,插入符号有三个用法:匹配每句话的开始,在方括号中表示取反,或者仅仅表示插入符号本身。(是什么上下文环境让grep或者Python知道一个给定的插入符号应该具有的功能呢?)金钱符号 $ 匹配每句话的结尾。因此,pattern / $/ 是一个匹配结尾空格得有用pattern,并且 /ˆThe dog.$/ 匹配一个只含有词组 The dog 的句子。(我们不得不在此处使用反斜杠,因为我们想要 . 表示句号而不是任意替代符号)

有两种其他的锚符号: \b 匹配一个单词的边缘, \B 匹配一个非边缘。因此, /\bthe\b/ 匹配单词 the 而不是单词 other 。更技术性地讲, 一个用于正则表达式的单词被定义为任意十进制数、下划线或者字母的序列。这基于单词在编程语言中的定义。举个例子, /\b99\b/ 将会在 There are 99 bottles of beer on the wall 中匹配字符串 99 (因为 99 跟在一个空格后面),但是不会在 There are 299 bottles of beer on the wall 中匹配 99 (因为 99 跟在一个数字后面)。但是,他会匹配 $99 中的 99 (因为99跟在一个金钱符号之后,而金钱符号不是一个十进制数、下划线或者字母)。

2.1.2 取或,分类,优先

假设我们需要搜索关于宠物的文本,也许我们特别对猫和狗感兴趣。在这种情况下,我们可能想要搜索字符串 cat 或者字符串 dog 。由于我们不能用方括号搜索“cat or dog”(我们为什么不能直接使用 /[catdog]/ ?),我们需要一个新的运算符,取或运算符,也可以称为管道符号 | 。pattern /cat|dog/ 可以匹配字符串 cat 或者字符串 dog

有时候我们需要用这个取或运算符在一个更大序列的中间。举个例子,假设我想要为我的侄子Daivid搜索关于宠物鱼的信息。我怎么可以同时指定 guppyguppies ?我们不能简单的用 /guppy|ies/ ,因为这只会匹配字符串 guppyies 。这是因为像 guppy 这样的序列优先于取或运算符 | 。为了使取或运算符支队特定pattern生效,我们需要使用括号运算符( and )。为了相邻运算符比如管道符号 | 以及星号 * 的使用,我们可以将一个pattern封闭在括号内使其看起来像一个单个字符。因此,pattern /gupp(y|ies)/ 可以根据我们的想法指定取或符号只对后缀 yies 起作用。

括号运算符 ( 在我们使用像星号 * 的计数器时也有用。不像 | 运算符,星号 * 运算符默认情况下只应用于单字符,而不是整个序列。假设我们想要匹配字符串的一段重复实例。也许我们有一段文字包含表格中的列标签 Column 1 Column 2 Column 3 。这个表达式 /Column [0-9]+ */ 不会匹配任何列的数值,而会匹配一个后面跟着任意个空格的单列。星号只作用于它前面的空格而非整个序列。我们可以用括号写出表达式 /(Column [0-9]+ *)*/ 去匹配单词 Column 后面可以跟0及以上个空格,并且整个pattern可以重复0及以上次。

这个一个运算符可以比其他运算符优先级高的想法要求我们用括号去指定我们想表达的含义。这个想法被正则表达式的运算符优先级制度进行格式化。下列表格给出了正则表达式的运算符优先级,从最高到最低的优先级。

名称 符号
Parenthesis ()
Counters * + ? {}
Sequences and anchors the ˆmy end$
Disjunction \

因此,因为计数器比文本序列的优先级更高, /the*/ 将匹配 theeeee 而不是 thethe 。因为文本序列比取或富豪的优先级高, /the|any/ 将匹配 the 或者 any 而不是 thany 或者 theny

pattern在另一种角度可能会模棱两可。考虑当匹配文本 once upon a time 时,表达式 /[a-z]*/ 的结果。因为 /[a-z]*/ 匹配0及以上各字母,这个表达式可以匹配空字符,或者指示首字母 oononc 或者 once 。在这些情况正则表达式总是匹配可以匹配到的最大字符串,我们称pattern式贪婪的,拓展去尽可能覆盖更多的字符串。

然而,有很多办法去强迫非贪婪匹配,使用 ? 筛选器的另一种用法。运算符 *?是一个星号匹配尽可能短的文本。运算符 +? 是一个加号匹配尽可能短的文本。

2.1.3 一个简单的例子

假设我们想要写一个正则表达式去寻找英语的冠词 the 。一个简单的(但是不正确的)pattern可能是

/the/

一个问题就是这个pattern将错过 the 位于句首因此首字母大写的情况(比如 The )。这可能将我们引导至下面的pattern:

/[tT]he/

但是我们仍然将错误的返回嵌入其他单词的文本(比如 other 或者 theology )。因此我们需要指定两边具有边界的单词实例:

/\b[tT]he\b/

假设我们想要不用 /\b/ 实现这个。我们可能因为 /\b/ 将不处理下划线和数字作为单词边界而弃用这个方法,但是我们可能想要在一些与下划线和数字相邻的上下文中找到 the 。我们需要指定我们想要 the 两边都没有英文字母的实例:

/[ˆa-zA-Z][tT]he[ˆa-zA-Z]/

但是这个pattern仍然有一个问题:他将不会找到位于句首的 the 。这是因为正则表达式 [ˆa-zA-Z] 被我们用来避免 the 的嵌入实例,意味着 the 的前面必须有一些单字符(尽管非字母)。我们可以通过指定 the 为句首或者 the 前面是一个非字母字符避免这个问题。在句尾也是同理。

/(ˆ|[ˆa-zA-Z])[tT]he([ˆa-zA-Z]|$)/

我们刚刚进行的操作是基于两种固定错误:错误肯定,我们错误地匹配了像 other 或者 there 的字符串;以及错误否定,我们错误地错过了像 The 的字符串。处理这两种问题在实现语音和语言处理系统中频繁出现。因此,应用中降低总错误率主要有两个努力方向:

  • 提高准确率(降低错误肯定)
  • 提高召回率(降低错误否定)

2.1.4 一个更复杂的例子

让我们使用正则表达式的力量尝试一个更重要的例子。假设我们想要开发一个帮助用户网上买电脑的应用。用户可能想要“any machine with at least 6 GHz and 500 GB of disk space for less than $1000”。想要实现这样的检索,我们首先需要有能力找到像 6 GHz500 GBMac$999.99。在这部分的最后,我们将找到这个任务的一些简单的正则表达式。

首先,让我们完成关于价格的正则表达式。这是一个用金钱符号后面跟十进制数字符串的正则表达式:

/$[0-9]+/

注意, $ 符号在这里有一个相比我们之前讨论的句尾功能不一样的功能。大多数正则表达式解析器足够聪明,可以意识到 $ 在这里不是表示句尾。(作为一个思维实验,试思考正则表达式解析器如何从上下文弄清楚 $ 的功能。)

现在我们需要处理价钱的小数部分。我们需要加一个十进制的小数点和十进制的小数点后两位:

/$[0-9]+.[0-9][0-9]/

这个pattern只允许 $199.99 但不允许 $199 。我们需要让钱的分位可选并确定他不在单词边界。

/(ˆ|\W)$[0-9]+(.[0-9][0-9])?\b/

最后一点,这个pattern允许像 $199999.99 这样非常贵的价格。我们需要限制价格的高低:

/(ˆ|\W)$[0-9]{0,3}(.[0-9][0-9])?\b/

硬盘空间怎么样呢?我们需要再一次允许可选的小数位(5.5 GB)。注意 ? 的使用可以使得最后的 s 是可选的,并且 / */ 表示“zero or more spaces”因为看可能总是有额外的空格在附近:

/\b[0-9]+(.[0-9]+)? *(GB|[Gg]igabytes?)\b/

改善正则表达式使得他只匹配高于 500 GB 是留给读者的练习。

2.1.5 更多的运算符

图2.7展示了一些普遍范围的别名。这些别名主要被用于节省打字时间。租好了星号 * 和加号 + ,我们还可以通过括在圆括号里使用明确的数字,比如计数器。正则表达式 /{3}/ 表示“exactly 3 occurrences of the previous character or expression”。因此,/a.{24}z/ 将会匹配 a 后面跟着24个点再跟一个 z (而不是 a 后面跟23或者25个点再跟一个 z

RE Expansion Match First Matches
\d [0-9] any digit Party of 5
\D [ˆ0-9] any non-digit Blue moon
\w [a-zA-Z0-9_] any alphanumeric/underscore Daiyu
\W [ˆ\w] a non-alphanumeric !!!!
\s [ \r\t\n\f] whitespace (space, tab)
\S [ˆ\s] Non-whitespace in Concord
图 2.7 一些常见字符集的别名

一个区间的数字也可以被指定。因此, /{n,m}/ 指定前面字符或者表达式 nm 次的出现次数。并且, /{n,}/ 表示前面表达式至少出现 n 次。正则表达式中的计数用法被总结在图2.8中。

RE Match
* zero or more occurrences of the previous char or expression
+ one or more occurrences of the previous char or expression
? exactly zero or one occurrence of the previous char or expression
{n} n occurrences of the previous char or expression
{n,m} from n to m occurrences of the previous char or expression
{n,} at least n occurrences of the previous char or expression
{,m} up to m occurrences of the previous char or expression
图 2.8 计数的一些正则表达式运算符

最后,特定特殊字符可以被特殊的基于反斜杠的记号进行指定(见图2.9)。最常见的就是转行符号 \n 以及缩进符号 \t 。想要指定这些特殊的字符本身(像 ., *, [, 和 \),在他们前面加上反斜杠(比如/./, /*/, /[/, 和 /\/)。

RE Match First Patterns Matched
* an asterisk “*” “K*A*P*L*A*N”
. a period “.” “Dr. Livingston, I presume”
\? a question mark “Why don’t they come and lend a hand?
\n a newline
\t a tab
图 2.9 一些需要被反斜杠转义的字符

2.1.6 替换,群组捕获,ELIZA

一个正则表达式的一个重要用法就是替换。举个例子,替换运算符 s/regexp1/pattern/ 被用在Python中,并且Unix命令行中,像vim或者sed允许被正则表达式特定的字符串被另一个字符串替换:

s/colour/color/

有能力指定一个匹配首个pattern的字符串的子部分往往很有用。举个例子,假设我们想要用尖括号括住文本中的所有整数,比如将 the 35 boxes 改为 the <35> boxes。我们喜欢拥有一种可以直接指向我们发现的整数的方法,这样我们就可以简单的添加括号。为了实现这个,我们将括号运算符( and )括在第一个pattern的周围并用第二个pattern的数字运算符 \1 指向后面。这是他的具体实现:

s/([0-9]+)/<\1>/

括号运算符和数字运算符也可以指定一个特定的在文本中必须出现两次的字符串或者表达式。举个例子,假设我们正在寻找pattern “the Xer they were, the Xer they will be“,这里我们想要限制两个X为相同的字符串。我们可以通过在第一个X周围加上括号运算符,并用数字运算符 \1 替换第二个X的方法实现,如下所示:

/the (.*)er they were, the \1er they will be/

这里 \1 将会被在括号运算符中第一个物品匹配的任何字符串替代。因此这将会匹配 the bigger they were, the bigger they will be 但是不会匹配 the bigger they were, the faster they will be

这个将一个pattern存在内存中的括号运算符的用法被称为群组捕获。每当一个群组捕获被使用时(比如,用一个括号运算符括住一个pattern),匹配的结果将会被存在数字标记的寄存器中。如果你匹配了两个不同的括号运算符集合, \2 表示你第二个捕获群组匹配的任何东西。因此:

/the (.*)er they (.*), the \1er we \2/

将会匹配 the faster they ran,the faster we ran 而不会匹配 the faster they ran,the faster we ate 。相似地,第三个捕获群组被存在 \3 中,第四个被存在 \4 中,依此类推。

括号运算符因此在正则表达式中拥有双重功能:我们被用于集合terms,以指定运算符应该采用的顺序,并且他们被用以捕获寄存器中的一些东西。偶尔我们可能想要用括号集合一些东西,而不像将匹配结果讯在寄存器中。这种情况,我们通过在左括号后面添加 ?: 指令,以这种形式 (?: pattern) 使用非捕获群组。

/(?:some|a few) (people|cats) like some \1/

将会匹配 some cats like some cats but not some cats like some a few

替换和捕获群组在实现简单对话机器人中,比如 ELIZA (Weizenbaum, 1966),非常有用。回想 ELIZA 通过像如下对话一样模拟一个罗氏心理学家:

  • User1: Men are all alike.
    ELIZA1: IN WHAT WAY
    User2: They’re always bugging us about something or other.
    ELIZA2: CAN YOU THINK OF A SPECIFIC EXAMPLE
    User3: Well, my boyfriend made me come here.
    ELIZA3: YOUR BOYFRIEND MADE YOU COME HERE
    User4: He says I’m depressed much of the time.
    ELIZA4: I AM SORRY TO HEAR YOU ARE DEPRESSED

ELIZA 通过一系列或者一连串正则表达式的替换进行工作,每个替换都会匹配或者替换输入文本的一些部分。输入文本是首字母大写的。第一个替换就是将所有 MY 的实例改变为 YOUR ,以及将 I’M 变为YOU ARE ,依此类推。下一个替换集匹配并替换其他输入中的pattern。这里有一些例子:

s/.* I’M (depressed|sad) .*/I AM SORRY TO HEAR YOU ARE \1/
s/.* I AM (depressed|sad) .*/WHY DO YOU THINK YOU ARE \1/
s/.* all .*/IN WHAT WAY/
s/.* always .*/CAN YOU THINK OF A SPECIFIC EXAMPLE/

因为多重替换可以应用于给出的输入,替换被分配了一个排名,并根据顺序被执行。创造pattern是练习2.3的主题,并且我们将在2.6章回到 ELIZA 架构的细节。

2.1.7 前瞻判别

最终,我们将会多次遇到需要预判未来的情况:在文本中提前看是否有pattern匹配,但是不提前匹配指针,这样我们可以在pattern出现时处理他们。

这些前瞻判别使用我们在前面部分,非捕获群组见到的 (? 语法。如果pattern出现,则运算符 (?= pattern) 为真,单宽度为0,举个例子,匹配指针并不提前。如果一个pattern不匹配,运算符 (?! pattern) 只返回真,但再一次说明,这是宽度为0,且不提前指针的。否定前瞻在我们解析一些复杂pattern但想要排除特殊情况时十分常用。举个例子,假设我们想要匹配句首的任何非 ”Volcano“起始的任意单个单词。我们可以这样用否定前瞻:

/ˆ(?!Volcano)[A-Za-z]+/


喵喵喵?