扩展示例

Extended Examples

用这两个例子作为本章的结束。

用PHP解析CSV

CSV Parsing with PHP

这里有一个用PHP解析CSV(逗号分隔值)的程序,原来的例子在第6章(☞271)。这个正则表达式使用了占有优先量词(☞142),而不是固化分组括号,因为它们看起来更清晰。首先,这是我们将要使用的正则表达式:

扩展示例 - 图1

然后,我们用它来解析$CSV文件中的一行:

扩展示例 - 图2

检查tagged data的嵌套正确性

Checking Tagged Data for Proper Nesting

这个例子有点复杂,它用到了许多有意思的知识:检查XML(或者是XHTML,或者任何标记的数据)是否包含孤立的或者错误匹配的标签。我的办法是检查正确匹配的tag,非tag文本,以及自封闭tag(self-closing tag,例如<br/>,用XML的语言来说就是一个“空元素tag”),希望我能找到整个字符串。

下面是完整的正则表达式:

扩展示例 - 图3

能够匹配的字符串不会包含错误匹配的tag(稍后会给出若干告诫)。

这可能相当复杂,但是如果分解为各个部分,就可以掌握了。外层的^(…)$包围表达式的主体,保证在返回success之前匹配整个目标字符串。主体包含在一组捕获型括号之内,我们马上会看到,这组括号容许在之后递归引用“主体”。

正则表达式的主体

正则表达式的主体,就是这三个多选分支(在正则表达式中的下画线标注,以便观察),它们包含在(?:…)*+中,容许任意的混合都能匹配。这三个多选分支匹配的分别是:tags、非tag文本,以及自封闭tag。

因为每个多选分支能够匹配的文本之间是没有冲突的(也就是说,如果一个多选分支能够匹配,另两个就不能匹配),我知道稍后的回溯永远不会容许另一个多选分支匹配同样的文本。利用这一点,我们可以使用占有优先的星号,提高“容许任何混合”括号的匹配效率。它告诉正则引擎,不要徒劳地回溯,如果找不到匹配,就很快出结果。

因为同样的原因,三个多选分支可以以任何顺序出现,我把最可能匹配的多选分支放在最前面(☞260)。

现在逐个看这些多选分支:

2个多选分支 非tag文本 我从它开始讲,因为[^<>]++很简单。这个多选分支匹配非tag 文本。在这里使用占有优先量词可能有点多此一举——外面的(?:…)*+)也是占有优先的,但是为了安全起见,我希望在我知道不会带来负面影响的地方使用占有优先量词。(通常使用占有优先量词是为了提高效率,但是它也会改变匹配的语意。这种修改可能有帮助,不过你必须清楚它的后果☞259)。

3个多选分支 自封闭tag 第3个多选分支<\w[^>]*+/>匹配自封闭tag,例如<br/>和<img…/>(自封闭tag在后面的尖括号之前有反斜线)。与之前一样,占有优先量词可能有点多余,但它肯定不会带来负面影响。

1个多选分支 一对匹配的tags。最后我们来看第1个多选分支:扩展示例 - 图4(?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…>,可以用<!ENTITY\b.*?>来处理。XML 中有许多类似的结构,他们中的大部分可以用<![A-Z].*?>取代<!ENTITY\b.*?>来处理。

虽然还有些问题,不过上面的办法应该能够应付绝大多数XML。下面是完整的PHP代码:

扩展示例 - 图5

HTML

常见的情况是,真实世界的HTML有各种各样的问题,这样的检测几乎没有实用价值,例如孤立元素或者失配的tag,以及独立出现的‘<’和‘>’字符。不过,即使是正确配对的HTML也有些特殊情况我们必须处理,注释和<script>tag。

HTML注释规范与XML注释一样:<!—.*?—>,使用模式修饰符s。

<script>部分是重要的,因为它可能包含‘<’和‘>’,所以必须容许<script…>和</script>之间出现任何字符。我们可以这样处理:<script\b[^>]*>.*?</script>。有趣的是,不包含禁止出现的‘<’和‘>’的字符的script序列会被第1个多选分支捕获,因为它走的也是“匹配的一组tag”的套路。如果<script>不包含任何其他字符,第1个多选分支会失败,这些文本留给新增的多选分支。

这里是HTML版本的PHP程序:

扩展示例 - 图6