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 标签,并将   还原成空格之后,体积可以缩小一半仅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         return value;
95     }
96 
97 };

标签:

2008年7月2日

有备无患:pinyin-urlify - 根据汉字自动生成拼音 URL

根据文章标题中的英语单词自动生成文章的 URL 是一个很好的体验,比如标题为“Girl's death sparks riot in China”,会被转换成“girls-death-sparks-riot-china”这样与之对应的 URL。这样既可获得 Cool URIs,又便于用户记忆,同时也有利于 SEOBloggerWordpress 也都支持类似体验。而中文的文章标题,也可以先将其中的汉字转化为拼音后再组成相应的 URL,比如标题为“俯卧撑”,可以被转换成“fu-wo-cheng”。

pinyin-urlify 是我这几天写的能根据汉字自动生成拼音 URL 的 Python 脚本。它可以将汉字映射成有英语字母组成的拼音,和多种西方语言的字母(拉丁语、希腊语、俄语等)映射成相近的英语字母。要说明的是,汉字-拼音的映射表是从 pyzh 项目中获得,而其他西方语言的映射表则是从 Django 项目中获得,特表感谢

pinyin-urlify 支持自定义停用词列表(stop words)和保留词列表(reserved words)。停用词列表中的单词都会被过滤,不会出现在生成的 URL 中。生成的 URL 如果和保留词列表中的某一个单词匹配,则会被替换成默认的 URL 字符串。pinyin-urlify 可以指定 URL 的最大长度。不过,如果遇到有较多汉字组成的文章标题的话,实际生成的 URL 可能会比限定的最大长度小一些。我希望能在以后修正这一点。

使用示例

>>> from urlify import urlify
>>> urlify(u'三个俯卧撑引发的血案', default='blog_post',
...        max_length=50, stop_words=[u'is', u'a', u'an'],
...        reserved_words=[u'new', u'edit'])
u'san-ge-fu-wo-cheng-yin-fa-de-xue'


欢迎大家用 svn 检出 pinyin-urlify 的副本来使用。欢迎任何意见或建议 :-)

$ svn checkout http://pinyin-urlify.googlecode.com/svn/trunk/ pinyin-urlify

标签: