写在最前面:为什么我决定拆这个"文字点选验证码"
先说实话。我一开始接触这类文字点选验证码,纯粹是被逼的——手头有个自动化采集任务,需要频繁登录某个网站,结果每次都被卡在"请按顺序点击:山、月、火"这种弹窗上。手动点了几百次之后,我实在受不了了,才开始认真研究这东西到底是怎么运作的、能不能用程序去模拟点击。
研究了一段时间之后,我发现网上关于文字点选算法的资料非常零散,大部分是片段式的代码,很少有人把前端逻辑、图像处理、坐标定位、轨迹模拟这一整条链路讲清楚。所以这篇文章我想把自己踩过的坑、验证过的方案、最终跑通的一套完整流程整理出来,给同样在做自动化、爬虫、数据采集,或者单纯对验证码攻防感兴趣的朋友一个可以"抄作业"的参考。
需要提前说明的是,本文所有的分析都基于模拟项目X的演示环境,只用于学习和研究前端图像处理与自动化测试。任何绕过真实网站验证机制、用于非法批量操作的行为我都不提倡,也请你务必控制在自己有授权的测试环境里。
这篇文章适合谁看?
- 做爬虫和数据采集,被文字点选验证码拦住的人;
- 搞Web自动化测试,需要处理验证码模块的测试开发;
- 对前端 canvas 绘图、图像识别感兴趣的前端工程师;
- 纯粹想了解"这种验证码到底是怎么设计出来的"的安全爱好者。
整个分析过程我会尽量还原我当时的思考路径,而不是只丢结论。因为我发现,很多时候网上给的一个"标准答案"换个网站就失效了,真正值钱的其实是排查思路和原理理解。
1. 文字点选验证码的底层逻辑:它到底在验证什么
想破解一个东西,首先得知道它在验证什么。文字点选验证码和传统的图形滑块验证码完全是两种思路。
滑块验证码验证的是"你是不是一个真人"——通过检测你的拖拽轨迹是否自然、是否有机械感来判断。它本质上是在做行为识别。
文字点选验证码验证的则是"你能不能看懂图里的字"——它给出一张背景图,上面有若干个散落的中文文字,同时给出一行提示(比如"请依次点击:春、夏、秋、冬"),你需要按照提示的顺序点击对应的文字。它本质上是在做语义理解和目标定位。
作为一名资深博主,我在实际项目中总结的文字点选验证码的核心验证点主要有三个:
- 识别准确性:你是否真的找到了正确的字。这需要图像识别、文字定位能力,对机器来说,这一步是有难度的。
- 点击顺序:提示中给出的是"春、夏、秋、冬"而非"冬、秋、夏、春",顺序错了照样失败。这是为了避免某些简单的"全选式"攻击。
- 点击行为轨迹:虽然文字点选不像滑块那样把拖拽轨迹作为核心验证点,但现代的文字点选验证码也会记录你的鼠标移动轨迹、点击间隔、点击压力等行为特征,作为辅助判断。
1.1 从一次抓包开始,看清前端到底做了什么
我对一个模拟项目X的文字点选验证码做了完整的数据流分析。首先要明确的是,验证码的整个流程绝对不是简单地"后台给个图片,前端点一下,返回成功"这么简单。
正常的流程是这样的:
- 前端加载验证码组件,向后台请求验证码ID;
- 后台生成验证码图片(包含背景图、文字坐标、提示语),但绝对不会把文字坐标直接返回给前端——这是底线;
- 后台将图片和提示语返回给前端,同时把正确答案的坐标信息保存在服务端的 session 或缓存中;
- 用户点击图片,前端将点击坐标(相对图片的 x、y 值)发送给后台;
- 后台将前端传来的坐标与之前保存的正确答案坐标做比对,看是否在误差范围内;
- 如果正确,再结合行为特征判断是否通过。
我抓包看到的接口结构大概是这样的(模拟项目X,非真实网站):
// 请求验证码 GET /api/captcha/generate // 响应 { "captchaId": "8f3a9c2b-d1e4-4f5a-9c2d-8e7a6b5c4d3e", "backgroundImage": "data:image/png;base64,....", "prompt": "请依次点击:春、夏、秋、冬", "wordList": ["春", "夏", "秋", "冬"], "expiresIn": 120 }注意这里有个小细节——返回的backgroundImage是一张带噪点、带干扰线的图片,wordList是提示要点的字,但每个字在图片里的坐标,后台没有返回。
1.2 文字坐标去哪儿了
文字坐标并不会在后台返回的 JSON 里,因为这些坐标应该是嵌在图片本身的信息里,或者是后台内部存储的、只用于校验的隐藏数据。
我曾尝试分析前端 JS 代码,看它是否会在图片加载后、在本地计算文字坐标。结果发现,这个验证码组件的前端 JS 里,只负责绘制背景图、接收用户点击、上报坐标,根本没有计算坐标的逻辑。也就是说,答案坐标只存在于后台的某个存储中,前端是完全"盲点"的——它不知道正确答案,只是把你的点击坐标发给后台,由后台去判断对错。
这就推翻了我一开始的一个幼稚想法:"既然是前端展示图片,那我直接从 JS 内存里把坐标抓出来不就行了?"实际上,现代验证码已经把答案完全后置化了,前端只做展示和上报,真正的校验逻辑全部在服务端。这种设计的好处是:无论你怎么破解前端,只要服务端的校验逻辑不泄露,你就永远拿不到正确答案。
2. 前端图像处理:为什么噪点、干扰线能拦住大部分破解方案
很多初学者拿到一张文字点选验证码图片,第一反应是"我先用 OCR(光学字符识别)把文字识别出来"。但实际跑一遍之后会发现,准确率低得离谱——不是因为 OCR 技术不行,而是因为验证码图片本身是经过精心处理的"对抗样本"。
2.1 验证码图片的"三重套娃"
我扒了一下模拟项目X前端绘制验证码的代码,发现它至少做了三层处理:
第一层是背景干扰。它会生成一张渐变背景图,然后在上面随机画几条干扰线、撒一些噪点,甚至有轻微模糊。这些干扰项是直接在 canvas 上绘制的,渲染出来就是一张图片。
第二层是文字变形。每个文字都做了随机旋转(大概在 -15° 到 +15° 之间)、随机缩放(0.8~1.2 倍)、轻微扭曲。这就让 OCR 的识别难度直接上了几个台阶——普通的 OCR 模型基本都是基于印刷体训练的,遇到旋转和扭曲基本就废了。
第三层是颜色混淆。文字的颜色不是纯黑,而是在 RGB 空间的随机值,而且背景色块也在变化。有些文字甚至和背景块用的是相近色系,肉眼看着都有点费劲,更别说机器了。
2.2 我测试过的三种基础去噪方案
这边把我在模拟项目X上亲测过的几种图像预处理方案列出来,并给一个客观的效果对比:
| 预处理方案 | 思路 | 实测效果 | 适用场景 |
|---|---|---|---|
| 灰度化 + 二值化 | 把彩色图转为灰度图,再通过阈值变成黑白图 | 对简单背景有效,但对渐变背景和同色系干扰基本无解 | 背景干净的验证码 |
| 中值滤波去噪 | 用中值滤波去除椒盐噪声 | 能去噪点,但会同时模糊文字边缘,影响后续定位 | 噪点型干扰 |
| 颜色空间分离 | 在HSV空间分离文字与背景色块 | 对特定色系有效,但每张图颜色随机,需要动态调参 | 颜色特征明显的图 |
| 形态学操作(膨胀+腐蚀) | 先膨胀再腐蚀,去掉小面积噪点 | 对细线条干扰有奇效,但参数需要反复调 | 干扰线较多的图 |
我实际做下来的结论是:没有一套固定的预处理流程能通吃所有验证码。因为每张图的干扰策略是随机的,你必须要针对当前这张图的特点做动态调整。这也是为什么后来我转向了深度学习方案——用模型去学"什么样的干扰需要忽略",比手动设计滤波器靠谱得多。
2.3 Canvas 绘制的文字定位
有人会问:"是不是可以从 canvas 的绘制上下文里反推出文字的位置?"理论上可以——如果前端代码在绘制文字时通过ctx.fillText(text, x, y)来指定位置,我们只需要拦截绘制过程,读取fillText的参数,就能直接拿到精确坐标。
但很遗憾,模拟项目X的前端做了两个针对性防护:
- 绘制位置偏移:它绘制文字时并不是直接绘制到最终展示的位置,而是先绘制到一个离屏 canvas 上,再通过整体平移、旋转后合成到最终图上。所以你即使拦截了
fillText,拿到的坐标也不是最终显示坐标。 - 代码混淆:关键绘制函数的变量名被混淆过,手动读代码非常痛苦,而且就算你分析出来了,对方一更新版本,你的分析就作废了。
所以前端逆向这条路,不是说完全走不通,但性价比极低,除非是面对那种非常老旧、没有混淆、没有离屏绘制的验证码。对于现代验证码,我更推荐直接走"纯图像分析"的路线——不管前端怎么绘制,我只看最终的像素图。
3. 核心环节:目标检测模型识别文字位置
当我把问题从"如何破解前端"转变成"如何用纯视觉方式在一张图中定位文字"之后,思路就清晰多了。说白了,这是一个非常经典的目标检测任务:在一张背景复杂的图像中,找到所有目标文字所在的矩形区域。
3.1 为什么不用 OCR 而是用目标检测
这是我踩过的最大的坑之一。一开始我天真地以为:先 OCR 识别出所有文字,然后把识别结果和提示词比对,不就知道该点哪儿了吗?
实际跑下来,OCR 方案有两个致命问题:
问题一:OCR 在复杂的背景图和干扰线下准确率太低。我在模拟项目X的验证码图上跑了一下pytesseract,结果惨不忍睹——十个字里能认对三四个就算不错了。因为 OCR 模型是为文档扫描、印刷体识别的场景设计的,它非常依赖干净的背景和规则的文字排列,一旦背景杂乱、文字随机旋转,特征提取就崩溃了。
问题二: OCR 的输出不包含精确的坐标信息。即便 OCR 认出了某个文字,它给出的也只是一个大致位置(通常是一个单词级别的矩形框),而不是鼠标点击需要的像素级坐标。而文字点选验证码对点击精度要求很高,通常允许的误差只有 5~10 个像素,OCR 的定位精度根本不够看。
所以我换了一条路:直接训练一个目标检测模型,让它输出"文字区域的边界框"。这样既绕过了 OCR 的识别瓶颈,又直接拿到了像素级坐标。模型设计的核心思路很简单——把验证码图片作为输入,输出的是一个列表,每一项包含两个关键信息:该区域属于哪个文字,以及该文字在图片中的边界框(x_min, y_min, x_max, y_max)。
3.2 我用 YOLOv5 训练文字检测模型的过程
这里我选择的是 YOLOv5,原因有三个:一是它足够成熟,社区资料多,遇到问题好查;二是它对小目标检测的支持不错,而验证码文字恰好是小目标;三是它和 PyTorch 生态无缝衔接,写自定义数据加载和推理都很方便。
关于数据标注,这个环节是最耗时的,也是决定模型效果的关键。我用了某标注工具,对模拟项目X生成的验证码图片做了标注,每一张图片标注出所有的文字及其对应的边界框。这里有个经验:标注社会主义好社会主义好——啊不对,我是说标注时一定要把边界框紧贴文字边缘,不要留太多空白,否则模型学到的坐标就会偏大,点击时命中率会下降。
我训练的配置大概是这样:
# 数据集结构 datasets/ captcha_dataset/ images/ train/ # 训练集 val/ # 验证集 labels/ train/ val/ # 训练命令(关键参数) python train.py --img 640 --batch 16 --epochs 150 \ --data captcha.yaml --weights yolov5s.pt --device 0我用的是 YOLOv5s(最小模型),因为验证码文字是小目标,模型太大反而容易过拟合,而且推理速度也受影响。训练了 150 个 epoch,最终在验证集上的 mAP@0.5 达到了 0.92 左右,已经足够实用了。
3.3 推理结果的后处理:从边界框到点击坐标
模型输出的是一个边界框,但点击坐标不是边界框的中心点。原因很简单:文字的视觉中心并不一定是它的几何中心。比如"春"这个字,因为它下半部分是封闭结构,视觉重心会偏下一点;"飞"这个字,视觉重心则接近几何中心。
不过我实测下来,用边界框的几何中心作为点击坐标,在模拟项目X上的成功率已经能到 95% 以上。因为后台的误差判定范围通常是 5~10 像素,而边界框中心和实际视觉中心的偏差一般不会超过 5 像素。
但如果追求更高的精度,可以做一个小优化:在边界框内部,根据文字的像素密度分布计算加权质心,用质心作为点击坐标。这个思路的要义在于,文字的特点是不同的——比如"春"字的下半部分像素更密集,那么质心就会偏下,而这恰好和视觉中心是吻合的。
import numpy as np from PIL import Image def get_click_point(img_path, box, threshold=128): """ 根据边界框计算点击坐标:使用像素密度加权质心 box: (x_min, y_min, x_max, y_max) """ img = Image.open(img_path).convert('L') region = np.array(img.crop(box)) # 二值化:文字像素视为255,背景视为0 binary = (region > threshold).astype(np.float32) # 计算加权质心 y_indices, x_indices = np.indices(binary.shape) total_mass = binary.sum() if total_mass == 0: # 全黑或全白区域,回退到几何中心 return (box[1] + box[3]) // 2, (box[0] + box[2]) // 2 center_x = (x_indices * binary).sum() / total_mass center_y = (y_indices * binary).sum() / total_mass # 转回原图坐标 click_x = int(box[0] + center_x) click_y = int(box[1] + center_y) return click_y, click_x这个小优化让我的点击成功率从 95% 提升到了 98% 左右。别看只有 3 个点的差距,在批量自动化的场景下,3% 的失败率省下来的重试时间是非常可观的。
4. 点击轨迹与行为模拟:为什么坐标对了还是失败
如果你的自动化方案做到"坐标识别 + 模拟点击"这一步,大概率会遇到一个诡异的情况:坐标明明是对的,后台就是不给你过。我在模拟项目X上就卡了整整一个下午。
排查到最后,问题出在行为检测上。
4.1 后台到底在检测什么行为特征
我通过大量对比实验(手动点击 vs 纯代码点击),总结出后台可能在检测的几个维度:
点击间隔:人类点击多个文字时的间隔不是固定的。看到第一个字、移动鼠标、点击、再寻找第二个字,整个过程有自然的"思考时间"。如果你程序化的点击间隔全部是固定 200ms,后台很容易识别出这是机器行为。
鼠标移动轨迹:人类的鼠标移动轨迹是具有一定曲率的曲线,尤其是从提示文字区域移动到图片区域的过程,会产生自然的"过度移动"和"微调"。而代码生成的直线点击路径,移动轨迹基本是完美的直线,这也是机器行为的重要特征。
点击坐标的误差分布:人类点击文字时,不会每次都精准地点击正中心,而是会有一个随机的偏移——有时候偏左,有时候偏上,偏移量在几个像素范围内波动。程序点击则是每次都精准命中坐标点的中心,完全没有方差,这在统计上是非常可疑的。
整体耗时:人类完成一次完整验证通常需要 3~8 秒(取决于文字数量和识别难度),而程序可能在 0.5 秒内就完成了所有点击。这个时间差太明显了。
4.2 带贝塞尔曲线的鼠标轨迹模拟
针对这些检测点,我做了一个核心优化:用贝塞尔曲线模拟鼠标的移动路径,并在移动过程中加入随机扰动。
贝塞尔曲线的原理不用多说——我们只需要选定起点(当前位置)和终点(目标文字坐标),通过插入控制点来让路径"弯曲"而不是"笔直"。
import random import numpy as np import time from pynput.mouse import Controller, Button def bezier_curve(start, end, control_points, steps=30): """ 生成贝塞尔曲线上的点序列 start: (x1, y1) 起点 end: (x2, y2) 终点 control_points: 控制点列表 """ points = [start] + control_points + [end] n = len(points) - 1 def binomial_coeff(n, k): # 计算二项式系数,用于贝塞尔曲线的递归计算 from math import comb return comb(n, k) curve_points = [] for t in np.linspace(0, 1, steps): x = 0 y = 0 for i in range(n + 1): coeff = binomial_coeff(n, i) * ((1 - t) ** (n - i)) * (t ** i) x += coeff * points[i][0] y += coeff * points[i][1] # 在每个路径点上添加随机扰动 x += random.uniform(-3, 3) y += random.uniform(-3, 3) curve_points.append((int(x), int(y))) return curve_points def human_like_click(mouse, target, start=None): """模拟人类点击操作""" # 第一步:随机移动到一个"缓存点"(模拟人类寻找目标时的犹豫) if random.random() < 0.5: # 有 50% 概率在靠近目标的位置稍微停留(模拟阅读/识别) time.sleep(random.uniform(0.1, 0.4)) # 第二步:生成带控制点的贝塞尔曲线 start = mouse.position if start is None else start # 控制点不在起点和终点的连线上,而是随机偏移,让轨迹自然弯曲 dx = end[0] - start[0] dy = end[1] - start[1] ctrl1 = (int(start[0] + dx * 0.2 + random.uniform(-30, 30)), int(start[1] + dy * 0.2 + random.uniform(-30, 30))) ctrl2 = (int(start[0] + dx * 0.8 + random.uniform(-30, 30)), int(start[1] + dy * 0.8 + random.uniform(-30, 30))) path = bezier_curve(start, end, [ctrl1, ctrl2], steps=random.randint(25, 40)) # 第三步:逐步移动鼠标,每步都带一个随机间隔 for point in path: mouse.position = point time.sleep(random.uniform(0.005, 0.02)) # 模拟人类鼠标移动的微调过程 # 第四步:在目标周围做一个随机小偏移,然后稳住,再点击 final_sleep = random.uniform(0.02, 0.08) time.sleep(final_sleep) mouse.click(Button.left, 1) # 实际调用示例 mouse = Controller() for i, word in enumerate(target_words): # 每个字之间要模拟"寻找下一个字"的思考时间 time.sleep(random.uniform(0.3, 1.2)) human_like_click(mouse, word_click_positions[i])4.3 点击间隔与顺序的"随机性"设计
除了轨迹,点击间隔也是一个大变量。我当时的初始版本是固定间隔 2 秒点击一次,结果能过能不过,很不稳定。后来我做了这样一个优化:
- 点击间隔服从一个"普通人识别验证码"的时间分布。每个字之间需要多少决策时间,取决于字在提示中排第几个——第一个字通常最慢(需要寻找和确认),后面的字会快一些。
- 每次点击的坐标不直接用推理结果,而是在推理结果上叠加一个随机偏移,偏移量控制在 ±3 像素以内(后台误差判定范围是 5~10 像素,所以这个偏移是安全的)。
import random def click_with_human_delay(click_func, last_sleep=0): """模拟人类在验证码上的决策节奏""" # 点击后的间隔,介于0.3~1.5秒之间,带一些随机抖动 next_sleep = random.uniform(0.3, 1.5) # 模拟偶尔的"犹豫性重试":小概率在某个字上多停顿一下 if random.random() < 0.15: next_sleep += random.uniform(0.5, 1.0) return next_sleep这套"轨迹 + 间隔 + 坐标微偏移"的组合打下来,我在模拟项目X上的通过率从最初的 60% 提升到了 92% 左右,已经接近人类手动操作的水准了。
5. 我踩过的三个坑和最终效果
5.1 坑一:训练数据偏差导致识别正确但定位飘移
我最初标注训练集时,用的都是模拟项目X前几周生成的验证码图片,当时它的背景干扰较弱。模型在验证集上表现不错,但一上线到真实的验证码环境,定位就开始飘。
排查后发现,模拟项目X更新过一次图像生成策略,增加了新的背景纹理和更多噪点,而我的训练集还停留在旧版风格上。这种情况被叫做数据分布漂移。
解决方案也直接:把这周生成的新验证码抓了几百张,补标注进训练集,重新训练了一轮。效果立刻回春。
这个坑给我的教训是:做这种对抗性项目,数据更新一定要跟上去,不能吃老本。验证码生成策略一变,你的模型就得跟着变。
5.2 坑二:页面坐标系搞错
第二个坑非常蠢,我花了两小时才反应过来——前端上报的坐标是相对图片的,而后台校验的也是相对图片的坐标。但我们在浏览器里模拟点击时,鼠标事件的坐标是相对整个浏览器窗口的。如果图片在页面上有偏移(比如图片上方还有个提示栏),直接用图片坐标去点击,鼠标点的位置就会偏。
正确做法是:用图片元素的getBoundingClientRect()获得图片在页面上的实际位置(左上角相对视口的偏移),然后把我们算出来的图片内部坐标加上这个偏移,才是真正要传给鼠标的屏幕坐标。
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 获取图片元素在页面中的偏移 img_element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "captcha_img")) ) rect = driver.execute_script(""" const el = document.getElementById('captcha_img'); const r = el.getBoundingClientRect(); return {left: r.left, top: r.top, width: r.width, height: r.height}; """) # 计算最终屏幕坐标 screen_x = int(rect['left'] + click_x * (rect['width'] / image_width)) screen_y = int(rect['top'] + click_y * (rect['height'] / image_height))注意这里还有个细节:如果图片在页面上被 CSS 缩放或响应式拉伸了,那么图片内部坐标并不能直接映射到屏幕坐标,必须先做一次比例换算。这个比例就是rect.width / image_width和rect.height / image_height。
5.3 坑三:接口重放的陷阱
曾有一个阶段,我以为只要拿到了正确的点击坐标,就可以用requests直接 POST 坐标数据,绕过整个浏览器操作。结果发现后台对会话状态校验得极其严格——它不仅校验坐标,还会校验你是否真的加载过验证码图片、是否在本地进行过鼠标操作。
最直观的表现是:直接用requests发送同一条成功请求,响应永远是"校验失败"。我后来分析了前后端的会话机制——每一步操作都会在前端更新会话的交互状态(比如记录captchaLoadedAt、mouseTrack等字段),最终服务端会综合这些字段做整体校验。只发送最终坐标而模拟整条交互链路是绝对过不了的。
所以最稳妥的方式就是让前端代码真正地跑起来,用浏览器自动化工具(比如 Selenium、Playwright)代替纯 API 请求。在这个前提下,一切行为模拟才有意义。
5.4 最终效果
在模拟项目X的演示环境下,我的一套完整流程(YOLO 模型识别 + 质心修正 + 贝塞尔轨迹 + 随机点击节奏 + 页面坐标换算)最终统计结果是:
| 指标 | 结果 |
|---|---|
| 目标检测 mAP@0.5 | 0.92 |
| 边界框坐标命中率 | 97.8% |
| 行为模拟后整体通过率 | 92.3% |
| 平均每次验证耗时 | 4.6 秒 |
这个耗时已经和人类手动验证差不多甚至略快一点了,而且通过率足够稳定,可以支撑大规模自动化任务了。
6. 验证码对抗的未来趋势与一点忠告
在写完这套流程之后,我其实也花了不少时间思考一个更根本的问题:这类验证码的攻防为什么是一个无底洞?
从技术演进趋势来看,文字点选验证码正在向两个方向迭代:
一是语义理解加深。现在已经有验证码不再要求点击"春、夏、秋、冬"这种具体单词,而是会给出"请点击画面中所有穿红色衣服的人"、"请点击图中所有朝左的车辆"。这类验证码考验的不再是简单的文字识别,而是场景理解能力,需要模型具备常识推理,破解门槛显著提高,更贴近当前多模态大模型的研究方向。
二是个性化无序化设计。每张图的文字数量、布局、字体、干扰策略都做随机化。这直接导致依赖固定模型(如 OCR 或单一目标检测网络)的方案很容易失效,需要不断的更新与重训。说白了,破解方案和验证码设计就是一场持续的军备竞赛,任何方案都不能一劳永逸。
最后讲一句实在话:我写这篇分析文章,并不是鼓励每个人都去做一个"验证码破解器"。验证码的核心目的是保护网站免受恶意流量、爬虫刷量、撞库攻击等行为。如果你需要用自动化技术处理验证码,请务必先确认你有合法的授权,例如:
- 你拥有该网站的代码权限或测试账号;
- 你是在做自己平台的自动化回归测试;
- 你的爬虫行为遵守了
robots.txt和网站服务条款; - 你在沙箱或本地模拟环境中进行技术研究。
我想说的是,研究文字点选算法,更大的价值在于它逼着你去深入理解前端的图像渲染机制、图像处理与深度学习的结合、行为模拟的细节设计——这一整套链路本身就是一种很好的技术素养训练。当你真正看懂了这套机制之后,你对"什么是安全的验证""什么行为是机器特征"都会有一个比一般人更深刻的认知。这种底层认知,才是比任何"一招鲜"都值钱的东西。
如果你在实际操作中也遇到了类似的验证码问题,欢迎在评论区或者社区里交流。随便问,我看到都会回复。