打造自己的在线收藏服务

前些时,中国雅虎关闭了“永久邮箱”,今天它彻底成为了历史。今年早些时候,Google 不顾众多粉丝的反对,执意关闭了 Google Reader,简洁好用的 Google Talk 也会被 Hangouts 替代,而且不再支持 XMPP 协议。

随着互联网走向成熟,免费服务背后的各种隐患不断暴露出来。任何不赚钱的,公司核心业务之外的服务,随时都可能被终止。很多时候,免费意味着免责,这些公司只会算计停止服务可以节省多少成本,全然不顾当初招揽用户时的种种承诺,用户辛辛苦苦积累了几年的数据也随着服务关闭灰飞烟灭。

于是,我开始把云当做临时获取数据的一种手段,而不是全部。重要数据,一定会在本地电脑存一份,定期 Time Machine 备份。我的 Gmail 邮件会用 Airmail 客户端收到本地,Evernote 里的笔记也是在本地客户端里记,同步到云端。对于不得不用的 Web 服务,也尽可能托管在自己的 VPS。私有 Git 仓库用的是自己搭的 Gitlab,博客很早就开始自己托管。当然,即使是 Linode 家的 VPS,也有不稳定的时候,所以我还在 VPS 装了 Dropbox,定期备份。

我很少使用浏览器自带收藏夹来保存有用的链接,因为不能满足我的同步需求。虽然现在 Chrome 和 Firefox 都支持了同步书签,但还不够迅速,也不能方便的从 Chrome 同步到 Firefox(用过一段时间的 Xmarks,不怎么好用),临时在别人电脑上用下自己的收藏也很麻烦。另一个原因是很多技术文章很快就打不开了或者访问速度慢,浏览器收藏只有链接没有快照是硬伤。其它例如 Tag、搜索等功能,也是在线收藏可以做得更好的地方。

大概五年前,我开始用 delicious 提供的在线收藏服务,后来由于种种原因改到百度搜藏。虽然百度搜藏几年没更新,但我用得很顺手,功能够、速度快。直到几个月前,没有任何征兆,百度搜藏变成了云收藏,自己又一次被抛弃。虽然他们后来回滚了,但我已经决定自建服务来彻底清理这个定时炸弹。

当晚花了三个多小时,自己的在线收藏服务上线了。代码上,继续使用 Python 下非常适合快速开发的 Django 框架,复用我博客的皮肤。功能上,通过点收藏夹书签来收藏链接。链接有快照,可以设为私有,有 Tag 可以搜索。对我来说,已经够用了。

我的在线收藏地址是:cang.qgy18.com:81。以下是这个系统开发过程中遇到的问题的简单记录,主要集中在生成快照这里。

--- 分割线 ---

首先是获取链接内容,用 python 的 urllib2.Request 方法可以很快搞定。获取到链接返回的内容之后,还需要进行一些处理,我用到好用的 BeautifulSoup 把文本转成 Dom 树,这里遇到了第一个问题:编码导致的乱码。

BeautifulSoup 会使用文本中 标签指定的编码,但是有些页面并没有通过这种方式指定编码。这时候,需要从 Response Header 中获取编码。如果响应头里也没有,只好当做默认的 utf-8 来处理了。下面几行是关键代码:

from BeautifulSoup import BeautifulSoup

charset = BeautifulSoup.CHARSET_RE.search(response.headers['content-type']) 
charset = charset and charset.group(3) or None 
soup = BeautifulSoup(response.read(), fromEncoding = charset)

另外,我要移除快照中的 script 等标签。同时,为了提高快照的打开速度,还要生成一个没有外链 CSS 的极速版。有了 BeautifulSoup,可以轻松移除不需要的标签,不想保留的属性也很容易删掉。

remove_tags = ['script', 'object', 'video', 'embed', 'iframe', 'noscript']
for tag in soup.findAll(remove_tags):
    tag.extract()

remove_attributes = ['onmouseover', 'onclick']
for attr in remove_attributes:
    for x in soup.findAll(attrs = {attr:True}):
        del x[attr]

还有一个问题:页面很多 A 链接或者图片的 src 属性不是使用的完整路径,也需要处理,不然在快照页面就找不到了。有两种做法:1)在快照页增加 <base href="当前URL" />;2)补全所有路径。因为 python 内置了很好用的 urlparse.urljoin 函数,我使用的是第二个方案。这部分代码类似下面这样:

for a in list(soup.findAll('a')):
    if a.get('href'):
        a['href'] = urlparse.urljoin(url, a['href'])

最后通过 soup.renderContents('utf-8') 输出字符串,存成文件就 OK 了。

由于抓取网页生成快照是一个耗时的操作,如果放在主线程会导致提交处理时间过长。这里我使用了 Gearman 来异步处理任务。

在 Ubuntu 上可以方便地用「apt-get install gearman-job-server」安装 Gearman。从源码安装也很方便,先安装 libboost-dev、libevent-dev 等依赖;然后直接 configure、make 和 make install 就可以了。如果编译过程中还提示缺少什么,apt-get 一般都有。装完后用「gearmand -d」启动服务,然后再安装 Python 的 Gearman 库,直接「easy_install gearman | pip install gearman」就可以。

一切就绪后,我们在数据保存时,创建这样一个 Producer,或者叫 Client:

from gearman import GearmanClient

gm_client = GearmanClient(GEARMAN_SERVERS)
gm_client.submit_job('snapshot', self.url.encode('utf-8'), background=True);

然后再写一个 Consumer 服务,或者称之为 Worker。

from gearman import GearmanWorker

gm_worker = GearmanWorker(GEARMAN_SERVERS)

def task(gearman_worker, gearman_job):
    download(gearman_job.data);
    return 'ok'

gm_worker.set_client_id('i_am_a_snapshot_worker')
gm_worker.register_task('snapshot', task)

gm_worker.work()

这样,Client 提交的是异步任务,不会等任务执行完毕,整个提交过程不会被阻塞。同时,Gearman 会把任务分配给对应的 Worker,再由后台 Worker 默默地生成快照。

更新:目前我用的收藏系统已经更换为奇舞团开发并开源的 cicada,详见:thinkjs-team/cicada

本文链接:参与评论 »

--EOF--

提醒:本文最后更新于 1191 天前,文中所描述的信息可能已发生改变,请谨慎使用。

Comments