Extended Examples
用这两个例子作为本章的结束。
用PHP解析CSV
CSV Parsing with PHP
这里有一个用PHP解析CSV(逗号分隔值)的程序,原来的例子在第6章(☞271)。这个正则表达式使用了占有优先量词(☞142),而不是固化分组括号,因为它们看起来更清晰。首先,这是我们将要使用的正则表达式:
然后,我们用它来解析$CSV文件中的一行:
检查tagged data的嵌套正确性
Checking Tagged Data for Proper Nesting
这个例子有点复杂,它用到了许多有意思的知识:检查XML(或者是XHTML,或者任何标记的数据)是否包含孤立的或者错误匹配的标签。我的办法是检查正确匹配的tag,非tag文本,以及自封闭tag(self-closing tag,例如<br/>,用XML的语言来说就是一个“空元素tag”),希望我能找到整个字符串。
下面是完整的正则表达式:
能够匹配的字符串不会包含错误匹配的tag(稍后会给出若干告诫)。
这可能相当复杂,但是如果分解为各个部分,就可以掌握了。外层的「^(…)$」包围表达式的主体,保证在返回success之前匹配整个目标字符串。主体包含在一组捕获型括号之内,我们马上会看到,这组括号容许在之后递归引用“主体”。
正则表达式的主体
正则表达式的主体,就是这三个多选分支(在正则表达式中的下画线标注,以便观察),它们包含在「(?:…)*+」中,容许任意的混合都能匹配。这三个多选分支匹配的分别是:tags、非tag文本,以及自封闭tag。
因为每个多选分支能够匹配的文本之间是没有冲突的(也就是说,如果一个多选分支能够匹配,另两个就不能匹配),我知道稍后的回溯永远不会容许另一个多选分支匹配同样的文本。利用这一点,我们可以使用占有优先的星号,提高“容许任何混合”括号的匹配效率。它告诉正则引擎,不要徒劳地回溯,如果找不到匹配,就很快出结果。
因为同样的原因,三个多选分支可以以任何顺序出现,我把最可能匹配的多选分支放在最前面(☞260)。
现在逐个看这些多选分支:
第2个多选分支 非tag文本 我从它开始讲,因为「[^<>]++」很简单。这个多选分支匹配非tag 文本。在这里使用占有优先量词可能有点多此一举——外面的「(?:…)*+)」也是占有优先的,但是为了安全起见,我希望在我知道不会带来负面影响的地方使用占有优先量词。(通常使用占有优先量词是为了提高效率,但是它也会改变匹配的语意。这种修改可能有帮助,不过你必须清楚它的后果☞259)。
第3个多选分支 自封闭tag 第3个多选分支「<w[^>]*+/>」匹配自封闭tag,例如<br/>和<img…/>(自封闭tag在后面的尖括号之前有反斜线)。与之前一样,占有优先量词可能有点多余,但它肯定不会带来负面影响。
第1个多选分支 一对匹配的tags。最后我们来看第1个多选分支:(?1)</2>」
这个子表达式的第一部分(以下画线标注)匹配开头的tag,用「(w++)」,也就是整个正则表达式的第 2 组捕获型括号(在「w++」中使用占有优先量词是很重要的,我们将会看到)匹配tag名称。
「(?<!/)」是否定型逆序环视(☞133),确保没有匹配斜线。我们把它放在匹配开头tag的子表达式中的「>」之前,确保没有匹配自封闭 tag,例如<hr/>(我们已经看到,自封闭的 tag由第3个多选分支处理)。
在开头 tag匹配之后,「(?1)」会递归地应用到第一组捕获型括号内的子表达式。它是之前提到的“主体”,也就是一块只包含对称tag的文本。它匹配之后应该匹配对应的结尾tag(closing tag),就是这个多选分支的第一部分匹配的(tag的名字捕获到第二组捕获型括号)。「</2>」开头的「</」确保它是一个结尾tag,「2>」中的反向引用确保是一个正确的结尾tag。
如果是检查HTML或者其他tag名不区分大小写的数据,请在正则表达式之前添加「(?i)」,或者使用模式修饰符i。
完成了!
占有优先量词
关于第1个多选分支「<(w++)[^>]*+(?<!/)>」中的「w++」的占有优先,我希望多说几句。如果流派的功能不够强大,不能使用占有优先量词或者固化分组(☞139),我会在这个多选分支的(w+)之后加上b:「<(w+)b[^>]*(?<!/)>」。
b很重要,它能够停止(w+)的匹配,例如,‘<link>…</li>’中第一个‘li’的匹配。这样会将‘nk’单独留在捕获型括号外面,导致后面的反向引用「2」引用的tag名不完整。
正常情况下这些都不会发生,因为w+是匹配优先的,会匹配整个tag名。不过,如果正则表达式应用到嵌套结构糟糕的文本中,它应该匹配失败,搜索中的回溯会强迫「w+」匹配不完整的tag名,例如‘<link>…</li>’。「b」能解决这个问题。
谢天谢地,PHP的强大的preg引擎支持占有优先量词,使用「(w++)」与附加「b」的意义一样:不容许回溯切割tag名,但是效率更高。
真实世界的XML
真实世界的XML比简单的匹配tag要复杂得多。我们还必须考虑XML注释、CDATA部分、处理指令和其他。
添加对XML注释的支持是很容易的,只需要增加第4个多选分支,「<!--.*?-->」,请务必使用「(?s)」或者是模式修饰符S,这样点号能够匹配换行符。
同样,CDATA 部分的格式是<![CDATA[…]]>,可以用另一个多选分支「<![CDATA[.*?]]>」来处理,‘<?xml·version="1.0"?>’之类的处理指令需要再添加一个多选分支:「<?.*??>」。
entity 声明的形式是<!ENTITY…>,可以用「<!ENTITYb.*?>」来处理。XML 中有许多类似的结构,他们中的大部分可以用「<![A-Z].*?>」取代「<!ENTITYb.*?>」来处理。
虽然还有些问题,不过上面的办法应该能够应付绝大多数XML。下面是完整的PHP代码:
HTML
常见的情况是,真实世界的HTML有各种各样的问题,这样的检测几乎没有实用价值,例如孤立元素或者失配的tag,以及独立出现的‘<’和‘>’字符。不过,即使是正确配对的HTML也有些特殊情况我们必须处理,注释和<script>tag。
HTML注释规范与XML注释一样:「<!--.*?-->」,使用模式修饰符s。
<script>部分是重要的,因为它可能包含‘<’和‘>’,所以必须容许<script…>和</script>之间出现任何字符。我们可以这样处理:「<scriptb[^>]*>.*?</script>」。有趣的是,不包含禁止出现的‘<’和‘>’的字符的script序列会被第1个多选分支捕获,因为它走的也是“匹配的一组tag”的套路。如果<script>不包含任何其他字符,第1个多选分支会失败,这些文本留给新增的多选分支。
这里是HTML版本的PHP程序: