-
参考书:精通正则表达式 。
-
本书中代码部分不多,而且拆的比较散,按照正则从入门到精通的顺序来组织内容,少量的 Perl 代码只是点缀,因而这个项目中的代码也较少,些许遗漏,还望海涵。
-
本项目使用 Maven 管理 Java 依赖,Perl 代码则需要相对应环境运行。
-
有句话希望你能牢牢记住:把必须匹配的情况考虑周全并写出一个匹配结果符合预期的正则表达式很容易,但把不需要匹配的情况也考虑周全并确保它们都将被排除在匹配结果以外往往要困难得多。
-
读者最好能养成按照字符来理解正则表达式的习惯。例如,不要这样:^cat 匹配以 cat 开头的行,而是应该这样理解:^cat 匹配的是以 c 作为一行的第一个字符,紧接着一个 a,紧接着一个 t 的文本。这两种理解的结果并无差异,但按照字符来解读更易于明白新遇到的正则表达式的内部逻辑。
-
请记住,排除型字符组表示“匹配一个未列出的字符(match a character that's not listed)”,而不是“不要匹配列出的字符(don't match what is listed)”。这两种说法看起来一样,但是 Iraq 的例子说明了其中的细微差异。有一种简单的理解排除型字符组的办法,就是把它们看作普通的字符组,里面包含的是除了“排除型字符组中所有字 符”以外的字符。
-
一个重要但常见的问题是,写正则表达式时,我们需要在对欲检索文本的了解程度与检索精确性之间求得平衡。例如,如果我们 知道,针对某个检索文本,03.19.76 这个正则表达式基本不可能匹配不期望的结果,使用它就是合理的。要想正确使用正则表达式,清楚地了解目标文本是非常重要的。
-
gr[ea]y 与 gr(a|e)y 的例子可能会让人觉得多选结构与字符组没太大的区别,但是请留神不要混淆这两个概念。一个字符组只能匹配目标文本中的单个字符,而每个多选结构自身都可能是完整的正则表达式,都可以匹配任意长度的文本。
-
反向引用是正则表达式的特性之一,它容许我们匹配与表达式先前部分匹配的同样的文本。在支持反向引用的工具软件中,括号能够“记忆”其中的子表达式匹配的文本,不论这些文本是什么,元字符序列 \1 都能记住它们。当然,在一个表达式中我们可以使用多个括号。再用 \1、 \2、 \3 等来 表示第一、第二、第三组括号匹配的文本。括号是按照开括号 '(' 从左至右的出现顺 序进行的,所以 ([a-z])([0-9])\1\2 中的 \1 代表 [a-z] 匹配的内容,而 \2 代表 [0-9] 匹配的内容。
-
这样使用的反斜线称为“转义符(escape)”——它作用的元字符会失去特殊含义,成了普通字符。如果你愿意,也可以把转义符和它之后的元字符看作特殊的元字符序列,这个元字符序列匹配的是元字符对应的普通字符。这两种看法是等价的。
-
任何语言中都存在不同的方言和口音,很不幸,正则表达式也一样。情况似乎是,每一种支持正则表达式的语言都提供了自己的“改进”。正则表达式不断发展,但多年的变化也造就了数目众多的正则表达式“流派”(flavor)。
-
编写正则表达式时,按照预期获得成功的匹配要花去一半的工夫,另一半的工夫用来考虑如何忽略那些不符合要求的文本。在实践中,这两方面都非常重要,但是目前我们只关注“获得成功匹配”的方面。即使我没有对这些例子进行最全面彻底的解释,它们仍然能够提供有用的启示。
-
由Perl语言的正则表达式开创的流派,在20世纪90年代中期因为其强大的表达能力广为人们所知,其他语言紧随其后,提供了汲取其中灵感的正则表达式(其中许多为了标明自己的**来源,直接给自己贴上“兼容Perl(Perl-Compatible)”的标签)。它们包括PHP、Python、Java的大量正则包,微软的.NET Framework、Tcl, 以及C的各种类库。不过,所有这些语言在重要的方面各有不同。而且 Perl 的正则表达式也在不断演化和发展(现在,有时候是受了其他语言的正则表达式的刺激)。像往常一样,总的局面变得越来越复杂,让人困惑。
-
字符(character)“字符”在计算机领域是一个有特殊意义的单词。一个字节所代表的单词取决于计算机如何解释。单个字节的值不会变化,但这个值所代表的字符却是由解释所用的编码来决定的。例如,值为64和53的字节,在ASCII编码中分别代表了字 符“@”和“5”,但在EBCDIC编码中,则是完全不同的字符(一个是空格,一个是控制字符)。
-
我选择以Perl开始,主要是因为,在所有流行的语言中Perl对正则表达式的支持很完整,且易于使用。而且,Per还提供了许多其他紧凑的数据处理结构(data-handling constructs),能够减少所需的“简单重复劳动”(dirty work),以便我们把精力集中到正则表达式上。
-
啊哈,现在看起来大不一样了。语句末尾出现了/x(在/g和/i之后),它对这个正则表达式做了两件简单但有意义的事情。首先,大多数空白字符会被忽略,用户能够以“宽松排列(free-format)”编排这个表达式,增强可读性。其次,它容许出现以#开头标记的注释。
-
但其缺陷——尤其是elisp的正则表达式的缺陷——在于,此流派过分依赖反斜线了,最终得到的正则表达式好像插满了牙签。
-
无论单词分界符怎么定义“单词字符”,单词分界符的测试通常只是简单的字符相邻测试。所有的正则引擎都不会对单词进行语意分析:它们认为“NE14AD8”是一个单词,而“M.I.T.”不是。
-
符号 | 有很多称呼,不过“或(or)”和“竖线(bar)”最为常见。 有的流派使用 | 。多选结构的优先级很低,所以 this and|or that 的匹配等价于 (this and)|(or that),而不是 this (and|or) that ,虽然 and|or 看上去是一个单位。
-
如果我们重新安排多选结构的顺序环视,把能够匹配的数字最短的放到最后,这个问题就解决了: Jan·([12][0-9]|3[01]|0?[1-9]) 。另一种办法是使用 Jan·(31|[123]0|[012]?[1-9]) 。但这也要求我们仔细地安排多选分支的顺序避免问题。还有一种办法是 Jan·(0[1-9]|[12][0-9]?|3[01]?|[49]) ,这样不论顺序环视如何都能获得正确结果。
-
DFA与NFA:实现难度的差异尽管存在限制,但简单的DFA和NFA引擎都很容易理解和实现。对效率(包括时间和空间效率)和增强性能的追求,令实现越来越复杂。
-
好的正则表达式必须在这些方面求得平衡:
- 只匹配期望的文本,排除不期望的文本。
- 必须易于控制和理解。
- 如果使用NFA引擎,必须保证效率(如果能够匹配,必须很快地返回匹配结果,如果不能匹配,应该在尽可能短的时间内报告匹配失败)。
-
需要指出的是,纵然本书是关于正则表达式的,但正则表达式也不总是最优解。例如,大多数程序设计语言都提供了处理文件名的非正则表达式函数。不过为了讲解正则表达式,我仍会继续下去。
-
通常来说优化能节省时间,但并非永远如此。只有在检测优化措施是否可行所 需的时间少于节省下来的匹配时间的情况下,优化才是有益的。事实上,如果引擎检查之后认为不可能进行优化,结果总是会更慢,因为最开始的检查需要花费时间。所以,在优化所需的时间,节省的时间,以及更重要的因素——优化的可能性之间,存在互相制约的关系。
-
举例来说, ^Subject:·(.*) 的 Subject:· 是必须出现的。程序可以检查整个字符串,或者使用 Boyer-Moore 搜索算法(这是一种很快的文件检索算法,字符串越长,效率越高)。没有采用 Boyer-Moore 算法的程序进行逐个字符检查也可以提高效率。选择目标字符串中不太可能出现的字符(例如 Subject:· 中的 t 之后的 :)能够进一步提高效率。
-
当然,总的来看可能还是利大于弊。许多人不关心正则表达式的效率——它们对正则表达式怀着一种恐惧心理,只希望能完成任务,而不关心如何完成。(你可能没见过这种情况,但是我希望这本书能够加强你的信心,就像标题说的那样,精通正则表达式)。
-
动态正则表达式的主要用途之一是匹配任意深度的嵌套结构(长久以来人们认为正则表达式对此无能为力)。匹配任意深度的嵌套括号是个重要的例子。。如果我们创建一个regex对象——比如$Level变量,就可以在动态正则中引用它(动态正则结构可以包含任意的Perl代码,只要结果能被解释为正则表达式,返回已存在的regex对象的Perl代码当然符合要求)。如果我们能把$Level之类的regex对象放入$LevelN,就可以用(??{$LevelN})来引用它:my $LevelN; # 必须首先声明,后面才能使用 $LevelN = qr/(([^()]|(??{$LevelN}))*)/x;