上周三我们有个合同管理系统要加 OCR 能力,产品说"扫描件丢进去就能出结构化文本"。在 Hacker News 上看到 Unlimited OCR 获得较高关注,开源项目也上线了,心想接进来应该很快。单页图片确实 10 分钟跑通了——但等我切到 batch 模式处理多页 PDF 的时候,连踩两个坑,折腾了大半天才搞明白。这篇把这两个坑讲透,希望能节省你的排查时间。

这篇适合谁

  • 已经注册了 Unlimited OCR API Key,准备接入多页 PDF 处理的后端开发
  • 单页调用跑通了但 batch/多页模式返回结果不完整,怀疑是自己代码问题的人
  • 想在 Python 项目里批量处理扫描合同、发票、报告等长文档的开发者
  • 用过 PaddleOCR 本地部署但嫌运维麻烦,想切云端 API 的团队

整体流程

  1. 注册账号拿 API Key(2 分钟)
  2. 跑通单页图片识别,验证 Key 可用
  3. 切到多页 PDF,理解 batch endpoint 的参数格式差异
  4. 处理多页响应的嵌套结构差异,写出通用解析函数
  5. 加上限流保护和错误重试,部署到生产
graph TD
    A[拿到 API Key] --> B[单页图片测试]
    B --> C{需要多页?}
    C -->|是| D[batch endpoint]
    C -->|否| E[直接用单页]
    D --> F[注意 pages 参数格式]
    F --> G[注意响应嵌套层级]
    G --> H[统一解析函数]
    H --> I[加限流+重试]

先说结论

差异点 单页 endpoint batch/多页 endpoint
pages 参数 不需要传 必须传数组 [1,2,3],不是字符串 "1,2,3"
响应里 blocks 层级 resp['blocks'] 直接是列表 resp['pages'][i]['blocks'] 多套一层
confidence 位置 顶层 resp['confidence'] 每个 page 对象里各有一个
典型报错 参数格式错误(400)或认证失败(401) 静默返回只有第一页(不报错!)

最坑的是第二点——多页模式下如果你用单页的解析逻辑去读 resp['blocks'],它不会报错,只会拿到第一页的内容,后面的页全部丢失。我排查了两个小时才发现数据其实返回了,只是取错了层级。

第一步:跑通单页,确认环境没问题

先确保你的 Key 是好的。去 Dashboard -> API Keys 页面拿到 Key 之后:

import requests

with open('image.png', 'rb') as f:
    resp = requests.post(
        'https://api.unlimitedocr.com/v1/ocr',
        headers={'Authorization': 'Bearer YOUR_API_KEY'},
        files={'file': f},
        data={'language': 'chi_sim'}
    )

如果返回 {"text": "...", "confidence": 85},说明通了。如果看到这个:

HTTP 401 Unauthorized
{"error": "Invalid API key or API key not provided"}

大概率是请求头写成了 Authorization: YOUR_API_KEY,漏掉了 Bearer 前缀。注意 Bearer 后面有个空格。

第二步:切到多页 PDF——第一个坑来了

单页跑通之后我直接把 PDF 丢上去,加了个 pages 参数想指定解析 1-5 页:

data={
    'language': 'chi_sim',
    'pages': '1,2,3,4,5'  # ← 错的!
}

返回 200,但只有第一页的结果。没有报错,没有 warning,就是静默地只给你一页。说实话一开始我以为是 PDF 本身有问题,换了三个文件才意识到是参数格式不对。

正确写法——pages 要传 JSON 数组:

import json

data={
    'language': 'chi_sim',
    'pages': json.dumps([1, 2, 3, 4, 5])
}

或者如果你用 json= 参数而不是 data=,直接传 list 就行。但用 files= + data= 组合时(上传文件必须这么写),data 里的值只能是字符串,所以得手动 json.dumps

文档里的示例全是单页的,压根没提这个区别。我是翻了 GitHub Issues 才确认的。

第三步:解析多页响应——第二个坑

单页响应长这样:

{
    "text": "识别结果...",
    "confidence": 87,
    "blocks": [{"text": "段落1", "bbox": [...]}]
}

多页响应长这样:

{
    "pages": [
        {"page": 1, "confidence": 85,
         "blocks": [{"text": "...", "bbox": [...]}]},
        {"page": 2, "confidence": 82,
         "blocks": [...]}
    ]
}

看到区别了吗?单页时 blocks 在顶层,多页时 blocks 藏在 pages[i] 里面。如果你写了一个通用解析函数直接读 resp['blocks'],多页模式下会抛 KeyError——或者更糟,如果你用了 .get('blocks', []),它会静默返回空列表,你以为没识别到内容。

我写了个兼容函数:

def parse_ocr_response(resp_json):
    if 'pages' in resp_json:
        return resp_json['pages']
    return [resp_json]  # 包装成统一格式

这样不管单页还是多页,后续逻辑都统一遍历列表就行。每个元素保证有 blocksconfidence

第四步:加上限流保护

Unlimited OCR 默认 Rate Limit 以官方文档为准,批量处理 PDF 的时候容易触发限流:

HTTP 429 Too Many Requests
{"error": "Rate limit exceeded. Please retry after 60 seconds"}

我用了个简单的信号量控制并发:

import asyncio

sem = asyncio.Semaphore(10)  # 最多 10 并发

async def ocr_with_limit(file_path):
    async with sem:
        # 调用 OCR API
        await asyncio.sleep(1.1)  # 保险间隔

控制每秒不超过 1 次请求是比较稳妥的起点,具体上限请以官方文档为准。如果业务量较大需要更高 QPS,可咨询官方的企业版方案。

不同场景怎么选

只处理单张图片/截图:直接用基础 endpoint,不用管 pages 参数,files={'file': f} 即可。

处理多页 PDF(合同/报告/发票):必须走 batch 模式,pages 参数传数组,解析时用上面的兼容函数。建议开 enhance_image: true,扫描件质量参差不齐。

混合场景(图片+PDF 都有):统一用兼容解析函数,上传前判断文件类型决定要不要传 pages。文件大小上限请以官方文档为准。

需要对接多种 AI 模型做后处理:OCR 拿到文本之后,很多人会丢给大模型做结构化提取。这时候如果你同时调多个模型 API,用 OpenRouter 或 ofox.io 这类聚合网关会方便一些,改个 base_url 就能切模型,不用每个厂商单独维护 Key 和 SDK。各平台的定价和加价比例以其官方定价页为准,可根据预算选择合适套餐。

中文识别场景language 必须显式指定 chi_sim(简体)或 chi_tra(繁体),别用 auto。图片分辨率不足时识别率会明显下降,开 enhance_image 有一定改善效果,但并非万能。

踩坑记录 / 常见问题 FAQ

Q: pages 参数传了数组但还是只返回第一页?

A: 检查你是不是用了 data= 传参。data 字典里的值必须是字符串,所以要 json.dumps([1, 2, 3])。如果你直接传了 Python list 对象,requests 会将 list 展开为重复字段(如 pages=1&pages=2&pages=3),而非 JSON 数组,服务端通常无法正确解析,就会 fallback 到只处理第一页。

Q: confidence 低于多少需要重试或人工复核?

A: 没有官方标准,需根据业务容忍度自行设定。作者经验是低于 65 的会进人工队列,可根据业务需求调整这个阈值。

Q: 文件超过限制怎么办?

A: 超出大小限制会直接返回 413 Payload Too Large。具体的单图和 PDF 上限请以官方文档为准。如果是高分辨率扫描件,可以先压缩再传。用 Pillow 降低 DPI 是常见做法,体积可显著减小(实际比例因图像内容和格式而异),识别率通常影响不大。

Q: 批量处理时怎么避免触发 429 限流?

A: 控制请求频率,用 asyncio.Semaphoretime.sleep 限速均可。具体的速率上限请以官方文档为准,如需更高 QPS 可咨询企业版方案。

Q: 返回结果乱码,中文全是问号?

A: 大概率是 language 参数没指定或者写错了。中文必须是 chi_simchi_tra,写 chinesecn 都不行,会返回 400 Bad Request: Language code 'cn' is not supported

小结

Unlimited OCR 接入确实快,单页场景基本是"贴个 Key 就能跑"的体验。但多页 PDF 场景有两个文档里没写清楚的坑:pages 参数必须是 JSON 数组而非逗号分隔字符串,响应结构的嵌套层级在单页和多页之间不一致。写一个兼容解析函数,加上限流逻辑,生产环境就基本稳了。后续版本会不会统一这两个 endpoint 的行为尚不确定,目前只能在应用层做适配。

更多推荐