小白都看得懂的使用Python生成随机验证码图片,以及后续优化方案

环境:Anaconda3-2020.02

首先我们分解一下需要做什么:

  1. 生成随机的字母字符串
  2. 生成生成随机的背景色
  3. 生成随机的字体颜色

把随机的字符串用随机的颜色渲染,然后放到一块随机的背景色上面,基本的验证码就好了

大概就是这个效果:

小白都看得懂的使用Python生成随机验证码图片,以及后续优化方案_第1张图片

那么我们来看看如何实现

1. 随机的字符串

随机字符串的生成比较简单,使用Random库,random.randomint(65, 90)即可(65-90刚好是A-Z的ASCII码)

注意:直接randomint生成出来的是个整数,python是弱类型语言,需要显式指定转化,所以要char(random.randomint(65, 90))

2. 随机的颜色

颜色嘛,RGB嘛,当然是三个random.randomint(0,255)啦,不用多解释吧

(字体和背景都可以复用这个功能)

组合一下,我们就有了初步的代码

import random

class TestCode:
    # 生成一个随机的长度为length的包含A-Z的字符串
    def GenerateRandomChar(self, length=4):
        res = ''
        for i in range(0, length):
            res += (chr(random.randint(65, 90)))  # 生成A-Z
        return res

    # 生成一个随机的字体色
    def GenerateRandomFontColor(self):
        return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

    # 生成一个随机的背景色
    def GenerateRandomCanveColor(self):
        return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

那么现在我们面临了一个问题:我生成的函数有了!怎么显示!

这个时候就要引入PIL库了,关于PIL的功能有很多文章介绍了,这里就不多赘述。

还是一样,我们来分解一下要做什么:

  1. 需要一块指定随机色的画板
  2. 需要把字显示在上面

怎么做呢?

1. 指定随机色的画板

需要画(图)板(片),那就生成一个:使用PIL的Image.new就行了

2. 显示文字

文字的显示,还是一样利用PIL的功能,使用ImageFont.load_default()加载默认字体,然后用ImageDraw.Draw来画上去

ok想明白了,我们动手写代码:

import random

import PIL.Image as Image
import PIL.ImageDraw as Draw
import PIL.ImageFont as Font


class TestCode:
    # 生成一个随机的长度为length的包含A-Z的字符串
    def GenerateRandomChar(self, length=4):
        res = ''
        for i in range(0, length):
            res += (chr(random.randint(65, 90)))  # 生成A-Z
        return res

    # 生成一个随机的字体色(必然大于(100,100,100))
    def GenerateRandomFontColor(self):
        return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

    # 生成一个随机的背景色(必然小于等于80,80,80))
    def GenerateRandomCanveColor(self):
        return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

    # 用传入色生成画布
    def GenerateCanve(self, width, height, color):
        return Image.new("RGB",(width, height), color)

    # 测试主函数
    def TestMain(self):
        font = Font.load_default()
        randomStr = self.GenerateRandomChar(10)
        randomFontColor = self.GenerateRandomFontColor()
        randomCanveColor = self.GenerateRandomCanveColor()
        resImg = self.GenerateCanve(300,100,randomCanveColor)
        drawer = Draw.Draw(resImg)
        drawer.text((10,30), text=randomStr, fill=randomFontColor, font=font)
        resImg.show()



if __name__ == '__main__':
    _test = TestCode()
    _test.TestMain()

运行一下:

小白都看得懂的使用Python生成随机验证码图片,以及后续优化方案_第2张图片

字,字好小!

怎么办?

让客户买放大镜!

当然不是!我们可以用PIL的功能把字放大,稍微调整一下,手动指定字体大小:font = Font.truetype("font.TTF",40)

font.TTF是某个字体文件,请换成自己的字体文件的路径哦,怎么找的话百度一下就好啦,聪明如你肯定找得到!

啊?为啥load_default的字体不能变化?

没有选项可以选择load_default字体的大小,因为这是一种光栅字体格式.如果你在ImageFont.py的代码中检查它实际上存储在base64中编码的字体信息

via http://www.voidcn.com/article/p-pmbjykvn-bus.html

未经查证,但是看起来很有道理

改好代码我们再执行一下?

看起来正常多了!再运行一下感受一下我的完美代码!

WTF!这色彩是怎么回事!有点难看清楚!

为什么会这样呢?我们来简单梳理一下

表征上:背景色和字体色十分接近

这就意味着,函数GenerateRandomFontColor和函数GenerateRandomCanveColor的返回值非常接近

我们看看代码

    # 生成一个随机的字体色
    def GenerateRandomFontColor(self):
        return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

    # 生成一个随机的背景色
    def GenerateRandomCanveColor(self):
        return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

大家都是random,都是0-255,生成出接近的颜色虽然概率较低,但是还是会有可能产生。

那么为了避免客户投诉我们歧视色弱或者歧视低端显示器,我们稍微调整一下代码

    # 生成一个随机的字体色(必然大于(100,100,100))
    def GenerateRandomFontColor(self):
        return random.randint(100, 255), random.randint(100, 255), random.randint(100, 255)

    # 生成一个随机的背景色(必然小于等于80,80,80))
    def GenerateRandomCanveColor(self):
        return random.randint(0, 80), random.randint(0, 80), random.randint(0, 80)

最接近的色彩是80,80,80到100,100,100,肉眼还是看的出区别的。

那么我们完成的代码是这个样子的:

import random

import PIL.Image as Image
import PIL.ImageDraw as Draw
import PIL.ImageFont as Font


class TestCode:
    # 生成一个随机的长度为length的包含A-Z的字符串
    def GenerateRandomChar(self, length=4):
        res = ''
        for i in range(0, length):
            res += (chr(random.randint(65, 90)))  # 生成A-Z
        return res

    # 生成一个随机的字体色(必然大于(100,100,100))
    def GenerateRandomFontColor(self):
        return random.randint(100, 255), random.randint(100, 255), random.randint(100, 255)

    # 生成一个随机的背景色(必然小于等于80,80,80))
    def GenerateRandomCanveColor(self):
        return random.randint(0, 80), random.randint(0, 80), random.randint(0, 80)

    # 用传入色生成画布
    def GenerateCanve(self, width, height, color):
        return Image.new("RGB",(width, height), color)

    # 测试主函数
    def TestMain(self):
        font = Font.truetype("font.TTF",40)
        randomStr = self.GenerateRandomChar(10)
        randomFontColor = self.GenerateRandomFontColor()
        randomCanveColor = self.GenerateRandomCanveColor()
        resImg = self.GenerateCanve(300,100,randomCanveColor)
        drawer = Draw.Draw(resImg)
        drawer.text((10,30), text=randomStr, fill=randomFontColor, font=font)
        resImg.show()



if __name__ == '__main__':
    _test = TestCode()
    _test.TestMain()

搞定收工?不不不,新需求来了:

所有字都是一个颜色,不行!

背景是纯色,不行!

那么,怎么处理让每个字颜色都不一样,让背景色也变成不是纯色呢?

我们来整理一下:

  1. 每个字颜色不一样,那我们每个字都单独用一个颜色渲染就好了
  2. 背景色不能是纯色,那我们每个像素点都随机生成好了

ok,理清楚了,我们来优化一下代码

import random

import PIL.Image as Image
import PIL.ImageDraw as Draw
import PIL.ImageFont as Font


class TestCode:
    # 生成一个随机A-Z
    def GenerateRandomChar(self):
        return chr(random.randint(65, 90))

    # 生成一个随机的字体色(必然大于(100,100,100))
    def GenerateRandomFontColor(self):
        return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

    # 生成一个随机的背景色(必然小于等于80,80,80))
    def GenerateRandomCanveColor(self):
        return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

    # 用生成随机色画布
    def GenerateCanve(self, width, height):
        res =  Image.new("RGB",(width, height))
        drawer = Draw.Draw(res)
        for x in range(0,width):
            for y in range(0, height):
                drawer.point((x,y), fill=self.GenerateRandomCanveColor())
        return res

    # 测试主函数
    def TestMain(self):
        font = Font.truetype("font.TTF",40)
        resImg = self.GenerateCanve(300,100)
        drawer = Draw.Draw(resImg)
        for i in range(0, 5):
            drawer.text((10, 10), text=self.GenerateRandomChar(), fill=self.GenerateRandomFontColor(), font=font)
        resImg.show()


if __name__ == '__main__':
    _test = TestCode()
    _test.TestMain()

运行!

字都堆在一起了喂!

为什么呢,因为我们循环里写字的drawer.text,给的坐标一直是10,10嘛

那么稍微调整一下,加入随机的间距

        thisX = random.randint(0,10)
        thisY = random.randint(10,40)
        for i in range(0, 5):
            drawer.text((thisX, thisY), text=self.GenerateRandomChar(), fill=self.GenerateRandomFontColor(), font=font)
            thisX = random.randint(0, 10)
            thisX += (i+1)*random.randint(50, 60)
            thisY = random.randint(10, 40)

再运行呢?

小白都看得懂的使用Python生成随机验证码图片,以及后续优化方案_第3张图片

哎,舒服多了

这个时候新需求又上来了:我想让字不那么正,稍微有个角度

好,满足你!我们可以用image的rotate方法来实现旋转,然后把旋转以后的字贴到底图上,搞定!

import random

import PIL.Image as Image
import PIL.ImageDraw as Draw
import PIL.ImageFont as Font
import PIL.ImageOps as ImageOps


class TestCode:
    # 生成一个随机A-Z
    def GenerateRandomChar(self):
        return chr(random.randint(65, 90))

    # 生成一个随机的字体色(必然大于(100,100,100))
    def GenerateRandomFontColor(self):
        return random.randint(130, 255), random.randint(130, 255), random.randint(130, 255)
    # 生成一个随机的背景色(必然小于等于80,80,80))
    def GenerateRandomCanveColor(self):
        return random.randint(0, 80), random.randint(0, 80), random.randint(0, 80)

    # 用生成随机色画布
    def GenerateCanve(self, width, height):
        res = Image.new("RGB", (width, height))
        drawer = Draw.Draw(res)
        for x in range(0, width):
            for y in range(0, height):
                drawer.point((x, y), fill=self.GenerateRandomCanveColor())
        return res

    # 生成一个随机字符的图片,并且旋转随机角度
    def GenerateRandomCharImgWithRotate(self):
        font = Font.truetype("font.TTF", 40)
        charImg = Image.new("L", (40, 40))
        drawer = Draw.Draw(charImg)
        drawer.text((0, 0), text=self.GenerateRandomChar(), fill=255, font=font)
        charImg = charImg.rotate(random.randint(0,80), expand=1)
        return charImg

    # 测试主函数
    def TestMain(self):
        resImg = self.GenerateCanve(400, 100)
        space = (400 - 4*10)/4
        for i in range(0, 4):
            thisX = random.randint((10 + 80)*(i), (10 + 80)*(i+1)-10)
            thisY = random.randint(10, 2 * 10)
            thisCharImg = self.GenerateRandomCharImgWithRotate()
            resImg.paste(ImageOps.colorize(thisCharImg, (0, 0, 0),
                                           self.GenerateRandomFontColor()),
                                           (thisX, thisY), thisCharImg)
        resImg.show()


if __name__ == '__main__':
    _test = TestCode()
    _test.TestMain()

运行一下:

那么,没有新需求了,代码写完了吗?

没有!

优化与重构应该贯穿于软件开发过程始终!要知道,经过训练的小学生(没有歧视小学生的意思)都可以写出计算机可以运行的代码,只有优秀的程序员才能写出人可以轻易看懂,并且非常好维护的代码!

接下来要做优化和重构:

首先我们注意到,方法GenerateRandomCanveColor和GenerateRandomFontColor除了randomint的范围外,完全一样,那么遇到这种情况,我们可以把重复的部分提取出来形成新的函数,来减少重复代码,提高可维护性,要记得,在代码的世界里,重复就是罪恶。

优化前:

    # 生成一个随机的字体色(必然大于(100,100,100))
    def GenerateRandomFontColor(self):
        return random.randint(130, 255), random.randint(130, 255), random.randint(130, 255)
    
    # 生成一个随机的背景色(必然小于等于80,80,80))
    def GenerateRandomCanveColor(self):
        return random.randint(0, 80), random.randint(0, 80), random.randint(0, 80)

优化后:

    # 生成一个范围在[min,max]范围内的颜色
    def GenerateColor(self, min, max):
        return random.randint(min, max), random.randint(min, max), random.randint(min, max)

    # 生成一个随机的字体色(必然大于(100,100,100))
    def GenerateRandomFontColor(self):
        return self.GenerateColor(100, 255)

    # 生成一个随机的背景色(必然小于等于80,80,80))
    def GenerateRandomCanveColor(self):
        return self.GenerateColor(0, 80)

可能会有人觉得这没什么大不了的,有些多此一举了。

很简单的一个例子:如果我现在不使用RGB,使用了CYMK,或者RGBA,原有的代码需要修改8处(6个random和2个新增的random),新代码就只有4处。而且如果要引入复杂规则,只有一个函数修改绝对比多个函数来的安全,首先就是你不会忘记有几个要改。(关于这点可以参考《重构:改善既有代码设计》的发散式变化和霰弹式修改章节)反正核心是:重复即是罪恶

运行一下,结果没啥问题,继续看看?

嗯,现在这个东西的使用很复杂啊

        resImg = self.GenerateCanve(400, 100)
        space = (400 - 4*10)/4
        for i in range(0, 4):
            thisX = random.randint((10 + 80)*(i), (10 + 80)*(i+1)-10)
            thisY = random.randint(10, 2 * 10)
            thisCharImg = self.GenerateRandomCharImgWithRotate()
            resImg.paste(ImageOps.colorize(thisCharImg, (0, 0, 0),
                                           self.GenerateRandomFontColor()),
                                           (thisX, thisY), thisCharImg)
        resImg.show()

用户要知道太多的细节了!

我们不想徒增使用者的工作量,毕竟程序员最讨厌的几件事情就是别人的东西太复杂,别人的东西没文档。

那么怎么办呢,我们可以提取出用户所需要的变化点,然后把不变化的内容进行封装

优化前:

    # 测试主函数
    def TestMain(self):
        resImg = self.GenerateCanve(400, 100)
        space = (400 - 4*10)/4
        for i in range(0, 4):
            thisX = random.randint((10 + 80)*(i), (10 + 80)*(i+1)-10)
            thisY = random.randint(10, 2 * 10)
            thisCharImg = self.GenerateRandomCharImgWithRotate()
            resImg.paste(ImageOps.colorize(thisCharImg, (0, 0, 0),
                                           self.GenerateRandomFontColor()),
                                           (thisX, thisY), thisCharImg)
        resImg.show()

优化后:

    # 生成一个二维码图片并返回
    def GenerateTestCode(self, width=400, height=100, charCount=4, alain=10):
        resImg = self.GenerateCanve(width, height)
        space = (width - charCount*alain)/charCount

        for i in range(0, charCount):
            thisX = random.randint((alain + space)*(i), (alain + space)*(i+1)-alain)
            thisY = random.randint(alain, 2 * alain)
            thisCharImg = self.GenerateRandomCharImgWithRotate()
            resImg.paste(ImageOps.colorize(thisCharImg, (0, 0, 0),
                                           self.GenerateRandomFontColor()),
                                           (thisX, thisY), thisCharImg)
        return resImg

    # 测试主函数
    def TestMain(self):
        resImg = self.GenerateTestCode()
        resImg.show()

这样修改以后,用户只需要调用一个函数就可以实现功能了,而且自己适当的修改函数参数,可以实现自定义的大小和字数,还有间距。

那么我们优化以后的代码是这个样子的:

import random

import PIL.Image as Image
import PIL.ImageDraw as Draw
import PIL.ImageFont as Font
import PIL.ImageOps as ImageOps


class TestCode:
    # 生成一个随机A-Z
    def GenerateRandomChar(self):
        return chr(random.randint(65, 90))

    # 生成一个范围在[min,max]范围内的颜色
    def GenerateColor(self, min, max):
        return random.randint(min, max), random.randint(min, max), random.randint(min, max)

    # 生成一个随机的字体色(必然大于(100,100,100))
    def GenerateRandomFontColor(self):
        return self.GenerateColor(100, 255)

    # 生成一个随机的背景色(必然小于等于80,80,80))
    def GenerateRandomCanveColor(self):
        return self.GenerateColor(0, 80)

    # 用生成随机色画布
    def GenerateCanve(self, width, height):
        res = Image.new("RGB", (width, height))
        drawer = Draw.Draw(res)
        for x in range(0, width):
            for y in range(0, height):
                drawer.point((x, y), fill=self.GenerateRandomCanveColor())
        return res

    # 生成一个随机字符的图片,并且旋转随机角度
    def GenerateRandomCharImgWithRotate(self):
        font = Font.truetype("font.TTF", 40)
        charImg = Image.new("L", (40, 40))
        drawer = Draw.Draw(charImg)
        drawer.text((0, 0), text=self.GenerateRandomChar(), fill=255, font=font)
        charImg = charImg.rotate(random.randint(0,80), expand=1)
        return charImg

    # 生成一个二维码图片并返回
    def GenerateTestCode(self, width=400, height=100, charCount=4, alain=10):
        resImg = self.GenerateCanve(width, height)
        space = (width - charCount*alain)/charCount

        for i in range(0, charCount):
            thisX = random.randint((alain + space)*(i), (alain + space)*(i+1)-alain)
            thisY = random.randint(alain, 2 * alain)
            thisCharImg = self.GenerateRandomCharImgWithRotate()
            resImg.paste(ImageOps.colorize(thisCharImg, (0, 0, 0),
                                           self.GenerateRandomFontColor()),
                                           (thisX, thisY), thisCharImg)
        return resImg

    # 测试主函数
    def TestMain(self):
        resImg = self.GenerateTestCode()
        resImg.show()


if __name__ == '__main__':
    _test = TestCode()
    _test.TestMain()

其实还有很多优化空间,比如字体被加载了N次什么的,这里篇幅已经很长了,就不再继续展开了。有兴趣的同学可以继续优化哟。

你可能感兴趣的:(编程#Python,python)