正则表达式实战(PHP版)

正则表达式非常强大,但也很难学。花点功夫是值得的。用Netbeans IDE Early Access for PHP虽然因为原来用它做过JAVA的缘故很容易上手,但调试不易让me这个新手学习进度很慢,因为要不停的试错试错再试错,效率很低。昨天装了Zend Studio后感觉太爽了,调试很好用。那就开始吧,解开me一直的心结——正则表达式。

正则表达式是一种可以用于模式匹配和替换的强有力的工具,英文叫Regular Expression,在POSIX中扩充为Extended Regular Expression,简称ereg。ereg共有三种形式:

  1. ereg。默认被php支持,是基础库的一部分,但是对一些新的正则表达式特性不支持,比如lazy模式等。不要被这个Extended迷惑了,要知道POSIX可是1986年制定的。
  2. mb_ereg。是ereg的变体,支持多字节字符(比如中韩日字体编码)。另外一种对多字节的支持方式是preg,需要带上/u修正符(php的4.20版本后有效)。
  3. 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 )。

【2008年11月26日更新】

在处置恶意访问ip的时候,用到了如下的正则:@^(\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:\d{1,3})?)(\s+evil)?$@i,用来匹配形如121.22.23.23 evil,把匹配结果存入数组后$matches[1]是121.22.23.23,$matches[2]则是空格加evil。

三、解決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,得到形如(?<postname>[\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\./%-]。

参考资料:

正则表达式中的特殊字符

正则表达式使用详解

正则表达式参考文档

over

PHP、MYSQL编程笔记

开发StatPressCN过程中恶补PHP和MYSQL知识,水平不行,只好边干边学,呵呵。对一些基础不高深的东东简单梳理下,免得忘了下次重来。

一、MYSQL

  1. 用MYSQL语句直接修改数据。update `db_name` set `urlrequested` = replace(`urlrequested`,’\”,”) where `urlrequested` like “%\'”,把以’结尾的链接请求末尾的’去掉。replace可以保证key的连续性,如果先删除后添加就没有这个好处了。
  2. 插入记录。INSERT INTO users(name, age) VALUES(‘姚明’, 25), (‘比尔.盖茨’, 50), (‘火星人’, 600);如果values中为空,则添加默认值为基础的记录。
  3. 更新记录。需要配合where条件判断。UPDATE users SET age = 30 WHERE id = 12;以上语句都很危险,如果where没有设置的话会更新所有的数据记录,所以在进行类似操作的时候一定要先备份,再审慎
  4. 删除数据有两个命令delete和TRUNCATE。DELETE FROM users WHERE name = ‘Mike’ ORDER BY id DESC LIMIT 6;如果有自动增加的字段,用没有where的delete和truncate都会让这个字段从0开始,如果不想这样,可以用带where 1的语句,不过那样的执行效率很多,因为需要遍历整个数据库的所有记录;删除数据时如果使用形如like ‘$tmpstr%’的语句,一定要判断下$tmpstr是否为空,否则很危险,可能删除掉所有数据,后果严重的很。
  5. 正则表达式在where语句中也可使用,好爽,这下me对正则表达式的学习可以用了。官方文档参见这里。

二、PHP

  1. 分开字符串。$p = explode(“=”,$url);
  2. 打印出数组中的所有元素。print_r($p);
  3. 计算数组中的元素个数。count($p);
  4. 数组运算,类似于java中集合的概念,很方便。生成一个空数组:$bloglist = Array();向数组中增加一个或几个元素(从末尾):array_push($bloglist,$matches[1]);添加到数组头部之前则是array_unshift($bloglist,$matches[1]) ;从数组尾部删除一个元素用array_pop(),从头部删除一个元素则用array_shift();让数组中元素唯一(即无重复元素,类似于结合set的概念):$bloglist = array_unique($bloglist);迭代输出每个元素可以在foreach中使用as:foreach($bloglist as $blog);判断某元素是否在数组中:in_array($lookingFor, $bloglist),返回布尔值真假;数组中每个元素都有个默认的key,可以这样输出看看:while (list($key,$value) = each($bloglist)) {echo “$key : $value<br>”;};还可以合并多个数组$bloglistall = array_merge($bloglist1, $bloglist2);得到两个数组的交集可用array_intersection($bloglist1, $bloglist2);差集可用array_diff($bloglist1, $bloglist2);如需要合集则先array_merge(),然后再用一次array_unique();对数组进行排序则用sort($bloglistall),默认是英文字母顺序;数组的key必须是唯一的,并且key可以设定为字符串,默认是整数。关于数组,更详细的阐释是Array的官方说明(e文版)。
  5. 奇妙的类型自动转换。print(“8&heart5″+20);输出结果为28。
  6. 字符串函数:strlen,得到字符串长度;substr($v,start_pos,length),截取指定长度的字符串,不指定长度则自动取到末尾;strripos($v,”postname”),是否包含指定子字符串,返回所处位置或;sscanf,扫描规律字符串得到相应变量;printf,格式化输出字符串;str_replace,替换子字符串;strstr,找到特定子字符串出现的位置;rtrim会删除末尾的空格和换行等字符,包括空格、\t、\n、\r、\0、\x0b;explode会自动处理行尾的空字符。
  7. 正则表达式。ereg(‘^/[0-9]{4}/[0-9]{2}/{0,1}$’,$out_url);匹配/2008/10
  8. 时间函数。println(strtotime(“now”));输出1224902867,然后用date函数格式化println(date(“c”,$d1));输出2008-10-24T18:03:42+08:00,println(date(“r”,$d1));输出Fri, 24 Oct 2008 18:03:42 +0800,如果用gmdate则输出格林威治标准时,println(gmdate(“r”,$d1));输出Fri, 24 Oct 2008 10:03:42 +0000;其中时区信息是通过date_default_timezone_set(‘prc’);设定的,prc指中国大陆,也可以是其他,有效值列表见这里;另外strtotime还可以对时间做修正,比如strtotime(“3 October 2005”)和strtotime(“1 days”,strtotime(“2 October 2005”))输出的结果都是一样的,可以用的修正符号有“+1 week 3 days 7 hours 5 seconds”,可以用+-号。
  9. file打开某个文件并把每行读入数组;fopen打开一个文件返回句柄,可以设定打开方式为读或者写,打开后内容置空;fwrite则写入内容,成功则返回true,否则是false;操作完毕后需要fclose关闭句柄。

三、WordPress

  1. get_option直接得到选项值,不用每次用mysql语句查询。
  2. get_permalink($id)得到帖子的固定链接地址,注意是包含域名的绝对路径,如果需要相对路径,则需要str_replace(get_bloginfo(“url”),””,get_permalink($id));
  3. update_option如果该项不存在会自动建立。技巧:如果不能确定某个选项是否存在,则可以先update后再delete。
  4. 数组可以一次性存入option中,存储形如a:2:{s:5:”title”;s:8:”my titile”;s:4:”body”;s:12:”Home is warm”;},a指的数组,2是元素个数,title是key名称,5是字符串长度,my titile是title的值,后面body同。
  5. 要调用插件或主题中的函数必须进入wp的主题运作机制,直接调用是无效的,出于安全考虑,同时也是因为无法得到相关环境设定和变量。官方提供的方式是模板,比如您可以自己定义一个page的新模板,然后创建page是选择新模板就可以了。模板标头有特殊要求,见这里
  6. php代码执行其实是以用户权限进行的,因为可以获取文件的在服务器上的本地地址并进行读写操作。

四、HTML

  1. 因为php可以控制输出逻辑,因此可以尽量使用html代码而不是通过php的echo或print输出,这样页面逻辑更清晰,也容易调试。
  2. 表格需要设定宽度后,表格单元中的左对齐右对齐才能起作用。
  3. input type=checkbox name=’statpress_showhotdepth’ value=’checked’ xxx

五、CSS

  1. 样式表嵌入标签的方法。<table class=’mytable’>……</table>,这种情况下mytable应该在外链的css或者当前页面自定义的css中已经定义过;或者直接写style:<table style=”outline:dotted 1px green;”>……</table>
  2. 在形如#sidebar h2{……}中,仅对那些class是sidebar中出现的h2才起作用。

web开发环境的搭建(for php)

为了方便程序开发,必须要搭建一个开发环境。需要一个http服务器、支持mysql和php,这样才能在本机上运行wordpress。在网上找到了xampp,用了一段时间,感觉不错。稍微记一下,聊以备忘。

下载apache friends – xampp

安装:下载安装版本一路next就行了,或者下载zip版本自己解压后运行xampp_setup.bat。

使用

用管理面板启动Apache和mysql即可。需要注意的是请修改xampp安装目录下的apache\conf\httpd.conf,把#LoadModule rewrite_module modules/mod_rewrite.so语句中的#去掉,否则不支持wordpress中的permalink。

至于mysql的优化,建议找专门的文章学学,me简单调整了下my.cnf中的参数,效果很一般,运行起来还是慢的要死,看来这是个专业活。

PHP、MYSQL和WordPress编程散记

为了解决StatPress存在的中文乱码问题并清除无效spider信息,me下功夫K了不少php、mysql以及WordPress的编程信息。怕下次再重新学习一道,记录一下知识点,聊以备忘。

一、PHP

  1. 连接字符串使用.,比如$nome.”|”.urldecode($tab[1])。
  2. 调用变量用$,函数则直接调用。
  3. 字符串处理的一些常用函数。strpos寻找字符串中某字符最先出现处,strrpos寻找字符串中某字符最后出现处,这两个函数都反馈整数值,如果没有找到则返回false;strrchar则返回字符最后出现处至结尾的子字符串,strstr则返回搜索字符串最后出现处至末尾的子字符串;substr取部分字符串,string substr(string string, int start, int [length]);start和length如果是负数则从末尾算起;ereg用正则表达式对字符串进行比较或返回拆分后的数组,eregi同ereg,只是大小写无关,ereg_replace和eregi_replace按照一定的规则进行替换;str_replace替换特定的子字符串,str_replace(“%body%”, “black”, “<body text=%body%>”);
  4. 处理url的函数有urlencode和urldecode,前者把”为什么”转为%CE%AA%CA%B2%C3%B4,后者再把它转回来。编码是为了适应浏览器对url的处理规则, 对字符串多解码一次也没什么影响,还是原样。还有个函数是parse_url,返回数组,可以通过component调用处理结果,支持的component有scheme、host、port、user、pass、query、fragment。
  5. iconv可以把字符串在不同的字符集间进行转换,比如iconv(“gb2312″,”utf-8”,$str)。
  6. gb2312的字符encode后占两位,也就是有两个%,而utf-8则是三位,每个字有三个%,示例:为什么如何使utf-8则encode后为%E4%B8%BA%E4%BB%80%E4%B9%88,共9位,如果是gb2312则为%CE%AA%CA%B2%C3%B4,只有6位。
  7. if中的条件判断语句不能用=而是==,否则就直接赋值了,比如$nome == “Baidu”。
  8. explode函数把由特定间隔符分割的字符串拆解成数组,比如$str = “wd=home”,$array = explode(“=”,$str)后得到array,其中array[0]=wd,array[1]=home。
  9. count则统计数组中元素的个数,count($array)的话就是2。
  10. 调试函数在,怎么忽然就该页为空了(在浏览器中啥都不显示),试了几次都不行。想想刚才都做了什么,也就是更改了页面的charset,从gb2312到utf-8,另外就是增加了两行代码。把代码删除,问题依旧;把charset改回去,还是不行。于是就怀疑是不是服务器不稳定,坏了。重新启动,问题居然依旧。这时候才想到去查服务器的log。一看不打紧,全是500,呵呵,标准的服务器内部错误,并且提示PHP Parse error: syntax error, unexpected T_STRING 在某个文件中云云。Google了下,还是没啥概念。在搜索结果中转来转去,忽然有点感觉了,还是代码出了问题,php无法解析导致的。再次检查,晕,还真是,后面加的两行代码导致的。调用函数居然没有用括号括住参数,具体如此println iconv(“gb2312″,”utf-8”,$stem);呵呵晕死。加上应该的括号后变成了这样println(iconv(“gb2312″,”utf-8”,$stem));搞定!
  11. date是用来输出时间日期为特定形式的函数,具体的时间日期值通过mktime获得。date_default_timezone_set(‘Asia/Shanghai’);居然只支持Shanghai、Chongqing等,但没有北京。$startday = mktime(0,0,0,2,30,2008);居然也有效,不过生成的日期是2008年3月1日,自动处理了。(strtotime(“now”)-$startday)/86400;获得从某特定日期到现在的所过天数。


二、MYSQL
(一)不知道PHP的函数是否可以直接用在SQL语句中,只好把要处理的记录全查出来后根据id一条条进行处理,示例代码如下:
$qry = $wpdb->get_results(“SELECT id, urlrequested FROM $table_name WHERE (urlrequested is not null) and (urlrequested != ”)”);
print “…”.count($qry).” select-ed; “;
foreach ($qry as $rk) {
$tmpstr = urldecode($rk->urlrequested);
$q=”UPDATE $table_name SET urlrequested = ‘$tmpstr’ WHERE id=”.$rk->id;
$wpdb->query($q);
}
print “”.__(‘done’,’statpress’).”
>”;
me非常担心这样的语句如果不能按照本意进行,会毁掉所有记录的那一栏数据:
update $table_name set urlrequested = urldecode(urlrequested);
(二)在本地安装的php、mysql和wordpress组合中导入网站导出的数据(为后缀名为sql的文本文件),命令语句:

mysql -uroot dataname < dataname_wp_20080427_287.sql

(三)创建数据库并授权以及添加用户。

用root登陆后 create database db_name;

grant all on db_name.* to db_user@host identified by ‘password’;

(四)update语句影响的set数目。为了使statpress在update时能精确显示信息,me增加了setcount参数进行统计,点击statpressUpdate时结果如下:

Updating OSes: 2006 sets are set to blank. 2006 sets are updated. done
Updating Browsers: 2009 sets are set to blank. 2009 sets are updated. done
Updating Spiders: 0 sets are set to blank. 0 sets are updated. done
Updating Feeds: 0 sets is set to blank. All is done.
Updating Search engines: 458 sets is set to blank!
2284 are select-ed, 458 are updated!

可以看出,在每个项目中被置空的数据记录居然数量不一样(OS中是2006个,Browser中则是2009个)。按道理像这样的语句 UPDATE $table_name SET spider = ”; 应该是更新数据库中的所有记录数才对啊。Google了半天,总算搞清楚了原因。那就是MYSQL在更新数据时如果该数据和将被更新的数据一致的话则不会发生作用,这样的话上面语句仅仅更新了那些不是”的记录。仔细想想,这种处理确实有理,可以提高sql语句运行效率,在数据库很大的时候当然会显得非常有必要。

(五)清除某些插件的残留物。插件启用后有些时候感觉不好用,总是要放弃的,但有些不友好的会留下一些东西,像创建的数据库啊以及在options中的记录等。用下面语句清除:feed_statistics、wp-poll、tantan的ga。

$str = ”;
foreach($wpdb->get_col(“show tables like ‘%wp_feed_%'”) as $db_name){
$str .= $db_name.”|”;
$wpdb->query(“drop table $db_name”);
}
$str .= $wpdb->query(“delete from wp_options where option_name like ‘%poll_%'”).”|”;
$str .= $wpdb->query(“delete from wp_options where option_name like ‘%feed_statistics_%'”).”|”;
$str .= $wpdb->query(“delete from wp_options where option_name like ‘%tantan_ga%'”).”|”;

三、WordPress
(一)、查看StatPress统计的记录,居然有这样访问的:
/2006/05/23/javascript:void($(‘akst_form’).style.display=’none’);
是不是有人想黑me啊。

又发现了怪怪的urlrequest,这次是¤§??? and 1=1,me把它敲入地址栏后就转换成了 http://heart5.com/%A1%E8%A1%EC???%20and%201=1,页面结果是Error 404,Not Found。

(二)、rss类的url跟随wordpress的permalink设置情况变化:如果是默认的?p=123则get_bloginfo(‘rss2_url’)得到形如http://host/?feed=rss2;如果permalink设置为/%year%/%month%/,则get_bloginfo(‘rss2_url’)得到形如http://host/feed/rss2。statpress对这种情况没有做特殊处理。此种关联也提醒我们不要随便改变wordpress的permalink,因为会影响很多事情。

(三)、在wordpress中所有插件中的函数可以互相调用,并且还可以用在模板中。强,不过,避免函数名称冲突就显得非常非常重要了。