easy-captcha-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easy_captcha/__init__.py +40 -0
- easy_captcha/base/__init__.py +17 -0
- easy_captcha/base/arithmetic_captcha_abstract.py +79 -0
- easy_captcha/base/captcha.py +337 -0
- easy_captcha/base/chinese_captcha_abstract.py +81 -0
- easy_captcha/base/randoms.py +77 -0
- easy_captcha/captcha/__init__.py +19 -0
- easy_captcha/captcha/arithmetic_captcha.py +109 -0
- easy_captcha/captcha/chinese_captcha.py +111 -0
- easy_captcha/captcha/chinese_gif_captcha.py +180 -0
- easy_captcha/captcha/gif_captcha.py +180 -0
- easy_captcha/captcha/spec_captcha.py +112 -0
- easy_captcha/constants.py +55 -0
- easy_captcha/fonts/actionj.ttf +0 -0
- easy_captcha/fonts/epilog.ttf +0 -0
- easy_captcha/fonts/fresnel.ttf +0 -0
- easy_captcha/fonts/headache.ttf +0 -0
- easy_captcha/fonts/lexo.ttf +0 -0
- easy_captcha/fonts/prefix.ttf +0 -0
- easy_captcha/fonts/progbot.ttf +0 -0
- easy_captcha/fonts/ransom.ttf +0 -0
- easy_captcha/fonts/robot.ttf +0 -0
- easy_captcha/fonts/scandal.ttf +0 -0
- easy_captcha_python-0.1.0.dist-info/METADATA +445 -0
- easy_captcha_python-0.1.0.dist-info/RECORD +28 -0
- easy_captcha_python-0.1.0.dist-info/WHEEL +5 -0
- easy_captcha_python-0.1.0.dist-info/licenses/LICENSE +674 -0
- easy_captcha_python-0.1.0.dist-info/top_level.txt +1 -0
easy_captcha/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
EasyCaptcha - Python图形验证码生成库
|
|
4
|
+
支持GIF、中文、算术等类型
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "1.0.0"
|
|
8
|
+
|
|
9
|
+
from .captcha.spec_captcha import SpecCaptcha
|
|
10
|
+
from .captcha.gif_captcha import GifCaptcha
|
|
11
|
+
from .captcha.chinese_captcha import ChineseCaptcha
|
|
12
|
+
from .captcha.chinese_gif_captcha import ChineseGifCaptcha
|
|
13
|
+
from .captcha.arithmetic_captcha import ArithmeticCaptcha
|
|
14
|
+
from .constants import (
|
|
15
|
+
TYPE_DEFAULT,
|
|
16
|
+
TYPE_ONLY_NUMBER,
|
|
17
|
+
TYPE_ONLY_CHAR,
|
|
18
|
+
TYPE_ONLY_UPPER,
|
|
19
|
+
TYPE_ONLY_LOWER,
|
|
20
|
+
TYPE_NUM_AND_UPPER,
|
|
21
|
+
FONT_1, FONT_2, FONT_3, FONT_4, FONT_5,
|
|
22
|
+
FONT_6, FONT_7, FONT_8, FONT_9, FONT_10
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
'SpecCaptcha',
|
|
27
|
+
'GifCaptcha',
|
|
28
|
+
'ChineseCaptcha',
|
|
29
|
+
'ChineseGifCaptcha',
|
|
30
|
+
'ArithmeticCaptcha',
|
|
31
|
+
'TYPE_DEFAULT',
|
|
32
|
+
'TYPE_ONLY_NUMBER',
|
|
33
|
+
'TYPE_ONLY_CHAR',
|
|
34
|
+
'TYPE_ONLY_UPPER',
|
|
35
|
+
'TYPE_ONLY_LOWER',
|
|
36
|
+
'TYPE_NUM_AND_UPPER',
|
|
37
|
+
'FONT_1', 'FONT_2', 'FONT_3', 'FONT_4', 'FONT_5',
|
|
38
|
+
'FONT_6', 'FONT_7', 'FONT_8', 'FONT_9', 'FONT_10'
|
|
39
|
+
]
|
|
40
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
基础类模块
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .randoms import Randoms
|
|
7
|
+
from .captcha import Captcha
|
|
8
|
+
from .arithmetic_captcha_abstract import ArithmeticCaptchaAbstract
|
|
9
|
+
from .chinese_captcha_abstract import ChineseCaptchaAbstract
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'Randoms',
|
|
13
|
+
'Captcha',
|
|
14
|
+
'ArithmeticCaptchaAbstract',
|
|
15
|
+
'ChineseCaptchaAbstract'
|
|
16
|
+
]
|
|
17
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
算术验证码抽象类
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .captcha import Captcha
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ArithmeticCaptchaAbstract(Captcha):
|
|
10
|
+
"""算术验证码抽象基类"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self._len = 2 # 算术验证码默认2位数运算
|
|
15
|
+
self._arithmetic_string = None # 算术公式字符串
|
|
16
|
+
|
|
17
|
+
def _alphas(self):
|
|
18
|
+
"""
|
|
19
|
+
生成随机算术验证码
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
list: 验证码字符列表(这里是计算结果)
|
|
23
|
+
"""
|
|
24
|
+
formula_parts = []
|
|
25
|
+
|
|
26
|
+
# 生成算术表达式
|
|
27
|
+
for i in range(self._len):
|
|
28
|
+
# 生成0-9的随机数字
|
|
29
|
+
num = self.num(10)
|
|
30
|
+
formula_parts.append(str(num))
|
|
31
|
+
|
|
32
|
+
# 如果不是最后一个数字,添加运算符
|
|
33
|
+
if i < self._len - 1:
|
|
34
|
+
operator_type = self.num(1, 4) # 1: +, 2: -, 3: x
|
|
35
|
+
if operator_type == 1:
|
|
36
|
+
formula_parts.append('+')
|
|
37
|
+
elif operator_type == 2:
|
|
38
|
+
formula_parts.append('-')
|
|
39
|
+
else: # operator_type == 3
|
|
40
|
+
formula_parts.append('x')
|
|
41
|
+
|
|
42
|
+
# 构建公式字符串
|
|
43
|
+
formula = ''.join(formula_parts)
|
|
44
|
+
|
|
45
|
+
# 计算结果
|
|
46
|
+
try:
|
|
47
|
+
# 将 'x' 替换为 '*' 进行计算
|
|
48
|
+
result = eval(formula.replace('x', '*'))
|
|
49
|
+
# 确保结果为整数
|
|
50
|
+
result = int(result)
|
|
51
|
+
self._chars = str(result)
|
|
52
|
+
except:
|
|
53
|
+
# 如果计算失败,默认为0
|
|
54
|
+
self._chars = '0'
|
|
55
|
+
|
|
56
|
+
# 保存公式字符串(显示用)
|
|
57
|
+
self._arithmetic_string = formula + '=?'
|
|
58
|
+
|
|
59
|
+
return list(self._chars)
|
|
60
|
+
|
|
61
|
+
def get_arithmetic_string(self):
|
|
62
|
+
"""
|
|
63
|
+
获取算术公式字符串
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str: 算术公式,如 "3+2=?"
|
|
67
|
+
"""
|
|
68
|
+
self.check_alpha()
|
|
69
|
+
return self._arithmetic_string
|
|
70
|
+
|
|
71
|
+
def set_arithmetic_string(self, arithmetic_string):
|
|
72
|
+
"""
|
|
73
|
+
设置算术公式字符串
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
arithmetic_string: 算术公式字符串
|
|
77
|
+
"""
|
|
78
|
+
self._arithmetic_string = arithmetic_string
|
|
79
|
+
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
验证码抽象基类
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import os
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from io import BytesIO
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
13
|
+
|
|
14
|
+
from .randoms import Randoms
|
|
15
|
+
from ..constants import *
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Captcha(Randoms, ABC):
|
|
19
|
+
"""验证码抽象基类"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._font = None
|
|
23
|
+
self._len = 5 # 验证码字符长度
|
|
24
|
+
self._width = 130 # 验证码宽度
|
|
25
|
+
self._height = 48 # 验证码高度
|
|
26
|
+
self._char_type = TYPE_DEFAULT # 验证码字符类型
|
|
27
|
+
self._chars = None # 当前验证码文本
|
|
28
|
+
self._font_size = 32 # 字体大小
|
|
29
|
+
|
|
30
|
+
def _alphas(self):
|
|
31
|
+
"""
|
|
32
|
+
生成随机验证码字符
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
list: 验证码字符列表
|
|
36
|
+
"""
|
|
37
|
+
chars = []
|
|
38
|
+
for i in range(self._len):
|
|
39
|
+
if self._char_type == TYPE_ONLY_NUMBER:
|
|
40
|
+
# 纯数字
|
|
41
|
+
chars.append(self.alpha(self.NUM_MAX_INDEX))
|
|
42
|
+
elif self._char_type == TYPE_ONLY_CHAR:
|
|
43
|
+
# 纯字母
|
|
44
|
+
chars.append(self.alpha(self.CHAR_MIN_INDEX, self.CHAR_MAX_INDEX))
|
|
45
|
+
elif self._char_type == TYPE_ONLY_UPPER:
|
|
46
|
+
# 纯大写字母
|
|
47
|
+
chars.append(self.alpha(self.UPPER_MIN_INDEX, self.UPPER_MAX_INDEX))
|
|
48
|
+
elif self._char_type == TYPE_ONLY_LOWER:
|
|
49
|
+
# 纯小写字母
|
|
50
|
+
chars.append(self.alpha(self.LOWER_MIN_INDEX, self.LOWER_MAX_INDEX))
|
|
51
|
+
elif self._char_type == TYPE_NUM_AND_UPPER:
|
|
52
|
+
# 数字和大写字母
|
|
53
|
+
chars.append(self.alpha(self.UPPER_MAX_INDEX))
|
|
54
|
+
else:
|
|
55
|
+
# 默认:数字和字母混合
|
|
56
|
+
chars.append(self.alpha())
|
|
57
|
+
|
|
58
|
+
self._chars = ''.join(chars)
|
|
59
|
+
return chars
|
|
60
|
+
|
|
61
|
+
def _color(self, fc=None, bc=None):
|
|
62
|
+
"""
|
|
63
|
+
生成随机颜色
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
fc: 前景色最小值 (0-255)
|
|
67
|
+
bc: 背景色最大值 (0-255)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
tuple: RGB颜色元组
|
|
71
|
+
"""
|
|
72
|
+
if fc is not None and bc is not None:
|
|
73
|
+
if fc > 255:
|
|
74
|
+
fc = 255
|
|
75
|
+
if bc > 255:
|
|
76
|
+
bc = 255
|
|
77
|
+
r = fc + self.num(bc - fc)
|
|
78
|
+
g = fc + self.num(bc - fc)
|
|
79
|
+
b = fc + self.num(bc - fc)
|
|
80
|
+
return (r, g, b)
|
|
81
|
+
else:
|
|
82
|
+
# 返回预定义的常用颜色
|
|
83
|
+
return COLORS[self.num(len(COLORS))]
|
|
84
|
+
|
|
85
|
+
def check_alpha(self):
|
|
86
|
+
"""检查验证码是否已生成,如果没有则生成"""
|
|
87
|
+
if self._chars is None:
|
|
88
|
+
self._alphas()
|
|
89
|
+
|
|
90
|
+
def text(self):
|
|
91
|
+
"""
|
|
92
|
+
获取验证码文本
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
str: 验证码文本
|
|
96
|
+
"""
|
|
97
|
+
self.check_alpha()
|
|
98
|
+
return self._chars
|
|
99
|
+
|
|
100
|
+
def text_char(self):
|
|
101
|
+
"""
|
|
102
|
+
获取验证码字符列表
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
list: 验证码字符列表
|
|
106
|
+
"""
|
|
107
|
+
self.check_alpha()
|
|
108
|
+
return list(self._chars)
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
def out(self, stream: BytesIO) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
输出验证码到流
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
stream: 输出流
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
bool: 是否成功
|
|
120
|
+
"""
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def to_base64(self, prefix: str = "") -> str:
|
|
125
|
+
"""
|
|
126
|
+
输出base64编码
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
prefix: base64前缀
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
str: base64编码字符串
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def _to_base64_impl(self, prefix: str = "") -> str:
|
|
137
|
+
"""
|
|
138
|
+
base64编码实现
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
prefix: base64前缀
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
str: base64编码字符串
|
|
145
|
+
"""
|
|
146
|
+
stream = BytesIO()
|
|
147
|
+
self.out(stream)
|
|
148
|
+
b64_data = base64.b64encode(stream.getvalue()).decode('utf-8')
|
|
149
|
+
return prefix + b64_data
|
|
150
|
+
|
|
151
|
+
def draw_line(self, num: int, draw: ImageDraw.Draw, color: Optional[Tuple[int, int, int]] = None):
|
|
152
|
+
"""
|
|
153
|
+
绘制干扰线
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
num: 线条数量
|
|
157
|
+
draw: ImageDraw对象
|
|
158
|
+
color: 线条颜色,None则随机
|
|
159
|
+
"""
|
|
160
|
+
for i in range(num):
|
|
161
|
+
line_color = color if color else self._color()
|
|
162
|
+
x1 = self.num(-10, self._width - 10)
|
|
163
|
+
y1 = self.num(5, self._height - 5)
|
|
164
|
+
x2 = self.num(10, self._width + 10)
|
|
165
|
+
y2 = self.num(2, self._height - 2)
|
|
166
|
+
draw.line([(x1, y1), (x2, y2)], fill=line_color, width=1)
|
|
167
|
+
|
|
168
|
+
def draw_oval(self, num: int, draw: ImageDraw.Draw, color: Optional[Tuple[int, int, int]] = None):
|
|
169
|
+
"""
|
|
170
|
+
绘制干扰圆
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
num: 圆圈数量
|
|
174
|
+
draw: ImageDraw对象
|
|
175
|
+
color: 圆圈颜色,None则随机
|
|
176
|
+
"""
|
|
177
|
+
for i in range(num):
|
|
178
|
+
oval_color = color if color else self._color()
|
|
179
|
+
w = 5 + self.num(10)
|
|
180
|
+
x = self.num(self._width - 25)
|
|
181
|
+
y = self.num(self._height - 15)
|
|
182
|
+
draw.ellipse([x, y, x + w, y + w], outline=oval_color)
|
|
183
|
+
|
|
184
|
+
def draw_bezier_curve(self, num: int, draw: ImageDraw.Draw, color: Optional[Tuple[int, int, int]] = None):
|
|
185
|
+
"""
|
|
186
|
+
绘制贝塞尔曲线
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
num: 曲线数量
|
|
190
|
+
draw: ImageDraw对象
|
|
191
|
+
color: 曲线颜色,None则随机
|
|
192
|
+
"""
|
|
193
|
+
for i in range(num):
|
|
194
|
+
curve_color = color if color else self._color()
|
|
195
|
+
|
|
196
|
+
# 起点和终点
|
|
197
|
+
x1 = 5
|
|
198
|
+
y1 = self.num(5, self._height // 2)
|
|
199
|
+
x2 = self._width - 5
|
|
200
|
+
y2 = self.num(self._height // 2, self._height - 5)
|
|
201
|
+
|
|
202
|
+
# 控制点
|
|
203
|
+
ctrlx = self.num(self._width // 4, self._width * 3 // 4)
|
|
204
|
+
ctrly = self.num(5, self._height - 5)
|
|
205
|
+
|
|
206
|
+
# 随机交换起点和终点的y坐标
|
|
207
|
+
if self.num(2) == 0:
|
|
208
|
+
y1, y2 = y2, y1
|
|
209
|
+
|
|
210
|
+
# 随机选择二阶或三阶贝塞尔曲线
|
|
211
|
+
if self.num(2) == 0:
|
|
212
|
+
# 二阶贝塞尔曲线
|
|
213
|
+
points = self._quadratic_bezier_points((x1, y1), (ctrlx, ctrly), (x2, y2), 50)
|
|
214
|
+
else:
|
|
215
|
+
# 三阶贝塞尔曲线
|
|
216
|
+
ctrlx1 = self.num(self._width // 4, self._width * 3 // 4)
|
|
217
|
+
ctrly1 = self.num(5, self._height - 5)
|
|
218
|
+
points = self._cubic_bezier_points((x1, y1), (ctrlx, ctrly), (ctrlx1, ctrly1), (x2, y2), 50)
|
|
219
|
+
|
|
220
|
+
draw.line(points, fill=curve_color, width=2)
|
|
221
|
+
|
|
222
|
+
def _quadratic_bezier_points(self, p0, p1, p2, num_points):
|
|
223
|
+
"""
|
|
224
|
+
计算二阶贝塞尔曲线上的点
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
p0: 起点 (x, y)
|
|
228
|
+
p1: 控制点 (x, y)
|
|
229
|
+
p2: 终点 (x, y)
|
|
230
|
+
num_points: 点的数量
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
list: 点的列表
|
|
234
|
+
"""
|
|
235
|
+
points = []
|
|
236
|
+
for i in range(num_points + 1):
|
|
237
|
+
t = i / num_points
|
|
238
|
+
x = (1 - t) ** 2 * p0[0] + 2 * (1 - t) * t * p1[0] + t ** 2 * p2[0]
|
|
239
|
+
y = (1 - t) ** 2 * p0[1] + 2 * (1 - t) * t * p1[1] + t ** 2 * p2[1]
|
|
240
|
+
points.append((x, y))
|
|
241
|
+
return points
|
|
242
|
+
|
|
243
|
+
def _cubic_bezier_points(self, p0, p1, p2, p3, num_points):
|
|
244
|
+
"""
|
|
245
|
+
计算三阶贝塞尔曲线上的点
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
p0: 起点 (x, y)
|
|
249
|
+
p1: 控制点1 (x, y)
|
|
250
|
+
p2: 控制点2 (x, y)
|
|
251
|
+
p3: 终点 (x, y)
|
|
252
|
+
num_points: 点的数量
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
list: 点的列表
|
|
256
|
+
"""
|
|
257
|
+
points = []
|
|
258
|
+
for i in range(num_points + 1):
|
|
259
|
+
t = i / num_points
|
|
260
|
+
x = (1 - t) ** 3 * p0[0] + 3 * (1 - t) ** 2 * t * p1[0] + 3 * (1 - t) * t ** 2 * p2[0] + t ** 3 * p3[0]
|
|
261
|
+
y = (1 - t) ** 3 * p0[1] + 3 * (1 - t) ** 2 * t * p1[1] + 3 * (1 - t) * t ** 2 * p2[1] + t ** 3 * p3[1]
|
|
262
|
+
points.append((x, y))
|
|
263
|
+
return points
|
|
264
|
+
|
|
265
|
+
def get_font(self):
|
|
266
|
+
"""
|
|
267
|
+
获取字体
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
ImageFont: 字体对象
|
|
271
|
+
"""
|
|
272
|
+
if self._font is None:
|
|
273
|
+
self.set_font(FONT_1)
|
|
274
|
+
return self._font
|
|
275
|
+
|
|
276
|
+
def set_font(self, font, size: int = 32):
|
|
277
|
+
"""
|
|
278
|
+
设置字体
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
font: 字体索引(0-9)或字体文件路径
|
|
282
|
+
size: 字体大小
|
|
283
|
+
"""
|
|
284
|
+
self._font_size = size
|
|
285
|
+
|
|
286
|
+
if isinstance(font, int):
|
|
287
|
+
# 使用内置字体
|
|
288
|
+
font_path = os.path.join(
|
|
289
|
+
os.path.dirname(os.path.dirname(__file__)),
|
|
290
|
+
'fonts',
|
|
291
|
+
FONT_NAMES[font]
|
|
292
|
+
)
|
|
293
|
+
try:
|
|
294
|
+
self._font = ImageFont.truetype(font_path, size)
|
|
295
|
+
except:
|
|
296
|
+
# 如果加载失败,使用默认字体
|
|
297
|
+
self._font = ImageFont.load_default()
|
|
298
|
+
else:
|
|
299
|
+
# 使用自定义字体文件
|
|
300
|
+
try:
|
|
301
|
+
self._font = ImageFont.truetype(font, size)
|
|
302
|
+
except:
|
|
303
|
+
self._font = ImageFont.load_default()
|
|
304
|
+
|
|
305
|
+
# Getter和Setter方法
|
|
306
|
+
@property
|
|
307
|
+
def len(self):
|
|
308
|
+
return self._len
|
|
309
|
+
|
|
310
|
+
@len.setter
|
|
311
|
+
def len(self, value):
|
|
312
|
+
self._len = value
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def width(self):
|
|
316
|
+
return self._width
|
|
317
|
+
|
|
318
|
+
@width.setter
|
|
319
|
+
def width(self, value):
|
|
320
|
+
self._width = value
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def height(self):
|
|
324
|
+
return self._height
|
|
325
|
+
|
|
326
|
+
@height.setter
|
|
327
|
+
def height(self, value):
|
|
328
|
+
self._height = value
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def char_type(self):
|
|
332
|
+
return self._char_type
|
|
333
|
+
|
|
334
|
+
@char_type.setter
|
|
335
|
+
def char_type(self, value):
|
|
336
|
+
self._char_type = value
|
|
337
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
中文验证码抽象类
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .captcha import Captcha
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChineseCaptchaAbstract(Captcha):
|
|
10
|
+
"""中文验证码抽象基类"""
|
|
11
|
+
|
|
12
|
+
# 常用汉字字符集
|
|
13
|
+
CHINESE_CHARS = (
|
|
14
|
+
"的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self._len = 4 # 中文验证码默认4个字
|
|
20
|
+
# 中文验证码使用系统字体
|
|
21
|
+
self.set_font_for_chinese()
|
|
22
|
+
|
|
23
|
+
def set_font_for_chinese(self):
|
|
24
|
+
"""设置中文字体"""
|
|
25
|
+
import platform
|
|
26
|
+
|
|
27
|
+
# 根据操作系统选择合适的中文字体
|
|
28
|
+
system = platform.system()
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
if system == 'Windows':
|
|
32
|
+
# Windows系统使用微软雅黑或宋体
|
|
33
|
+
try:
|
|
34
|
+
from PIL import ImageFont
|
|
35
|
+
self._font = ImageFont.truetype("msyh.ttc", 28) # 微软雅黑
|
|
36
|
+
except:
|
|
37
|
+
try:
|
|
38
|
+
self._font = ImageFont.truetype("simsun.ttc", 28) # 宋体
|
|
39
|
+
except:
|
|
40
|
+
self._font = ImageFont.truetype("C:/Windows/Fonts/simhei.ttf", 28) # 黑体
|
|
41
|
+
elif system == 'Darwin': # macOS
|
|
42
|
+
from PIL import ImageFont
|
|
43
|
+
self._font = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 28)
|
|
44
|
+
else: # Linux
|
|
45
|
+
from PIL import ImageFont
|
|
46
|
+
# 尝试常见的Linux中文字体
|
|
47
|
+
try:
|
|
48
|
+
self._font = ImageFont.truetype("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", 28)
|
|
49
|
+
except:
|
|
50
|
+
try:
|
|
51
|
+
self._font = ImageFont.truetype("/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", 28)
|
|
52
|
+
except:
|
|
53
|
+
self._font = ImageFont.load_default()
|
|
54
|
+
except:
|
|
55
|
+
from PIL import ImageFont
|
|
56
|
+
self._font = ImageFont.load_default()
|
|
57
|
+
|
|
58
|
+
def _alphas(self):
|
|
59
|
+
"""
|
|
60
|
+
生成随机中文验证码
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
list: 验证码字符列表
|
|
64
|
+
"""
|
|
65
|
+
chars = []
|
|
66
|
+
for i in range(self._len):
|
|
67
|
+
chars.append(self._alpha_han())
|
|
68
|
+
|
|
69
|
+
self._chars = ''.join(chars)
|
|
70
|
+
return chars
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def _alpha_han(cls):
|
|
74
|
+
"""
|
|
75
|
+
返回随机汉字
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
str: 随机汉字
|
|
79
|
+
"""
|
|
80
|
+
return cls.CHINESE_CHARS[cls.num(len(cls.CHINESE_CHARS))]
|
|
81
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
随机数工具类
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import secrets
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Randoms:
|
|
10
|
+
"""随机数生成工具类"""
|
|
11
|
+
|
|
12
|
+
# 验证码字符集,去除了0、O、I、L等容易混淆的字母
|
|
13
|
+
ALPHA = [
|
|
14
|
+
'2', '3', '4', '5', '6', '7', '8', '9',
|
|
15
|
+
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
|
16
|
+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# 数字的最大索引(不包括)
|
|
20
|
+
NUM_MAX_INDEX = 8
|
|
21
|
+
# 字符的最小索引(包括)
|
|
22
|
+
CHAR_MIN_INDEX = NUM_MAX_INDEX
|
|
23
|
+
# 字符的最大索引(不包括)
|
|
24
|
+
CHAR_MAX_INDEX = len(ALPHA)
|
|
25
|
+
# 大写字符最小索引
|
|
26
|
+
UPPER_MIN_INDEX = CHAR_MIN_INDEX
|
|
27
|
+
# 大写字符最大索引
|
|
28
|
+
UPPER_MAX_INDEX = UPPER_MIN_INDEX + 23
|
|
29
|
+
# 小写字母最小索引
|
|
30
|
+
LOWER_MIN_INDEX = UPPER_MAX_INDEX
|
|
31
|
+
# 小写字母最大索引
|
|
32
|
+
LOWER_MAX_INDEX = CHAR_MAX_INDEX
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def num(min_val=None, max_val=None):
|
|
36
|
+
"""
|
|
37
|
+
生成随机数
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
min_val: 最小值(包括),如果为None则从0开始
|
|
41
|
+
max_val: 最大值(不包括),如果为None则min_val作为最大值
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
int: 随机数
|
|
45
|
+
"""
|
|
46
|
+
if min_val is None and max_val is None:
|
|
47
|
+
raise ValueError("至少需要提供一个参数")
|
|
48
|
+
|
|
49
|
+
if max_val is None:
|
|
50
|
+
# 只提供一个参数,生成0到min_val之间的随机数
|
|
51
|
+
return secrets.randbelow(min_val)
|
|
52
|
+
else:
|
|
53
|
+
# 提供两个参数,生成min_val到max_val之间的随机数
|
|
54
|
+
return min_val + secrets.randbelow(max_val - min_val)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def alpha(cls, min_index=None, max_index=None):
|
|
58
|
+
"""
|
|
59
|
+
返回ALPHA中的随机字符
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
min_index: 最小索引(包括)
|
|
63
|
+
max_index: 最大索引(不包括)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str: 随机字符
|
|
67
|
+
"""
|
|
68
|
+
if min_index is None and max_index is None:
|
|
69
|
+
# 返回所有字符中的随机一个
|
|
70
|
+
return cls.ALPHA[cls.num(len(cls.ALPHA))]
|
|
71
|
+
elif max_index is None:
|
|
72
|
+
# 返回0到min_index之间的随机字符
|
|
73
|
+
return cls.ALPHA[cls.num(min_index)]
|
|
74
|
+
else:
|
|
75
|
+
# 返回min_index到max_index之间的随机字符
|
|
76
|
+
return cls.ALPHA[cls.num(min_index, max_index)]
|
|
77
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
验证码实现模块
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .spec_captcha import SpecCaptcha
|
|
7
|
+
from .gif_captcha import GifCaptcha
|
|
8
|
+
from .chinese_captcha import ChineseCaptcha
|
|
9
|
+
from .chinese_gif_captcha import ChineseGifCaptcha
|
|
10
|
+
from .arithmetic_captcha import ArithmeticCaptcha
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'SpecCaptcha',
|
|
14
|
+
'GifCaptcha',
|
|
15
|
+
'ChineseCaptcha',
|
|
16
|
+
'ChineseGifCaptcha',
|
|
17
|
+
'ArithmeticCaptcha'
|
|
18
|
+
]
|
|
19
|
+
|