PIL应用之生成验证码图片

现在的网页中,为了防止机器人提交表单,图片验证码是很常见的应对手段之一。这里就不详细介绍了,相信大家都遇到过。

现在就给出用Python的PIL库实现验证码图片的代码。代码中有详细注释。

#!/usr/bin/env python
#coding=utf-8

import random
from PIL import Image, ImageDraw, ImageFont, ImageFilter

_letter_cases = "abcdefghjkmnpqrstuvwxy" # 小写字母,去除可能干扰的i,l,o,z
_upper_cases = _letter_cases.upper() # 大写字母
_numbers = ''.join(map(str, range(3, 10))) # 数字
init_chars = ''.join((_letter_cases, _upper_cases, _numbers))

def create_validate_code(size=(120, 30),
                         chars=init_chars,
                         img_type="GIF",
                         mode="RGB",
                         bg_color=(255, 255, 255),
                         fg_color=(0, 0, 255),
                         font_size=18,
                         font_type="ae_AlArabiya.ttf",
                         length=4,
                         draw_lines=True,
                         n_line=(1, 2),
                         draw_points=True,
                         point_chance = 2):
    '''
    @todo: 生成验证码图片
    @param size: 图片的大小,格式(宽,高),默认为(120, 30)
    @param chars: 允许的字符集合,格式字符串
    @param img_type: 图片保存的格式,默认为GIF,可选的为GIF,JPEG,TIFF,PNG
    @param mode: 图片模式,默认为RGB
    @param bg_color: 背景颜色,默认为白色
    @param fg_color: 前景色,验证码字符颜色,默认为蓝色#0000FF
    @param font_size: 验证码字体大小
    @param font_type: 验证码字体,默认为 ae_AlArabiya.ttf
    @param length: 验证码字符个数
    @param draw_lines: 是否划干扰线
    @param n_lines: 干扰线的条数范围,格式元组,默认为(1, 2),只有draw_lines为True时有效
    @param draw_points: 是否画干扰点
    @param point_chance: 干扰点出现的概率,大小范围[0, 100]
    @return: [0]: PIL Image实例
    @return: [1]: 验证码图片中的字符串 
    '''

    width, height = size # 宽, 高
    img = Image.new(mode, size, bg_color) # 创建图形
    draw = ImageDraw.Draw(img) # 创建画笔

    def get_chars():
        '''生成给定长度的字符串,返回列表格式'''
        return random.sample(chars, length)

    def create_lines():
        '''绘制干扰线'''
        line_num = random.randint(*n_line) # 干扰线条数

        for i in range(line_num):
            # 起始点
            begin = (random.randint(0, size[0]), random.randint(0, size[1]))
            #结束点
            end = (random.randint(0, size[0]), random.randint(0, size[1]))
            draw.line([begin, end], fill=(0, 0, 0))

    def create_points():
        '''绘制干扰点'''
        chance = min(100, max(0, int(point_chance))) # 大小限制在[0, 100]
        
        for w in xrange(width):
            for h in xrange(height):
                tmp = random.randint(0, 100)
                if tmp > 100 - chance:
                    draw.point((w, h), fill=(0, 0, 0))

    def create_strs():
        '''绘制验证码字符'''
        c_chars = get_chars()
        strs = ' %s ' % ' '.join(c_chars) # 每个字符前后以空格隔开
        
        font = ImageFont.truetype(font_type, font_size)
        font_width, font_height = font.getsize(strs)

        draw.text(((width - font_width) / 3, (height - font_height) / 3),
                    strs, font=font, fill=fg_color)
        
        return ''.join(c_chars)

    if draw_lines:
        create_lines()
    if draw_points:
        create_points()
    strs = create_strs()

    # 图形扭曲参数
    params = [1 - float(random.randint(1, 2)) / 100,
              0,
              0,
              0,
              1 - float(random.randint(1, 10)) / 100,
              float(random.randint(1, 2)) / 500,
              0.001,
              float(random.randint(1, 2)) / 500
              ]
    img = img.transform(size, Image.PERSPECTIVE, params) # 创建扭曲

    img = img.filter(ImageFilter.EDGE_ENHANCE_MORE) # 滤镜,边界加强(阈值更大)

    return img, strs

if __name__ == "__main__":
    code_img = create_validate_code()
    code_img.save("validate.gif", "GIF")

最后结果返回一个元组,第一个返回值是Image类的实例,第二个参数是图片中的字符串(比较是否正确的作用)。

需要提醒的是,如果在生成ImageFont.truetype实例的时候抛出IOError异常,有可能是运行代码的电脑没有包含指定的字体,需要下载安装。

生成的验证码图片效果:

validate

这时候,细心的同学可能要问,如果每次生成验证码,都要先保存生成的图片,再显示到页面。这么做让人太不能接受了。这个时候,我们需要使用python内置的StringIO模块,它有着类似file对象的行为,但是它操作的是内存文件。于是,我们可以这么写代码:

try:
    import cStringIO as StringIO
except ImportError:
    import StringIO

mstream = StringIO.StringIO()
    
img = create_validate_code()[0]
img.save(mstream, "GIF")

这样,我们需要输出的图片的时候只要使用“mstream.getvalue()”即可。比如在Django里,我们首先定义这样的url:

from django.conf.urls.defaults import *

urlpatterns = patterns('example.views',
    url(r'^validate/$', 'validate', name='validate'),
)

在views中,我们把正确的字符串保存在session中,这样当用户提交表单的时候,就可以和session中的正确字符串进行比较。

from django.shortcuts import HttpResponse

from validate import create_validate_code

def validate(request):
    mstream = StringIO.StringIO()
    
    validate_code = create_validate_code()
    img = validate_code[0]
    img.save(mstream, "GIF")
    
    request.session['validate'] = validate_code[1]
    
    return HttpResponse(mstream.getvalue(), "image/gif")

标签

赞这篇文章

分享到

33个评论

  1. settings

    想请教一下这个验证码的使用问题:就是最后一步那个内存中生成的图片文件怎么样传递到模板当中,显示为一个图片?我想了半天也没想出来。能否指点一下?

  2. @秦续业 作者

    呃,我上面不是写了一个URL吗,你直接在页面中写<img src="/validate/" />就可以了呀。

  3. wl6179

    楼主您好,django本身不是有了CSRF机制吗,CSRF是否可以在一定程度上防止垃圾信息提交呢?!还是说CSRF根本就不能防止重复提交这类问题?如果可以,请您回复的同时也通过email提醒我一声,谢谢楼主!

  4. 秦续业 作者

    CSRF和垃圾信息防范似乎没有什么联系,不过据说开启CSRF能够阻止一些简单的自动脚本。
    开启了CSRF还是会收到很多垃圾消息,如果想要阻止重复提交和spam消息,还是要在业务逻辑中实现吧。这只是我的个人意见。

    ps,我的博客是有邮件回复功能的。

  5. wl6179

    哈哈,牛啊,网站做的挺智能的,大赞一个!哦,知道了,看来CSRF还是没有什么用,是吧~还得靠我们的验证码、ip识别限制提交次数等方法。看了你的好文,很实战,总谈到一些开发中真实要用到的功能,期待更多好文了……我想我得订阅你的博客了:)

  6. wl6179

    秦,为何你的代码总会用到 url(r'^validate/$', 'validate', name='validate'), 的 name='validate'?? 你用的django是啥版本呀,为何我一用这个name属性就挂掉呢

  7. wl6179

    OK知道了,我的是1.3.1,看来dj可能不再兼容这个name属性了……老秦留意了,呵呵~~

  8. 秦续业 作者

    刚刚看了一下官方文档,看这一段,https://docs.djangoproject.com/en/1.4/topics/http/urls/#url

  9. wl6179

    老秦,你整的这个字体 font_type="ae_AlArabiya.ttf", 太罕见了;我报错弄了接近1个小时才发现,原来不是最可疑的IO、机器配置问题(报错说no resource还以为是要装什么插件,还是IO调用不到资源等呢~),而是字体缺少的问题……前人已摔下悬崖,后人请借鉴…呵呵~~~ 老秦找一个通用一点的字体放上去吧,可能你的是Ubuntu系统的字体,我这个老Win就摔下去了……

  10. 新手

    我刚学没两天 想问个问题: 现在已经做到生成验证码图片了,但是在后台该怎样判断用户输入的验证码是否正确呢?


    我是先 verifycode = request.POST['code'] 获取用户输入的验证码

    然后对比 if verifycode == request.session('identify_code'):


    但是这样会报错啊


    请问该怎么写呢?

    PS:生成验证码图片的代码中,已经用

    request.session['identify_code'] = rand_str

    将验证码放到session里面了

  11. 报的错误
    MOD_PYTHON ERROR

    ProcessId: 15809
    Interpreter: 'localhost'

    ServerName: 'localhost'
    DocumentRoot: '/home/aldrich/workspace'

    URI: '/validate/'
    Location: '/'
    Directory: None
    Filename: '/home/aldrich/workspace/validate'
    PathInfo: '/'

    Phase: 'PythonHandler'
    Handler: 'django.core.handlers.modpython'

    Traceback (most recent call last):

    File "/usr/lib/python2.7/dist-packages/mod_python/importer.py", line 1537, in HandlerDispatch
    default=default_handler, arg=req, silent=hlist.silent)

    File "/usr/lib/python2.7/dist-packages/mod_python/importer.py", line 1229, in _process_target
    result = _execute_target(config, req, object, arg)

    File "/usr/lib/python2.7/dist-packages/mod_python/importer.py", line 1128, in _execute_target
    result = object(arg)

    File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/modpython.py", line 180, in handler
    return ModPythonHandler()(req)

    File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/modpython.py", line 158, in __call__
    response = self.get_response(request)

    File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 179, in get_response
    response = self.handle_uncaught_exception(request, resolver, sys.exc_info())

    File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 221, in handle_uncaught_exception
    return debug.technical_500_response(request, *exc_info)

    File "/usr/local/lib/python2.7/dist-packages/django/views/debug.py", line 66, in technical_500_response
    html = reporter.get_traceback_html()

    File "/usr/local/lib/python2.7/dist-packages/django/views/debug.py", line 286, in get_traceback_html
    c = Context(self.get_traceback_data())

    File "/usr/local/lib/python2.7/dist-packages/django/views/debug.py", line 244, in get_traceback_data
    frames = self.get_traceback_frames()

    File "/usr/local/lib/python2.7/dist-packages/django/views/debug.py", line 390, in get_traceback_frames
    pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(filename, lineno, 7, loader, module_name)

    File "/usr/local/lib/python2.7/dist-packages/django/views/debug.py", line 371, in _get_lines_from_file
    context_line = source[lineno].strip('\n')

    IndexError: list index out of range

  12. 秦续业 作者

    这个……是django报的错。
    stackoverflow上有人说到这个:http://stackoverflow.com/questions/3257414/django-in-debug-py-list-index-out-of-range
    大意是清一下pyc文件,重启Apache等等。然后再看看报什么错……

  13. 你好,现在出现cannot open resource,看到你的帖子说是缺少字体的原因,但是我将改字体放在文件所在目录时还是有这个错误

  14. Joel

    您好,我是在用你的ChineBlog代码试着去运行,但是没有成功,原因是No module named Image
    第三方库包都安装好了的
    求解答。。谢谢!

  15. rory_jia

    现在单纯的运行验证码,可以得到想要的图片,现在使用的并没有什么框架 只是用了mod_python 和Cheetah 实验了好多次 都不能在前台得到该验证码 不知这种情况下 是如何返回验证码图片让前台能够访问 以及显示!

  16. Dr_苏泊

    Hi,大神,你好,根据你的代码修改为windows下的字体文件后,运行报这个错误,求解惑
    F:\MyPython>yanzhengmatupian.py
    Traceback (most recent call last):
    File "F:\MyPython\yanzhengmatupian.py", line 110, in <module>
    code_img.save("validate.gif", "GIF ")
    AttributeError: 'tuple' object has no attribute 'save'

  17. danjack

    File "E:\plugins\PyDev\plugins\org.python.pydev_2.8.2.2013090511\pysrc\pydevd.py", line 1446, in <module>
    debugger.run(setup['file'], None, None)
    File "E:\plugins\PyDev\plugins\org.python.pydev_2.8.2.2013090511\pysrc\pydevd.py", line 1092, in run
    pydev_imports.execfile(file, globals, locals) #execute the script
    File "D:\SGM\trunk\Src\cms\base\generateCode.py", line 112, in <module>
    code_img = create_validate_code()
    File "D:\SGM\trunk\Src\cms\base\generateCode.py", line 93, in create_validate_code
    strs = create_strs()
    File "D:\SGM\trunk\Src\cms\base\generateCode.py", line 81, in create_strs
    font = ImageFont.truetype(font_type, font_size)
    File "E:\python27\lib\site-packages\PIL\ImageFont.py", line 218, in truetype
    return FreeTypeFont(filename, size, index, encoding)
    File "E:\python27\lib\site-packages\PIL\ImageFont.py", line 134, in __init__
    self.font = core.getfont(file, size, index, encoding)
    File "E:\python27\lib\site-packages\PIL\ImageFont.py", line 34, in __getattr__
    raise ImportError("The _imagingft C module is not installed")
    ImportError: The _imagingft C module is not installed

  18. 心竞自然凉

    今天刚在python开发者大会上听了大佬的分享,哈哈,我还是个py初学者,虽然还听不懂,但是真的讲的不错呢,印象深刻,赞赞赞!

给作者留言

关于作者

残阳似血(@秦续业),程序猿一枚,把梦想揣进口袋的挨踢工作者。现加入阿里云,研究僧毕业于上海交通大学软件学院ADC实验室。熟悉分布式数据分析(DataFrame并行化框架)、基于图模型的分布式数据库和并行计算、Dpark/Spark以及Python web开发(Django、tornado)等。

博客分类

点击排行

标签云

扫描访问

主题

残阳似血的微博