同步PicasaWeb照片到WordPress

原先为了方便管理照片和备份Blog, 所以都是引用PicasaWeb的外链图片. 不过现在PicasaWeb被盾, 导致Blog里的图片也都看不到了, 被人抱怨. 如果再逐个post去重新链接图片也忒烦人了. 估计wordpress里也没这种插件, 因为只有国人才有这需求 :( 因此自己粗粗的写了一个, 可以直接从PicasaWeb抓取图片到Web服务器. 这样唯一需要的就是Active这个插件, 就可以在blog里重现以前的图片了.

  • 自动抓取图片到Web服务器, 替换原来的图片链接 (以前的PicasaWeb是不支持外链的, 必须要嵌套一个 <a> 标签, 现在就直接把这个标签去掉了.)
  • 使用了curl_multi_select()函数, 支持一定的并发, 适用于图片很多的情况.
  • 已经抓取过来的图片不会被重新抓取.
  • 图片直接使用wp里的附件管理函数保存, 因此可以在wp的后台里查看和管理这些图片.

目前不支持代理服务器, 所以墙内的Web服务器没法使用. 目前只支持PicasaWeb. 插件可以从下边直接下载, 然后保存到 wp-content/plugins 目录激活即可. 插件从这里下载.

BTW: 写完没预料的一个问题是, Google的爬虫好快啊 , 没过一会发觉自己的图片已经全部被download到服务器上了, 哈哈

主要思路是在加载一个filter到the_content, the_content_rssthe_excerpt_rss 这几个和输出相关的hooks上。然后,在filter里利用正则表达式匹配picasaweb相关的链接,并替换成服务器的本地缓存图片的链接。如果本地缓存的图片不存在,则首先从picasaweb上抓取这些图片。这些图片缓存同时可以在wordpress的Media库中进行管理。

这样设计的好处是,使得插件的设计和配置都很简单,不用任何数据库操作;重新部署blog的时候无须关心缓存图片是否缺失;另外,由于是在输出hooks上增加filter,因此不会改写数据库。缺点是如果图片很多,会有较多的IO操作,且图片文件都放在同一目录不能重名。

该插件由于替换了img文件的url,所以可能和Lightbox 2之类的有冲突,尚未仔细确认,但应该可以通过调整filter的加载顺序来解决。

PicasaWeb类中主要使用 curl 库来进行图片抓取。

下边是大致的程序框架:

:::php
class PicasaWeb
{
    // 定义正则表达式的匹配模式
    var $pattern = "|(<a\s+href=\"http://picasaweb\.google\.com/[^\"]+\">)?<img\s+src=\"(http://lh[0-9]\.ggpht\.[^\"]+)\"(\s+alt=\"([^\"]*)\")?\s*/?>(</a>)?|si";

    var $url_list = array();
    var $threads = 5;
    var $timeout = 10;             // seconds

    // filter函数
    function sync($content)
    {
        // 1) 正则匹配 picasaweb 图片
        preg_match_all($this->pattern, $content, $match, PREG_SET_ORDER);
        // 2) 查询匹配到的图片是否已经有本地缓存
        $this->fillUrls($match);
        // 3) 爬取没有本地缓存的url
        $this->fetchAllPhotos();
        // 4) 上边都是预备工作,这里对输出文本进行替换
        return preg_replace_callback($this->pattern,
                array(&$this, 'replacePhoto'),
                $content);
    }

    // 爬虫函数,利用 curl_multi_select() 来支持并发抓取,该函数使用 select() 实现IO的多路复用
    // urls = array(url => cache url,
    //              ...)
    function crawlPhotos(&$urls = array(), $threads = 5, $timeout = 30)
    {
        // Urls to download
        $mcurl = curl_multi_init();
        $threadsRunning = 0;
        $urls_id = 0;

        reset($urls); // start again first item
        $url_item = current($urls);
        for(;;) {
            // Fill up the slots
            while($threadsRunning < $threads && $url_item !== FALSE){
                // if not cached, run a curl job
                if(empty($url_item["local"])){
                    $this->log("URL item: ". $url_item["url"]);

                    $ch = curl_init();
                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
                    curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
                    curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
                    curl_setopt($ch, CURLOPT_URL, $url_item["url"]);    // url
                    curl_multi_add_handle($mcurl, $ch);
                    $threadsRunning++;
                }

                $url_item = next($urls);
            }
            // Check if done
            if ($threadsRunning == 0 && $url_item === FALSE)
                break;
            // Let mcurl do it's thing
            curl_multi_select($mcurl);
            while(($mcRes = curl_multi_exec($mcurl, $mcActive)) == CURLM_CALL_MULTI_PERFORM) usleep(100000);
            if($mcRes != CURLM_OK) break;
            while($done = curl_multi_info_read($mcurl)) {
                $ch = $done['handle'];
                $done_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
                $done_content = curl_multi_getcontent($ch);
                if(curl_errno($ch) == 0) {
                    $urls[$this->checksum($done_url)]["local"] = $this->storePhotoToCache($done_content, $done_url);
                    $this->log("Succeed to cache url: ".$done_url);
                } else {
                    $this->log("Link <a href='$done_url'>$done_url</a> failed: ".curl_error($ch)."\n");
                }
                curl_multi_remove_handle($mcurl, $ch);
                curl_close($ch);
                $threadsRunning--;
            }
        }
        curl_multi_close($mcurl);
        $this->log( 'Done.' );
    }

    // 利用wordpress的attachment相关函数来操作图片缓存,这些图片则都可以保存
    // 到wordpress的Media库中,你可以从后台进行管理。
    function storePhotoToCache($content, $url)
    {
        $filename = $this->fileName($url);
        $title = $this->url_list[$this->checksum($url)]["alt"];
        // wp_upload_bits() 返回 array( 'file' => $new_file, 'url' => $url, 'error' => false );
        $newfile = $this->wp_upload_bits($filename, $content);

        $filepath = $newfile["file"];
        $filetype = wp_check_filetype($filepath);
        $photo = array(
                       "post_title" => $title,
                       "post_content" => $filename,
                       "post_status" => "inherit",
                       "post_parent" => 0,
                       "post_mime_type" => $filetype["type"],
                       "guid" => $newfile["url"]);
        $postid = wp_insert_attachment($photo, $filepath);
        if( !is_wp_error($postid)){
            wp_update_attachment_metadata( $postid, wp_generate_attachment_metadata( $postid, $filepath ) );
        }
        // TODO: convert local path to url
        return $newfile["url"];
    }

    function wp_upload_dir()
    {
        // Hack了 wordpress 的 wp_upload_dir(),让它不再根据时间来创建目录,因为现在
        // 所有的图片都放到同一个目录之下。
    }

    function wp_upload_bits( $name, $bits)
    {
        // Hack了 wordpress 的 wp_upload_bits(),省略了两个无用的参数。
    }
}
2010-04-10 05:32
status: part
comments powered by Disqus