Python爬虫遇到验证码怎么破?OCR识别与打码平台接入

桃夭
桃夭
发布时间:2026-09-18 | 7人浏览过
原创

Python爬虫遇到验证码时,不应该把所有验证码都当成一个问题处理。简单的数字、字母图片验证码,在自己拥有或明确获得授权的系统中,可以通过OCR识别和图像预处理完成自动化;复杂的交互式验证码、行为验证和风险验证,本质上属于访问控制机制,更合适的做法通常是改用官方API、登录后的授权接口,或者保留人工确认环节。

本文重点讲清楚三个实用问题:怎样判断验证码是否适合OCR、怎样使用Python完成一个本地验证码OCR流程,以及需要人工参与时,怎样设计一个通用的“打码/人工审核服务”接口。示例均以本地测试图片、自有系统或获得授权的自动化任务为前提,不讨论绕过第三方网站安全控制的方法。

使用边界:验证码通常用于防止自动化滥用。对不属于自己的站点,应优先使用网站提供的API、数据导出功能或取得明确授权。不要通过验证码识别、代理轮换、浏览器指纹伪装等方式规避第三方网站的访问限制。

一、先判断:遇到的验证码到底该怎么处理

看到验证码后,最容易犯的错误是立即寻找“识别率最高的模型”。实际上,先判断验证码类型更重要,因为不同验证码背后的验证机制完全不同。

传统图片验证码通常只是让用户输入图片里的数字或字母。例如内部管理系统登录页显示一张“5827”的图片,这种验证的核心输入就是图片本身,比较适合OCR。

如果页面要求点击指定物体、拖动滑块、完成拼图,甚至只显示“正在验证浏览器”,问题就不再只是图片文字识别。服务器可能同时分析会话状态、浏览器行为、设备信息和风险评分。此时即使读取出图片内容,也不等于完成验证。

SVG 配图 1
图1:验证码出现后,不要直接进入识别流程,先确认授权范围和验证码类型。
验证码类型 典型表现 推荐处理方式
简单图片文字 4~6位数字、字母,输入框提交 自有或授权系统中可尝试OCR
干扰型文字图片 旋转字符、噪点、干扰线 图像预处理后OCR,必要时人工确认
点击/拼图/滑块 需要鼠标交互完成验证 优先人工流程或调整自有系统测试接口
行为或风险验证 验证浏览器、设备、会话和行为 不要用OCR解决,优先官方接口或授权方式

二、简单图片验证码为什么可以用OCR

OCR的任务本质上是把图像里的文字转换成字符。假设测试系统中的验证码图片是:

6 8 2 9

只要数字轮廓足够清晰,OCR引擎就有机会返回字符串 6829。真正影响效果的通常不是Python代码本身,而是输入图片的质量。

常见干扰包括浅色背景、噪点、字符粘连、干扰线、低分辨率和字符旋转。对于这些情况,通常先使用 Pillow 或 OpenCV 对图片进行灰度化、放大、二值化,再交给OCR。

准备测试环境

下面以 pytesseract 配合 Pillow 为例。Python包可以在项目虚拟环境中安装:

python -m venv .venv
source .venv/bin/activate
pip install pillow pytesseract

Windows下激活虚拟环境可以使用:

.venv\Scripts\activate

pytesseract只是Python调用接口,本机还需要安装Tesseract OCR程序。安装完成后可以通过下面的命令确认系统能够找到它:

tesseract --version

不同操作系统的软件安装方式可能变化,因此这里不绑定具体版本号。只需要确认命令能够正常输出版本信息即可。

三、先做图像预处理,再交给OCR

假设项目目录中有一张来自自己测试系统的验证码图片:

captcha_demo/
├── captcha.png
└── recognize.py

比起直接把原图送进OCR,更稳妥的处理顺序是:读取图片、转灰度、放大、二值化、OCR识别、清理结果、检查格式。

SVG 配图 2
图2:OCR识别不是一个函数调用,而是一条“预处理 → 识别 → 校验”的流程。

完整Python示例

创建 recognize.py:

import re
from pathlib import Path

from PIL import Image, ImageOps
import pytesseract


IMAGE_PATH = Path("captcha.png")


def preprocess_image(path: Path) -> Image.Image:
    """对本地测试验证码进行基础预处理。"""
    image = Image.open(path)

    # 1. 转成灰度图
    gray = ImageOps.grayscale(image)

    # 2. 放大,便于OCR识别较小的字符
    scale = 3
    enlarged = gray.resize(
        (gray.width * scale, gray.height * scale)
    )

    # 3. 简单二值化
    threshold = 160
    binary = enlarged.point(
        lambda pixel: 255 if pixel > threshold else 0
    )

    return binary


def recognize_digits(image: Image.Image) -> str:
    """
    示例假定测试验证码只包含数字。
    --psm 7 表示把图片视为单行文本。
    """
    text = pytesseract.image_to_string(
        image,
        config="--psm 7 -c tessedit_char_whitelist=0123456789"
    )

    # 清除换行、空格等非数字内容
    return re.sub(r"\D", "", text)


def validate_result(text: str, expected_length: int = 4) -> bool:
    return text.isdigit() and len(text) == expected_length


def main():
    if not IMAGE_PATH.exists():
        raise FileNotFoundError(f"找不到测试图片: {IMAGE_PATH}")

    processed = preprocess_image(IMAGE_PATH)
    processed.save("captcha_processed.png")

    result = recognize_digits(processed)

    print(f"OCR候选结果: {result!r}")

    if validate_result(result):
        print("格式校验通过")
    else:
        print("格式校验失败,需要重新识别或人工确认")


if __name__ == "__main__":
    main()

在 captcha_demo 目录中运行:

python recognize.py

假设测试图片中的内容恰好为 6829,可能看到类似下面的结果。

示例输出:
OCR候选结果: '6829'
格式校验通过

这里有一个容易忽略的细节:程序没有因为OCR返回了内容就直接接受,而是又检查了一次“必须全部是数字,而且长度必须为4”。

如果自己的验证码规则是6位字母数字混合,就应该修改白名单和校验规则,而不是简单删除所有非数字字符。

四、二值化阈值不是越大越好

上面的 threshold = 160 只是一个演示值,并不存在适用于所有验证码的固定阈值。

二值化的逻辑是把灰度像素分成两类。例如:

pixel > 160  → 白色
pixel <= 160 → 黑色

如果阈值过低,浅色干扰线可能被保留下来;阈值过高,原本较浅的文字笔画又可能被删除。因此实际项目应该准备一批自有系统的测试样本,再根据样本效果选择参数。

更稳妥的测试方法,是一次生成几种预处理结果进行人工观察:

thresholds = [120, 140, 160, 180]

for value in thresholds:
    binary = enlarged.point(
        lambda pixel, t=value: 255 if pixel > t else 0
    )
    binary.save(f"captcha_threshold_{value}.png")

运行后会得到:

captcha_threshold_120.png
captcha_threshold_140.png
captcha_threshold_160.png
captcha_threshold_180.png

这样可以直接比较哪个阈值更容易保留字符轮廓,而不是凭感觉不断改代码。

五、OCR识别失败时,不要无限重试

自动化程序处理中最危险的设计之一,是“识别失败就一直请求新的验证码”。这会让程序进入死循环,也可能给自己的服务制造大量无效请求。

更合理的做法是设置有限次数:

MAX_ATTEMPTS = 3

for attempt in range(1, MAX_ATTEMPTS + 1):
    result = recognize_digits(processed)

    if validate_result(result):
        print("识别成功:", result)
        break

    print(f"第 {attempt} 次识别未通过格式校验")
else:
    print("自动识别失败,进入人工确认流程")

需要注意的是,真实业务中“验证码提交失败”与“OCR格式错误”是两个不同阶段。

  • OCR返回 68,而规则要求4位数字,这是本地就能判断的格式错误。
  • OCR返回 6829,格式正确,但服务器判断验证码错误,这是识别内容错误。
  • 验证码明明正确却仍然失败,则还要检查验证码是否与当前会话、Cookie或业务请求绑定。
SVG 配图 3
图3:即使在自有系统中,验证码图片也可能与Session绑定,识别结果必须回到原会话中验证。

六、在自有系统里抓取验证码时,要保留同一个Session

如果正在对自己的Web系统进行自动化测试,可以使用 requests.Session() 保持Cookie。下面使用本机测试地址作为例子:

import requests

session = requests.Session()

captcha_response = session.get(
    "http://127.0.0.1:8000/test/captcha",
    timeout=10
)

captcha_response.raise_for_status()

with open("captcha.png", "wb") as f:
    f.write(captcha_response.content)

之后如果需要把识别结果提交到自有测试接口,也应该继续使用同一个 session 对象:

payload = {
    "username": "test_user",
    "captcha": "6829"
}

response = session.post(
    "http://127.0.0.1:8000/test/check",
    data=payload,
    timeout=10
)

response.raise_for_status()
print(response.text)

这里的重点不是URL,而是Session关系。验证码图片由会话A生成,就应该在会话A中提交。如果重新创建一个 requests.post(),服务器可能会把它看成另一个会话。

在自己的开发和测试环境中,如果验证码严重影响自动化测试,更推荐提供专门的测试配置,例如测试环境固定验证码、测试账号绕过验证码,或者暴露仅测试环境可用的认证接口。这通常比训练OCR模型更稳定。

七、什么时候需要人工打码或人工审核

OCR并不适合所有图片验证码。如果字符扭曲严重、样本变化大,继续堆积预处理规则可能比人工确认更复杂。

对于获得授权的业务自动化,可以设计一个人工审核队列:

  1. Python程序上传待确认图片。
  2. 审核服务创建一个任务ID。
  3. 工作人员在内部页面查看图片并输入结果。
  4. Python程序根据任务ID获取结果。
  5. 如果超时或审核失败,终止当前任务,而不是无限循环。

这种结构与商业“打码平台”的基本接口模式相似,但建议把它理解成通用的异步人工任务接口。不同服务的字段、鉴权和计费规则都可能变化,实际接入时应以所选择服务的官方文档为准。

八、设计一个通用的打码平台适配层

不建议把某个服务商的API字段直接写满整个爬虫项目。更好的设计是定义一个统一接口,例如:

class CaptchaSolver:
    def submit(self, image_path: str) -> str:
        """提交图片,返回任务ID。"""
        raise NotImplementedError

    def get_result(self, task_id: str) -> str | None:
        """查询任务结果;尚未完成时返回None。"""
        raise NotImplementedError

之后无论接自己的人工审核服务,还是经过授权使用的第三方人工服务,爬虫主体只依赖 CaptchaSolver,不依赖具体供应商。

例如,一个内部人工审核服务可能约定:

POST /api/captcha/tasks
GET  /api/captcha/tasks/{task_id}

提交任务时:

import requests


class InternalHumanSolver:
    def __init__(self, base_url: str, api_token: str):
        self.base_url = base_url.rstrip("/")
        self.headers = {
            "Authorization": f"Bearer {api_token}"
        }

    def submit(self, image_path: str) -> str:
        with open(image_path, "rb") as image_file:
            response = requests.post(
                f"{self.base_url}/api/captcha/tasks",
                headers=self.headers,
                files={"image": image_file},
                timeout=15
            )

        response.raise_for_status()
        data = response.json()

        return data["task_id"]

    def get_result(self, task_id: str) -> str | None:
        response = requests.get(
            f"{self.base_url}/api/captcha/tasks/{task_id}",
            headers=self.headers,
            timeout=10
        )

        response.raise_for_status()
        data = response.json()

        if data["status"] == "done":
            return data["text"]

        if data["status"] == "failed":
            raise RuntimeError("人工审核任务失败")

        return None

这段代码描述的是一种通用接口模式,并不对应某个真实服务商。真实平台可能使用JSON上传、Base64、对象存储地址或其他任务字段,因此不能直接照搬字段名称。

加入有限轮询

人工任务通常不会立即完成,所以程序需要查询状态,但不能高频无限请求:

import time


def wait_for_result(
    solver,
    task_id: str,
    max_wait_seconds: int = 60,
    interval_seconds: int = 3
) -> str:
    deadline = time.monotonic() + max_wait_seconds

    while time.monotonic() < deadline:
        result = solver.get_result(task_id)

        if result:
            return result

        time.sleep(interval_seconds)

    raise TimeoutError("等待人工审核结果超时")

完整调用逻辑类似:

solver = InternalHumanSolver(
    base_url="http://127.0.0.1:9000",
    api_token="YOUR_TEST_TOKEN"
)

task_id = solver.submit("captcha.png")

try:
    text = wait_for_result(solver, task_id)
    print("人工确认结果:", text)
except TimeoutError:
    print("本次任务结束,等待人工确认超时")

实际项目中不要把访问令牌直接写死在源代码里。可以从环境变量读取:

import os

api_token = os.environ["CAPTCHA_REVIEW_TOKEN"]

九、OCR和人工审核最好组合,而不是二选一

对于自己的系统,一个比较实际的架构是:OCR先处理容易的样本,只有自动识别结果不可信时才进入人工审核。

例如,验证码规则明确规定为4位数字,那么:

def choose_result(ocr_text: str):
    if ocr_text.isdigit() and len(ocr_text) == 4:
        return {
            "status": "candidate",
            "text": ocr_text
        }

    return {
        "status": "need_human",
        "text": None
    }

这里故意使用了 candidate,而不是 success。原因是格式正确只能说明“看起来像一个合法验证码”,不能证明OCR识别内容一定正确。真正成功仍然要由服务器返回结果决定。

SVG 配图 4
图4:比较稳妥的方案是“OCR优先、人工兜底、服务器最终验证”,同时保持会话和重试上限。

十、不要只依赖OCR返回的“置信度”

一些OCR引擎能够返回置信度,但置信度只是模型自己的判断,并不能直接等价为“验证码正确概率”。

例如模型可能非常确定地把数字 8 看成 3。因此判断是否自动采用结果时,可以综合三个条件:

  • 输出格式是否符合验证码规则;
  • OCR引擎自身是否给出足够可信的结果;
  • 服务器最终是否接受验证码。

如果系统对错误提交有严格次数限制,那么即使OCR结果看起来正常,也应该更加保守,必要时进入人工确认,而不是连续尝试多个猜测。

十一、验证码失败后应该重新获取还是继续提交

通常验证码被提交一次后是否还能继续使用,完全取决于服务端设计。有的验证码一次提交后立即失效,有的只在成功后失效,还有的存在几十秒或几分钟有效期。

因此自己的系统应该把状态定义清楚。例如服务端返回:

{
  "code": "CAPTCHA_INVALID",
  "message": "验证码错误"
}

客户端看到 CAPTCHA_INVALID 后,应该丢弃当前验证码,并重新获取新的测试验证码,而不是继续猜测同一张图片。

如果返回:

{
  "code": "CAPTCHA_EXPIRED",
  "message": "验证码已过期"
}

则问题不是OCR识别错误,而是整个处理过程耗时过长。人工审核平台接入后尤其要注意这种情况,因为人工任务可能超过验证码有效期。

十二、常见问题怎么排查

1. OCR总是返回空字符串

先把预处理后的图片保存下来检查:

processed.save("captcha_processed.png")

如果保存后的图像里字符已经看不清,就应该先调整预处理,而不是继续修改OCR参数。

2. 数字8经常识别成3

这通常属于字符轮廓问题。可以尝试提高图像分辨率、调整二值化阈值,或者针对自有验证码准备足够多的测试样本评估识别错误类型。

不要只针对一张验证码不断调参数,否则很容易得到“只适合这一张图”的处理规则。

3. OCR明明识别正确,服务器还是说验证码错误

重点检查三个地方:

  • 获取验证码与提交验证码是否使用同一个Session;
  • 验证码是否已经过期或被前一次提交消耗;
  • 页面上是否还有CSRF Token、业务ID等与当前请求绑定的参数。

如果目标是自己的系统,查看服务端日志通常比继续猜客户端原因有效得多。

4. 人工审核结果回来时验证码已经失效

说明当前验证码有效期和人工流程不匹配。对于自有系统,应该调整测试机制,而不是不断缩短人工审核轮询间隔。

例如可以在测试环境延长验证码有效期,或给自动化测试账号配置专用验证策略。

5. 是否应该直接接商业打码平台

技术上这类服务通常采用“提交任务 → 获取任务ID → 查询结果”的异步模式,但是否应该使用取决于目标系统的授权关系。

如果是在自己的系统、经过客户明确授权的测试环境或内部业务流程中,可以按照服务商的官方API文档实现前面介绍的适配层。如果目的是规避第三方网站的反自动化验证,应改用该网站提供的API、开放平台或其他许可方式。

十三、实际项目中应该怎样选择

如果只是处理自己系统中的简单数字验证码,可以从 Pillow + Tesseract 这样的基础方案开始,不必一开始就训练模型。

当验证码稍有噪声时,先改善图像预处理。灰度化、放大和二值化往往比不断更换OCR库更值得尝试。

如果OCR只能处理一部分样本,可以采用“自动识别 + 人工审核”的方式,把无法可靠判断的图片送入内部审核队列,并设置明确的超时和失败状态。

如果遇到的是滑块、点击、行为验证或风险控制,不要继续把它当OCR问题。这些机制的目的本身就是控制自动化访问。对于第三方站点应使用官方授权的数据入口;对于自己的系统,则可以从测试环境、接口设计和测试账号策略上解决。

十四、总结

Python爬虫遇到验证码时,真正需要解决的不是“找到一个万能验证码识别工具”,而是先判断验证码属于哪一种验证机制。

对于自有或明确授权系统中的简单图片文字验证码,可以采用这样的流程:

  1. 使用同一个Session获取验证码图片;
  2. 灰度化、放大和二值化图片;
  3. 通过OCR得到候选字符;
  4. 按照长度和字符范围进行格式校验;
  5. 低可信结果转入人工审核,而不是无限重试;
  6. 使用原Session提交,并由服务器完成最终验证;
  7. 为OCR、人工任务和网络请求分别设置超时与最大重试次数。

如果需要接入“打码平台”,代码层面最好使用统一的 CaptchaSolver 适配接口,把“提交图片、获得任务ID、查询结果”与具体业务逻辑分开。这样无论后面更换内部人工审核系统还是经过授权使用其他服务,都不需要重写整个爬虫。

验证码一旦涉及滑块、行为分析、设备风险控制或第三方站点的反自动化机制,就已经超出了普通OCR的范围。此时最稳定的解决办法通常不是继续增加破解逻辑,而是使用官方API、取得访问授权,或者在自己控制的测试环境中改变验证方式。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表