在Python中用PIL做验证码

  1. 基本操作
  2. 加入变化
  3. 最后完成
  4. 代码

为了防垃圾机器人,验证码是一种常用的手段。而自己来实现验证码也是很简单的事,只需要了解一点图像处理的方法就可以了。 PIL 是 Python 的一个图像处理库,可以很方便地处理位图。

首先考虑验证的制作方法,我们只想简单点的情况:

  1. 生成一个固定大小的白色图片。
  2. 在图片上随机写几个字母。

就是这样,最简单的情况。我们先实现,再看怎么能加点变化,以至不那么容易被破掉。

后面不会细讲 PIL 的使用方法,有兴趣,请浏览官方的文档

1. 基本操作

先来看如何得到一张带指定字母的图片。 PIL 中对图片的操作一般是通过 image 对象来完成,这个对象可以是从图片文件中得到的,已经包含了位图信息的对象。也可以一个我们指定大小创建的,不包含图片信息的“空对象”。

  # -*- coding: utf-8 -*-
  
  from PIL import Image
  
  im = Image.new('1', (100, 100), 'white')
  im.show()

上面所示的代码,我们就可以得到一个 100x100 的白底空图片了。并且你可以看到图片显示了出来。

Image 的 new 方法新创建一个图片对象,第一个参数指定“模式”,不同的模式对应的每个像素的颜色表示也不同。比如:

  • 1 按文档说是单色模式,但事实上它和 L 一样,是灰度模式。
  • L 灰度模式,每个像素的颜色使用 0-255 的整数表示。
  • RGBA 三元色加透明度的表示方式,每个像素的颜色使用类似 (12,34,23,1) 的 tuple 表示。

关于模式不细说了,我们只使用最简单的单色,我们最终的图片也只需要黑白两色。

new 的第二个参数指定图片的大小,第三个参数指定背影色。

show 方法是使用系统提供的工具把图片马上显示出来。

接下来要做的事,就是在这张白色图片上写几个字符了,这要用到 ImageDraw 对象:

  # -*- coding: utf-8 -*-
  
  from PIL import Image
  from PIL import ImageDraw
  
  im = Image.new('1', (100, 100), 'white')
  draw = ImageDraw.Draw(im)
  draw.text((0, 0), 'hello world!')
  im.show()

结果如下图:

我们使用 Draw 对象的在新创建的白底图片的 (0,0) 位置写了 hello world! 。

字有些小,这是使用默认的字体的原因。我们可以使用指定的字体来生成验证码。

对于传统的位图字体, PCF, BDF 扩展名结束,先使用 PIL 提供的 pilfont.py 工具,产生 PIL 使用的专用字体文件:

  pilfont.py  xxx.pcf

当前目录下,会得到两个需要的文件, xxx.pil 和 xxx.pbm ,然后要使用字体时:

  # -*- coding: utf-8 -*-
  
  from PIL import Image
  from PIL import ImageDraw
  from PIL import ImageFont
  FONT = ImageFont.load('xxx.pil')
  
  im = Image.new('1', (100, 100), 'white')
  draw = ImageDraw.Draw(im)
  draw.text((0, 0), 'hello world!', font=FONT)
  im.show()

在使用 text 方法时,使用 font 参数指定字体就可以了。

现在字会变得大多了:

如何要使用现在常用的矢量字体,可以这样:

  font = ImageFont.truetype("arial.ttf", 15)

好了,能写出字了,就可以当验证码来用了。剩下就是加入一些图像的变化,以使验证码不容易被机器识别。

2. 加入变化

现在我们只是生成了一张图片,至于图片中的字母,随便找一个 OCR 软件都可以识别出来,我们还需要对它做一些变化处理。

验证码识别的难点之一就是字符分割,只要单个字符分割出来了,通过提取的样本进行最简单的匹配都可以达到很高的识别率。而给字符分割制造麻烦的最简单办法就是让字符与字符粘在一起。

前面已经介绍了如何在图片上写字。而让字符粘在一起,只需要分别控制每个字符的位置即可实现。这里,我自己实现的方法,是在一个足够小的区域中,让每个字符随机分布,因为随机选择的区域有限,所以,字符与字符之前有很大的概率会连在一起。另外,随机分布的话,还需要判断字符与字符之间的水平距离差,这个差值要大于一个临界值,以使人可以容易分辨出字符从左到右的顺序。

代码看最后的吧,这里使用示意图说明实现方法:

假设我们最后得到的图片长是 3-4 的距离,那么 4 个字符可以随机分布的区域在 1-2 之间,因为字符的位置是按矩形的左上角算的,避免出免字符超出边界而看不到的情况。如图所示,当 1-2 之间距离足够小的时间, 4 个字符就有很大的概率会重叠了。

另外的一点,就是对于字符与字符之间的水平距离,比如图中 5 和 6 的水平距离,它们的距离应该大于一个值,以保证这 4 个字符可以被看得出从左到右的顺序。而我们的字符是随机生成,并且是随机分布,所以,我们最后也是根据这 4 个字符的 X 轴位置的升序排列来得到“正确答案”的。

字符随机分布后,为了进一步加大机器识别的难度,我们还可以添加几根干扰线,这个就比较简单了。如图所示:

我们把整个图片看成 4 个象限,干扰线总是从第一象限的随机一点开始,以另外三个象限的随机一点结束。这样,干扰线同样也有很大的概率可以覆盖到图片上的字符。

关于画线,在 PIL 中,可以使用 ImageDraw 对象的 line 方法:

  # -*- coding: utf-8 -*-
  
  import PIL
  from PIL import Image
  from PIL import ImageDraw
  
  im = Image.new('1', (500, 500), 'white')
  draw = ImageDraw.Draw(im)
  draw.line(((0, 0), (100, 200)))
  im.show()

3. 最后完成

要做的事差不多了,最后输出图片就可以了。因为我们是验证码应用,所以不需要把图像的数据写到具体的文件当中,只需要输出字节流让应用返回给浏览器即可。

保存图像信息,直接使用 Image 对象的 save 方法即可。这个方法接受两个参数,第一个参数是要写入的文件对象,第二个参数是指定文件类型。

  fileio = StringIO()
  im.save(fileio, 'gif')
  im.show()  

文件对象我们就使用 cStringIO 模块中的 StringIO 来代替了。

最后的效果是这样的:

更麻烦的,你可以给字符加入旋转效果,写一个字符就随机旋转一定角度。 PIL 本身提供了对图片进行线性变换的一些操作方法。如果这些不能满足你,你也可以精确控制每一个像素的值。

4. 代码

  # -*- coding: utf-8 -*-
  #AUTHOR: yeshengzou # # gmail.com
  #DATE: 2012.4.23
  #LICENCE: GPLv3
  
  import PIL
  from PIL import Image
  from PIL import ImageDraw
  from PIL import ImageFont
  from random import randint
  from cStringIO import StringIO
  
  CHAR = 'acdefghijkmnpqrstuvwxyABCDEFGHJKLMNPQRSTUVWXY345789'
  LEN = len(CHAR) - 1
  PADDING = 30 
  X_SPACE = 6 #两个字符之间最少相隔多少个像素
  TRY_COUNT = 30 #随机字符的位置尝试最多多少次,避免死循环
  WIDTH = 70
  HEIGHT = 40
  FONT = ImageFont.load('font.pil')
  
  def gen():
      im = Image.new('1', (WIDTH, HEIGHT), 'white')
      draw = ImageDraw.Draw(im)
      w, h = im.size
  
      #S = [(x, y, 'c')]
      S = []
      x_list = []
      y_list = []
      n = 0
      while True:
          n += 1
          if n > TRY_COUNT:
              break
          x = randint(0, w - PADDING)
          flag = True
          for i in x_list:
              if abs(x - i) < X_SPACE:
                  flag = False
                  continue
              if not flag:
                  break
          if not flag:
              continue
  
          y = randint(0, h - PADDING)
          x_list.append(x)
          y_list.append(y)
          S.append((x, y, CHAR[randint(0, LEN)]))
          if len(S) == 4:
              break
  
      for x, y, c in S:
          draw.text((x, y), c, font=FONT)
  
      #加3根干扰线
      for i in range(3):
          x1 = randint(0, (w - PADDING) / 2)
          y1 = randint(0, (h- PADDING / 2))
          x2 = randint(0, w)
          y2 = randint((h - PADDING / 2), h)
          draw.line(((x1, y1), (x2, y2)), fill=0, width=1)
  
      S.sort(lambda x, y: 1 if x[0] > y[0] else -1)
      char = [x[2] for x in S]
      fileio = StringIO()
      im.save(fileio, 'gif')
      im.show()
      return ''.join(char), fileio
  
  if __name__ == '__main__':
      print gen()  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值