9 正则表达式
本章是[Sitaram05]的一个修改版本。
一个正则表达式(regexp)值封装一个被一个字符串或字节字符串(bytestring)描述的模式。当你调用像regexp-match那样的函数时,正则表达式匹配器尝试对(一部分)其它的字符串或字节字符串匹配这个模式,我们将其称为文本字符串(text string)。文本字符串被视为原始文本,而不会视为一个模式。
在《Racket参考》中的“正则表达式(regexp)”部分提供有更多关于正则表达式的内容。
一个字符串或字节字符串(byte string)可以被直接用作一个正则表达式(regexp)模式,也可以用#rx来形成一个字面上的正则表达式值来做前缀。例如,#rx"abc"是一个基于字符串的正则表达式值,并且#rx"abc"是一个基于字节字符串的正则表达式值。或者,一个字符串或字节字符串可以用#px做前缀,就像在#px"abc"中,给一个稍微扩展的字符串内的模式的语法。
在一个正则表达式模式的大多数字符都表示在文本字符串中自相匹配。因此,该模式#rx"abc"匹配在继承中包含字符a、b和c的一个字符串。其它字符充当元字符(metacharacters),而且许多字符序列充当元序列(metasequences)。也就是说,它们指定的东西不是它们字面本身。例如,在模#rx"a.c"中,字符a和c代表它们自己,但元字符.可以匹配任何字符。因此,该模式#rx"a.c"在继承中匹配一个a、任意字符以及c。
当我们想要在一个Racket字符串或正则表达式原义里的一个字面原义的\,我们必须将它转义以便它出现在所有字符串中。Racket字符串使用\作为转义字符,所以我们用两个\结束:一个Racket字符串\转义正则表达式\,它接着转义.。另一个将要在Racket字符串里转义的字符是"。
如果我们需要匹配字符.本身,我们可以在它前面加上一个\来转义。字符序列\.就是一个元序列(metasequence),因为它不匹配它本身而只是.。所以,连续匹配a、.和c,我们使用正则表达式模#rx"a\\.c"。双\字符是一个Racket字符串技巧,不是正则表达式模式本身。
regexp-quote函数接受一个字符串或字节字符串并产生一个正则表达式值。当你构建一个模式来匹配多个字符串时用regexp,因为一个模式在它可以被用在一个匹配之前被编译成了一个正则表达式值。这个pregexp函数类似于regexp,除了使用扩展语法之外。正则表达式值作为带#rx或#px的字面形式被编译一次并且当它们被读取时都这样。
regexp-quote函数接受任意的字符串并返回一个模式匹配原始字符串。特别是,在输入字符串中的字符,可以作为正则表达式元字符用一个反斜杠转义,所以只有它们自己使他们安全地匹配。
> (regexp-quote "cons") "cons"
> (regexp-quote "list?") "list\\?"
regexp-quote函数在从一个混合的正则表达式字符串和字面的字符串构建一个完整的正则表达式是有用的。
regexp-match-positions函数接受一个正则表达模式和一个文本字符串,如果这个正则表达式匹配(某部分)这个文本字符串则返回一个匹配,或这如果这个正则表达式不匹配这个字符串则返回#f。一个成功的匹配产生一个索引序对(index pairs)列表。
> (regexp-match-positions #rx"brain" "bird") #f
> (regexp-match-positions #rx"needle" "hay needle stack") '((4 . 10))
在第二个例子中,整数4和10确定匹配的子串。这个4是起始(包含)索引,同时这个10是匹配子字符串的结尾(不包含)索引:
> (substring "hay needle stack" 4 10) "needle"
第一个例子中,regexp-match-positions的返回列表只包含一个索引序对,并且那个索引序对代表由正则表达式匹配整个字符串。当我们论述了子模式(subpatterns)后,我们将明白为什么一个单独的匹配操作可以产生一个子匹配(submatch)列表。
regexp-match-positions函数接受可选的第三和第四个参数,他们在这个匹配应该发生之中指定文本字符串的指标。
> (regexp-match-positions #rx"needle" "his needle stack -- my needle stack -- her needle stack" 20 39) '((23 . 29))
注意,返回指标仍然与完整的文字符串相对应。
regexp-match函数类似于regexp-match-positions,但它不是返回索引序对,它返回这个匹配的子字符串:
> (regexp-match #rx"brain" "bird") #f
> (regexp-match #rx"needle" "hay needle stack") '("needle")
当regexp-match在字节字符串表达式中使用时,结果是一个匹配的字节子字符串:
> (regexp-match #rx#"needle" #"hay needle stack") '(#"needle")
一个字节字符串正则表达式可以被应用到一个字符串,而且一个字符串正则表达式可以应用到一个字节字符串。在这两种情况下,结果都是一个字节字符串。在内部,所有的正则表达式匹配是以字节为单位,并且一个字符串正则表达式被扩展到一个匹配字符的UTF-8编码的正则表达式。为最大限度地提高效率,使用字节字符串匹配代替字符串匹配,因为匹配字节直接避开了UTF-8编码。
如果你有在端口中的数据,这里无需首先将其读取到字符串中。像regexp-match这样的函数可以在端口上直接匹配:
> (define-values (i o) (make-pipe)) > (write "hay needle stack" o) > (close-output-port o) > (regexp-match #rx#"needle" i) '(#"needle")
regexp-match?函数类似于regexp-match-positions,但只简单地返回一个指示是否匹配成功的布尔值:
> (regexp-match? #rx"brain" "bird") #f
> (regexp-match? #rx"needle" "hay needle stack") #t
regexp-split函数接受两个参数,一个正则表达式模式和一个文本字符串,并返回一个文本字符串的子串列表;这个模式识别分隔子字符串的分隔符。
> (regexp-split #rx":" "/bin:/usr/bin:/usr/bin/X11:/usr/local/bin") '("/bin" "/usr/bin" "/usr/bin/X11" "/usr/local/bin")
> (regexp-split #rx" " "pea soup") '("pea" "soup")
如果第一个参数匹配空字符串,那么返回所有的单字符子字符串的列表。
> (regexp-split #rx"" "smithereens") '("" "s" "m" "i" "t" "h" "e" "r" "e" "e" "n" "s" "")
因此,识别一个或多个空格作为分隔符,注意使用正则表达#rx" +",而不是#rx" *"。
> (regexp-split #rx" +" "split pea soup") '("split" "pea" "soup")
> (regexp-split #rx" *" "split pea soup") '("" "s" "p" "l" "i" "t" "" "p" "e" "a" "" "s" "o" "u" "p" "")
regexp-replace函数用另一个字符串替换这个文本字符串匹配的部分。第一个参数是模式,第二个参数是文本字符串,第三个参数是要插入的字符串或者一个将匹配转换为插入字符串的过程。
> (regexp-replace #rx"te" "liberte" "ty") "liberty"
> (regexp-replace #rx"." "racket" string-upcase) "Racket"
如果该模式没有出现在这个文本字符串中,返回的字符串与这个文本字符串相同。
regexp-replace*函数通过这个插入的字符串在这个文本字符串中代替所有相匹配的内容:
> (regexp-replace* #rx"te" "liberte egalite fraternite" "ty") "liberty egality fratyrnity"
> (regexp-replace* #rx"[ds]" "drracket" string-upcase) "Drracket"
这个判断(assertions) ^和$分别标识这个文本字符串的开头和结尾。它们确保在文本字符串的一个或其它尾部相邻的正则表达式匹配:
> (regexp-match-positions #rx"^contact" "first contact") #f
以上正则表达式匹配失败是因为contact没有出现在文本字符串的开头。在
> (regexp-match-positions #rx"laugh$" "laugh laugh laugh laugh") '((18 . 23))
中,正则表达式匹配最后的laugh。
这个元序列\b判断一个字的边界存在,但这个元序列只能与#px语法一起工作。在
> (regexp-match-positions #px"yack\\b" "yackety yack") '((8 . 12))
里,yackety中的这个yack不结束于字边界,所以它并不匹配。第二yack在字边界结束,所以匹配。
元序列\B(也只有#px)对\b有相反的影响;它判断一个字边界不存在。在
> (regexp-match-positions #px"an\\B" "an analysis") '((3 . 5))
里,这个不在一个字边界结束的an被匹配。
通常,在正则表达式中的一个字符匹配文本字符串中的相同字符。有时使用一个正则表达式元序列(metasequence)来引用一个单个字符是有必要的或方便的。例如,这个元序列\.匹配句点字符。
这个元字符(metacharacter).匹配任意字符(除了在多行模式(multi-line mode)中的换行,参见《回廊》(Cloisters)):
> (regexp-match #rx"p.t" "pet") '("pet")
上面的模式也匹配pat、pit、pot、put和p8t,但不匹配peat或pfffft。
一个字符类(character class)匹配来自一组字符中的任意一个字符。对这种情况的一个典型的格式是括号字符类(bracketed character class)[...],它匹配任意一个来自包含在括号内的非空序列的字符。因此,#rx"p[aeiou]t"匹配pat、pet、pit、pot、put,别的都不匹配。
在括号内,一个-介于两个字符之间指定字符之间的Unicode范围。例如,#rx"ta[b-dgn-p]"匹配tab、tac、tad、tag、tan、tao和tap。
在左括号后的一个初始^将反转通过剩下的内容指定的集合;也就是说,它指定的这个字符集排除括号中标识的字符集。例如,#rx"do[^g]"匹配所有以 do开始但不是dog的三字符序列。
注意括号之内的元字符^,它在括号里边的意义与在外边的意义截然不同。大多数其它的元字符(.、*、+、?,等等)当在括号之内时不再是元字符,即使你一直也许一直不予承认以求得内心平静。仅当它在括号内,并且当它既不是括号之间的第一个字符也不是最后一个字符时,一个-是一个元字符。
括号内的字符类不能包含其它被括号包裹的字符类(虽然它们包含字符类的某些其它类型,见下)。因此,在一个被括起来的字符类里的一个[不必是一个元字符;它可以代表自身。比如,#rx"[a[b]"匹配a、[和b。
此外,由于空括号字符类是不允许的,一个]立即出现在开左括号后也不必是一个元字符。比如,#rx"[]ab]"匹配]、a和b。
在#px语法里,一些标准的字符类可以方便地表示为元序列以代替明确的括号内表达式:\d匹配一个数字(与[0-9]一样);\s匹配一个ASCII空白字符;而\w匹配可以是一个“字(word)”的一部分的一个字符。
遵循正则表达式惯例,我们确定”字“字符为[A-Za-z0-9_],虽然这些对一个可能会看重一个”字“的Racket使用者来说过于严格。
这些元序列的这个大写版本代表对应字符类的倒转:\D匹配一个非数字,\S匹配一个非空格字符,而\W匹配一个非“字”字符。
在把这些元序列放进一个Racket字符串里时,记得要包含一个双反斜杠:
> (regexp-match #px"\\d\\d" "0 dear, 1 have 2 read catch 22 before 9") '("22")
这些字符类可以使用进一个括号表达式中。比如,#px"[a-z\\d]"匹配一个小写字母或一个数字。
一个POSIX(可移植性操作系统接口)字符类是表[:...:]的一种特殊的元序列(metasequence),这个表只能用在 #px语法中的一个括号表达式内。这个POSIX类支持
-
[:alnum:] — ASCII字母和数字
-
[:alpha:] — ASCII字母
-
[:ascii:] — ASCII字符
-
[:blank:] — ASCII 等宽空格:空格和tab
-
[:cntrl:] — “控制(control)”字符:ASCII 0到32
-
[:digit:] — ASCII码数字,同\d
-
[:graph:] — 使用墨水的ASCII字符
-
[:lower:] — ASCII小写字母
-
[:print:] — ASCII墨水用户加等宽空白
-
[:space:] — ASCII空白, 同\s
-
[:upper:] — ASCII大写字母
-
[:word:] — ASCII字母和_,同\w
-
[:xdigit:] — ASCII十六进制数字
例如,这个#px"[[:alpha:]_]"匹配一个字母或下划线
> (regexp-match #px"[[:alpha:]_]" "--x--") '("x")
> (regexp-match #px"[[:alpha:]_]" "--_--") '("_")
> (regexp-match #px"[[:alpha:]_]" "--:--") #f
这个POSIX类符号只(only)适用于在一个带括号的表达式内。比如[:alpha:],当不在一个带括号的表达式内时,不会被当做字母类读取。确切地说,它是(来自以前的准则)包含字符::、a、l、p、h的字符类。
> (regexp-match #px"[:alpha:]" "--a--") '("a")
> (regexp-match #px"[:alpha:]" "--x--") #f
这个量词(quantifier) *、 +和 ?分别匹配:零个或多个,一个或多个,以及零个或一个前面子模式的实例。
> (regexp-match-positions #rx"c[ad]*r" "cadaddadddr") '((0 . 11))
> (regexp-match-positions #rx"c[ad]*r" "cr") '((0 . 2))
> (regexp-match-positions #rx"c[ad]+r" "cadaddadddr") '((0 . 11))
> (regexp-match-positions #rx"c[ad]+r" "cr") #f
> (regexp-match-positions #rx"c[ad]?r" "cadaddadddr") #f
> (regexp-match-positions #rx"c[ad]?r" "cr") '((0 . 2))
> (regexp-match-positions #rx"c[ad]?r" "car") '((0 . 3))
在#px语法里,你可以使用括号来指定比用*、+、?更精细的调整量:
-
这个量词{m}精确匹配前面子模式的m实例;m必须是一个非负整数。
-
这个量词{m,n}最少匹配m且最多匹配n个实例。m和n是非负整数,m小于或等于n。你可以省略一个数字或两个数字都省略,在这种情况下默认m为0,n为无穷大。
很明显,+和?是{1,}和{0,1}的缩写,*是{,}的缩写,这个和{0,}一样。
> (regexp-match #px"[aeiou]{3}" "vacuous") '("uou")
> (regexp-match #px"[aeiou]{3}" "evolve") #f
> (regexp-match #px"[aeiou]{2,3}" "evolve") #f
> (regexp-match #px"[aeiou]{2,3}" "zeugma") '("eu")
到目前为止所描述的量词都是贪婪的(greedy):它们匹配最大数量的实例,这样会导致一个对整个模式的总体匹配。
> (regexp-match #rx"<.*>" "<tag1> <tag2> <tag3>") '("<tag1> <tag2> <tag3>")
为了使这些量词成为非贪婪的(non-greedy),给它们追加一个?。非贪婪量词匹配最小数量的实例需要确保整体匹配。
> (regexp-match #rx"<.*?>" "<tag1> <tag2> <tag3>") '("<tag1>")
非贪婪量词分别为:*?、+?、??、{m}?、{m,n}?。注意元字符?的这两种使用。
簇(Clustering)——圈占于括号内(...)——识别封闭的子模式(subpattern)作为一个单一的实体。它导致匹配器去捕获子匹配(submatch),或者字符串的一部分匹配这个子模式,除了整体匹配除外:
> (regexp-match #rx"([a-z]+) ([0-9]+), ([0-9]+)" "jan 1, 1970") '("jan 1, 1970" "jan" "1" "1970")
簇也导致一个后面的量词把整个封闭模式作为一个实体处理:
> (regexp-match #rx"(pu )*" "pu pu platter") '("pu pu " "pu ")
返回的匹配项数量总是等于在正则表达式中指定的子模式数量,即使一个特定的子模式碰巧匹配不止一个子字符串或根本没有子串。
> (regexp-match #rx"([a-z ]+;)*" "lather; rinse; repeat;") '("lather; rinse; repeat;" " repeat;")
在这里,这个*量化的子模式匹配三次,但这是被返回的最后一个匹配项。
对一个量化的子模式来说不匹配也是可能的,即使是整个模式匹配。在这种情况下,这个失败的子匹配被#f体现。
> (define date-re ; 匹配“月年”或“月日年” ; 子模式匹配天,如果目前 #rx"([a-z]+) +([0-9]+,)? *([0-9]+)") > (regexp-match date-re "jan 1, 1970") '("jan 1, 1970" "jan" "1," "1970")
> (regexp-match date-re "jan 1970") '("jan 1970" "jan" #f "1970")
子匹配可用于插入程序regexp-replace和regexp-replace*的字符串参数。这个插入的字符串可以使用\n做为反向引用(backreference)来反向引用第n个匹配项,它是匹配第n个子模式的子字符串。一个\0引用整个匹配,并且它也可以被指定为\&。
> (regexp-replace #rx"_(.+?)_" "the _nina_, the _pinta_, and the _santa maria_" "*\\1*") "the *nina*, the _pinta_, and the _santa maria_"
> (regexp-replace* #rx"_(.+?)_" "the _nina_, the _pinta_, and the _santa maria_" "*\\1*") "the *nina*, the *pinta*, and the *santa maria*"
> (regexp-replace #px"(\\S+) (\\S+) (\\S+)" "eat to live" "\\3 \\2 \\1") "live to eat"
在插入字符串中使用\\来指定一个字面反斜杠。同样,\$代表一个空字符串,并且对从一个紧随其后的数字来分离一个反向引用\n是有用的。
反向引用也可以用在一个#px模式内部来引用返回给这个模式里的一个已经匹配的子模式。\n代表第n个子匹配的一个精确重复。注意这个\0,它在一个插入的字符串中是有用的,在正则表达式模式内部毫无意义,因为整个正则表达式不匹配而无法对它反向引用。
> (regexp-match #px"([a-z]+) and \\1" "billions and billions") '("billions and billions" "billions")
注意,这个反向引用不是简单的一个以前子模式的重复。而是一个已经被子模式匹配的特定子字符串的重复。
在上面的例子中,反向引用只能匹配billions。它不会匹配millions,即使这个子模式重新回到——([a-z]+)——这样做会没有问题:
> (regexp-match #px"([a-z]+) and \\1" "billions and millions") #f
下面的示例在一个数字字符串中标记所有立即重复的模式:
> (regexp-replace* #px"(\\d+)\\1" "123340983242432420980980234" "{\\1,\\1}") "12{3,3}40983{24,24}3242{098,098}0234"
下面的示例修正重叠字:
> (regexp-replace* #px"\\b(\\S+) \\1\\b" (string-append "now is the the time for all good men to " "to come to the aid of of the party") "\\1") "now is the time for all good men to come to the aid of the party"
它通常需要指定一个簇(通常用于量化)但没有触发子匹配信息的捕捉。这种簇被称为非捕捉(non-capturing)。为了创建一个非捕捉簇,使用(?:代替(作为这个簇开启器。
在下面的例子中,一个非捕捉簇消除了一个给定UNIX路径名的“目录”部分,并一个捕捉簇识别出基本名。
但不要用正则表达式解析路径。使用诸如split-path之类的函数来代替。
> (regexp-match #rx"^(?:[a-z]*/)*([a-z]+)$" "/usr/local/bin/racket") '("/usr/local/bin/racket" "racket")
非捕捉簇的?和:之间的位置称为回廊(cloister)。你可以在此处放置修改器,以使簇的子模式(subpattern)得到特殊处理。修饰符i使子模式不敏感地匹配大小写:
回廊(cloister)是一个有用的术语,如果最终可爱,来自Perl的住持创造的词语。
> (regexp-match #rx"(?i:hearth)" "HeartH") '("HeartH")
修饰符m使子模式subpattern)在多行模式(multi-line mode)匹配,在.的位置不匹配换行符,^仅在一个新行后可以匹配,而$仅在一个新行前可以匹配。
> (regexp-match #rx"." "\na\n") '("\n")
> (regexp-match #rx"(?m:.)" "\na\n") '("a")
> (regexp-match #rx"^A plan$" "A man\nA plan\nA canal") #f
> (regexp-match #rx"(?m:^A plan$)" "A man\nA plan\nA canal") '("A plan")
你可以在回廊里放置多个修饰符:
> (regexp-match #rx"(?mi:^A Plan$)" "a man\na plan\na canal") '("a plan")
在修饰符前的一个减号反转它的意义。因此,你可以在子簇(subcluster)中使用-i以翻转由封闭簇导致的案例不敏感。
> (regexp-match #rx"(?i:the (?-i:TeX)book)" "The TeXbook") '("The TeXbook")
上述正表达式将允许任何针对the和book的包装,但它坚持认为TeX没有不同的包装。
你可以通过用|分隔它们来指定替补(alternate)子模式(subpatterns)的列表。在最近的封闭簇里|分隔子模式(或在整个模式字符串里,假如没有封闭括号)。
> (regexp-match #rx"f(ee|i|o|um)" "a small, final fee") '("fi" "i")
> (regexp-replace* #rx"([yi])s(e[sdr]?|ing|ation)" (string-append "analyse an energising organisation" " pulsing with noisy organisms") "\\1z\\2") "analyze an energizing organization pulsing with noisy organisms"
不过注意,如果你想使用簇仅仅是指定替补子模式列表,却不想指定匹配项,那么使用(?:代替(。
> (regexp-match #rx"f(?:ee|i|o|um)" "fun for all") '("fo")
注意替补的一个重要事情是,最左匹配替补不管长短。因此,如果一个替补是后一个替补的前缀,后者可能没有机会匹配。
> (regexp-match #rx"call|call-with-current-continuation" "call-with-current-continuation") '("call")
若要让较长的替补进行匹配,请将其放在较短的替补之前:
> (regexp-match #rx"call-with-current-continuation|call" "call-with-current-continuation") '("call-with-current-continuation")
在任何情况下,对整个正则表达式的整体匹配始终优先于整体的不匹配。在下面例子中,较长的替补仍然获胜,因为其首选的较短前缀无法产生整体匹配。
> (regexp-match #rx"(?:call|call-with-current-continuation) constrained" "call-with-current-continuation constrained") '("call-with-current-continuation constrained")
我们已经看到贪婪量词匹配最大次数,但最重要的是整体匹配成功。考虑以下内容
> (regexp-match #rx"a*a" "aaaa") '("aaaa")
这个正则表达式包括两个子正则表达式:a*后跟a。子正则表达式a*不能匹配文本字符串aaaa里的所有的四个a,即使*是一个贪婪量词也一样。它可能仅匹配前面的三个,剩下最后一个留给第二子正则表达式。这样确保完整的正则表达式匹配成功。
正则表达式匹配器通过一个称为回溯(backtracking)的过程实现来这个。匹配器暂时允许贪婪量词匹配所有四个a,但当整体匹配处于危险中时,它回溯(backtracks)到一个不那么贪婪的三个a的匹配。如果这也失败了,与以下调用一样
> (regexp-match #rx"a*aa" "aaaa") '("aaaa")
匹配器回溯得更远。只有当所有可能的回溯尝试都没有成功时,才承认整体失败。
回溯并不局限于贪婪量词。非贪婪量词尽可能少地匹配实例,并逐步回溯到越来越多的实例,以获得整体匹配。替补中也有回溯,因为当局部成功的向左替补未能产生整体匹配时,会尝试更向右的替补。
有时禁用回溯是有效的。例如,我们可能希望作出选择,或者我们知道尝试替补是徒劳的。一个非回溯正则表达式是括在(?>...)里的。
> (regexp-match #rx"(?>a+)." "aaaa") #f
在这个调用里,子正则表达式?>a+贪婪地匹配所有四个a,并且剥夺了回溯的机会。因此,整体匹配被拒绝。于是,这个正则表达式的效果是匹配一个或多个a,后跟一些绝对不是a的内容。
你的模式中可以有前寻或后寻以确保子模式发生或不发生。这些“环顾”主张是通过将选中的子模式放入一个前导字符为:?=(用于主动前寻),?!(被动前寻),?<=(主动后寻),?<!(被动后寻)。请注意,主张中的子模式不会在最终结果中生成匹配;它只允许或不允许其余的匹配。
带?=的主动前寻检查其子模式是否可以立即与文本字符串中当前位置的左侧匹配。
> (regexp-match-positions #rx"grey(?=hound)" "i left my grey socks at the greyhound") '((28 . 32))
正则表达式#rx"grey(?=hound)"匹配grey,但仅仅如果它后面紧跟着hound时成立。因此,文本字符串中的第一个grey不匹配。
被动后寻?!检查其子模式是否可能立即与左侧匹配。
> (regexp-match-positions #rx"grey(?!hound)" "the gray greyhound ate the grey socks") '((27 . 31))
正则表达式#rx"grey(?!hound)"匹配grey,但前提是后面不跟有hound。因此,grey仅仅在socks之前才匹配。
带?<=的主动后寻检查其子模式是否可以立即与文本字符串中当前位置的左侧匹配。
> (regexp-match-positions #rx"(?<=grey)hound" "the hound in the picture is not a greyhound") '((38 . 43))
正则表达式#rx"(?<=grey)hound"匹配hound,但前提是前面是grey。
带?<!的被动后寻检查其子模式是否可能立即与左侧匹配。
> (regexp-match-positions #rx"(?<!grey)hound" "the greyhound in the picture is not a hound") '((38 . 43))
正则表达式#rx"(?<!grey)hound"匹配hound,但前提是前面没有grey。
在不混淆的情况下,前寻和后寻可以很方便。
下面是一个扩展的例子,来自Friedl的《精通正则表达式(Mastering Regular Expressions)》,第189页,它涵盖了本章中描述的许多特性。问题是要设计一个正则表达式,它将匹配任何且仅匹配IP地址或点分的四个四位数:四个由三个点分隔的数字,每个数字介于0和255之间。
首先,我们定义一个与0到255匹配的子正则表达式n0-255:
> (define n0-255 (string-append "(?:" "\\d|" ; 0 through 9 "\\d\\d|" ; 00 through 99 "[01]\\d\\d|" ; 000 through 199 "2[0-4]\\d|" ; 200 through 249 "25[0-5]" ; 250 through 255 ")"))
请注意n0-255将前缀列为首选替代项,这是我们在替补要注意的。但是,由于我们打算显式地锚定这个子正则表达式以强制进行整体匹配,所以交替的顺序并不重要。
前两个替补简单地得到所有的单位和双位数字。因为允许0填充,所以我们需要同时匹配1和01。我们在得到三位数的数字时需要小心,因为必须排除255以上的数字。因此,我们交替使用000到199,然后是200到249,最后是250到255。
IP地址是一个由四个个n0-255组成的字符串,用三个点分隔。
> (define ip-re1 (string-append "^" ; 前面什么都没有 n0-255 ; 第一个n0-255, "(?:" ; 接着是子模式 "\\." ; 被一个点跟着 n0-255 ; 一个 n0-255, ")" ; 它被 "{3}" ; 恰好重复三遍 "$")) ; 后边什么也没有
让我们试试看:
> (regexp-match (pregexp ip-re1) "1.2.3.4") '("1.2.3.4")
> (regexp-match (pregexp ip-re1) "55.155.255.265") #f
这很好,除此之外我们还有
> (regexp-match (pregexp ip-re1) "0.00.000.00") '("0.00.000.00")
所有零序列都不是有效的IP地址!前寻以救援。在开始匹配ip-re1之前,我们前寻以确保我们没有所有的零。我们可以使用主动前寻来确保有一个数字不是零。
> (define ip-re (pregexp (string-append "(?=.*[1-9])" ; ensure there's a non-0 digit ip-re1)))
或者我们可以使用被动前寻来确保前面的内容不只是由零和点组成。
> (define ip-re (pregexp (string-append "(?![0.]*$)" ; 不只是零点和点 ; (注:.不是在[...]里面的匹配器) ip-re1)))
正则表达式ip-re将匹配所有且仅匹配有效的IP地址。
> (regexp-match ip-re "1.2.3.4") '("1.2.3.4")
> (regexp-match ip-re "0.0.0.0") #f