2.8 匹配多个选择分支之一
问题描述
创建一个正则表达式,当把它重复应用到目标文本Mary, Jane, and Sue went to Mary's house之上时,会匹配到Mary、Jane、Sue,且能再次匹配到Mary。之后再进行的匹配尝试都会失败。
解决方案
Mary|Jane|Sue
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
讨论
竖线(vertical bar),或称作管道符号(pipe symbol),会把正则表达式拆分成多个选择分支。‹Mary|Jane|Sue›会在每次匹配尝试时匹配Mary,或者Jane,或者Sue。每次只会匹配一个名字,但是每次却可以匹配不同的名字。
本书中的所有正则表达式流派都会使用正则制导的引擎。正则表达式依赖于这台引擎来工作。这里所说的正则制导1FF的含义是,在目标文本中的每个字符位置会首先匹配该正则表达式的所有可能排列,然后才会到下一个字符位置进行匹配尝试。
当你把‹Mary|Jane|Sue›应用到Mary, Jane, and Sue went to Mary's house的时候,在字符串起始处立即就会找到匹配Mary。
当你把同一个正则表达式应用到余下的字符串的时候,比如说,你可以在文本编辑器中单击“查找下一个”,正则引擎就会尝试在该字符串中的第一个逗号处匹配‹Mary›。匹配会失败。然后,它会在同一个位置尝试去匹配‹Jane›,这也会失败。接着在逗号处匹配‹Sue›,当然也会失败。只有在所有匹配都失败之后,正则引擎才会前进到字符串中的下一个字符。从第一个空格开始匹配,3个选择分支都会得到同样的失败结果。
从字母J开始匹配的时候,第一个选择分支‹Mary›会出现匹配失败。接着第二个选择分支,也就是‹Jane›,会在字母J处匹配成功。它匹配的是Jane。正则引擎宣布匹配成功。
需要注意的是:虽然在目标文本中还存在另外一个Mary,而且在正则表达式中‹Mary›出现在‹Jane›之前,但是这里匹配到的依然是Jane。至少在这个例子中,正则表达式中各分支的顺序并不重要。正则表达式会查找最左边的匹配。它会从左向右扫描目标文本,在扫描的每一步中都会尝试正则表达式中的所有选择分支,而当其中任意一个选择分支产生一个合法匹配的时候,匹配过程就会停在这个位置。
如果我们再次对字符串余下部分进行查找,那么会找到Sue。而第四次查找则会再一次找到Mary。如果你告诉正则引擎进行第五次查找,就会失败,因为这三个选择分支都无法匹配余下的’s house字符串。
只有当在字符串中的同一个位置存在两个选择分支同时匹配的时候,正则式中的选择分支的顺序才有意义。例如,正则式‹Jane|Janet›在匹配目标文本Her name is Janet的时候,就会有两个选择分支在同一位置出现匹配。在此正则表达式中并不存在单词边界。事实上,‹Jane›是否只匹配到Her name is Janet中的单词Janet的一部分并不重要。
‹Jane|Janet›之所以会匹配到Her name is Janet中的Jane,是因为一个正则制导的正则表达式引擎是遵循“浅尝辄止”的工作原则的(eager)。除了会从左向右扫描目标文本,查找最左匹配之外,它还会从左向右扫描正则式中的选择分支。而一旦它找到一个匹配的选择分支,正则引擎就会立即停止。
当‹Jane|Janet›到达了Her name is Janet中的J的时候,第一个选择分支‹Jane›,成功匹配。第二个选择分支则根本没有进行尝试。如果我们告诉引擎接着查找下一个匹配的话,这时候在目标文本中剩下的只有t。此时两个选择分支都不能成功匹配。
要想不让Jane抢夺Janet的光环,有两种方式。第一种方式是把较长的选择分支放在前面:‹Janet|Jane›。另外一种更为可靠的方式是清晰地表达我们所期望的结果:我们在查找名字,而名字应该是完整的单词。正则表达式并不会处理单词,但是它们可以处理单词边界。
因此,‹bJaneb|bJanetb›和‹bJanetb|bJaneb›都会匹配到Her name is Janet中的Janet。由于单词边界的原因,只有一个选择分支会成功匹配。在这里我们看到选择分支的顺序依然无关紧要。