环境:Anaconda3-2020.02
首先我们分解一下需要做什么:
把随机的字符串用随机的颜色渲染,然后放到一块随机的背景色上面,基本的验证码就好了
大概就是这个效果:
那么我们来看看如何实现
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. 指定随机色的画板
需要画(图)板(片),那就生成一个:使用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()
运行一下:
字,字好小!
怎么办?
让客户买放大镜!
当然不是!我们可以用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()
搞定收工?不不不,新需求来了:
所有字都是一个颜色,不行!
背景是纯色,不行!
那么,怎么处理让每个字颜色都不一样,让背景色也变成不是纯色呢?
我们来整理一下:
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)
再运行呢?
哎,舒服多了
这个时候新需求又上来了:我想让字不那么正,稍微有个角度
好,满足你!我们可以用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次什么的,这里篇幅已经很长了,就不再继续展开了。有兴趣的同学可以继续优化哟。