正则表达式的真正威力(1)

匹配上下文无关语言

举个上下文无关语言的例子,{a^n b^n, n>0},意思就是“一连串a接着相同数量的b”。相应的正则表达式(PCRE)是:

/^(a(?1)?b)$/

(?1)引用第一个匹配的子模式(由最外面的小括号包围),也就是(a(?1)?b)。你可以用子模式替换(?1),所以这形成了递归的依赖:

/^(a(?1)?b)$/
/^(a(a(?1)?b)?b)$/
/^(a(a(a(?1)?b)?b)?b)$/
/^(a(a(a(a(?1)?b)?b)?b)?b)$/
# 等等

从上面的解释中可以看出,这个正则表达式可以匹配包含相同数量a和b的字符串。

所以正则表达式可以匹配一些非正则的上下文无关语法。然而可以匹配所有的吗?要回答这个问题,首先需要明确上下文无关语法是如何定义的。所有的产生式规则采取如下形式:

A -> β

这里的A同样是一个非终结符,β是任意的一串终结符和非终结符。例如下面的语法:

function_declaration -> T_FUNCTION is_ref T_STRING '(' parameter_list ')' '{' inner_statement_list '}'

is_ref -> '&'
is_ref -> ε

parameter_list -> non_empty_parameter_list
parameter_list -> ε

non_empty_parameter_list -> parameter
non_empty_parameter_list -> non_empty_parameter_list ',' parameter

// ... ... ...

上面是PHP语法的一个选段。可能和以前使用的有些不同,但是也很容易理解。需要注意的是这里的大写单词T_SOMETHING都是终结符,这些符号被称之为token。比如T_FUNCTION代表function关键词,T_STRING是标签token(如getUserById或者some_other_name)。

用这个例子我想说明的是:上下文无关语法已经足够强大到编码很复杂的语言。这就是为何基本所有的编程语言都是上下文无关语法。这当然也包括结构化的HTML。

回到正题,正则表达式可以匹配所有的上下文无关语法吗?答案是肯定的。很容易证明正则表达式(至少对于PCRE及相似)提供了非常相似的构造方法。

/
    (?(DEFINE)
        (?<addr_spec> (?&local_part) @ (?&domain) )
        (?<local_part> (?&dot_atom) | (?&quoted_string) | (?&obs_local_part) )
        (?<domain> (?&dot_atom) | (?&domain_literal) | (?&obs_domain) )
        (?<domain_literal> (?&CFWS)? \[ (?: (?&FWS)? (?&dtext) )* (?&FWS)? \] (?&CFWS)? )
        (?<dtext> [\x21-\x5a] | [\x5e-\x7e] | (?&obs_dtext) )
        (?<quoted_pair> \\ (?: (?&VCHAR) | (?&WSP) ) | (?&obs_qp) )
        (?<dot_atom> (?&CFWS)? (?&dot_atom_text) (?&CFWS)? )
        (?<dot_atom_text> (?&atext) (?: \. (?&atext) )* )
        (?<atext> [a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+ )
        (?<atom> (?&CFWS)? (?&atext) (?&CFWS)? )
        (?<word> (?&atom) | (?&quoted_string) )
        (?<quoted_string> (?&CFWS)? " (?: (?&FWS)? (?&qcontent) )* (?&FWS)? " (?&CFWS)? )
        (?<qcontent> (?&qtext) | (?&quoted_pair) )
        (?<qtext> \x21 | [\x23-\x5b] | [\x5d-\x7e] | (?&obs_qtext) )

        # comments and whitespace
        (?<FWS> (?: (?&WSP)* \r\n )? (?&WSP)+ | (?&obs_FWS) )
        (?<CFWS> (?: (?&FWS)? (?&comment) )+ (?&FWS)? | (?&FWS) )
        (?<comment> \( (?: (?&FWS)? (?&ccontent) )* (?&FWS)? \) )
        (?<ccontent> (?&ctext) | (?&quoted_pair) | (?&comment) )
        (?<ctext> [\x21-\x27] | [\x2a-\x5b] | [\x5d-\x7e] | (?&obs_ctext) )

        # obsolete tokens
        (?<obs_domain> (?&atom) (?: \. (?&atom) )* )
        (?<obs_local_part> (?&word) (?: \. (?&word) )* )
        (?<obs_dtext> (?&obs_NO_WS_CTL) | (?&quoted_pair) )
        (?<obs_qp> \\ (?: \x00 | (?&obs_NO_WS_CTL) | \n | \r ) )
        (?<obs_FWS> (?&WSP)+ (?: \r\n (?&WSP)+ )* )
        (?<obs_ctext> (?&obs_NO_WS_CTL) )
        (?<obs_qtext> (?&obs_NO_WS_CTL) )
        (?<obs_NO_WS_CTL> [\x01-\x08] | \x0b | \x0c | [\x0e-\x1f] | \x7f )

        # character class definitions
        (?<VCHAR> [\x21-\x7E] )
        (?<WSP> [ \t] )
    )
    ^(?&addr_spec)$
/x

上面的正则表达式用于匹配RFC 5322标准指定的电子邮箱地址。仅仅是将BNF规则转换成了PCRE能理解的格式。语法很简单,所有的规则定义都放到了DEFINE断言中,这些规则仅仅是定义并不直接用于匹配。只有末尾的^(?&addr_spec)$部分用于匹配。这些规则定义是命名的子模式。在前面的(a(?1)?b)例子中,1引用了第一个子模式。如果由很多的子模式这样用数字来指定是不现实的,所以可以对之进行命名。因此?<xyz ...>定义了名字为xyz的模式。然后使用(?&xyz)来进行引用。

上述则正则表达式还使用了x修饰符。用于指示引擎忽略空格并且允许#形式的注释。这样就可以很好的格式化正则,便于其他人理解。这样将文法映射成正则表达式就很直接了:

A -> B C
A -> C D
// 变成了
(?<A> (?&B) (?&C)
		|  (?&C) (?&D))

唯一的问题是:正则表达式不支持左递归。例如上述的形参列表定义:

non_empty_parameter_list -> parameter
non_empty_parameter_list -> non_empty_parameter_list ',' parameter

不能直接转换成正则,下面是不能工作的:

(?<non_empty_parameter_list>
    (?&parameter)
  | (?&non_empty_parameter_list) , (?&parameter)
)

原因是non_empty_parameter_list出现在自身规则定义的最左边。这被称为左递归,在文法定义中是很常见的,因为最常用的LALR(1)语法解析器相比于右递归比较擅长处理左递归。

但是请不要惊慌,这难不倒正则表达式。每一个左递归文法都可以转化为右递归。对于上例那就是简单的交换两个部分:

non_empty_parameter_list -> parameter
non_empty_parameter_list -> parameter ',' non_empty_parameter_list

因此正则表达式是可以匹配任意上下文无关语言的(基本是程序员使用的所有语言)。唯一的问题是:即使正则可以很好的匹配,也不能进行语法分析。语法分析意味着将一些字符串转化为抽象语法树。至少对于PCRE的正则是不可能的(当然如果你使用Perl那就另当别论了,因为在Perl中正则可以插入任意代码)。

当然,上面基于DEFINE的正则定义是非常有用的。通常情况下你不需要进行语法解析,仅仅是匹配(如电子邮箱)或者提取一些数据(不是整个解析树)。大部分复杂的字符串处理问题都可以简单的通过基于文法的正则来解决。

这里我重申一下:结构良好的HTML是上下文无关的。因此你可以使用正则表达式来匹配。但是要注意两点:首先,现实场景中你见到的大部分HTML都不是结构良好的。其次,你可以做并不代表你应该这样做。你还可以通过Brainfuck语言来编写软件呢,但是基于同样的原因你不会这样做。

我的观点是,通常情况下当你需要处理HTML时,请优先使用DOM库。这可以帮你处理恶意的HTML并且省了很多麻烦。当一些特定的场合才需要编写正则表达式。同时我不得不承认,即便我经常告诉别人不要使用正则来解析HTML,我自己却经常这么干。仅仅是因为在这些特定场合下使用正则更简单。
订阅号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值