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.
@@ -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
+