保存网页为单个文件/Save webpages All-in-one file

看到好的网页需要保存到本地的时候,有几种保存方式可选择,保存为网页文件(仅网页)的话,会得到一个html,但是不包含所有的图片等附加文件,保存为网页文件(全部)的话,会得到一个html文件和一个目录,图片等所有附加文件都在目录中,但是要想保存为一个文件并且所有的图片等附加文件还都完整的话,目前只有ie可以实现,那就是保存为mht文件,那么这个mht文件到底是什么呢?

mht其实是一种MHTML文件,MHTML是MIME HTML (Multipurpose Internet Mail Extension HTML)的简称,它的初衷是在邮件中嵌入HTML内容,RFC2557详细描述了文档定义。ie保存的mht文件、html格式的电子邮件、甚至chm文件都采用了MHTML或者相似的技术。

遗憾的是,不同的浏览器对mhtml的支持程度不尽相同,并且不同的浏览器保存的mhtml文件也不一定兼容(本部分内容主要参考自wikipedia):

IE: 自从1999年的ie5就支持保存为mht格式了,但在保存一些复杂页面的时候会出错。 Opera: 自从opera 9.0(build 8264, 发布于2006-3-10)起支持保存mhtml文件。 Firefox: mozilla、firefox系列浏览器到目前为止仍然不直接支持保存mhtml文件功能,虽然个人非常喜欢firefox,但是这的确是个缺点,甚至早在1999年就有人在Bugzilla上提出了这个问题,但没有得到开发人员的足够重视。不过倒是有个插件Mozilla Archive Format地址二)填补了这一空白,但是目前只支持到ff1.5,针对ff2只有一个非官方的build。据说MAF生成的mht文件和微软系列不全兼容。 Safari: 自动2005年4月29日的2.0版本,苹果上的safari支持保存网页为mhtml文件,但是却不支持显示mhtml文件。

由于浏览器对mhtml的支持相当的不统一,mhtml的使用受到了很大的限制,就我个人的感觉,还是以微软系列的mht为主流,甚至kde上还有一款kmhtConvert软件,可以把微软的mht格式转换为kde的mhtml格式——war文件。

由于mhtml的原理,所有支持mhtml的浏览器或插件采用的方式大多是变相的文件打包的方式,也就是相当于把网页完整保存下来,然后把html文件和目录下的图片等其他文件通过某种方法进行大包,并更改html文件中的链接,通过特殊的链接格式引用包中的资源文件。其实,除了mhtml以外,我们还有另外一个选择,那就是data: URI scheme


data: URI scheme和mhtml的不同之处在于,mhtml解决问题的方式是对多个文件打包,而data: URI则是直接把文件的内容包含在地址当中。比如我引用了一个图片文件http://www.fwolf.com/favorite.ico,mhtml的做法是把这个ico文件打包进去,而data: URI则是直接把地址http://www.fwolf.com/favorite.ico替换为诸如data: image/png; base64, iVBORw0KG…..这样的代码,从而实现所有的内容都在一个文件中的目的。

data: URI被大多数浏览器支持,并且语法由RFC2397定义,不同浏览器的处理效果基本一致,当然,和往常一样,不被ie系列支持。不过从我个人来讲,不管是windows平台还是linux平台,我都会选择firefox浏览器,所以可以无视掉ie。

当然data: URI也不是万能药水,它也有自己的优缺点:(本部分参考自wikipedia优点:

  • 增加了web访问的请求次数,比如一个包含2个图片引用的网页文件,一共会产生3次web请求——一次是文件本身,另外两次分别是那两个图片。这样会节省一些网络资源,因为http协议是无状态协议,每次请求都一定的系统开销。
  • 一般的浏览器默认配置都是最多同时使用2个连接访问服务器,所以请求次数的减少也节省了连接资源。有些web服务器从服务端也可以作类似的配置。
  • 浏览器缓存中的文件数目减少了。
  • 在一些访问受限制的场合可以使用,比如一个web界面的超文本编辑器,就可以通过这种方式在编辑区中插入图片。(不明白它在说什么)
  • 我想这同样适用于需要隐藏图片地址的场合,因为图片的地址就是它的数据,而不是到你服务器上某个文件的链接,所以这一点对付图片盗链者来说简直太有效了。
  • 可以避免页面采用https连接,图片等文件采用普通http连接所产生的“本页即包括安全内容,又包含不安全的内容”这样的情况。
  • 最重要的一点,通过它可以实现把网页保存为一个单独的文件 :)。

缺点:

  • 嵌入的内容在客户端要重新进行编码和显示,增加了一点点客户端系统开销。
  • 如果同一项资源被多次引用,也只能重复嵌入它的数据,没有“重复利用”这一说,如果网页中同一个图片被显示了100次的话,数据量的增加是惊人的。
  • 浏览器对URI长度通常都有限制,比如opera限制为4k,不过这只影响你把date: URI写到地址栏或者<a>标记中的情况。
  • 不支持数据压缩,采用base64编码的数据体积会增加1/3,采用url-encode编码的数据体积会增加2倍。不过如果web服务器启用了压缩,这一点的影响就可以抵消了。
  • 不被微软的IE支持。

data: URI的基本使用格式如下:

data:[<MIME-type>][;base64|charset=some_charset],<data>

mime-type是嵌入数据的mime类型,比如png图片就是image/png。 如果后面跟base64,说明后面的data是采用base64方式进行编码的,毕竟数据都需要编码以免和其他的网页内容相冲突。所以如果不采用base64编码,就必须使用urlencode把数据进行转换,在这种情况下可以使用charset=来指定内容的字符集。

下面贴几个data: URI实际应用的例子,先看效果,再贴源码,注意我贴的源码中英文引号会被WP替换为中文引号,另外为了显示方便会增加一些回车换行,如果要复制到本地实验的话最好直接查看本页面的源代码。

首先是一个图片的例子,我的图片也许大了些,不过用来作qq自定义表情发给朋友还是不错滴。 这个图片用来作qq表情传给别人不错哦 注意这可是很“长”的一行哦,源代码:

<img src=”data:image/gif;base64, R0lGODlhIwGVAPecAHmCwQAA/yBKv+P1//z+/7zo/6De/9Hv/1FOhMbj+I2x7SBd1IGAlur4//7/ /0hGh9Tw/+by/HiX3Vh4y3iW28nt/5/e/zVjzUJruzVmzyA9ryA5qzhSteb2//X8//f8/93z/6jh ……(省略若干行) RT3asYpVxKLsEujHwTGxjqh8pTRp1zruHGEQX5CCEZhwgzj2cpSDzCErxThAQq5tfMEk5Sq/B8xX LlF/8FSm/6b5xynaUD3zTFwpa/cixHbu05BWfCcwT+lCfZJPn148nyfniEzRCbSVoKwWMRUWEAA7 ” alt=”这个图片用来作qq表情传给别人不错哦” />

下面是一个在inline css中嵌入背景图的例子: data: URI 源代码:

<span style=”padding-right: 20px; background: url(data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lE QVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQ AAAAAElFTkSuQmCC) top right no-repeat;”> data: URI</span>

再来一个嵌入javascript代码的例子,注意不同于上面两个例子,这次使用的是urlencode编码方式: 点一下这里–>Fwolf作品 源代码:

<script type=”text/javascript” src=”data:text/javascript;charset=utf-8,function%20test_urlencode_js%28%29%7Balert%28%27%E8%BF%99%E7%AE%97%E6%98%AF%E4%B8%80%E7%A7%8D%E5%8A%A0%E5%AF%86%E4%B9%88%EF%BC%9F%27%29%3B%7D”></script> <span onclick=”javascript:test_urlencode_js();”>点一下这里–&gt;Fwolf作品</span>

参考: MHTML on wikipedia Maf extension project for mozilla & firefox Mozilla Archive Format on Firefox Addons RFC2557: MIME Encapsulation of Aggregate Documents, such as HTML (MHTML) Bugzilla: Full rfc2557 MHTML multipart/related support in BROWSER(from 1999) Using HTML in E-mail kmhtConvert data: URI scheme RFC2397: The “data” URL scheme Using Data URLs Effectively with Cascading Style Sheets HTML as media container format

后记 @ 2008-01-12

opera也支持mht格式了,包括它的linux版本,不知道从什么时候开始的。

其实这篇文章在2007年6月就写好了,当时就想着写一个小工具,利用data:URI这个特性,能够把网页都保存到一个文件当中去,然后再发表文章,可这一等就是半年多,终于利用2008年春节的空档时间,把这个工具已经写好了,有兴趣的可以试一试,还不知道我的服务器能否支撑得住呢 :-)。

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处理就不会出现乱码了。