raksmart活动促销

分享

写回答

发帖

[经验] 网页的 gzip 压缩和 HM 主机问题 分享

HostMonster HostMonster 6293 人阅读 | 5 人回复

发表于 2011-1-30 12:02:11 | 显示全部楼层 |阅读模式

分享两篇自己的文章,关于进行网页 gzip 压缩的内容,含在 HM 虚拟主机上启动 gzip 的一些问题。在 HM 上折腾了很久 gzip 的事,有过弯路,愿自己的经验能帮到别人。

---------------

第一篇 使用 gzip 或 deflate 压缩网页内容

gzip 压缩基础知识,mod_deflate 使用,以 wordpress 为例(不局限于 wp),原文:http://codingdao.com/wp/post/gzip-deflate-compress-web-content/

----------------

使用 gzip 或 deflate 压缩网页内容

使用 gzip 或 deflate 压缩网页内容,可以达到加快网页传输速度的效果,这对于文本性质的数据 (html, xml, txt, js,css) 很明显。但副作用是增加了服务器的处理器和内存开销,对于访问压力大的网站还得做静态与动态内容的分离与 cache。

缘由

在 HTTP (RFC2616) 中请求消息中有一个头域叫做 Accept-Encoding,它表示浏览器可以接受的数据编码方式,这里Encoding 不是指媒体编码(媒体编码由请求消息的 Accept 和响应消息的 Content-Type负责),而是指数据传输时采用的压缩编码,通常的取值是 gzip,deflate;相应地,在 HTTP 响应消息中有一个头域叫做Content-Encoding,它表示 HTTP 响应消息体数据采用的压缩编码。

有两种常用的 Content-Encoding:gzip 和 deflate。如果指定 gzip,表示 HTTP 响应消息体是按 gzip 方式压缩的,我曾经用 zlib 写过一个 HTTP 协议测试器,发现这些消息体就是 .gz 文件。

而如果 Content-Encoding 指定 deflate,则情况有点复杂:deflate (RFC1951)是一种基本的压缩算法,gzip (RFC1952) 和 zlib (RFC1950) 格式都是基于 deflate压缩数据的包裹。RFC2616 中说当 Content-Encoding 指定 deflate 时,应该以 zlib 方式传输数据。而且zlib 格式比 gzip 更适合作为网络传输压缩编码,原因是:更短的头部、更好的流操作、和更快的校验(zlib的设计目标就是作为传输压缩编码,而 gzip 更多的用在文件压缩编码)。但各浏览器对 Content-Encoding=deflate的处理不一致,所以为了兼容性还是用 Content-Encoding=gzip 的压缩方法多些。

参考:zlib 使用介绍

这是我用 Firefox 的 HttpFox 抓取的访问 Google 主页的 HTTP 消息,它的主页就是用 gzip 压缩的。

判断服务器主机支持的压缩

网上流传一种说法用 PHP 函数 phpinfo() 查看 _SERVER["HTTP_ACCEPT_ENCODING"]这项的值,来判断服务器主机支持的压缩,这是不对的。_SERVER["HTTP_ACCEPT_ENCODING"] 是判断浏览器当前请求中的Accept-Encoding 头域的值,表示的是浏览器的能力而非服务器的(参考:$_SERVER),之所以能显示 gzip 或 deflate 是因为主流浏览器都支持至少一种压缩编码。例如,自建的 Apache 服务在没有启用 mod_deflate 时用 Firefox3 浏览也会在 phpinfo() 页面返回 gzip,deflate。

对于 PHP 的 zlib 模块可以用 if (extension_loaded('zlib')) 代码测试;对于 Apache 可以用 <IfModule mod_deflate.c> 测试。

使用 Apache 的 mod_deflate 压缩

参考:Apache Module mod_deflate

在 Apache 配置文件 (normally httpd.conf) 或目录级配置文件 (normally .htaccess) 中加入配置,如下:

全局配置文件 (httpd.conf)
  1. # 加载 mod_deflate 模块
  2. LoadModule deflate_module modules/mod_deflate.so

  3. # 指定目录下的文件使用压缩
  4. <Directory "/www-root/dir">

  5. # 压缩率,从 最小压缩率 = 1 到 最高压缩率 = 9
  6. DeflateCompressionLevel 5

  7. SetOutputFilter DEFLATE

  8. # 压缩日志
  9. DeflateFilterNote Input instream
  10. DeflateFilterNote Output outstream
  11. DeflateFilterNote Ratio ratio
  12. LogFormat '"%r" %{outstream}n/%{instream}n (%{ratio}n%%)' deflate
  13. CustomLog logs/deflate_log.log deflate

  14. </Directory>
复制代码
目录级配置文件 (.htaccess)

要启用 .htaccess,一定要在全局配置文件中将相应目录的 AllowOverride赋值为“可覆盖全局设置”的值,如:AllowOverride FileInfo 或 AllowOverride All。.htaccess的配置,对这个目录下的所有 子目录和文件 起作用,子目录下仍可以有 .htaccess 来覆盖上级目录的设置,以此类推。.htaccess中不能有 <Directory> 指示。

(1). 方式一,根据 MIME 类型使用压缩
  1. <IfModule mod_deflate.c>

  2. # 根据 MIME 类型使用压缩
  3. AddOutputFilterByType DEFLATE text/plain
  4. AddOutputFilterByType DEFLATE text/html
  5. AddOutputFilterByType DEFLATE text/xml
  6. AddOutputFilterByType DEFLATE text/css
  7. AddOutputFilterByType DEFLATE application/xml
  8. AddOutputFilterByType DEFLATE application/xhtml+xml
  9. AddOutputFilterByType DEFLATE application/rss+xml
  10. AddOutputFilterByType DEFLATE application/javascript
  11. AddOutputFilterByType DEFLATE application/x-javascript
  12. AddOutputFilterByType DEFLATE application/x-httpd-php
  13. AddOutputFilterByType DEFLATE application/x-httpd-fastphp
  14. AddOutputFilterByType DEFLATE image/svg+xml

  15. </IfModule>
复制代码
(2). 方式二,除过排除类型文件 和 特殊的浏览器外,对其它文件均启动压缩
  1. <IfModule mod_deflate.c>

  2. # 启动压缩
  3. SetOutputFilter DEFLATE

  4. # 解决一些对压缩处理有问题的浏览器

  5. # Netscape 4.x 问题
  6. BrowserMatch ^Mozilla/4 gzip-only-text/html

  7. # Netscape 4.06-4.08 问题
  8. BrowserMatch ^Mozilla/4\.0[678] no-gzip

  9. # MSIE 没有问题,放过
  10. BrowserMatch \bMSIE !no-gzip !gzip-only-text/html

  11. # 由于 Apache 2.0.48 中 mod_setenvif 的 bug,上面的表达式
  12. # 可能不工作,此时可以使用下面的
  13. # BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html

  14. # 不压缩图像文件
  15. SetEnvIfNoCase Request_URI \
  16. \.(?:gif|jpe?g|png)$ no-gzip dont-vary

  17. # 让袋里服务器不转发正确的头部
  18. Header append Vary User-Agent env=!dont-vary

  19. </IfModule>
复制代码
使用 PHP 的 zlib 库压缩

上面走的是 Apache→mod_deflate→zlib 的路线,这里走的是 PHP→zlib 的路线。例如,对于 WordPress 可以使用这种压缩方法。

参考:Output Compression

在 WordPress 目录下的 index.php 中加入:
  1. // 判断浏览器是否支持 gzip
  2. if (ereg('gzip', $_SERVER['HTTP_ACCEPT_ENCODING']))
  3. {
  4.         // 绕过 URI 目录 bypass-dir 下的内容不压缩
  5.         $bypass = '/bypass-dir';
  6.         if (substr($_SERVER['REQUEST_URI'], 0, strlen($bypass)) != $bypass)
  7.                 ob_start('ob_gzhandler');
  8. }
复制代码
这个 ob_start('ob_gzhandler') 只针对 text/html 进行压缩,而 css、js 都没有压缩。可以使用 万戈 写的 gzip.php 来压缩 css、js,其实现原理是用 mod_rewrite 将 css、js 请求重定向到 gzip.php,再用 gzip.php 读取响应文件进行压缩。步骤如下:

1. 在 WordPress 目录下的 .htaccess 中加重定向规则:
  1. RewriteRule (.*.css$|.*.js$) gzip.php?$1 [L]
复制代码
2. 保存下面代码为 gzip.php,放到 WordPress 目录下:
  1. <?php
  2. // FROM: http://wange.im/turn-on-gzip-speed-up-wordpress.html

  3. define('ABSPATH', dirname(__FILE__).'/');

  4. // Gzip 压缩开关
  5. $cache = true;

  6. // 存放gz文件的目录,确保可写
  7. $cachedir = 'wp-cache/';

  8. $gzip = strstr($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip');
  9. $deflate = strstr($_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate');
  10. $encoding = $gzip ? 'gzip' : ($deflate ? 'deflate' : 'none');

  11. if (!isset($_SERVER['QUERY_STRING'])) exit();

  12. $key = array_shift(explode('?', $_SERVER['QUERY_STRING']));
  13. $key = str_replace('../','',$key);

  14. $filename = ABSPATH.$key;

  15. $symbol = '^';

  16. $rel_path = str_replace(ABSPATH,'',dirname($filename));
  17. $namespace = str_replace('/',$symbol,$rel_path);

  18. // 生成gz文件路径
  19. $cache_filename = ABSPATH.$cachedir.$namespace.$symbol.basename($filename).'.gz';

  20. // 默认的类型信息
  21. $type = "Content-type: text/html";

  22. // 根据后缀判断文件类型信息
  23. $ext = array_pop(explode('.', $filename));
  24. switch ($ext)
  25. {
  26.         case 'css':
  27.                 $type = "Content-type: text/css";
  28.                 break;
  29.         case 'js':
  30.                 $type = "Content-type: text/javascript";
  31.                 break;
  32.         default:
  33.                 exit();
  34. }

  35. if ($cache)
  36. {
  37.         // 假如存在gz文件
  38.         if (file_exists($cache_filename))
  39.         {

  40.                 $mtime = filemtime($cache_filename);
  41.                 $gmt_mtime = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';

  42.                 // 浏览器cache中的文件修改日期是否一致,将返回304
  43.                 if ((isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
  44.                         array_shift(explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE'])) ==  $gmt_mtime))
  45.                 {
  46.                         header ("HTTP/1.1 304 Not Modified");
  47.                         header("Expires: ");
  48.                         header("Cache-Control: ");
  49.                         header("Pragma: ");
  50.                         header($type);
  51.                         header("Tips: Cache Not Modified (Gzip)");
  52.                         header ('Content-Length: 0');
  53.                 }
  54.                 // 读取gz文件输出
  55.                 else
  56.                 {
  57.                         $content = file_get_contents($cache_filename);
  58.                         header("Last-Modified:" . $gmt_mtime);
  59.                         header("Expires: ");
  60.                         header("Cache-Control: ");
  61.                         header("Pragma: ");
  62.                         header($type);
  63.                         header("Tips: Normal Respond (Gzip)");
  64.                         header("Content-Encoding: gzip");
  65.                         echo $content;
  66.                 }
  67.         }
  68.         // 没有对应的gz文件
  69.         else if (file_exists($filename))
  70.         {
  71.                 $mtime = mktime();
  72.                 $gmt_mtime = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';

  73.                 // 读取文件
  74.                 $content = file_get_contents($filename);
  75.                 // 压缩文件内容
  76.                 $content = gzencode($content, 9, $gzip ? FORCE_GZIP : FORCE_DEFLATE);

  77.                 header("Last-Modified:" . $gmt_mtime);
  78.                 header("Expires: ");
  79.                 header("Cache-Control: ");
  80.                 header("Pragma: ");
  81.                 header($type);
  82.                 header("Tips: Build Gzip File (Gzip)");
  83.                 header ("Content-Encoding: " . $encoding);
  84.                 header ('Content-Length: ' . strlen($content));
  85.                 echo $content;

  86.                 // 写入gz文件,供下次使用
  87.                 if ($fp = fopen($cache_filename, 'w'))
  88.                 {
  89.                         fwrite($fp, $content);
  90.                         fclose($fp);
  91.                 }
  92.         }
  93.         else
  94.                 header("HTTP/1.0 404 Not Found");
  95. }
  96. // 处理不使用Gzip模式下的输出。原理基本同上
  97. else
  98. {
  99.         if (file_exists($filename))
  100.         {
  101.                 $mtime = filemtime($filename);
  102.                 $gmt_mtime = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';

  103.                 if ((isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
  104.                         array_shift(explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE'])) ==  $gmt_mtime))
  105.                 {
  106.                         header ("HTTP/1.1 304 Not Modified");
  107.                         header("Expires: ");
  108.                         header("Cache-Control: ");
  109.                         header("Pragma: ");
  110.                         header($type);
  111.                         header("Tips: Cache Not Modified");
  112.                         header ('Content-Length: 0');
  113.                 }
  114.                 else
  115.                 {
  116.                         header("Last-Modified:" . $gmt_mtime);
  117.                         header("Expires: ");
  118.                         header("Cache-Control: ");
  119.                         header("Pragma: ");
  120.                         header($type);
  121.                         header("Tips: Normal Respond");
  122.                         $content = readfile($filename);
  123.                         echo $content;
  124.                 }
  125.         }
  126.         else
  127.                 header("HTTP/1.0 404 Not Found");
  128. }

  129. ?>
复制代码
其它压缩方法

WordPress 还可以使用 gzippy 插件进行压缩。

测试页面压缩效率的在线服务

Port80 Software httpZip
WhatsMyIP HTTP Compression Test
站长工具 网页GZIP压缩检测

回答|共 5 个

silon212

发表于 2011-1-30 12:17:46 | 显示全部楼层

第二篇 再谈网页的 gzip 压缩

PHP ob_gzhandler、zlib.output_compression 压缩方法,HM 主机 gzip 压缩问题,压缩 .css、.js 文件,缓存压缩后的文件,原文:http://codingdao.com/wp/post/gzip-compress-web-extra/

----------------

前篇:使用 gzip 或 deflate 压缩网页内容

因为前篇中提到的 mod_deflate 网页压缩的方法,在某些主机服务商的配置中没有完全的控制权限,如我的 HostMonster (HM) 虚拟主机(非专用 IP),最近又搞了搞 gzip 网页压缩的事情,写下总结,当做前篇的补充和深入。

此篇的主要内容有:

(1). 介绍和实测前篇中没有详细说明的几种 gzip 压缩网页的方法。

(2). 针对在 HM 虚拟主机(非专用 IP)上做 gzip 网页压缩,进行说明。

(3). 记录自己的一个缓存 gzip 压缩网页方案:mod_rewrite 重定向 css/js + gzip 压缩脚本 (Python CGI)

另外几种 gzip 压缩网页的方法

用 PHP ob_gzhandler 压缩

这个方法在前篇中也提到过:用它可以 gzip 压缩 PHP 的动态页面,通过 mod_rewrite + gzip 压缩脚本,也可以实现压缩 .css、.js 等文本文件。

这里是另一种利用 ob_gzhandler 压缩 .css、.js 的方法。

    * ob_gzhandler 压缩 PHP 页面配置步骤:

      (1). 在 php.ini 中设置 output_handler = ob_gzhandler

      (2). 在 .htaccess 中加入 php_value output_handler ob_gzhandler(HM 虚拟主机不允许)

      (3). 在 php 脚本前加入 ob_start('ob_gzhandler')
   
    * 压缩 .css、.js。在 .css 的头部加入:
  1. <?php
  2. if (extension_loaded('zlib'))
  3.         ob_start('ob_gzhandler');
  4. header("Content-Type: text/css");
  5. ?>
复制代码
对于 .js 将上面的 MIME 类型换成 application/javascript。

在 .css、.js 的尾部加入:
  1. <?php
  2. if (extension_loaded('zlib'))
  3.         ob_end_flush();
  4. ?>
复制代码
更改 .css、.js 文件名为 .css.php、.js.php,并更改引用 css、javascript 脚本的代码,如:
  1. <script src="a.js.php" type="text/javascript"></script>
  2. <link href="a.css.php" rel="stylesheet" type="text/css" />
复制代码
用 PHP 配置中的 zlib.output_compression

在 php.ini 中设置:

output_buffering = Off
output_handler =
zlib.output_compression = On

注意:

    * 对于 zlib.output_compression 和 ob_gzhandler 两种压缩方法,两者应该只选其中一种方法。当启动 zlib.output_compression 压缩时,应关闭每个应用自己的 gzip 压缩,如 Discuz、WordPress 的 gzip 功能。
   
    * 对于虚拟主机用户,php.ini 的位置在 web 主目录下,如 HM 的 public_html,如果没有可以登录 cPanel 后台,在 PHP 配置中产生。

      HM 虚拟主机默认的 php.ini 配置不会向下级目录传递(表示每个子目录都由自己的 php.ini 来控制),可以在 cPanel 的 PHP 配置中将其改为使用单一的 Single php.ini(表示 web 根目录下的 php.ini 将应用到所有子目录)。

    * 可以在 .htaccess 中配置 PHP,代替上面的在 php.ini 中的配置 (mod_php),如下:

php_flag zlib.output_compression On
php_value zlib.output_compression_level 8

    但是在 HM 虚拟主机上 mod_php 因为安全原因被禁止了,所以不行。

    * 压缩静态页面。在 .htaccess 中添加如下 Handler,让 Apache 将 .html 当做 PHP 脚本编译,这会增加服务器处理静态页面的开销。

      AddHandler x-httpd-php .html .htm

HostMonster 虚拟主机的 gzip 网页压缩

最近再次做 gzip 网页压缩的一个原因是:以前配好的 HM 主机 gzip 网页压缩突然失效了,而中途没有更改任何的服务配置文件。

为此,我问了 HM 的服务人员,他说:HM 的 gzip 网页压缩不用用户操心,HM 会视服务器 CPU 负载情况来启动 gzip 压缩,如果 CPU 负载过高,就不会启动压缩了,这是 HM 唯一的 gzip 压缩策略(原话)。

后来,我的网页压缩功能又正常了,看来确实如 HM 服务人员所说,是 HM 主机自动进行 gzip 压缩的,并不是自己配置的 .htaccess 中的 mod_deflate 起作用,现在将自己的 mod_deflate 配置删掉仍然会有 gzip 压缩。

以下是我观察的 HM 虚拟主机的 gzip 压缩策略总结:

    * 静态页面:没有 Vary: Accept-Encoding 和 Content-Encoding: gzip,即不启用 gzip 压缩,但用 304 进行客户端缓存。
    * 动态页面:有 Vary: Accept-Encoding 和 Content-Encoding: gzip,但只对页面进行压缩,对 css、javascript、xml 等其它文本文件不进行压缩。

Vary: Accept-Encoding 的作用,是让 HM 的前端服务器缓存页面返回时根据客户端的 Accept-Encoding 来决定是否传回压缩的数据。

我的 gzip 压缩方案

我用这个方法来解决压缩 .css、.js 等文件的问题。

压缩的开销问题

为什么 HM 会根据 CPU 负载来启动压缩?为什么会有 Vary: Accept-Encoding?其实大家都明白,对每次请求都做 gzip 压缩确实挺耗 CPU,所以和 gzip 压缩相比,对于提高整个 Web 系统的性能而言,更重要的是 cache,各种层次的 cache。

比如上文提到的用 AddHandler x-httpd-php .html 的方法让服务器把静态页面当做动态页面,从而进行压缩后传输,这是最不可取的,本来静态页面可用 304 Not Modified 进行客户端缓存,但是用了此压缩方法后反而会传输更多的数据。

所以为了配合 gzip 压缩,较好的方法是将之前压缩后的数据保存起来 (cache),以后请求该资源时,让服务器直接返回缓存的压缩数据即可。这种方法对 .css、.js、.xml 等静态文件特别适用。

我的方法是用 mod_rewrite 重定向 .css、.js 文件到对应的 .gz 文件来完成上述压缩缓存功能,步骤如下:

1. 配置 .htaccess

在需要压缩网页的目录下,配置 .htaccess,如下:
  1. # gzip 压缩 css/js cache BEGIN

  2. # 针对 .js.gz 设定 MIME 和 Encoding
  3. <Files *.js.gz>
  4.         AddEncoding x-gzip .js
  5.         ForceType application/javascript
  6. </Files>
  7. <Files *.css.gz>
  8.         AddEncoding x-gzip .css
  9.         ForceType text/css
  10. </Files>

  11. <IfModule mod_rewrite.c>
  12. RewriteEngine On

  13. # 要启动压缩的目录,如 WordPress 目录 /wp/
  14. RewriteBase /wp/

  15. # 如果 .css、.js 已压缩的缓存 .gz 文件存在,则重定向到 .gz
  16. # 注意使用 QSA,表示重定向时包含 URL 查询串
  17. RewriteCond %{HTTP:Accept-Encoding} gzip
  18. RewriteCond %{REQUEST_FILENAME}.gz -f
  19. RewriteRule ^(.*\.css|.*\.js)$ $1.gz [L,QSA,NC]

  20. # 如果 .css、.js 对应的 .gz 不存在,并且浏览器支持 gzip
  21. # 则交给 mygzip.py 处理请求
  22. RewriteCond %{HTTP:Accept-Encoding} gzip
  23. RewriteCond %{REQUEST_FILENAME} (\.css|\.js)$ [NC]
  24. RewriteCond %{REQUEST_FILENAME}.gz !-f
  25. RewriteRule ^(.*\.css|.*\.js)$ /cgi-bin/mygzip.py [L,QSA,NC]

  26. </IfModule>
  27. # gzip 压缩 css/js cache END
复制代码
为什么要有 <Files>?因为重定向到 *.css.gz 后,对于 .gz 文件,服务器返回默认的 MIME 和 Encoding 是:

Content-Type: application/x-gzip
没有 Content-Encoding

ForceType text/css 的作用是让 MIME 类型变为 Content-Type: text/css。

AddEncoding x-gzip .css 的作用是增加 Content-Encoding: gzip。使用 AddEncoding x-gzip .gz 也可以达到同样效果,但使用 AddEncoding x-gzip .css.gz 不行。

而用 <Files> 指示是让 ForceType 和 AddEncoding 命令约束在一种文件扩展名上,即 *.css.gz。比如,此时浏览器请求 a.gz 和 a.css.gz,两者会有不一样的行为,前者的 MIME 仍然是默认的 application/x-gzip,浏览器一般的处理是提示下载;而后者是 text/css 类型,直接会在浏览器中显示文本。

潜在的问题:因为使用 AddEncoding,可能导致一些早期不支持 gzip 压缩的浏览器在浏览网页时出现问题。

2. 准备 CGI 脚本 mygzip.py

将 mygzip.py 保存到上述 .htaccess 中提及的位置,如 /cgi-bin/mygzip.py。

要使用 Python CGI 脚本,设置 Apache 配置文件或 .htaccess:

AddHandler cgi-script .py .pyc

并注意保存 CGI 脚本目录的权限,至少应该有 Options ExecCGI。一般情况下对于目录 /cgi-bin,主机服务商都已设置好 CGI 目录配置。

mygzip.py 的代码如下,.py 脚本应使用 #coding=utf-8 指示的字符集编码保存(因为 Python 脚本默认编码为 ASCII,但脚本中有中文注释):
  1. #!/usr/bin/env python
  2. #coding=utf-8

  3. ##########
  4. #
  5. # 本 cgi 脚本使用 gzip 压缩服务端文件,如 .css .js 等,并将压缩内容传回浏览器
  6. # 可以使用 .htaccess 缓存压缩后的 .gz 文件,以便之后请求这些文件时,不再调用本脚本进行压缩
  7. #
  8. # 执行本 cgi 脚本的前置条件:
  9. # 浏览器支持 gzip 压缩,即 Accept-Encoding 中含 gzip
  10. # 来自于对 .css .js 等文件请求的重定向,原始文件由服务器变量 REQUEST_URI REDIRECT_URL 等推出
  11. #
  12. # CAUTION:
  13. # 本脚本不能命名为 gzip.py,因为 gzip 库的名字为这个,如果命名为 gzip.py,import gzip 会引起
  14. # 递归导入自己,而不是 gzip 库
  15. #
  16. # by breaker.zy@gmail.com, 2010-12
  17. #
  18. ##########

  19. import datetime
  20. import sys
  21. import os.path
  22. import cgi
  23. import gzip

  24. O_BINARY = 0
  25. O_TEXT = 1
  26. if sys.platform == 'win32' :
  27.     import msvcrt
  28.     O_BINARY = os.O_BINARY
  29.     O_TEXT = os.O_TEXT

  30. MIME_EXTS = {'.css' : 'text/css', '.js' : 'text/javascript'}
  31. LOG_NAME = 'mygzip.log'
  32. LOG_SIZE_LIMIT = 1 << 20

  33. # 出错页
  34. def errpage(stat_code, stat_desc, content) :
  35.     page = '''Status: %s %s
  36. Content-Type: text/plain

  37. %s''' % (stat_code, stat_desc, content)
  38.     print page

  39. # 设置文件模式,只对 Windows 有效,Unix 不区别 'w' 和 'wb' 模式
  40. def setfmode(fd, mode) :
  41.     if sys.platform == 'win32' :
  42.         fd.flush()
  43.         msvcrt.setmode(fd.fileno(), mode)

  44. # path.join() 在 Windows 下不适合
  45. def myjoin(dir_name, file_name) :
  46.     if dir_name[-1] != '/' and file_name[0] != '/' :
  47.         full_name = dir_name + '/' + file_name
  48.     else :
  49.         full_name = dir_name + file_name
  50.     return full_name

  51. def main() :
  52.     fname_old = myjoin(os.environ['DOCUMENT_ROOT'], os.environ['REDIRECT_URL'])
  53.     now = datetime.datetime.now()

  54.     # 打开日志
  55.     logsize = 0
  56.     try :
  57.         logsize = os.path.getsize(LOG_NAME)
  58.     except Exception, ex :
  59.         pass

  60.     if logsize > LOG_SIZE_LIMIT :
  61.         f_log = open(LOG_NAME, 'w')
  62.     else :
  63.         f_log = open(LOG_NAME, 'a')

  64.     # 将要产生的 gzip 文件全路径
  65.     fpath_gz = fname_old + '.gz'

  66.     # 读取原始文件
  67.     try :
  68.         f_old = open(fname_old, 'rb')
  69.     except IOError, ex :
  70.         errpage('404', 'Not Found', 'open() error: %s' % str(ex))
  71.         exit(-1)

  72.     # 保存原来的工作目录
  73.     old_wd = os.getcwd()
  74.     # 进入原始文件所在的目录,这样压缩可以直接使用文件名,而不用路径,.gz 文件中也不会有目录结构
  75.     wd = os.path.dirname(fname_old)
  76.     os.chdir(wd)
  77.     fname_gz = os.path.basename(fpath_gz)

  78.     f_log.write('%s %s\n' % (now.strftime('%Y-%m-%d %H:%M:%S'), fpath_gz))

  79.     # gzip 写入压缩文件
  80.     f_gz = gzip.open(fname_gz, 'wb')
  81.     f_gz.writelines(f_old)
  82.     f_gz.close()
  83.     f_old.close()

  84.     # 恢复原来的工作目录
  85.     os.chdir(old_wd)

  86.     # 计算 MIME 类型
  87.     mimetype = 'text/plain'

  88.     for ext in MIME_EXTS.keys() :
  89.         if fname_old.endswith(ext) :
  90.             mimetype = MIME_EXTS[ext]
  91.             break

  92.     print 'Content-Type:', mimetype
  93.     print 'Content-Encoding: gzip'
  94.     print 'Accept-Ranges: bytes'
  95.     print 'Content-Length: %d' % os.path.getsize(fpath_gz)
  96.     print ''
  97.     f_gz = open(fpath_gz, 'rb')

  98.     # 需将 Windows 的 stdout 设为 binary 模式,因为 Windows python stdout 默认
  99.     # 的文本模式会转换 \n => \r\n,而输出的 .gz 是二进制文件,不能改变其任何字节
  100.     setfmode(sys.stdout, O_BINARY)
  101.     sys.stdout.writelines(f_gz)
  102.     setfmode(sys.stdout, O_TEXT)
  103.     f_gz.close()

  104.     f_log.close()

  105. if __name__ == '__main__' :
  106.     main()
复制代码
HM 虚拟主机使用 Python 2.4。

使用 mygzip.py 的 gzip 压缩

配置好后,第一次正常浏览网页时,mygzip.py 将压缩该网页引用的所有 .css、.js 文件,并保存到同目录同名带 .gz 后缀的文件中。第一次以后访问同一个页面,便不再执行 mygzip.py,只有 mod_rewrite 负责传回 .gz 文件,直到删除相应的 .gz 文件。

可以查看 mygzip.py 同目录下的日志 mygzip.log,这里保存所有压缩过的文件的路径。

对于更改后的 .css、.js 文件,需要将其之前对应的 .css、.js 手工删除,浏览网页时,再次让 mygzip.py 产生新的 .gz 文件。

结束

最近试运行了几天 mygzip.py 效果还行。也有一些其它工作可以配合这种压缩方案去做,比如将缓存的 .gz 保存到内存盘改进性能,根据日志 mygzip.log 进行缓存的 .gz 文件的清理等。

开心私塾

发表于 2011-1-30 12:40:36 | 显示全部楼层

恩 写的不错,支持一下

silon212

发表于 2011-2-10 17:13:01 | 显示全部楼层

对之前的 mygzip.py (CGI) 的使用做一下更新

(1). 之前的 mygzip.py 有点小问题,如果是 HM 主机用的是 py2.4 则没有全局的 exit() 函数,所以要把 exit(-1) 改成 sys.exit(-1),当时开发时在 py2.6 下,所以没发现这个 BUG。

(2). 写了一个 clean_mygzip.py,它是一个清理 mygzip.py 的压缩日志 mygzip.log 的程序,配合 mygzip.py 起来很好。

功能是根据“压缩的时间范围条件”和“压缩后的 gzip 文件名模式”匹配日志 mygzip.log 中记录的 .gz 文件路径,然后删除这些缓存的 .gz 文件,并清理日志中对应的行。

当更新 .css、.js 后,可以用 clean_mygzip.py 清理以前经 mygzip.py 压缩后缓存的 .css.gz、.js.gz 文件。也可用 cron 定时执行 clean_mygzip.py

其实,用 find 命令的 -iname 和 -newer 选项也可以完成 clean_mygzip.py 的批量删除缓存 .gz 的作用,但是考虑 web 服务目录下可能有除 mygzip.py 生成的 .gz 之外的其它的 .gz 文件,所以最好还是读取 mygzip.py 的压缩日志,根据日志的记录来删除缓存的 .gz。

使用示例:
  1. # 最简单的使用方法:删除所有 mygzip.log 中记录的 .gz 文件,并清空日志
  2. clean_mygzip.py -l mygzip.log

  3. # 删除 mygzip.log 中所有匹配文件名 index.css 或 index.js 的 .gz 文件,如 index.css.gz
  4. clean_mygzip.py -f "index\.(css|js)" -l mygzip.log

  5. # 删除 mygzip.log 中压缩时间从 2011-01-27 21:24:57 到现在的 .gz 文件
  6. clean_mygzip.py -t 2011/1/27@21:24:57- -l /path/to/mygzip.log
复制代码
clean_mygzip.py 代码如下:
  1. #!/usr/bin/env python
  2. #coding=utf-8

  3. ##########
  4. #
  5. # 本脚本是配合 mygzip.py CGI 脚本的程序
  6. # 本脚本读取 mygzip.py 生成的 gzip 压缩日志文件 mygzip.log,根据指定时间和 gzip
  7. # 路径名条件,删除日志中匹配的 gzip 文件,并清理日志中的匹配行
  8. #
  9. # 使用示例:
  10. # clean_mygzip.py -f "index\.(css|js)" -t 2011/1/27@21:24:57- -l /path/to/mygzip.log
  11. # 表示删除 mygzip.log 中记录的 gzip 压缩时间从 2011-01-27 21:24:57 到现在的 gzip 文件,
  12. # 并且这些 gzip 文件名为 index.css.gz 或 index.js.gz;删除掉 gzip 文件后,也会清理日志中
  13. # 的对应行
  14. #
  15. # -t 指定的时间格式为 'BEG-END',BEG 和 END 的格式都为 'YYYY/MM/DD@HH:MM:SS'
  16. #    BEG 为开始时间,如果省略 BEG,取开始时间为 1900/01/01@00:00:00
  17. #    END 为结束时间,如果省略 END,取结束时间为当前时间(web 服务器)
  18. # -f 指定的 gzip 文件路径正则表达式,如果其中有空格字符,需用 "" 括起来(不要用 '' 括起来)
  19. #    如 -f "index style\.css"
  20. #
  21. # 如果省略 -t,表示对所有的压缩时间都会清理,是否清理由 -f 指定的文件名模式(正则)决定
  22. # 如果省略 -f,表示对所有的 gzip 文件名都会清理,是否清理由 -t 指定的时间范围决定
  23. # -t、-f 都省略,表示将删除日志中所有行对应的 gzip 文件,日志也将会清空
  24. #
  25. # 文件共享操作问题:
  26. # 因为 mygzip.log 日志可能正在由 mygzip.py CGI 脚本访问,并且
  27. # 需要删除的 gzip 文件,可能正在由 web 服务访问
  28. # 如果删除 gzip 文件失败,会将删除失败的日志行写入新的日志文件:和 -l 指定的日志文件同目录同名带 .err 的文件
  29. # 如果删除原 -l 日志文件失败,则保留处理后的临时日志文件,此后可手工重命名
  30. #
  31. # by breaker.zy@gmail.com, 2011-01
  32. #
  33. ##########

  34. import sys
  35. import os.path
  36. import datetime
  37. import time
  38. import getopt
  39. import re
  40. import random

  41. SCRIPT_NAME = ''
  42. GZIP_PATTERN = ''
  43. GZIP_LOG = ''

  44. NOW = datetime.datetime.now()
  45. TIME_BEGIN = None
  46. TIME_END = None
  47. DEF_TIME_BEGIN = datetime.datetime(1900, 1, 1)
  48. DEF_TIME_END = NOW

  49. STRTIME_END = 19
  50. ERR_LOG_SUFFIX = '.err'

  51. def usage() :

  52.     print 'usage:'
  53.     print SCRIPT_NAME, '-t TIME-RANGE -f GZIP-FILE-REGEX -l MYGZIP.log\n'

  54.     print "  all gzip files matched by -t AND -f condition in MYGZIP.log will be deleted, and the matched line in log file will be removed.\n"

  55.     print "-t    give time range, format is 'YYYY/MM/DD@HH:MM:SS-YYYY/MM/DD@HH:MM:SS'"
  56.     print "      if begin time omited, it uses '%s'" % datetime.datetime(1900,1,1).strftime('%Y/%m/%d@%H:%M:%S')
  57.     print "      if end time omited, it uses Current Time."
  58.     print "      if -t omited, any time is accepted.\n"

  59.     print "-f    give the gzip file path pattern."
  60.     print "      it uses regular expression to match file path."
  61.     print "      if -f omited, any gzip file path is matched.\n"

  62.     print "-l    give the mygzip generated log file path."
  63.     print "      -l is mandatory."

  64. # 解决 datetime.strptime() 在 Python 2.5 之后才可用的问题
  65. def strptime_(strdate, datefmt) :
  66.     return datetime.datetime(*(time.strptime(strdate, datefmt)[0:6]))

  67. # 解析命令行参数
  68. def parse_opts() :

  69.     global SCRIPT_NAME, GZIP_LOG, GZIP_PATTERN, TIME_BEGIN, TIME_END

  70.     SCRIPT_NAME = os.path.basename(__file__)

  71.     try :
  72.         opts, left_args = getopt.getopt(sys.argv[1:], 't:f:l:')
  73.     except Exception, ex :
  74.         print 'parse options error:', str(ex), '\n'
  75.         usage()
  76.         sys.exit(-1)

  77.     time_range = ''
  78.     for o, a in opts :
  79.         if o == '-t' :
  80.             time_range = a.strip()
  81.         elif o == '-f' :
  82.             GZIP_PATTERN = a
  83.         elif o == '-l' :
  84.             GZIP_LOG = a.strip()

  85.     if GZIP_LOG == '' :
  86.         print '-l is mandatory, and MUST NOT be none.\n'
  87.         usage()
  88.         sys.exit(-1)

  89.     # 解析 -t 选项

  90.     if time_range != '' :
  91.     # BEGIN
  92.         time_range = time_range.split('-')
  93.         if len(time_range) != 2 :
  94.             print '-t time range format is wrong.\n'
  95.             usage()
  96.             sys.exit(-1)

  97.         dt = []
  98.         try :
  99.             for i in (0, 1) :
  100.                 if time_range[i].strip() == '' :
  101.                     if i == 0 :
  102.                         dt.append(DEF_TIME_BEGIN)
  103.                     else :
  104.                         dt.append(DEF_TIME_END)
  105.                 else :
  106.                     dt.append(strptime_(time_range[i], '%Y/%m/%d@%H:%M:%S'))
  107.         except Exception, ex :
  108.             print '-t time range format is wrong:', str(ex), '\n'
  109.             usage()
  110.             sys.exit(-1)

  111.         TIME_BEGIN = dt[0]
  112.         TIME_END = dt[1]

  113.         if TIME_BEGIN > TIME_END :
  114.             print "-t time range is weird: begin time '%s' is later than end time '%s'\n" % (TIME_BEGIN.strftime('%Y/%m/%d@%H:%M:%S'), TIME_END.strftime('%Y/%m/%d@%H:%M:%S'))
  115.             usage()
  116.             sys.exit(-1)
  117.     # END

  118. # TEST
  119. def test() :

  120.     print '***** TEST BEGIN *****'
  121.     if TIME_BEGIN != None and TIME_END != None :
  122.         print 'begin time: %s, end time: %s' % (TIME_BEGIN.strftime('%Y/%m/%d@%H:%M:%S'), TIME_END.strftime('%Y/%m/%d@%H:%M:%S'))
  123.     else :
  124.         print '-t is not given.'

  125.     print "gzip pattern: [%s]" % GZIP_PATTERN
  126.     print "log file: [%s]" % GZIP_LOG
  127.     print '***** TEST END *****'

  128. def main() :

  129.     try :
  130.         f_log = open(GZIP_LOG, 'r')
  131.     except Exception, ex :
  132.         print 'open mygzip log file error:', str(ex)
  133.         sys.exit(-1)

  134.     # 处理日志的每一行

  135.     ln = 0
  136.     will_del = False
  137.     update_log = False
  138.     remainlns = []
  139.     failedlns = []    # 匹配删除 gzip 文件的行,但删除 gzip 文件失败的行
  140.     for line in f_log :
  141.     # BEGIN
  142.         line = line.strip()
  143.         ln += 1
  144.         strtime = line[:STRTIME_END]
  145.         gzpath = line[STRTIME_END + 1:].lstrip()

  146.         # TEST
  147.         #print 'strtime: [%s]' % strtime
  148.         #print 'gzpath: [%s]' % gzpath

  149.         try :
  150.             dt = strptime_(strtime, '%Y-%m-%d %H:%M:%S')
  151.             if gzpath == '' :
  152.                 raise AssertionError, 'there is no gzip file path in log file'
  153.         except Exception, ex :
  154.             print 'log file may corrupt:'
  155.             print '    log file format is wrong:', str(ex)
  156.             print '    line %d, log file path: %s' % (ln, f_log.name)
  157.             sys.exit(-1)

  158.         # 判断 -t 条件(时间匹配)
  159.         if TIME_BEGIN != None and TIME_END != None :
  160.             will_del = (dt >= TIME_BEGIN and dt <= TIME_END)
  161.         else :
  162.             will_del = True

  163.         #  -t、-f 之间是与关系,必需同时满足才删除 gzip 文件
  164.         if not will_del :
  165.             remainlns.append(line)
  166.             continue

  167.         # 判断 -f 条件(gzip 文件路径匹配)

  168.         if GZIP_PATTERN == '' :
  169.             will_del = True
  170.         else :
  171.             will_del = (re.search(GZIP_PATTERN, gzpath) != None)

  172.         # 删除 gzip 文件

  173.         # TEST
  174.         #print 'will_del:', will_del
  175.         if will_del :
  176.             update_log = True
  177.             if not remove_file(gzpath) :
  178.                 failedlns.append(line)
  179.         else :
  180.             remainlns.append(line)
  181.     # END

  182.     # TEST
  183.     #print 'failedlns:', failedlns
  184.     #print 'remainlns:', remainlns

  185.     f_log.close()

  186.     if update_log :
  187.         write_log(remainlns, failedlns)

  188. # 写入处理后的 mygzip 日志文件,remainlns 是原日志中未匹配删除 gzip 文件的行,failedlns 是匹配删除 gzip 文件的行,但删除 gzip 文件失败
  189. def write_log(remainlns, failedlns) :

  190.     tmpf = '%s_%s_%d' % (GZIP_LOG, NOW.strftime('%y%m%d%H%M%S'), random.randint(0,99))
  191.     while os.path.exists(tmpf) :
  192.         tmpf = '%s_%s_%d' % (GZIP_LOG, NOW.strftime('%y%m%d%H%M%S'), random.randint(0,99))

  193.     f_log = open(tmpf, 'w')
  194.     f_log.writelines(['%s%s' % (i, '\n') for i in remainlns])
  195.     f_log.close()

  196.     try :
  197.         if not remove_file(GZIP_LOG) :
  198.             raise AssertionError, 'cannot remove old log file: %s' % GZIP_LOG
  199.         os.rename(tmpf, GZIP_LOG)
  200.     except Exception, ex:
  201.         print 'rename temp file to old log file name error:', str(ex)
  202.         print '    temp file:', os.path.realpath(tmpf)
  203.         print '    old log file:', os.path.realpath(GZIP_LOG)

  204.     if failedlns != [] :
  205.         f_err = open(GZIP_LOG + ERR_LOG_SUFFIX, 'a')
  206.         f_err.writelines(['%s%s' % (i, '\n') for i in failedlns])
  207.         f_err.close()

  208. # 删除 path 指定文件,retry 是删除失败时重试的次数,retry_wait 是重试前等待的时间间隔(秒)
  209. def remove_file(path, retry = 3, retry_wait = 1) :

  210.     if not os.path.exists(path) :
  211.         return True

  212.     removed = False
  213.     i = 0
  214.     while i < retry and not removed :
  215.         i += 1
  216.         try :
  217.             os.remove(path)
  218.             removed = True
  219.         except Exception, ex :
  220.             if not os.path.exists(path) :
  221.                 removed = True
  222.             time.sleep(retry_wait)

  223.     return removed

  224. if __name__ == '__main__' :

  225.     parse_opts()
  226. #    test()
  227.     main()
复制代码

silon212

发表于 2011-2-10 17:23:16 | 显示全部楼层

回复 3# 的帖子

纯属乱写,难登大雅。觉得脚本有什么毛病,尽请行家指教。

开心私塾

发表于 2011-2-11 09:11:14 | 显示全部楼层

回复 5# 的帖子

楼主太谦虚了,
您需要登录后才可以回帖 登录 | 注册

本版积分规则

HostMonster讨论

HostMonster
优惠码:优惠链接
介绍:HostMonster美国主机商成立于1996年,总部位于犹他州普罗沃,提供主机托管服务,在业内有比较有实力,而且口碑也不错。HostMonster美国主机性价比较高,而且基本不会额外收取费用,其无限型方案支持无限磁盘空间,无限带宽和电子邮件帐户,提供免费域名1个。
查看更多

silon212

发表主题