同步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 库来进行图片抓取。

下边是大致的程序框架:

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(),省略了两个无用的参数。
	}
}