PaddleOCR+Flask:从零搭建离线OCR识别Web服务
发布时间:2026/10/22 19:42:36 锦皓数字建站

做反向文字提取这类需求我第一反应就是先跑通 PaddleOCR再套一层 Flask 服务化让前端或者其他系统能直接通过 HTTP 调用。这篇文章主要记录我从安装 PaddleOCR、本地测试识别效果到最后用 Flask 把它封装成 Web 服务的完整过程重点放在工程落地上而不是算法理论。如果你正打算给已有系统补一个 OCR 能力或者想把自己手里的 OCR 脚本做成接口给同事用这篇文章可以直接照着抄。我要先说明一点OCR 服务化听起来不难但真正动手的时候版本选择、模型初始化时机、并发访问方式这些细节很容易让人一头雾水。本文依赖我实际跑过的环境踩过的坑都会展开讲清楚。1. 为什么选 PaddleOCR 而不是其他方案1.1 我当时的选型思路做文字识别市面上的选择并不少纯离线方案有 Tesseract在线服务有各大云厂商的 OCR API开源深度学习方案则比较流行。我在这个项目里最终选了 PaddleOCR核心原因是它能把“离线部署”和“识别效果”这两件事同时满足。云厂商的 OCR API 确实省事上传图片直接返回结果识别率也很稳。但放在内部工具里就有几个很难受的问题一是图片内容要传到外部服务器数据敏感度高的场景直接劝退二是调用量和费用挂钩批量跑起来成本不可控三是离线环境就完全没法用。所以我一开始就定了一个前提必须能完全本地化部署。Tesseract 我也用过老牌工具胜在轻量、依赖简单。但它的中文识别效果说实话不够理想尤其是复杂背景、倾斜文本和手写风格的内容错误率会明显升高。项目里需要处理的图片五花八门包括拍照的表格、扫描件、带旋转方向的票据Tesseract 需要花大量时间去做预处理和调参往往还达不到想要的效果。PaddleOCR 的优势就在这个问题上体现出来了它内置了文本检测、方向分类、文本识别三个模块基本是开箱即用的流程。我只需要把图片喂进去它自己会完成“文字在哪儿、方向对不对、内容是什么”的完整链路。再加上它对中文支持得特别好模型也能离线下载这是我们内部系统的理想方案。1.2 PaddleOCR 里到底发生了什么很多人第一次接触 PaddleOCR 时会被“检测、分类、识别”这几个概念绕晕。我用一个生活化的类比解释一下这就好比一个阅读辅助工具它的工作分三步。第一步是“检测”相当于在一张照片里用笔画出所有文字所在区域不管文字是什么内容。PaddleOCR 的检测模块是基于可微分二值化的分割模型专门检测文本框位置输出每个文本区域的四角坐标。第二步是“方向分类”判断每个文本区域里的文字是正的、倒的还是旋转了 90 度。很多 OCR 工具忽略这一步但实际拍照图片里文字方向乱来很常见票据、手机拍照图尤其明显。第三步是“识别”对校正为水平方向的文字区域做字符识别输出文字字符串和置信度。这三个模块组合在一起才是 PaddleOCR 的完整识别能力。如果只做单独识别遇到旋转文字时准确率会大幅下降。所以我部署的时候默认都是开启方向分类的。1.3 这套方案适合谁我拍板用 PaddleOCR Flask 这套组合面向的读者也比较明确已经装好 Python 环境、能写简单 Flask 接口的开发者可以直接跳过原理学落地。后端团队想给现有业务系统快速接一个 OCR 微服务不需要了解深度学习训练细节。有离线文字识别需求比如内部工单系统、档案扫描、图片信息提取这类场景。因为 PaddleOCR 本身提供了训练能力如果你不满足于现成模型效果还可以在自己的数据上做微调。我这个项目没到那一步所以全文只讲推理部署不讲训练。如果你只是需要快速把 OCR 能力用起来本文就够了。2. 安装环境与首次识别测试2.1 Python 和 PaddlePaddle 的版本匹配PaddleOCR 是 PaddlePaddle 深度学习框架之上的套件所以第一步不是直接装 PaddleOCR而是先装底层框架。版本匹配是这个环节最大的坑Python 版本、PaddlePaddle 版本、操作系统位数之间都有兼容关系。我用的是 Python 3.8 PaddlePaddle 2.6.0 PaddleOCR 2.7.3 这一组合在 Linux 环境下非常稳定。如果你的机器是 M 系列芯片或者 ARM 架构需要留意相关指令集匹配如果 Windows 平台也可以但部署到生产环境时我更建议用 Linux。先创建一个干净的虚拟环境避免污染系统中的其他 Python 包conda create -n paddle python3.8 -y conda activate paddle然后安装 PaddlePaddle。CPU 版和 GPU 版只能选一个装不要两个都装否则大概率会出现底层依赖冲突。我的项目里因为服务器没有独立显卡所以装的是 CPU 版pip install paddlepaddle2.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple这里我特意指定了清华源因为 pypi 官方源下载动辄几十 MB 的包时速度不太稳定尤其是大型依赖包。如果后续需要 GPU 推理则要安装对应的 GPU 版本还要提前准备好 CUDA 和 cuDNN 环境这个复杂度会高不少后文单独说。接着安装 PaddleOCR 本体pip install paddleocr2.7.3 -i https://pypi.tuna.tsinghua.edu.cn/simple安装过程中最有价值的经验是一定要把版本号写死。PaddleOCR 迭代很快接口在 2.x 各个小版本之间都有变动如果不锁版本很可能你昨天写的代码过两天换个环境就跑不通了接口名变了莫名其妙开始报错。2.2 快速跑通第一段识别脚本装完之后先别急着上 Flask先用一个最简单的脚本验证环境是否正常。我一般这样写from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) result ocr.ocr(test.png, clsTrue) for line in result[0]: print(line)这段代码的作用是初始化一个支持中文、开启方向分类的 OCR 实例然后对test.png做识别输出每一行文字对应的坐标和识别结果。第一次运行PaddleOCR()时程序会自动检测本地模型是否存在不存在就会启动下载。我在公司内网第一次跑的时候模型下载非常慢而且没有进度提醒看起来就像卡死了。等了大概十分钟才下载完成模型文件默认缓存在当前用户目录下。这里补充一个重要说明不同 PaddleOCR 版本返回结果的格式有差异。我使用的 2.7.3 版本返回是嵌套列表外层第一项是图片中的所有行每一项里包含坐标、文本值和置信度。如果你用的是 3.x 版本结果结构可能变化比如聚类逻辑更复杂所以看到代码时先确认自己装的版本。跑通之后把一张中文图片丢进去如果终端能打印出类似下面这样的内容说明整个流程没问题[[[78.0, 136.0], [222.0, 136.0], [222.0, 168.0], [78.0, 168.0]], (欢迎使用, 0.9998)]2.3 手动下载模型和参数微调如果你的部署环境是离线内网模型自动下载这条路就走不通了。解决方法是找一台能联网的机器先将模型缓存目录打包再拷贝到目标机器上。PaddleOCR 的模型都存放在~/.paddlex目录下按模型类型存放。把整个目录拷过去之后离线环境也能正常初始化。关于参数调优我第一次测试时就发现文档里不显眼的地方藏着一堆关键字参数比如det_db_thresh、det_db_box_thresh、rec_batch_num。这几个参数直接决定检测效果det_db_thresh检测阶段二值化的阈值默认是 0.3。调高后文字区域更容易被过滤适合背景干净、文字清晰的图片。det_db_box_thresh检测框的判定阈值默认 0.6。调高可以过滤掉低置信度的文本框减少误检。rec_batch_num识别阶段每批处理的文本行数默认 6。调大有利于提高 GPU 利用率但 CPU 下过大的批次反而会拖慢单张图片的处理时间。我业务的图片大多是扫描件背景干净所以我会把det_db_thresh调到 0.5能去掉很多背景纹理误检。但你不要盲抄建议先用一张包含噪声的图片在默认参数下跑一遍搞清楚结果里的多余文本框是什么再决定往哪个方向调。3. 基于 Flask 服务化的设计与实现3.1 服务化之前的架构思考脚本能跑通只是第一步同事不可能每人来你电脑上敲命令。我设想的最简服务形态是业务方通过 HTTP 请求上传一张图片服务端返回识别后的文本列表。这个阶段我提前想清楚了几个架构约束避免后面返工。第一个约束是模型只初始化一次。PaddleOCR 初始化时要做模型加载和权重初始化CPU 上首次初始化可能要几秒到十几秒。如果在每个请求里都初始化一个新实例服务吞吐量会低到没法用。正确做法是在模块加载时完成初始化之后所有请求复用同一个实例。第二个约束是输入方式要兼容。最简单的场景是网页表单直接上传文件但很多调用方实际上是内部系统它们不一定会做文件上传更喜欢把图片转成 Base64 字符串放在 JSON 里传过来。所以我在接口里同时支持这两种方式。第三个约束是返回结构要清晰。我不希望前端拿到一个乱七八糟的字符串而是一个包含识别文本、坐标和置信度的结构化 JSON调用方自己想怎么处理都方便。3.2 最简可用的 Flask 主程序下面这段代码是我项目中实际使用的核心服务稍作脱敏后贴出来整体是可直接运行的最小版本import base64 import threading import time import numpy as np from flask import Flask, jsonify, request from PIL import Image from paddleocr import PaddleOCR app Flask(__name__) app.config[MAX_CONTENT_LENGTH] 10 * 1024 * 1024 # 限制上传 10MB # 在模块加载阶段完成模型初始化避免每次请求重建 ocr_engine PaddleOCR(use_angle_clsTrue, langch, show_logFalse) ocr_lock threading.Lock() def parse_image_from_request(): 从请求中提取图片字节流支持文件上传和 base64 两种方式 if file in request.files: return request.files[file].read() json_data request.get_json() if json_data and image in json_data: return base64.b64decode(json_data[image]) return None def recognize(image_bytes): 执行 OCR 识别返回结构化结果 image_stream Image.open(io.BytesIO(image_bytes)).convert(RGB) image_array np.array(image_stream) start time.time() with ocr_lock: result ocr_engine.ocr(image_array, clsTrue) cost time.time() - start lines [] if result and result[0]: for line in result[0]: points line[0] text line[1][0] score float(line[1][1]) lines.append({ text: text, confidence: score, box: points }) return lines, cost app.route(/health, methods[GET]) def health(): return jsonify({status: ok}) app.route(/ocr, methods[POST]) def ocr_api(): image_bytes parse_image_from_request() if image_bytes is None: return jsonify({error: no image provided}), 400 try: lines, cost recognize(image_bytes) return jsonify({ lines: lines, time_cost: round(cost, 3) }) except Exception as exc: return jsonify({error: str(exc)}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)我重点说几个代码里的细节。app.config[MAX_CONTENT_LENGTH]是 Flask 内置的请求体大小限制。如果同事一不小心传了个几十 MB 的高清原图计算资源会被大量浪费所以我把上限卡在 10MB超过直接返回 413接口不至于被打挂。Image.open(...).convert(RGB)这行非常关键。有部分图片比如带透明通道的 PNG或者某些扫描设备产生的特殊色彩空间图片直接转成 numpy 数组时会有通道不一致问题导致识别模块报错。我在这里统一转成 RGB 三通道从源头避免这类兼容问题。ocr_lock则是对模型访问加锁避免 Flask 在多线程模式下多个请求同时访问同一个 PaddleOCR 实例导致崩溃。这个细节很多人不写但我实测过高并发时偶尔会出现奇怪的内存错误加上线程锁之后非常稳。代价是识别请求会串行但对我们内部工具来说完全够用。3.3 手动验证接口程序写完先启动然后我用curl做快速验证python app.py终端会输出类似下面的日志* Running on all addresses (0.0.0.0) * Running on http://0.0.0.0:5000新开一个终端模拟表带上传curl -X POST http://127.0.0.1:5000/ocr \ -F file/tmp/test.png这里注意一点后面要跟绝对路径不要用相对路径否则curl的行为在不同系统上可能不一致。运行后返回 JSON包含lines和time_cost。如果一切正常整个流程就是通的了。3.4 哪些细节决定了这个接口好不好用接口能跑通只是最低要求以下几个细节决定这个接口在生产环境里好不好用。第一返回里一定要保留坐标信息。业务方拿到文字后经常需要知道文字出现在图片的哪个位置比如“发票号码”四个字出现在屏幕右上角。如果不返回坐标对方后续接业务逻辑时会完全卡住还得再让你加接口。第二错误信息要能被调用方看懂。我捕获所有异常后把str(exc)原样返回虽然不美观但对调试很有帮助。生产环境你可能不想把底层异常细节暴露给调用方那就只返回一个统一错误码具体日志打到服务端。我们内部工具为了方便诊断暂时选择把异常透传出来。第三对空图片或透明图片的处理。我在解析阶段已经把不支持的数据挡在门外但仍要注意如果图片能解析但内容完全空白OCR 返回的结果集中result[0]会是个空列表此时接口也要返回正常的空结果 JSON而不是抛出异常。上面的代码用if result and result[0]已经处理了这种情况。4. 部署为生产可用的 Web 服务4.1 不要把 Flask 开发服务器当成生产服务器Flask 自带的app.run()使用的开发服务器在设计上并不适合生产环境它在并发能力上存在明显短板一个进程只能处理少量并发请求性能不佳而且资源控制不稳定。我的做法是外层使用 Gunicorn 做 WSGI 服务器让 Flask 只负责业务逻辑。Gunicorn 的启动命令如下gunicorn -w 2 -b 0.0.0.0:5000 --timeout 120 app:app参数含义分别是-w 2启动 2 个 worker 进程一般设成CPU 核数 1左右。注意每个进程会独立加载一份 PaddleOCR 模型所以它不是越多越好内存占用会翻倍。-b 0.0.0.0:5000监听所有网卡让局域网内其他机器也能访问。--timeout 120每个请求最大执行 120 秒。CPU 识别大图时可能很慢如果超时时间太短比如默认 30 秒经常出现正常识别慢但是被 Gunicorn 强杀的情况。4.2 为什么我建议用反向代理而不是直接暴露端口如果服务要暴露给公司其他系统调用我不会直接让 Gunicorn 监听公网端口而是会在前面加一层反向代理。反向代理承担静态文件、日志、访问控制、缓冲等额外职责。虽然咱们的诉求是 OCR 服务但加上反代能让接口更可控。一个最简的 Nginx 配置是server { listen 80; server_name ocr.internal.example; client_max_body_size 10m; location / { proxy_pass http://127.0.0.1:5000; proxy_read_timeout 180s; proxy_next_upstream error timeout; } }proxy_read_timeout 180s同样是为了匹配慢识别场景避免代理层先于应用层把请求断开。如果不调这个参数你经常会遇到接口本身在跑但前端提示 504 超时排查起来非常困难。配合反向代理后调用方只需要访问http://ocr.internal.example/ocr完全不用关心后端是 Flask 还是 Gunicorn也不存在跨域问题。4.3 容器化部署的注意点我们团队习惯把这类服务都打包成镜像部署时一条命令就能上线。PaddleOCR 的 Dockerfile 有固定三个注意点值得单独提一下。FROM python:3.8-slim RUN apt-get update apt-get install -y \ libgl1 \ libglib2.0-0 \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 5000 CMD [gunicorn, -w, 2, -b, 0.0.0.0:5000, --timeout, 120, app:app]第一基础镜像选择python:3.8-slim而不是alpine。Alpine 镜像虽然更小但 OpenCV 和 PaddlePaddle 这种底层依赖在 Alpine 上经常出现二进制兼容问题编译起来会把人逼疯。用 Debian 系基础镜像最省心。第二必须安装libgl1和libglib2.0-0。PaddleOCR 底层依赖 OpenCVOpenCV 运行需要这两个图形库。很多人在容器里启动时遇到libGL.so.1: cannot open shared object file就是缺了libgl1导致的网上搜半天也找不到原因。第三容器里跑模型时模型文件必须在镜像内。如果你用了模型缓存目录需要把缓存目录一起 COPY 进镜像或者通过数据卷挂载。我建议直接 COPY 进去因为模型是只读的没必要挂数据卷。4.4 性能观测和瓶颈分析部署完成后不能光看“能跑”还需要知道它能承受多少并发、单张图要多久。我用了一个最土的方式写脚本并发发 20 个请求看time_cost和错误率。实测下来CPU 环境下单张约 800×600 的普通文本图片识别耗时大约 12 秒并发请求时因为加了锁实际是串行排队所以 5 个并发请求的总耗时接近 5 个单张耗时的累加。如果你的场景并发量上来了建议改成多进程或多副本部署而不是在单进程里堆线程。性能优化方面还有两个可以马上落地的操作。一个是在初始化时关闭不需要的部分比如如果你的场景不涉及旋转文字就把use_angle_clsFalse能省掉方向分类的时间。另一个是调大rec_batch_num比如把批处理数量从 6 改成 16CPU 实测有些场景能提升一些吞吐但提升幅度取决于具体图片中包含的文本行数需要实测对比。5. 常见问题实录与排查技巧5.1 高频问题速查表我把部署过程中和其他人踩过的问题汇总成一张速查表你在遇到类似情况时直接对照排查能省不少时间。现象可能原因解决办法ImportError: libGL.so.1系统缺少 OpenCV 依赖图形库pip install opencv-python-headless或安装libgl1容器场景在 Dockerfile 里安装首次初始化极慢像卡死正在下载模型无日志提示等待或手动下载模型放进缓存目录识别结果为空列表图片无文字、阈值过高、方向未开启调低det_db_thresh开启use_angle_cls确认图片内容中文显示为乱码编码设置问题或识别模型未正确加载确认langch输出端统一 UTF-8并发请求时进程崩溃多个线程访问同一个 PaddleOCR 实例加线程锁或改为多进程模式请求上传大图被拒超过MAX_CONTENT_LENGTH限制上调 Flask 配置或压缩图片后再传接口返回 504代理层或 Gunicorn 超时时间太短调大proxy_read_timeout和gunicorn --timeout模型下载走了默认源不稳定未指定镜像源使用-i指定镜像源或内网离线加载缓存5.2 调试技巧别让日志成为摆设我见过很多部署问题其实日志里已经写得很清楚了但大家没养成看日志习惯导致排查效率很低。PaddleOCR 本身会在终端输出每个阶段的状态初始化完成后会显示Predict begin和Predict end之类的过程信息。我在正式服务代码里把这些日志控制到可控级别同时用 Flask 自身的app.logger记录每个请求的耗时、图片大小、返回行数。这样排查问题时能直接定位到“是识别太慢”还是“图片解析失败”。另外提供一个我看不上但关键时刻很管用的技巧先把图片固定在本地再用相同的参数跑脚本对比服务端和脚本的识别结果。如果脚本都失败了那说明是图片内容问题不是服务问题如果脚本成功而服务失败问题一定出在 Flask 请求处理或代码封装上。这样的定位方式对新手很友好。5.3 我对这个项目踩过最深的坑整个项目里让我印象最深的一个坑是关于图片格式兼容性的。最开始我没有做convert(RGB)处理测试时用普通 JPG 一切正常直到同事传了一张带 PNG 透明通道的图片接口直接报了维度错误。我当时的第一个念头是 PaddleOCR 不支持翻了文档半天也没有直接答案最后逐行调试才发现问题出在图片通道数上。做统一的通道转换之后这类问题再也没出现过。第二个让我印象很深的坑是模型初始化放错了位置。最初我是在每次请求里调用PaddleOCR()导致并发请求时出现一堆重复初始化的报错还把服务内存彻底打满。后来查清楚以后把初始化提到模块顶层同时搭配线程锁整个服务的稳定性才真正有了保障。这两个问题如果不经历一遍很难意识到它们有多关键。5.4 体会与扩展方向从我这边实际运行的内部服务来看用 PaddleOCR 加 Flask 搭出来的这套 OCR 服务足以应对日常批量识别需求而且完全离线可控。如果你后续想进一步扩展有几个方向很实际在识别结果中加入按坐标排序输出段落级文本而不是一行的散装文本。接入消息队列把图片 URL 发到队列异步识别后再回调结果适合大图批量场景。自己标注少量业务数据微调识别模型提升特定字体或版式的准确率。增加一个简单的图片预览前端调用方上传图片后能直观看到识别框和文本内部工具体验会好很多。我个人的感觉是OCR 服务化最大的价值不在于把脚本改成接口这件事本身而在于通过服务化让更多人能低门槛使用识别能力把重复的人工录入解放出来。如果你也正在做类似的事建议先按本文把最小闭环跑通再逐步迭代。遇到问题欢迎根据上面提到的排查思路逐条对照大概率能解决大部分卡壳点。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。