2008年11月28日

jQuery Douban - 简化豆瓣 API 开发的 jQuery 插件

什么是 jQuery Douban

jQuery Douban 是一个简化豆瓣 API 调用和 GData JSON 分析的 jQuery 插件,支持 OAuth 授权认证和各种豆瓣数据读写操作的功能。目前支持的API有用户、条目、收藏、评论、广播、日记、推荐和标签 API,部分支持同城活动的 API。这里有一个简单的读取用户收藏的 Demo

什么时候使用 jQuery Douban

  • 你正好在用 jQuery
  • 你正好想从客户端调用豆瓣 API,而不是从服务器
  • 或者你正在为豆瓣编写 GreaseMonkey 脚本

为什么不直接使用豆瓣API

  • 更喜欢 jQuery 风格的代码
  • 可以用有跨域超能力 Javascript 库,比如 Gears
  • 看到 {"$t":"9787543632608","@name":"isbn13"} 风格的JSON 会头晕

怎样使用 jQuery Douban

示范添加一条广播先。
jQuery Douban 依赖于 jquery,oauth 和 sha1 三个 js 库,所以先要导入这三个 js 文件。
<script type="text/javascript" src="/scripts/jquery.js" ></script>
<script type="text/javascript" src="/scripts/sha1.js" ></script>
<script type="text/javascript" src="/scripts/oauth.js" ></script>
<script type="text/javascript" src="/scripts/jquery.douban.js" ></script>
然后可以添加广播了
// 创建豆瓣服务实例
var service = $.douban({ key: "apiKey", secret: "apiSecret" });
// 登录豆瓣帐户
var login = service.login({ key: "accessKey", secret: "accessSecret"});
if (login) {
     // 添加一条广播
     var miniblog = service.miniblog.add("添加一条广播");
     // 弹出“添加成功:添加一条广播”
     alert("添加成功:" + miniblog.content);
     // 删除一条广播
     service.miniblog.remove(miniblog);
 }


哪里可以得到 jQuery Douban

jQuery Douban 的主页是在 Google Code 上,Homepage: http://code.google.com/p/jquery-douban/。 但代码用 GitHub 来管理,Repo URL: http://github.com/wuyuntao/jquery-douban/tree/master。可以用 Git 检出代码
$ git clone git://github.com/wuyuntao/jquery-douban.git

2008年11月27日

有备无患:用 Google Gears 跨域调用豆瓣 API

有两种可以实现 Gears 的跨域(Cross-Origin)访问的办法。第一种相对安全,但是要在豆瓣的服务器上安置一个 cross-origin worker

Cross-origin worker 可以由 WorkerPool API 创建。WorkerPool 类似于线程池的概念,它让 javascript 从页面上解放出来,可以在后台独立运行,父子 workers 之间也可以互相通信,而且不同服务器上的 workers(也就是 Cross-origin workers)也可以互相传递数据,这样就实现了一个安全的浏览器跨域访问方式。


当然,我们不可能在豆瓣的服务器上动手脚。所以,我们只能在迂回中前进,第二种方法就是自建一个跨域代理(cross-origin proxy),并在上面安置 worker 供 Gears 调用,尽管这有被 XSS 攻击的潜在风险。

举例来说,我们要从 http://foo.com/ 上调用豆瓣 API,发送一条广播到 http://api.douban.com/miniblog/saying。这通常受到 Same-Origin Policy 的限制。所以,我们通过父 worker 向安置在跨域代理上的 cross-origin worker 发送指令,让它去访问 http://bar.com/proxy?url=http://api.douban.com/people/wyt。成功后,从向本地的 worker 报告工作成果。

我先用 App Engine 写了一个简单的跨域代理,仅做参考。然后在 app.yaml 中设置好 worker.js 的访问路径。

- url: /worker.js
   static_files: media/scripts/worker.js
   upload: media/scripts/worker.js

接着编写 worker.js。以前 Gears 只可以在 workers 之间传递文本,现在已经可以传递函数(function)以外的其他对象,比如 Array 和 Object。所以我们可以向 cross-origin worker 传递一个包含了 HTTP request 所需参数的字典(dict),比如说

var options = {
     'url': 'http://bar.com/proxy?url=http://api.douban.com/miniblogs/saying',
     'type': 'POST',
     'headers': {
         'Authorization': 'OAuth realm="", ...',
         'Content-Type': 'application/atom+xml'
     },
     'data': '<?xml version="1.0" encoding="UTF-8"?><entry ...'
 };

worker.js 中通过 Gears 的 HttpRequest API,向代理发送请求,并将代理返回的结果发送给本地的父 worker 供浏览器调用。这里有一个简单的实现 gears_worker.js,只做了返回成功后的处理。

// 新建 Worker
var wp = google.gears.workerPool;

// 允许 Worker 跨域脚本访问
 wp.allowCrossOrigin();

 wp.onmessage = function(a, b, message) {
     var s = message.body;

     // 新建 HTTP Request
     var request = google.gears.factory.create('beta.httprequest');

     request.onreadystatechange = function() {
         // 将响应文本返回给父 Worker
         if (request.readyState == 4) {
             wp.sendMessage(request.responseText, message.sender);
         }
     };

     // 打开 URL
     request.open(s.type, s.url);

     // 设置请求 Headers
     for (var name in s.headers)
         request.setRequestHeader(name, s.headers[name]);

     // 发送请求
     if (s.data) request.send(s.data);
     else request.send()
 };
全都部署完成之后,我们就可以在 http://foo.com/ 上通过运行 javascript 调用豆瓣 API 了。下面是一个示例
// 新建 WorkerPool
var workerPool = google.gears.factory.create('beta.workerpool');

// 处理响应
 workerPool.onmessage = function(a, b, message) {
     var data = eval("(" + message.body + ")");
     alert(data);
 };

// 创建子 worker 并将预先定义好的 options 字典发送过去
var childWorkerId = workerPool.createWorkerFromUrl('http://bar.com/worker.js');
 workerPool.sendMessage(options, childWorkerId);

这样部署好之后,就可以用 Google Gears 跨域调用豆瓣 API 了。但还是有一个问题,因为所有的请求都集中到跨域代理,如果没有申请放宽限制的话,很快就会被 403 吧。果然,最好的办法还是豆瓣官方支持 Gears 的远程调用吗?

参考页面




P.S. Blogger.com 莫名的被 GFWed 了。为什么 Blogger API 可以通过 https:// 访问,可 Blogger 自己却不可以像 GMail 和 GDocs 一样通过 https:// 呢,现在就只能靠 GDocs 发布了。。。

2008年10月14日

悠言悠闲:linux 下玩/wine 東方地霊殿


  1. 东方地灵殿(東方地霊殿 〜 Subterranean Animism.)是日本同人社团上海爱莉丝幻乐团制作的一款弹幕系射击游戏
  2. wine 的版本为 1.1.5
  3. 无法启动 th11.exe,需要下载,或者从 Windows 下面复制一个 d3dx9_36.dll 文件,
    $ cp d3dx9_36.dll ~/.wine/drive_c/windows/system32/
  4. 运行地灵殿的窗口模式的时候,分辨率似乎被限制在了640x480。没有找到解决办法
  5. 如果使用虚拟桌面(Virtual Desktop) + 地灵殿的全屏模式,感觉会有点卡,帧速降到50fps左右
  6. 如果使用虚拟桌面 + 窗口模式,游戏界面黑屏有声音
  7. 如果取消虚拟桌面 + 窗口模式,感觉上速度和 Windows 下面差不多,不过限速 60fps,实际差多少也不知道就对了
  8. 运行正确的话,wine 应该只会输出下面这几行消息,
    $ wine th11.exe 
    fixme:win:EnumDisplayDevicesW ((null),0,0x32f8d4,0x00000000), stub!
    fixme:d3d:WineD3D_ChoosePixelFormat Add OpenGL context recreation support to SetDepthStencilSurface
    fixme:d3d:WineD3D_ChoosePixelFormat Add OpenGL context recreation support to SetDepthStencilSurface
    fixme:win:WINNLSEnableIME hUnknown1 (nil) bUnknown2 1: stub!
    
  9. 有一篇更加详细的说明,東方地霊殿に関するメモ(日语)

2008年9月20日

有备无患:豆瓣离线 - 利用 Google Gears 实现离线浏览豆瓣的 GreaseMonkey 脚本

豆瓣离线(Douban Offline)是一个可以让用户可以离线浏览豆瓣的 GreaseMonkey 脚本。它可以在网络无法使用的情况下,浏览保存在本地的页面。它也可以用来收藏或备份豆瓣上的条目、小组和友邻等页面。

豆瓣离线使用 Google Gears API 来实现离线浏览的功能。Google Gears 是一款 Google 开发的软件。Gears 通过 SQLite 数据库让客户端能够把网页暂存起来,并通过内部服务器(Local server)把数据库中暂存的网页重现,从而让用户实现离线上网的功能。Google DocsGoogle Reader 都支持用 Gears 将本地暂存的资料与网络做同步。

豆瓣离线现在版本为 0.1,还有许多的不完善之处,欢迎大家的建议、意见和 Bug 报告:-)


功能

  • 保存豆瓣上的某个页面,以及相关的图片、CSS 和 Javascript 脚本
  • 按分类浏览已缓存的页面,目前的分类有:条目,小组和用户

使用方法

  • 如果希望保存当前页面,我们先找到导航栏“退出”附近的“离线”。点击“离线”,待离线状态栏展开完毕后,点击“离线”下面的“收藏此页面”。注意,点击之后不要马上关闭窗口,因为相关的文件可能还在下载中。另外我们也可以打开 FireBug 的终端(Console)来看看究竟那些文件被下载下来了。
  • 如果希望浏览已缓存的页面,点击 Firefox -> File(文件) -> Work Offline(离线工作)来强制 Firefox 进入离线工作的状态。点击“离线” -> “离线浏览”,然后在缓存页面目录,我们可以点击其中的链接访问已被缓存的页面。
  • 如果希望恢复在线浏览,在同样的地方点击“在线浏览”。
  • 第一次使用豆瓣离线脚本时,Gears 会弹出两个安全警告对话框,都选择 Allow(允许)就可以了。

安装需求

  1. Firefox (>=3.0.0): http://www.mozilla.com/en-US/firefox/
  2. GreaseMonkey (>=0.8):https://addons.mozilla.org/en-US/firefox/addon/748
  3. Google Gears (>=0.4):http://gears.google.com/

下载


大家可以从我的 GitHub Repo 中检出最新的脚本试用
git clone git://github.com/wuyuntao/douban-offline.git

或者从 UserScripts 网站上下载
下载


2008-09-23 UPDATE 0.2:

豆瓣离线脚本更新为 0.2,有下面这些改动:
  1. 可以手动切换上线/离线浏览
  2. 可以更新缓存页
  3. 新增两个分类,日记和相册
  4. 选中分类有高亮


2008-10-15 下载链接更新:

祸不单行。github 前几天数据库出了问题,到现在还没有恢复; userscripts.org 貌似被 GFW 给封了;今天 GAE 提供的四个 IP 中的又一个阵亡了。。。不管怎样,上面两个下载的链接都不管用了,现在提供一个本地的下载链接。
下载

2008-10-22 UPDATE 0.4:

豆瓣离线脚本更新为 0.4,有下面这些改动:
  1. 增加了翻页的功能
  2. 增加了评论和豆邮两个标签
  3. 增加了一条 Douban Helper 的控制台命令:write(wyt:Douban Helper 请安装最新版本
  4. 改正了相册日记等页面离线按钮位置不正确的bug

2008年9月14日

有备无患:Google Reader 风的豆瓣广播 widget 更新

今天为 Google Reader 风的豆瓣广播 widget 新增了三个选项:手动设置宽度 width,隐藏标题 hidetitle 和隐藏主页链接 hidefooter。谢谢 cowasiapan建议先。

使用方法和以前的说明一样,只需要将下面这段代码插入到你的 blog 模板中,并替换其中的参数。

<script type="text/javascript" src="http://luliban.com/scripts/miniblog.js?username=wyt&amp;maxresults=16&amp;style=blue"></script>

参数列表如下

  • username: 用户名,或者用户 id。(必填)
  • maxresults: 输出广播条目的数量。最大值为50,默认为10。
  • style: widget 的配色样式,默认为绿色。其他样式包括 black(黑)、blue(蓝)、gray(灰)、green(绿)、khaki(卡其色)、pink(粉红)、slate(蓝灰) 和 none(无)。其中,none 为不带任何样式的原始 HTML,适合希望自定义广播风格的 bloggers。
  • width: 手动设定 widget 的宽度,输入应为宽度的像素值,如 width=300。默认为自适应宽度(推荐)。
  • hidetitle: 如果设 hidetitle=true,将隐藏顶部标题。默认为 false。
  • hidefooter: 如果设 hidefooter=true,将隐藏底部豆瓣主页的链接。默认为 false。

隐藏标题和主页的效果如下

2008年9月12日

有备无患:升级到 Git 1.6.01 之后无法启动 git-daemon 的解决办法

git 升级到 1.6 之后,发现 git-daemon 不能启动,自然也不能作 clone 或 push 之类的操作。原因是原来的 /usr/bin/git-daemon 搬到了 /usr/libexec/git-core 目录,但是 Gentoo 的启动脚本没有随之更新。不过有人已经提交了这个 bug

自己也可动手,将启动脚本 /etc/init.d/git-daemon 中的 start 函数修改如下。
start() {
    ebegin "Starting git-daemon"
        start-stop-daemon --start --quiet --background \
        --exec /usr/libexec/git-core/git-daemon -- ${GITDAEMON_OPTS}
    eend $?
}

另外,我用来架设 Git 版本库的 gitosis 也和 git 1.6 有一点兼容问题,原因是 /usr/bin/git-shell 也搬到了 /usr/libexec/git-core 目录。好在已经有 patch 推出,只需要修改 /usr/lib/python2.5/site-packages/gitosis/serve.py,将其中调用 git-shell 的指令,替换成调用 git shell 即可。
--- a/gitosis/serve.py
+++ b/gitosis/serve.py
@@ -201,6 +201,6 @@ class Main(app.App):
             sys.exit(1)
         main_log.debug('Serving %s', newcmd)
-        os.execvp('git-shell', ['git-shell', '-c', newcmd])
+        os.execvp('git', ['git', 'shell', '-c', newcmd])
         main_log.error('Cannot execute git-shell.')
         sys.exit(1)

2008年9月10日

南言北哲:Battier or Artest,谁将首发?

1,Battier or Artest,谁将首发?

Artest。联盟最好的人盯人防守者之一,上赛季场均20.5分的进攻水准,以及38%的三分球命中率。加上曾在主教练 Rick Adelman 手底下打过一年球,对火箭现在这套动态进攻的体系了然在胸。如果在训练营和季前赛中一切顺利,他极有可能会取代 Shane Battier 成为火箭的正印前锋。

有同学认为,Artest 应妨 Luis Scola 之例,先从替补打起,走循序渐进的路线。可是你要知道,Scola 虽然在 FIBA 摸爬滚打了多年,但是他也需要时间来适应 NBA,另一方面主教练也需要时间来了解他的技术特点,所以 Scola 必须从替补打起。Artest 却是已经在联盟混迹了整九年,别说 Adelman,就是联盟其余的29位主教练也同样对他知根知底。

另外,虽然 Artest 曾经表过态,说愿意从替补打起。可是,以史为鉴,Artest 不是甘居替补之人——05年他炮轰 Jermaine O'Neal 出手权多和进攻效率低,被交易到 Sacramento 之后,又与 Mike Bibby 争当“国王”。(wyt: 最终都以阿泰“抢班夺权”失败而告终。)

2,如果 Battier 做替补,能不能胜任第六人的角色?

像 Battier 这样的防守型球员也一样可以胜任第六人,这甚至还有可能成为一股潮流。

联盟的大多数第六人都是长于进攻的球员,比如被交易回国王的 Bobby Jackson,湖人签下肥约的 Sasha Vujacic,小牛老将 Jerry Stackhouse,还有掘金的 J.R. Smith 等,他们的主要作用是在主力下场的时候带领其他替补球员,提供持续的进攻力,并为主力争取更多的休息时间。(wyt:我不喜欢把 Manu Ginobili 和 Jason Terry 归类到第六人。如果非要说,我更愿意称他们为“伪第六人”,或者,“联盟第六人奖项欺诈师”)

但是对于拥有所谓的“三巨头”的球队,这个角色常常是由“三巨头”的其中之一所担任,比如凯尔特人的 Ray Allen 和马刺的 Manu Ginobili。因而这些球队对第六人在进攻端的要求也有所不同。以 Boston 为例,上赛季第六人就是场均只有7.4分进帐的防守悍将 James Posey。总决赛中出色的表现,也让黄蜂弃用原来的第六人 Jannero Pargo,花重金将 Posey 挖来作为下赛季他们的第六人。另外,湖人下赛季也有可能让 Odom 出任第六人。

Battier 之于火箭,就像 Posey 之于 Boston 和 New Olean。而且 Battier 有更出色的协防能力和不错的背身技巧,可以想见新赛季的第四节,我们经常会看到 Battier 和 Artest 一起打完最后一分钟。

3,如果 Artest 做先发,出手次数会不会不够?

这很可能是杞人忧天。

上赛季,有三支球队的核心球员被称作“三巨头”,分别是凯尔特人的 Garnett、Pierce 和 Allen,马刺的 Duncan、Parker 和 Ginobili,以及湖人的 Kobe、Odom 和 Gasol。我们首先来看看他们的出手分配情况,并和火箭作一下对比。


表格中的 FGA 为出手次数 (Field Goal Attempts),FTA 为罚球次数 (Free Throw Attempts),TP 为球队节奏 (Team Pace),TSA 为每100次球权的真实出手次数 (True Shooting Attempts),所使用的统计公式为 TSA = (FGA + 0.44 * FTA) / TP * 100。要说明的,湖人的数据是“打劫” Gasol 以后的全明星之后的数据,而其他球队是整个赛季的数据。



从图表中,我们可以看出光是姚明,T-Mac 和 Battier 三个人的出手次数,就已经和其他几个三巨头基本持平。所以即使 Artest 取代 Battier 成为先发,只要姚明和 T-Mac 愿意牺牲自己的球权,火箭的出手次数对于 Artest 来说是绰绰有余的。

另外,由于伤病的关系,姚明和 T-Mac 都有相当长一段时间单独带队,所以也有同学质疑,这段时间他们的出手数是不是突增的,会不会影响到最后的统计结果。我也捎带统计了一下他们同时带队的10月、11月和2月份,姚明的出手数是20.0次,而 T-Mac 的是 24.4次。相比整个赛季并没有太大的变化,这主要是因为无论是姚明还是 T-Mac 带队,他们都是以火箭这个团队来进攻,谁也没有像 Kobe 在 Bynum 和 Gasol 缺席的时候那样贸然增加自己的出手。

2008年9月5日

有备无患:用 Gaupol 生成文本字幕

Gaupol 是一个用 Python 编写的文本字幕文件的编辑工具,支持包括.ssa、.ass、.srt和.sub在内的多种字幕文件格式,并提供文本校正的方法以及时间处理。其用户界面基于 pyGTK,注重对多文档和翻译的批量处理。最新版本为 0.13

下面的命令示范了利用 Gaupol 生成一个 SubRip 格式的字幕文件,以作备忘。

$ python
>>> from gaupol.files.subrip import SubRip
>>> from gaupol import NEWLINE
>>> srt = SubRip('/home/wyt/demo.srt', encoding='utf-8', newline=NEWLINE.UNIX)
>>> srt.write(starts=[u'00:00:5.000', u'00:00:15.000', u'00:00:25.000'], \
              ends=[u'00:00:10.000', u'00:00:20.000', u'00:00:30.000'], \
              texts=[u'字幕测试一', u'字幕测试二', u'字幕测试三'])

生成的 .srt 字幕文件如下

1
00:00:5,000 --> 00:00:10,000
字幕测试一

2
00:00:15,000 --> 00:00:20,000
字幕测试二

3
00:00:25,000 --> 00:00:30,000
字幕测试三

2008年8月10日

有备无患:truncatehanzi - 按字数截取中文的 Django template filter

Django 自带的一个模板过滤器(Template Filter)truncatewords,可以按单词数截取字符串生成摘要。不过,truncatewords 只能用空格来分隔单词,对汉字就无能为力了。所以,我写了另一个模板过滤器 truncatehanzi,它通过 unicode 编码来区分西方字符和 CJK 汉字,对包括英语字母在内的西方字符以空格或其他符号来分隔并计算单词数,而对 CJK 汉字则是按字数计算。

使用方法和 truncatewords 一样,
{{ value|truncatehanzi:6 }}
如果 value 为“截取English、Γρεεκ等字母语言和CJK汉字。”,输出“截取English、Γρεεκ等字...”。

代码分为两个部分,截取汉字的脚本 truncate_hanzi.py 和生成过滤器的脚本 filters.py。

truncate_hanzi.py

 # -*- coding: UTF-8 -*-

import re
from django.utils.encoding import force_unicode
from django.utils.functional import allow_lazy

def truncate_hanzi(s, num):
    s = force_unicode(s)
    length = int(num)
    if length <= 0:
        return u'...'
    # Set up regex for alphanumeric characters
    # \u00c0-\u02af: Latin
    # \u0370-\u1fff: Greek and alphabet characters in other language
    re_alnum = re.compile(ur'[a-zA-Z0-9_\-\u00c0-\u02af\u0370-\u1fff]', re.U)
    # Set up regex for hanzi
    # \u3040-\ufaff: CJK characters
    re_hanzi = re.compile(ur'[\u3040-\ufaff]', re.U)
    hanzi = u''
    hanzi_len = 0
    word_temp = u''
    for char in s:
        # Check for alphabet characters
        if re_alnum.match(char):
            word_temp += char
            continue
        if word_temp:
            hanzi += word_temp
            hanzi_len += 1
            word_temp = ''
        # Check for length
        if hanzi_len >= length:
            if not hanzi.endswith('...'):
                hanzi += '...'
                break
        # Check for hanzi
        if re_hanzi.match(char):
            hanzi_len += 1
        hanzi += char
    hanzi += word_temp
    return hanzi
truncate_hanzi = allow_lazy(truncate_hanzi, unicode)

def demo():
    print truncate_hanzi('截取段落工具,支持English、Γρεεκ等字母语言和CJK汉字。', 6)
    print truncate_hanzi('截取段落工具,支持English、Γρεεκ等字母语言和CJK汉字。', 11)
    print truncate_hanzi('截取段落工具,支持English、Γρεεκ等字母语言和CJK汉字。', 20)

if __name__ == '__main__':
    demo()

filters.py

 # -*- coding: UTF-8 -*-
from django.template import Library
from django.template.defaultfilters import stringfilter

register = Library()

@stringfilter
def truncatehanzi(value, arg):
    """
    Truncates a string after a certain number of words including
    alphanumeric and CJK characters.

    Argument: Number of words to truncate after.
    """
    from truncate_hanzi import truncate_hanzi
    try:
        length = int(arg)
    except ValueError: # Invalid literal for int().
        return value # Fail silently.
    return truncate_hanzi(value, length)
truncatehanzi.is_safe = True

register.filter('truncatehanzi', truncatehanzi)

2008年8月6日

有备无患:一个 jQuery 的时间选择插件

Yet another jQuery time picker plugin. Google 上兜了一圈,没找到中意的时间选择(Time Picker)插件,于是自己写了一个。timepicker 是一个弹出式的对话框,分为上下两个部分,上半部分是可以分别设定时分秒的计数器,下半部分提供一些常用的时间选项,比如“Now”,“Noon”,“8 p.m.”等。通过聚焦(focus)到相应的 input 控件,可以激活 timepicker。选择好时间后,点击对话框底部的 Close 按钮会将结果保存到 input 控件中,而点击 Clear 按钮会清除 input 控件中已存的时间。键盘上的 Tab 键和 ESC 键也有与之对应的功能。

这是我的第一个 jQuery 插件,欢迎大家使用,我也会继续完善这个插件。如果有任何的 Bug,建议和意见,欢迎在文章后留言,或者 Email 给我,或者在插件的主页写下你的评论。:-)

使用方法

$('#time-picker').timepicker();

截图


Yet another jQuery time picker plugin.

演示

演示网址



下载

你可以选择下载压缩档。
下载

或者用 Git 从架设在 GitHub 的软件仓库中检出源代码使用。
$ git clone git://github.com/wuyuntao/timepicker.git

2008年8月4日

有备无患:Google Reader 风的豆瓣广播 widget

最近,豆瓣连续发布了几个重要的 API 更新,包括了备受期待的友邻广播相关的接口。特别是广播的 API,自从用豆瓣广播取代了 Twitter 当作吐口水的工具之后,我一直期待广播也有像 TwitterFox 一样的客户端,有个 widget 可以在 blog 上显示最近的状态,而这些都是要以广播的 API 为基础的。

言归正传,我写了一个类似 Google Reader 的分享阅读剪辑(Shared Item Clips)风格的豆瓣广播 widget,可以放在 blog 的侧边栏。使用的方法很简单,只需要将下面这段代码插入到你的 blog 模板中。(wyt:如果不知道怎么操作的话,可以参考豆瓣秀指南。)

<script type="text/javascript" src="http://luliban.com/scripts/miniblog.js?username=wyt&amp;maxresults=16&amp;style=blue"></script>

注意上面代码中粗体的部分,首先我们要将``wyt``换成自己的用户名;``maxresults``为 widget 显示的广播条数(最大值为50,默认为10,可以省略);``style``表示 widget 的配色,和 Google Reader 分享阅读的配色基本一致,包括:black、blue、gray、green、khaki、pink、slate 和 none。默认为 green,如果输入的 style 不在上述配色中,则返回 none。none 为不带任何 style 的原始 HTML,适合希望自定义广播风格的 blogger。

最终效果如下图。想看看实物 demo 的话,可以点击这里。要说明的是,看得到漂亮的圆角效果的只有 Firefox 用户(wyt:这也是 Google Reader 的风格之一,:p),IE、OperaSafari 用户对不住了。如果有任何的建议或意见,欢迎留言 :-)

2008年7月18日

有备无患:在 Django 上使用 reCAPTCHA 生成验证码

reCAPTCHA 是由 CAPTCHA 的原作者,也是人类计算学(Human Computation)的专家 Luis von Ahn 所设计一个免费的 anti-bot 服务,它在随机提取的单词上加上一些扭曲和可阻碍电脑自动识别的噪声,以利用人类远强于电脑的图形识别能力来检查屏幕前坐着的是人类,还是 bot 或自动脚本。因此,reCAPTCHA 经常被用于生成网站注册或者文章评论的验证码。

除此之外,reCAPTCHA 还有一项非常公益的设计。虽然 OCR (全称 Optical Character Recognitio,中译光学字元识别)早已被投入使用,但是对那些印刷模糊或分辨度不足的实体书籍而言,OCR 的识别率难说完美。而 reCAPTCHA 将这些上无法由电脑自动识别的单词扫描下来,以图片的形式存入数据库,并在网络上以验证码(CAPTCHA)的形式交由人类来破译,而破译的结果将被用于帮助世界上的实体书加快数字化的进程。

你可能会问,既然电脑无法识别这些单词,那么 reCAPTCHA 系统又怎么知道用户填写的是正确的验证码呢?reCAPTCHA 的网站上有详细的解释

一个不能被 OCR 正确识别的新单词,总是会和另一个已知答案的单词一起提交给用户。接下来用户需要同时识别这两个单词。如果用户正确识别出其中已知答案的那个单词,系统即假设用户所识别出的未知答案的新单词也可能是正确的。如果其他的用户也识别出相同的答案,系统则逐步提高这种可能性。最终当这种可能性超过某个阈值时,系统就可以将其认作已知答案的单词了。

言归正传,reCAPTCHA 有丰富的 API,包括对 PHP、JAVA、Ruby,自然还有 Python 的支持。如果要在 Django 上使用 reCAPTCHA,我们可以从 PyPI 下载 reCAPTCHA 的 Python 客户端,或者直接用 easy_install 安装。


# easy_install recaptcha-client

不知道为什么,虽然安装成功了,但是我不能导入 recaptcha 模块(wyt:import captcha 或 import recaptcha 都没有用,哪位如果顺利导入过的话,能不能分享一下你的经验呢?先谢了),所以我是把 egg 中的脚本直接复制到我的 Django 项目目录下了。

接下来,我们需要在 reCAPTCHA 注册并申请一对公钥/私钥,并将其保存在 settings.py 中。

settings.py

1 # reCAPTCHA keys
2 RECAPTCHA_PUBLIC_KEY = "6LdqgAIAAAUSHDmo4IIBmsjUsduAUMUoBDZc3J_T"
3 RECAPTCHA_PRIVATE_KEY = "6LdqgAIAAbjsJKLbj2KOjPO6D4isfJ_AzLSO_256"

然后,我们要为注册表单添加验证码了。在 Django Snippets 上,oggy 已经将 reCAPTCHA 抽象成一个 RecaptchaForm 类,所以我们只要让注册表单(Registration Form)继承这个类,就可以为注册表单添加验证码的功能了。由于 Django newform 默认是根据 field 的定义顺序来生成表单,所以在继承 RecaptchaForm 的时候,也应该把它放在最后继承。

recaptcha/forms.py

01 from django import newforms as forms
02 from django.newforms import ValidationError
03 from django.conf import settings
04 from recaptcha import captcha
05 
06 
07 class RecaptchaWidget(forms.Widget):
08     """ A Widget which "renders" the output of captcha.displayhtml """
09     def render(self, *args, **kwargs):
10         return captcha.displayhtml(settings.RECAPTCHA_PUBLIC_KEY)
11 
12 class DummyWidget(forms.Widget):
13     """
14     A dummy Widget class for a placeholder input field which will
15     be created by captcha.displayhtml
16 
17     """
18     # make sure that labels are not displayed either
19     is_hidden=True
20     def render(self, *args, **kwargs):
21         return ''
22 
23 class RecaptchaForm(forms.Form):
24     """
25     A form class which uses reCAPTCHA for user validation.
26    
27     If the captcha is not guessed correctly, a ValidationError is raised
28     for the appropriate field
29     """
30     recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
31     recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
32 
33     def __init__(self, request, *args, **kwargs):
34         super(RecaptchaForm, self).__init__(*args, **kwargs)
35         self._request = request
36 
37     def clean_recaptcha_response_field(self):
38         if 'recaptcha_challenge_field' in self.cleaned_data:
39             self.validate_captcha()
40         return self.cleaned_data['recaptcha_response_field']
41 
42     def clean_recaptcha_challenge_field(self):
43         if 'recaptcha_response_field' in self.cleaned_data:
44             self.validate_captcha()
45         return self.cleaned_data['recaptcha_challenge_field']
46 
47     def validate_captcha(self):
48         rcf = self.cleaned_data['recaptcha_challenge_field']
49         rrf = self.cleaned_data['recaptcha_response_field']
50         ip_address = self._request.META['REMOTE_ADDR']
51         check = captcha.submit(rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, ip_address)
52         if not check.is_valid:
53             raise ValidationError('You have not entered the correct words')


user/forms.py

01 from myproject.recaptcha.forms import RecaptchaForm
02 
03 class RegistrationForm(forms.Form):
04     """
05     Form for registering a new user account.
06      
07     """
08 
09 class RegistrationFormWithRecaptcha(RegistrationForm, RecaptchaForm):
10     """
11     Subclass of ``RegistrationForm`` and ``RecaptchaForm`` which can tell whether its user
12     is a human or a computer.
13 
14     """

2008年7月4日

有备无患:浏览器端缓存(基于 jQuery)

最近在翻《Ajax 设计模式》,所以想把其中的一部分模式实现出来练手 jQuery,今天写了一个简单的引入 LRU 算法的浏览器端缓存(Browser Side Cache)。

浏览器端缓存用来保留服务器返回的查询结果。这种缓存是一个 Javascript 里类似映射的对象,存储成对的查询结果;查询是缓存的键(Key),服务器返回的结果是缓存的值(Value)。因此,每当浏览器需要查询服务器时,先检查缓存。如果该查询是缓存中的一个键,则与键对应的值将被当作结果,而不必再向服务器查询。

LRU (Least Recently Used)是将存储在缓存中的自上一次获取之后,最长时间未被使用的项目(Item)丢弃的一种算法。可以用两个数组(Array)来实现,其一是用来存储查询结果的键值对(Key-Value Pairs),其二是一个先进先出(FIFO)的队列,每一个新项目被塞入队列的尾部,并随着后续项目的跟进,逐渐逼近队列的头部。当队列全满时,每次向尾部塞入一个新项目,就要从头部弹出一个旧项目。而每当一个项目被缓存查询时,它会被送回到队列的尾部,这样可以确保最长时间未被使用的项目总是在开头处。

另外,还有一种常用的缓存算法,LFU (Least Frequently Used),它将自上一次获取后最少被使用的项目丢弃。

顺便一说,下面贴出的代码是由代码发芽网生产的纯 HTML。代码发芽网是一个“无需插件支持 Blog 代码高亮,支持近百种编程语言,多种配色主题支持,代码版本管理”的代码片段管理网站。因为不是由 Javascript 脚本来高亮处理,所以在 feed 里也一样可以看到效果,这一点很赞。不过,由于用处不大的 id 和 class 也包含在生成的 HTML 里,所以体积偏大。比如说下面这段代码,在删除了多余的 id 和 class,套上代码专用的 precode 标签,并将 &nbsp; 还原成空格之后,体积可以缩小一半仅17KB(原来35KB),更适合 blog 来贴代码。


cache.js

01 function Cache(size) {
02     /* A browser side LRU cache 
03      * Author: Wu Yuntao <http://luliban.com/blog/>
04      * License: GPLv3
05      *
06      * Usage:
07      * var cache = new Cache(10);   // create a new cache object
08      * cache.put('w', 'wiki');      // put an item into cache
09      * cache.get('w');              // get the value of item with key
10      * cache.remove('w');           // remove an item with specified key
11      * cache.initialize()           // re-initialize the cache
12      * cache.size(10)               // resize the cache
13      */
14     this.initialize(size);
15 }
16 
17 Cache.prototype = {
18     initialize: function(size) {
19         /* Initialize cache.
20          * ``size`` is the number of maxmium items this cache should hold.
21          * Default is maxium integer.
22          */
23         this._keys = new Array();
24         this._items = new Array();
25         this._size = size || this._size || Number.MAX_VALUE;
26     },
27 
28     is_empty: function() {
29         /* Check if cache is empty
30          */
31         return (this._keys.length == 0 && this._items.length == 0);
32     },
33 
34     size: function(size) {
35         /* Resize cache if ``size`` is specified, or return accual size of cache.
36          */
37         if (typeof size == 'undefined') return this._keys.length;
38         return this._size = size;
39     },
40 
41     put: function(key, value) {
42         /* Put a new item into cache, if the size of cache reaches limit,
43          * cache will remove the least recently used (LRU) automatically.
44          * ``key`` of an item should be a string.
45          * ``value`` of an item could be anything, string, array or object.
46          * If ``value`` is not defined, returns null.
47          */
48         if (typeof value != 'undefined') {
49             this._keys.push(key);
50             this._items[key] = value;
51 
52             if (this._keys.length > this._size) {
53                 this.remove_least();
54             }
55         }
56         return value;
57     },
58 
59     get: function(key) {
60         /* Retrieve an item by its key and move it to the tail of cache.
61          * If the item of ``key`` does not exist, returns null.
62          */
63         if (typeof this._items[key] == 'undefined') return null;
64         var used_key = this._remove_key(key);
65         this._keys.push(used_key);
66         return this._items[key];
67     },
68 
69     remove_least: function() {
70         /* Manually remove the least recently used item. */
71         if (!this.is_empty()) this.remove(this._keys[0]);
72     },
73 
74     remove: function(key) {
75         /* Remove an item by its key.
76          * If the item of ``key`` does not exist, returns null.
77          */
78         if (typeof this._items[key] == 'undefined') return null;
79         this._remove_key(key);
80         return this._remove_value(key);
81     },
82 
83     _remove_key: function(key) {
84         /* Remove the ``key`` in ``this._keys``.
85          */
86         var i = $.inArray(key, this._keys);
87         return this._keys.splice(i, 1);
88     },
89 
90     _remove_value: function(key) {
91         /* Remove the item in ``this._items``. */
92         var value = this._items[key];
93         delete this._items[key];
94