技术开发 频道

Web程序员必须掌握的五个正则表达式

【IT168 技术文档】摘要:本文是《程序员必须掌握的基础正则表达式》的后续,我们将带领大家探索正则表达式在Web应用中的用途,通过最常见的一些判断功能的实现,让大家领略正则表达式的强大之处。

在接下去的文章里我假设你已经有一些正则表达式的基本知识。如果你是一个正则表达式的新手(我喜欢称做 /n0{2}b/)。(译者注:原文是N00b,是newbie的网络缩写),或者你需要快速的回顾一下,可以去看看我的前一篇文章《程序员必须掌握的基础正则表达式》,它不会让你失望的。

好了,废话不多说,下面我列出我在日常web开发工作中发现的五个最有用的正则表达式。

匹配一个用户名

这个表达式非常简单,可是在你想为一个站点建立用户注册系统的时候确实非常有价值。为了开发上的简便,我们通常希望把用户起名字能用的字符限制在一个有限的集合里(比如一些敏感的字眼或者可能用来做注入攻击的名字,象delete这种都不能允许),同时我们还要防止有的人恶意冒用别人的用户名(比如一个用户叫Bill Gates,另一个人起名叫Bill Gates,区别只是两个单词见多了一个空格,当然也可能是换行符这种不可见字符,它们在浏览器里面看上去就象同一个人)。

要是不用正则表达式的话,这个检查会是一个乏味的劳动—先把字符串切割成独立的单词,再一个个检查每个单词的合法性。如果使用正则表达式,就会变成一个很轻松的事情。首先,让我们定义我们想接受的用户名,为了简单起见,我们的示例被限制为只接受下面的条件:

1. 字母数字字符(英文字母和数字)

2. 下划线(_)

与此同时,我们还强制用户名最短3个字符最长不超过16个字符。下面就是符合上述描述条件的正则表达式:
/[a-zA-Z0-9_]{3,16}/

如果你非常熟悉正则表达式的话,你会发现这个表达式好像缺了一些东西。不用着急,接下去我会讲到。

如果你读过我前面一篇正则表达式的入门文章,你可能已经分析出这个表达式如何工作了。首先,我们定义一个字符分类,它将匹配任何字母(a到z以及A到Z)和任何数字(0到9),以及_ (下划线)字符。下面接着一个数量范围,它告诉解析引擎我们只接受3到16个之间的字符数量。因为这个数量是指整个字符分类包括的所有字符而不是一个特定的字符,所以这个数量范围跟在字符分类后面。这个表达式将匹配所有长度在3到16之间,由我们的受限字符集里面字符构成的字符串。

那么到底缺了什么?由于我们写的表达式是匹配一个字符串的任何部分。它将不止认为‘mike_84′是合法字符串,同样也会匹配类似‘%! mike_84&’这种包含了我们不希望出现的东西的字符串。我们需要用到行定位点, ^ (caret) 和 $ (dollar) 字符将把我们的表达式限制在一个字符串的起点和终点位置,这样可以确保整个用户名符合我们的设定,而不是一部分。

所以修订版的正则表达式象这样:
/^[a-zA-Z0-9_]{3,16}$/

下面是一个PHP的代码片段,演示如何在产品中使用这个表达式(我们也可以同样应用在perl、Java、ruby甚至javacript里面做这种检验工作)。
function validate_username( $username ) { if(preg_match('/^[a-zA-Z0-9_]{3,16}$/', $_GET['username'])) { return true; } return false; }
匹配一个XHTML/XML 标记

如果你想抓取一个站点的数据或者试图快速从一个XML文档中取出信息的话,关于匹配一个XML 或者XHTML的标记的表达式将会非常有用。下面这个简单的正则表达式就可以完成这类抽取工作(表达式里面的单词‘tag’ 在实际应用中可以用任何你想寻找的标记名称替换):
{<tag[^>]*>(.*?)</tag>}

这里涉及到两个正则表达式里面的概念:多数优先(greedy quantifier)和少数优先(lazy quantifier)。

我们详细说明一下:问号跟着星号的写法把表达式的计数方式变成了少数优先(lazy quantifier)。缺省情况下,正则表达式的匹配是多数优先(greedy quantifier),多数优先的意思是将匹配字符串中尽可能多的字符,相反,少数优先(Lazy quantifiers)将匹配字符串中尽可能少的字符。听上去很拗口,我们还是用这个实例来说明一下,如果你在这个例子里面使用多数优先(greedy quantifier)匹配法,我们的正则表达式将无法在下面这样的字符串下正常解析:
<tag>item 1</tag><tag>item 2</tag>

因为是多数优先,表达式将会一直匹配到最后那个>标记结束,而不是一个个>来匹配,这样得到的内容是:>item 1</tag><tag>item 2</tag,和我们希望的item 1相去甚远。

下面是这个简单的PHP函数用来抽取每个匹配的XML或者XHTML标记里面的内容到一个数组:
function get_tag( $tag, $xml ) { $tag = preg_quote($tag); preg_match_all('{<'.$tag.'[^>]*>(.*?)</'.$tag.'>.'}', $xml, $matches, PREG_PATTERN_ORDER); return $matches[1]; }
匹配一个包含特定属性和值的XHTML/XML标记 (例如class 或者tag)

这个表达式很像我们上面这个例子中的那个,除了我们希望这个标记包含了一个特定的属性值。比如在你想得到含有某个特定class或者ID值的标记里面的内容时,这个表达式非常有用。这个表达式只是比前一个例子里面的稍微复杂一点(同样,里面的tag、attribute和value在实际应用中用你希望寻找的值代替):
{<tag[^>]*attribute\s*=\s*(["'])value\\1[^>]*>(.*?)</tag>}

我们用一个字符分类去允许在属性值两端的字符可以是单引号或者双引号。这里有另一个正则表达式里面的概念:反向引用(backreference)。

跟在这个正则表达式中value那个词后面的写法(在这里是\1)叫做反向引用(backreference)。在实际解析过程中,我们可以认为它将被前面这个(["'])中实际匹配到的字符代替(要么是一个单引号要么是一个双引号)。用这个技术我们可以确定这个属性值的前后两个引号是一致的。

下面是一个演示你怎么样能够使用这个表达式从一个XHTML文档中取出指定信息的PHP函数。这个函数接受属性、对应值、输入文本和一个可选的标记名称做为参数。如果不指定标记名称,它将匹配所有含有特定属性和对应值的标记。
function get_tag( $attr, $value, $xml, $tag=null ) { if( is_null($tag) ) $tag = '\w+'; else $tag = preg_quote($tag); $attr = preg_quote($attr); $value = preg_quote($value); $tag_regex = "/<(".$tag.")[^>]*$attr\s*=\s*". "(['\"])$value\\2[^>]*>(.*?)<\/\\1>/" preg_match_all($tag_regex, $xml, $matches, PREG_PATTERN_ORDER); return $matches[3]; }
匹配和分析一个email地址

这个表达式来自于Cal Henderson,他是编写Flickr的程序员,以及Building Scalable Web Sites一书的作者(这本书非常值得看)。更多的信息可以参考Cal关于分析email地址的文章。

这是一个庞大的正则表达式,但是当我们把它分成几个部分以后就会发现很容易理解。构造一个正则表达式多少有点像用巴克斯范式(BNF)描述一个语法。我们说做email判断的正则表达式比较容易,因为我们为了检测匹配需要做的很多事情已经在电子邮件的规范里面用BNF描述过了。这是关于email地址相关的规范,叫做RFC 822。下面就是一个检查有效e-mail地址的PHP函数:
function is_valid_email_address($email){ $qtext = '[^\x0d\x22\x5c\x80-\xff]'; $dtext = '[^\x0d\x5b-\x5d\x80-\xff]'; $atom = '[^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c'. '\x3e\x40\x5b-\x5d\x7f-\xff]+'; $quoted_pair = '\x5c[\x00-\x7f]'; $domain_literal = "\x5b($dtext|$quoted_pair)*\x5d"; $quoted_string = "\x22($qtext|$quoted_pair)*\x22"; $domain_ref = $atom; $sub_domain = "($domain_ref|$domain_literal)"; $word = "($atom|$quoted_string)"; $domain = "$sub_domain(\x2e$sub_domain)*"; $local_part = "$word(\x2e$word)*"; $addr_spec = "$local_part\x40$domain"; return preg_match("!^$addr_spec$!", $email) ? 1 : 0; }
这个里面比较新鲜的是‘\x##’这种写法,它表示一个字符的十六进制数值。这个只是一种另类的写法,用一个字符的编码值来代表字符(这个##代表的是特定字符的ASCII编码值)。总的来看,这是一个实际上非常简单,同时又让人一眼看上去上觉得非常复杂的正则表达式。在这里我就不再多做进一步分析了,因为在原作者文章里面有,感兴趣的朋友可以参考Cal的文章。

Tim Fletche已经吧Cal的原始版本PHP函数移植到了Ruby 和Perl,如何你的开发语言是这些,你可以去看看。
匹配一个URL

匹配一个URL很像匹配一个email地址,除了你需要做更多的控制,同时必须容忍这个表达式在某些极端的情况下面会出现的判断错误。我经常在项目中使用这个表达式,比如在一个网站留言栏里,如果出现一个URL,我会自动把它转化为链接。象判断email的表达式一样,这个正则表达式是一个庞然大物,不过相信我,它很容易理解。

这个表达式将涉及到一个正则表达式里面的概念:正则表达式修饰符(Pattern modifiers),我在这个表达式里面使用了两个修饰符-- ‘x’ 和‘i’ 修饰符。正则表达式修饰符是跟在表达式最后,它的作用是改变解析引擎处理表达式的方法。

具体到现在的例子,‘x’ 修饰符告诉解析引擎忽略空格,除非出现在转义符(’\’)后面或者在一个字符分类([…])里面,它同时还通知解析引擎在字符分类以外任何地方跟在’#’字符后面的文字都视为注释文字(也就是在解析的时候忽略它们)。‘i’修饰符通知解析引擎对于字符串分析时不区分字母的大小写(case insensitive)。在这种比较复杂的表达式中,使用正则表达式修饰符可以极大的简化表达式,比如可以让你不用对匹配条件都指定大写字母以及小写字母。我现在用的这个正则表达式来源于Jeffrey Friedl 在他的著作Mastering Regular Expressions中写的一个表达式。
{ \b # Match the leading part (proto://hostname, or just hostname) ( # http://, or https:// leading part (https?)://[-\w]+(\.\w[-\w]*)+ | # or, try to find a hostname with more specific sub-expression (?i: [a-z0-9] (?:[-a-z0-9]*[a-z0-9])? \. )+ # sub domains # Now ending .com, etc. For these, require lowercase (?-i: com\b | edu\b | biz\b | gov\b | in(?:t|fo)\b # .int or .info | mil\b | net\b | org\b | [a-z][a-z]\.[a-z][a-z]\b # two-letter country code ) ) # Allow an optional port number ( : \d+ )? # The rest of the URL is optional, and begins with / ( / # The rest are heuristics for what seems to work well [^.!,?;"\'<>()[]{}sx7F-\xFF]* ( [.!,?]+ [^.!,?;”\’<>()\[\]{\}s\x7F-\xFF]+ )* )? }ix
这个表达式里面的注释解释得比较清晰,因此我认为我们不需要完整的分析整个表达式。我只想补充一下它可能会判断错误的一些地方 - 首先,这个表达式可能会匹配到一些无效的URL – 因为这个表达式假设任何两个字母的组合是一个有效的优异域名后缀(TLD),比如.cn、.en,但是我们知道有些组合可能还不存在。另一个问题是:它也不认识那些最近加到IANA列表里面的TLD,比如:.travel、 .name和.museum,不过这个问题你可以通过自己加上这些判断来解决 -- 首先从IANA下载最新的TLD列表,然后把新增加的这些补充到表达式中间对应的那块区域后面。

除了这些可能的例外,基本上这个正则表达式的匹配结果在99.9%以上的情况下是正确的。下面这个PHP函数能够分析一段文本,把它发现的所有URL转换成链接。在这个例子里面我假设你已经用上面的正则表达式正确地设置了变量$url_regex的值,所以在这里我就省略不重复了。
function auto_link( $text ) { $url_regex = ... return preg_replace( $url_regex, '<a href="$0"^gt;$0=</a>', $text ); }
以上就是我要介绍的所有内容。如果你觉得我遗漏了应该介绍的表达式或者有任何改进建议,请留下留言让我知道。

相关文章:《程序员必须掌握的基础正则表达式》
0
相关文章