正则表达式实战(PHP版)
正则表达式非常强大,但也很难学。花点功夫是值得的。用Netbeans IDE Early Access for PHP虽然因为原来用它做过JAVA的缘故很容易上手,但调试不易让me这个新手学习进度很慢,因为要不停的试错试错再试错,效率很低。昨天装了Zend Studio后感觉太爽了,调试很好用。那就开始吧,解开me一直的心结——正则表达式。
正则表达式是一种可以用于模式匹配和替换的强有力的工具,英文叫Regular Expression,在POSIX中扩充为Extended Regular Expression,简称ereg。ereg共有三种形式:
ereg。默认被php支持,是基础库的一部分,但是对一些新的正则表达式特性不支持,比如lazy模式等。不要被这个Extended迷惑了,要知道POSIX可是1986年制定的。
mb_ereg。是ereg的变体,支持多字节字符(比如中韩日字体编码)。另外一种对多字节的支持方式是preg,需要带上/u修正符(php的4.20版本后有效)。
preg。这个p指的是perl,也就是兼容perl语法的正则表达式。要支持preg需要编译PCRE库,或者直接在服务器端安装。
以上三种形式ereg是兼容性最强的,因为是从基础函数库层面支持的;但preg功能更丰富,速度也快;mb_ereg则对多字节字符支持的很好。比如“中国”的编码是“D6D0B9FA”,用ereg('/./',$str)返回D6,如果设定字符编码后用mb_eregx_encoding("CP936")就可以返回正确的结果“中”。如果php脚本用的utf-8编码,则可以通过/u修正符来匹配多字节字符。
弄清楚了来龙去脉,下面实战。
一、b[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}b 用来匹配电子邮件地址,比如xxx@gmail.com;
二、preg_match(string $pattern , string $subject [, array &$matches [, int $flags [, int $offset ]]])返回整数,0则说明未匹配成功,1则说明匹配成功,并会把匹配结果存入$matches中,其中matches[0]存储全匹配结果,mataches[1]则存储子模板匹配结果,其中子模板指的是模板中用括号括起来的部分。示例如下:
preg_match('@^(?:http://)?([^/]+)@i',"http://www.php.net/index.html", $matches);
print_r($matches);
输出:Array ( [0] => http://www.php.net [1] => www.php.net )
正则表达式详解:@^(?:http://)?([^/]+)@i:@是模板限定符,这个可以是其他的比如/等,因为此模板中有//需要匹配,所以要用不同于/的@,当然其他也行;i是修正符,指的是忽略大小写;^,指必须位于行首;(?:http://),是子模板,其中的?代表此子子模板的匹配项不存入matches,仅仅匹配就行了,没有这个?,则Array[1]应该是http://;第二个?指的前面的模板出现0次或1次;([^/]+)又是一个子模板,匹配/,+则代表需要出现一次或多次。
再来一次:
preg_match('@^(http://)?([^/]+)@i',"http://www.php.net/index.html", $matches);
print_r($matches);
输出:Array ( [0] => http://www.php.net [1] => http:// [2] => www.php.net )
我们再举个其他例子:32选7第2006095期 04091316242830+25 ,提取其中的中奖号码包括特别号。
preg_match("@((0[1-9]|[1-9][0-9]){7})(?:[+])(0[1-9]|[1-9][0-9])@",$str,$matches);
print_r($matches);
输出:Array ( [0] => 04091316242830+25 [1] => 04091316242830 [2] => 30 [3] => 25 )
详解:((0[1-9]|[1-9][0-9]){7})用来匹配04091316242830,其中的(0[1-9]|[1-9][0-9])用来匹配两位数字,比如03、32之类的,{7}要求必须出现七次,整体用()括起来是为了让这个结果存入matches中;(?:[+])用来匹配+号,?:则是为了仅匹配而不存入matches。
刚才的结果中Array[2]=>30,是中奖号的最后一个数字,对我们没有任何意义,要想得到中奖号和特别号,其他统统不要怎么办。把正则表达式写成这样:@((?:0[1-9]|[1-9][0-9]){7})(?:[+])(0[1-9]|[1-9][0-9])@,OK了,输出:Array ( [0] => 04091316242830+25 [1] => 04091316242830 [2] => 25 )。
三、解決StatPressCN的永固鏈接和页面请求匹配处理问题
WordPress出于用户友好的角度出发,允许blogger自己设定永固链接以让页面网址对用户更友好(默认是?p=234,确实够不友好的)。在StatPressCN中记录页面访问时需要识别到底是哪篇文章被访问了,以方便准确统计数据。这中间就存在一个永固链接结构变更的问题,比如原来设定的是/2008/09/statpresscn.html,后来改为/2008/09/12/statpresscn.html。统计工具只会记录当时的网址,它不会理会到底是哪篇文章被访问了。me提供了一个解决方案就是在进入统计数据库的时候根据当前的永固链接结构识别文章的id并记录,这样无论如何更改永固链接结构,都可以正确显示到底是哪篇文章被访问了,做数据统计分析的时候肯定更全面客观准确。
根据永固链接结构来解析被访问网址(也就是页面请求),获得文章在WordPress数据库中的id。me虽然感觉可以通过正则表达式解决,但因为当时不会用(很严重的畏难情绪),只好用最笨的办法:一个个数看%postname%在永固链接结构/%year%/%month%/%day%/%postname%.html的位置(其实是查斜杠/的次数),然后在被访问网址中查到相应的位置,提取该文章的postname,然后再反查post数据库,得到文章id。这个方法虽然暂时可以解决问题,但很不完善,也存在一定的风险。比如说,如果有人把永固链接结构设为/%year%%postname%.html了,那查斜杠/的方式就完全失效;即使大家规规矩矩把变量用斜杠/隔起来,那也无法排除部分在链接中添加个人语句的可能,比如me为了宣传自己的大名,完全可以设定永固链接结构为/%year%%month%/%day%/%postname%heart5.html的样子,那通过最后一个斜杠和html前的小数点.获取的postname就是无效的,当然也不可能通过post数据库反查出id来。学习了正则表达式,嘿,问题就好办了。
function heart5_get_post_name_or_id($url,$per_struct){
$per_str_reg = preg_replace("@%([w%-]+)%@","(?<$1>[w%-]+)",$per_struct);//[w%-]代表任何数字字母以及百分号%和减号-,这是链接地址中允许出现的字符。第二个参数是提取第一个得到的变量名并替换为正则表达式,同时用该变量做key,得到形如(?
[w%-]+)。本语句是替换永固俩接结构为正则表达式用于以后的变量提取。
$per_str_reg = "@^".$per_str_reg."$@";//装配成正则表达式
preg_match($per_str_reg,$url,$matches);//进行匹配
if(key_exists("post_id",$matches)){//先看是否能得到postid,如得到则返回
return $matches["post_id"];
}else if(key_exists("postname",$matches)){//再看是否能得到postname,如得到则返回
return $matches["postname"];
}
return false;//匹配不成功则返回false
}
在处理形如/2008/09/12/statpresscn.html的链接匹配时因为遗漏了小数点.的处理,写成了[w/%-]结果一直无法匹配成功,后来才发现原来还需要匹配小数点.,唉。正确的应该写成[w./%-]。
参考资料:
一般情况下,正则表达式的匹配是贪婪模式的,比如下面这个例子:
字符串:....src="http://www.21pt.com/1.mp3" type="application/x-mplayer2" ....
要求的结果:http://www.21pt.com/1.mp3
如果匹配表达式写为:/src="(.*)"/,则得不到正确的结果,因为最后一个双引号的匹配是贪婪模式的。
解决办法:匹配表达式写为:
/src="(.*)".?/
上面表达式中,".?是非贪婪模式匹配。也就是说,只要在一个字符后面跟上限定个数的特殊字符,匹配就是非贪婪模式了。