DOMDocument->loadHTML()处理中文的一点问题

DOM是php比较新的xml和html处理类,可以像javascript那样方便的操作DOM树,网上更多的是介绍它处理XML的情况,今天我来介绍一个用它处理html时的中文问题,php版本为5.1.6,所有php代码均为utf8编码。

我要处理的html是使用curl从网页上读取过来的,一个是百度的首页,gb2312字符集,一个是有道的首页,utf8字符集,两者的html头部分分别如下:

<html><head><title>百度一下,你就知道   </title><meta http-equiv=Content-Type content="text/html;charset=gb2312">

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="/pack23501M/index.css" type="text/css"/>
<script type="text/javascript" src="/pack23501M/all.js"></script>
<title>有道</title>

可以看出百度的代码非常不规范,而有道就好多了,这虽然是题外话,其实还是有些关系的,后面会提到。

以上两段html代码,用同样的方式处理结果却不同,比如下面简单的处理(输出网页的title):

$dom = new DOMDocument();
@$dom->loadHTML($html);
echo $dom->getElementsByTagName('title')->item(0)->nodeValue;
...
$html = $dom->saveHTML();

有道的输出结果是正常的,百度却是乱码:

ç™¾åº¦ä¸€ä¸‹ï¼Œä½ å°±çŸ¥é“

由于php文档的loadHTML上说了,DOM内部处理全部都是utf8的,所以除了传入内容要utf8化之外,传入的内容中最好还有声明字符集的html代码:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

注意,这就是DOM处理html和xml最大的不同了,xml一般要求在第一行就显示的声明字符集,而html则灵活得多,可声明可不声明。不过不管输出的内容是正常还是乱码,dom内的nodeValue和最终的输出结果都是一致的,说明dom工作正常,问题就在输入数据上。

于是,针对百度的gb2312网页内容,增加了两项处理,第一项是使用mb_convert_encoding把网页内容由gb2312编码转换为utf8编码,第二项是把html中的:

<meta http-equiv=Content-Type content="text/html;charset=gb2312">

替换成了utf8的:

<meta http-equiv="Content-Type" content="text/html;charset=utf-8">

这样按说应该是可以了,但百度的处理结果仍然是乱码,百思而不得其解,偶然当中发现如果$html的值是这样的话输出是正常的:

$html = mb_convert_encoding('<title>测试test</title>', 'gb2312', 'utf-8');
$html = '<meta http-equiv="Content-Type" content="text/html;charset=gb2312">' . $this->html;

这说明,DOM正确识别了html代码中的Content-Type描述,即使html是gb2312编码的,DOM也能夠自动转换为正确的代码。

现在的情况是这样的:

  • DOM工作正常
  • html已经转换为utf8编码
  • Content-Type描述也已经调整

怎么还是会出问题呢?先看看下面的Content-Type描述代码:

$meta = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';

看清楚喽,如果用$meta直接替换百度html代码中的那句meta,不会生效,仍然乱码;可如果把$meta添加到整个html代码前面,也就是<html>前面,输出就正常了,神奇吧。

于是我就推测,之前百度代码处理乱码的原因,可能是在它的html代码中,meta前面有个含有中文的<title>,DOM在解析到<title>的时候,遇到了非ascii字符,而这时没有解析到<meta>,DOM不知道整个html代码是什么字符集,也就无法正确判断<title>的编码,于是糊里糊涂的进行了错误的字符集转换。

为了证实我的猜测,试着这样处理一下:只修改<meta>,把定义位置放在<title>前面,把缺少的引号加上,但是字符集声明仍然为gb2312,html代码也不进行iconv转换,就像下面这样(注意为gb2312编码):

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=gb2312">
<title>百度一下,你就知道   </title>

执行,输出正常,而且是正常的gb2312编码,没有乱码。所以我的猜测是正确的,关于Content-Type的meta声明一定要放在<title>前面才行。另外上例中如果把nodeValue输出,是utf8编码的,也就是DOM的内部使用编码,说明DOM输入和输出的时候都会进行字符集转换(根据html代码中的字符集声明)。

最后,总结一下,curl读过来的网页数据,全部iconv为utf8编码,然后把声明Content-Type的<meta>替换到紧跟在<head>的位置上,再用DOM处理就不会出现乱码了。

CSS裸奔节之强制脱衣

今天是CSS裸奔节,所谓CSS裸奔,就是把网站的所有css去掉,这样所有的显示格式什么的就都没有了,只剩下html实体显示出来,起源好像是比较无聊的想法,就是剥去css的外衣,让大家看看你的(x)html代码是否工整。

不过今天偶不打算裸奔,而是告诉大家一个可以看到任何网站裸奔形象的方法,参考我以前写的用js丰富你的书签功能,创建一个书签,也可以放到书签工具栏上,内容如下:

javascript:void((function(){var style=document.getElementsByTagName('style');for (i=0; i<style.length; i++)style.item(i).disabled=true;var link = document.getElementsByTagName('link');for (i=0; i<link.length; i++)if ('text/css' == link.item(i).type)link.item(i).disabled=true;var obj = document.getElementsByTagName('*');for (i=0; i<obj.length; i++){obj1 = obj.item(i);if (obj1.attributes.getNamedItem('style')){obj1.attributes.getNamedItem('style').value = '';}}})());

注意一定要是一行才行,作为书签地址就可以了,然后打开任何一个网站,点这个书签,是不是把css全部都脱掉了?懒人也可以直接把这个链接拖到工具栏上使用:

CSS裸奔

公布源代码如下,没有太复杂的地方,有了DOM说明,知道各个对象的方法和属性之后,把stylelink全部禁用,然后把inline的style=""的value全部设置为空串就可以了:

javascript:void((function(){
/* 用style方式引用的css */
var style=document.getElementsByTagName('style');
var s='Style:' + style.length;
for (i=0; i<style.length; i++)
{
    /*s += style.item(i) + '|';*/
    style.item(i).disabled=true;
}
/* 用link方式引用的css */
var link = document.getElementsByTagName('link');
s += 'Link:' + link.length;
for (i=0; i<link.length; i++)
    if ('text/css' == link.item(i).type)
        {
            /*s += link.item(i).href + "|\\n";*/
            link.item(i).disabled=true;
        }
/* 任意对象的style="" */
var obj = document.getElementsByTagName('body').item(0).childNodes;
var obj = document.getElementsByTagName('*');
s += 'Obj:' + obj.length;
for (i=0; i<obj.length; i++)
{   
    obj1 = obj.item(i);
    if (obj1.attributes.getNamedItem('style'))
    {
        s += obj1.nodeName + obj1.attributes.getNamedItem('style').value + "\\n";
        obj1.attributes.getNamedItem('style').value = "";
    }
}
/*alert(s);*/
})());

这个文件删除掉注释以后,可以用下面这个命令转换为一行:

cat css1.js |tr -d \\\\n |sed -r -e 's/$/\\n/' -e 's/\\t//g'

这样就得到了上面那段源代码。

javascript程序调试比较麻烦,以前我的方法是在vim中编辑,然后用命令转化为一行,再手工粘贴到书签的地址中,执行,然后有错误再回到编辑。不过今天发现一个好方法,其实也就是脑子转个弯儿的事,创建一个html文件,在里面把javascript代码调试好,然后再转换为可以用在书签中的形式。

另外Firefox2的Error Console虽然能够监测js错误,但是如果不小心点了上面的“Messages”,就会发现错误不显示了,需要再点“All”查看所有信息,今天为这是纳闷了半天,因为切换显示错误分类的时候没有任何征兆。

参考: