snoopy.class.php中_striplink方法的正则分析

上周提到老爷机坏了,昨天早上早早的抱去修理,维修工程师更换硬件排除法最后得知,故障出现在显卡,插入显卡,主板灯亮,按开关,没反映,或者CPU风扇抖动一下即停止。卸下显卡,开机正常。这位维修人员的结论是显卡坏了,建议更换显卡。我们有N卡9200,只要320元。我提出把显卡放到别的主机上试试,被拒绝了,好吧,那我也拒绝。最后,对方收我20元检测费。虽然我心里有点不情愿,他只是换了几个零件的简单排除法得出很有可能的结论而已,并没有告诉我准确的病因,是显卡的哪里出问题了?能否维修?他都没有告诉我。尽管这样,我还是给了,也没争论。

主机抱回来之后,自己卸下了显卡。开机,上网,打算到网上买块新的。老的显卡是我07年在学校买的。ATI RADEON X550,在当初,还算比较前卫的硬件。周五还赞扬其能勤奋工作到现在,而结果得知出问题的就是它。工作了3年多,不到四年,还能接收吧。主板换了华硕P5GC-MX/1333 ,作为硬件小白的我,也不知道换什么显卡较好。熟悉硬件的朋友,麻烦留言告诉一下,感谢了。现在打开网页,拉动浏览器滑动条的时候,卡的比较厉害,播放电影更是,像放映幻灯片一样。实在不能接受。遂网上搜了相关信息。价格在500左右。是有点贵了。遂暂时作罢。一整天心情都很低落,或者说,自从老爷机出问题了,心情一直很差。中午窝在家里,洗了洗衣服,趁太阳好,把冬天穿的衣服拿出来晒了晒。又挪动电脑附近的两个大桌子,打扫卫生,清理一番。一直忙到下午3点,头饿的晕忽忽的。匆忙到沙县小吃随便吃了点,下午,看会书,睡了会觉,早上送GF回家,起得太早了,困的厉害。睡醒之后,都7点多了,照照镜子,看看自己蓬松的发型,猥琐的样子,不禁笑了一笑。洗了把脸,到小区的理发店里剪了头发,由于是小区内,专门给老头老太太理发的,所以费用仅5元,在上海,应该没有比这还低了吧。晚上,把显示器接到电视盒上,看了会电视,差不多都是娱乐类的弱智脑残节目,我极为反感觉,又将显示器接回主机箱,开机到当当网上看了一番。因工作需要,要做个代码自动审计化的程序,我的想法是,不仅仅停留在对关键危险函数的正则匹配,而是做到语法分析,识别未初始化的变量、未过滤的富文本字段、SQL注入的方法等。想必,肯定的熟悉《编译原理》以及LEX、YACC之类相关知识,后悔大学没认真听,造成自己基础薄太弱了。当然,我也觉得大学的时候,直接教学生这么底层的知识没有循序渐进的引导学生去深入更好。可以从最简单的网页制作开始,然后,让学生对其感兴趣,再引导到动态,然后,联合数据库,然后效率优化,然后代码执行原理,然后编译原理类似这种步骤引导学生更好。比起现在的方式,可能更让学生感兴趣。(扯远了)东看西看,忙到了12点多才睡。

对于已经习惯8点左右起床上班的我来说,生物钟在早上8点左右,准时醒,有时候想赖床,但也无法再次入睡。起床洗漱,打开电脑,玩了两局web的三国杀,逛逛论坛,觉得饿了,才去煮点面条吃。回来继续,无意中打开PPC,看到一位网友“落叶人生”同学的帖子问一个正则问题,纠结了一下是否解释一下,以便给对方释疑解惑,也加固自己的知识。犹豫半天,觉得还是写下吧。下面正题:

PPC链接http://bbs.phpchina.com/thread-189797-1-1.html

  1. preg_match_all(“‘<\s*a\s.*?href\s*=\s*([\"\'])?(?(1)(.*?)\\1|([^\s\>]+))[^>]*>?(.*?)</a>’isx”,$document,$links);

对于一般的规范链 接能很好的使用,但不知道为什么对href=后面的网址不含引号的情况无法提取成功,哪个朋友帮分析下?谢谢!

其实,我没明白他的意思是想提取链接地址href后面的内容的,还是提取整个标签a中间的所有。

在下面的回帖中,他提到了这个正则来自snoopy.class.php的_striplink方法中的正则,遂到sf.net上下载一份原版到本地。

代码如下

	function _striplinks($document)
	{
		preg_match_all("'<\s*a\s.*?href\s*=\s*			# find <a href=
						([\"\'])?					# find single or double quote
						(?(1) (.*?)\\1 | ([^\s\>]+))		# if quote found, match up to next matching
													# quote, otherwise match up to next space
						'isx",$document,$links);

		// catenate the non-empty matches from the conditional subpattern

		while(list($key,$val) = each($links[2]))
		{
			if(!empty($val))
				$match[] = $val;
		}

		while(list($key,$val) = each($links[3]))
		{
			if(!empty($val))
				$match[] = $val;
		}

		// return the links
		return $match;
	}

先看下修饰符部分。【isx】,大家都知道【i】是不区分大小写;【s】是点号通配模式,也就是元字符【.】可以匹配换行符。在这里的作用是为了防止链接中出现换行的情况;
【x】宽松排列和注释模式,也就是空白字符不会被当成正则表达式的一部分,#后面的为注释,方便阅读的作用了。顺便说下括号后面的双引号后面的单引号,跟修饰符i前面的单引号,他们只是正则的起始标识,不是正则的一部分。其实,表达式里有单引号了,这里最好使用别的,比如斜杠,或者闭合的大括号等。
再看表达式【<\s*a\s.*?href\s*=\s*】,其后面也注释了,是匹配“<a href=”的,第一个【\s】匹配”<”到”a”之间的空白字符,不过一般都没的吧。都直接写”<a……”。后面的【\s】也是空白字符,不详细解释了。在后面的【.*?】是匹配“<a “到“href=”之间的内容,比如”title=”…..”、target=”….”等。【href=】中没有元字符,就是匹配【href=】.

再后面【([\"\'])?】这里的单引号,双引号是被PHP语法转义的,真正的表达式是【(["'])?】,匹配单引号和双音号,并且,用小括号分配了组(整个表达式的第一组),后面有【?】量词,意味这可有可无。

再后面很重要了。去掉PHP的转义字符之后【(?(1) (.*?)\1 | ([^\s>]+)】,这是一个正则表达式的高级用法,叫“条件判断”表达式。语法为【(? if then| else)】。这里的if部分为【(1)】也就是前面说的“整个表达式的第一组”的内容,如果为真,则使用表达式【(.*?)\1】来匹配(\1是引用捕获的第一个组的内容),如果【(1)】没匹配到,则使用【([^\s>]+)】来匹配。这样,就可以实现没有引号的情况下,也是可以匹配到了。(正则表达式的条件判断语法,【|else】可以不写,表示不用else部分的表达式)

对于楼主的问题。应该怎么改?我刚刚刷新了帖子,看到楼主的正则是可以匹配的呀。

汗,贴下PHP的代码吧。


if (preg_match('%<\s*a\s.*?href\s*=\s*(["\'])?(?(1)(.*?)\1|([^\s>]+))[^>]*>(.*?)</a>%si', $subject)) {
	# Successful match
} else {
	# Match attempt failed
}

文章标题跟内容有很大出入,见谅哈。

周末记事-还是我的老爷机

我的老爷机:
06年买的主机箱+飞利浦CRT显示器,07年更换电源风扇,08年上半年分别更换了主板,硬盘,下面年,在内存最低价的时候,99块包快递买了DDR2 667的内存条一根,更新了老内存。09年再次更换了电源风扇,10年由于换地方住,搬家,桌子太靠墙,CRT大屁股显示器放不下,买了优派的22寸液晶显示器,鸟枪换炮了,哈。CRT显示器忍痛割爱以50元的价格卖给了小贩,哎,心痛。接着,鼠标又不灵了,更换了鼠标。从06年坚守到现在的器件只有CPU、主机箱、显卡,光驱、键盘了。

昨天晚上,10点半左右,老爷机又突然间罢工了,顿时,我心急如焚呀,心想即将到来的周末,作为宅男,我不能接受。赶紧调试吧。首先,我以为是天气太热,导致CPU温度过高,激活主板自动保护策略,强制关机的。赶紧把CPU风扇拿出来,放到空调边吹了会,装上去之后,开机,还是不行,主板灯亮了,但是CPU风扇不转,而且,电源也没反映,我愕然了,难道电源又坏了不成?好吧,我认输,周末再说吧,睡觉。

前序:
在老爷机罢工之前,一群内网友求助正则匹配html标签中 table之间的内容,要发现的第一个table之间的内容。字符串大约为

<table class="t1 t2"><tr><td><table class="t1 t2">sssssssssssssssss</table></td></tr><tr>
<td><table class="t1 t2">ss
sssssssssssssss</table></td></tr></table>

同学A提供【<table[\s\S]*</table>】(具体记不清了,大体是这个)

这个正则的匹配结果是不能满足其要求的,很明显,【*】为量词优先,导致贪婪匹配把后面最后一个</table>之前的字符串全部匹配进去了。

见截图:

A同学提供的不严谨的正则表达式

接着,另外以为网友提到,后面加U修饰符。对于这个正则,对于这个要求,加U修饰符能达到目的吗? 答案是可以的。为什么?这种方式可靠吗?有风险吗?有其他方法吗?

先来解释一下U(大写字母U,非小写,小写的是uincode字符模式)是干吗的? 是对“匹配优先量词(贪婪)”与“忽略优先量词(非贪婪)”的取反,也就是说,原来是【*】、【+】的,会按照【*?】、【+?】来执行,如果是【*?】、【+?】的,则按照【*】、【+】来执行,这个修饰符很怪,很乱,如果正则为【\w+】,为了达到“非贪婪匹配”,使用了修饰符U。过几天,你修改了这个正则,修改为【\w+\s+?】,你的本意是在原来的基础上,加上非贪婪的空白字符的匹配,可是,后面的大写字母U修饰符也对你前面的非贪婪取反了,从而,导致匹配错误,所以,建议大家不要使用大写字母的U修饰符,直接在表达式里写好贪婪还是非贪婪。

我的答案是【<table[^>]+?>(?>((?!</table>).(?<!<table))+)</table>】。

我靠,你这这什么玩意?这么乱?这是什么意思?

解释一下:

  1. 【<table[^>]+?>】部分是匹配“<table class=”t1 t2″>”这种table的开头,包括属性的字符串,其中,使用了忽略优先量词(非贪婪),由于其属性不会太多,常规下不超过50个字符串,所以,没有使用固化分组,或者占有优先量词来减少回溯。
  2. 【</table>】(最后面的)是匹配字符串结束的。不用解释了吧。
  3. 【(?>((?!</table>).(?<!<table))+)】这个比较长,主要是匹配<table….到</table>中间的字符串,其中最外面一层【(?>)】是占有优先量词,用来防止不必要的回溯,防止中间超长的字符串造成崩溃。【(?!</table>)】是顺序零宽断言非操作,匹配位置,针对其后面的规则【.】,意思就是说,规则【.】后面不能是字符串“</table>”,同样正则【(?<!<table】是逆序零宽断言非操作,也匹配位置,针对其前面的规则【.】,意思就是说,规则【.】前面不能是字符串“<table…”,这样,两个零宽断言来确定一个字符串,如果是多个字符串,则加上匹配优先量词【+】,但是这个【+】的位置要注意了,不能直接放在【.】的后面,因为要保证每个【.】匹配的字符串,前后的规则都符合两个零宽断言,则需要放在外面,也就是上面所写的那样。

OK,解释完毕,你明白了没?如果没,那是我的错。请留言,批评,我承认错误。(当使用匹配的时候,为了更准确,建议开启不区分大小写模式,以及,多行匹配模式,即修饰符i和修饰符s,小写的s,不是大写的,大写的S是另外的含义<以后写>。)

见截图:

相对比较严谨,比较准确的正则表达式

测试代码:

$str='111<table class="t1 t2"><tr><td><table class="t1 t2">sssssssssssssssss</table></td></tr><tr>
<td><table class="t1 t2">ss
sssssssssssssss</table></td></tr></table>ssss';
preg_match_all('%<table[^>]+?>(?>((?!</table>).(?<!<table))+)</table>%si', $str, $result, PREG_PATTERN_ORDER);
print_r($result[0]);

我的答案可以解决问题吗?可以。准确吗?还行,不太准。为什么?如果table中间有嵌套,则无法正确匹配。如何修复?正则递归匹配。如果递归?

留给你研究吧。CFC4N小试正则表达式里已经介绍了一个关于递归匹配的例子,你可以参考一下。

后续:

老爷机挂了,我一夜没睡着,早上6点10分就醒了,起来之后,捣鼓两下,也没心情了,已经知道大体的症状。手头没工具,也没法测试,算了,索性洗漱去公司。走的早也好,地铁上没多少人,一路不拥挤,唯一的缺点就是没拿到“时代报”。不过到了公司楼下,有派发的,顺手拿了一份。到座位上,心思还在我心爱的老爷机上,也有重新买台新机器的想法,到DELL官方网站上搜了几下,发现价格奇贵,光主机,都4K-5K,看了一会,我就大小这个念头了,作为寄生在这大城市的P民,也别要求太高了。讲究用吧。哎,打算周末冒热抱出去找维修铺,各位,祝福我吧。

PS:下午,拿了早上没看的报纸去厕所,心想,这次终于不干等了,可以打发时间了。当我到厕所里的时候,我震惊了,我靠,马桶上起码5份报纸,而且,还都不一样。汗。