# invoice-ocr **Repository Path**: li_shuai_carson/invoice-ocr ## Basic Information - **Project Name**: invoice-ocr - **Description**: 把自己写的一个小工具开源了:本地离线运行的票据识别服务。 起因是处理一批发票时两条路都不太顺——传到云上按次付费,票据数据要出域; 或者拿通用 OCR 自己从零写后处理。于是做了第三个选项: RapidOCR + ONNX,纯 CPU 推理。不要显卡、不要云 API、不要密钥, 装好就能断网跑。 - **Primary Language**: Python - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-11-05 - **Last Updated**: 2026-11-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 发票识别服务 · Invoice OCR [![star](https://gitee.com/li_shuai_carson/invoice-ocr/badge/star.svg?theme=dark)](https://gitee.com/li_shuai_carson/invoice-ocr/stargazers) [![fork](https://gitee.com/li_shuai_carson/invoice-ocr/badge/fork.svg?theme=dark)](https://gitee.com/li_shuai_carson/invoice-ocr) [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [赞助支持](https://ifdian.net/a/lslist666) ![封面](assets/cover.png) **本地离线运行的增值税发票识别服务。CPU 即可推理,无需云 OCR API、无需 GPU、不需要任何密钥,票据数据不出内网;请求与响应格式对齐腾讯云 `VatInvoiceOCR`,原有代码零改动即可切换。** `Python 3.10+` · `FastAPI` · `RapidOCR / PP-OCRv6` · `Docker 一键启动` · `MIT License` > ⚠️ **本工具仅做票据文字识别与字段结构化提取,所有识别结果必须人工复核。** > 不提供发票验真、不提供纳税申报、不提供抵扣计算。详见 [DISCLAIMER.md](DISCLAIMER.md)。 --- ## 目录 - [为什么会有这个项目](#为什么会有这个项目) - [30 秒启动](#30-秒启动) - [架构](#架构) - [接口与字段](#接口与字段) - [3 条 curl 示例](#3-条-curl-示例) - [多用户与额度管理](#多用户与额度管理) - [实测数据](#实测数据) - [已知限制](#已知限制) - [许可与商业支持](#许可与商业支持) - [捐赠支持](#捐赠支持) --- ## 为什么会有这个项目 市面上的选择基本是两类,各有各的别扭: | | 云 OCR API(腾讯/百度/阿里等) | 通用开源 OCR(PaddleOCR 等) | |---|---|---| | 识别准确率 | 高 | 中,需要自己写大量后处理 | | 票据数据位置 | 上传到第三方云 | 本地 | | 成本 | 按调用次数计费,量大就贵 | 免费 | | 发票字段结构化 | 现成 | **几乎没有,要自己从零写** | | 部署门槛 | 低 | 高,常常要 GPU | 这个项目补的是中间的空白:**用本地 CPU 推理,但直接产出结构化的发票字段和明细表。** 具体做了这些事(这部分才是真正耗时的): - **票据头抽取**:发票号码、开票日期、购销双方信息、合计金额与税额、价税合计大小写 - **明细表还原**:商品名称/规格/单位/数量/单价/金额/税率/税额逐列对齐 - **折行归列**:长商品名换行时,按字符坐标把折行片段分回「名称/规格/单位」各自的列,而不是简单拼接 - **粘连拆分**:`数量`与`单价`挤在一个检测框(如 `1887.28`)时反推拆分 - **合计行容错**:OCR 把「合计」识别成「计」时,用下一行的「价税合计」反证补全 - **全电票纠正**:20 位号码 + 标题误识(如「国统(普通发票)」)时纠正为标准名称 - **备注区定位**:按版面区域(价税合计行 → 签章行,且横坐标在标签右侧)收集,避免把相邻内容当备注 同时保留了一个 `provider=tencent` 开关:如果你已经买了腾讯云额度、想让本服务直接透传结果,改一行配置即可(此时不做本地识别)。 --- ## 30 秒启动 ```bash git clone https://gitee.com/li_shuai_carson/invoice-ocr.git && cd invoice-ocr cp .env.example .env docker compose up -d --build ``` 首次构建约 2~4 分钟(装依赖 + 31MB 模型随包内置,不联网下载模型)。启动后: ```bash curl http://localhost:8010/health # {"status":"ok","provider":"local","engine":"rapidocr 3.10.0","auth_enabled":false,...} ``` 镜像已经包含 PP-OCRv6 检测/识别模型(约 31MB),**构建完成后可完全断网运行**, 适合内网、隔离网络、客户现场部署。 不用 Docker 也可以: ```bash pip install -r requirements.txt python -m uvicorn app.main:app --host 0.0.0.0 --port 8010 ``` > 首次识别会有 3~10 秒的模型加载耗时,之后常驻内存。建议先用 `/health` 打一次预热。 --- ## 架构 ```mermaid flowchart LR A["调用方
Java / Python / curl"] -->|"POST /ocr/vatinvoice"| B["FastAPI 网关
app/main.py"] B --> C{"鉴权
X-API-Key"} C -->|"通过"| D["图片来源解析
Base64 / URL / PDF"] C -->|"拒绝"| Z["401"] D --> E["版面识别
RapidOCR · PP-OCRv6
ONNX Runtime · CPU"] E --> F["票据头抽取
extract/vat.py"] E --> G["明细还原
extract/items.py
layout.py · postprocess.py"] F --> H["响应组装
腾讯同构 JSON"] G --> H H --> I["调用方"] D -.->|"大 PDF 分页"| J["/ocr/vatpdf
NextPages 续传"] ``` 代码分层很薄,方便改造: ``` app/ ├── main.py 路由、鉴权中间件、错误处理 ├── schemas.py 腾讯云请求/响应模型(PascalCase / camelCase / snake_case 都接受) ├── service.py 编排:取图 → 识别 → 抽取 → 组装 ├── image_loader.py Base64 / URL / 本地 / PDF 统一转 PIL.Image ├── general.py RecognizeGeneralInvoice 结构组装 ├── auth.py users.py 多用户、额度、时段校验(SQLite) ├── ocr/ │ ├── base.py OcrLine / OcrResult 抽象 │ ├── local.py RapidOCR 本地推理(默认) │ └── tencent.py 腾讯云 TC3 签名透传(可选) └── extract/ ├── layout.py 文本行 → 行/列/区域 ├── vat.py 票据头字段 ├── items.py 明细表 └── postprocess.py 数值归一化、校验 ``` --- ## 接口与字段 | 路径 | 说明 | |---|---| | `POST /ocr/vatinvoice` | 腾讯 `VatInvoiceOCR` 同构,单图/单页 PDF | | `POST /ocr/vatpdf` | 多页 PDF 批量,`NextPages` / `RemainingPages` 续传,单次 ≤10 页 | | `POST /ocr/generalinvoice` | 腾讯 `RecognizeGeneralInvoice` 同构,`MixedInvoiceItems` 结构 | | `POST /ocr/upload` | `multipart/form-data`,字段名 `file` | | `POST /` | 按 `X-TC-Action` 头分发到上面两个 Action | | `GET /health` | 存活检查 + 引擎信息 | ### 字段对照 腾讯云 `VatInvoiceOCR` 的 `VatInvoiceInfos` 是一个 `[{Name, Value}]` 扁平数组, 共 **52 个 `Name`**。本服务**完整保留这 52 个字段位**,未识别到的字段返回空字符串, 因此调用方不需要改字段映射逻辑。 | 分组 | 字段(腾讯 `Name`) | 取值方式 | |---|---|---| | 票据头 | 发票名称、发票代码、发票号码、开票日期、机器编号、校验码 | 票面文字直读 + 版式定位 | | 密码区 | 密码区1、密码区2、密码区3、密码区4 | 按区域分行读取 | | 合计 | 合计金额、合计税额、价税合计(大写)、小写金额 | 票面直读,并与明细汇总交叉校验 | | 销售方 | 销售方名称、销售方识别号、销售方地址、电话、销售方开户行及账号 | 票面直读 | | 购买方 | 购买方名称、购买方识别号、购买方地址、电话、购买方开户行及账号 | 票面直读 | | 落款 | 收款人、复核、开票人 | 票面直读 | | 票据属性 | 发票类型、发票消费类型、服务类型、联次、是否代开、省、市、是否有公司印章 | 版式推断 / 关键字匹配 | | 专项标记 | 成品油标志、通行费标志、车船税、车牌号、类型、通行日期起、通行日期止 | 关键字匹配,无则为空 | | 其他 | 打印发票代码、打印发票号码、二维码、备注 | 直读 + 区域定位 | `Items[]` 明细共 **17 个字段**,与腾讯完全一致: `LineNo` `Name` `Spec` `Unit` `Quantity` `UnitPrice` `AmountWithoutTax` `TaxRate` `TaxAmount` `TaxClassifyCode` `VehicleType` `VehicleBrand` `DeparturePlace` `ArrivalPlace` `TransportItemsName` `ConstructionPlace` `ConstructionName` > `UnitPrice` 保留票面原始精度(例如 `294.247787610619`),不做四舍五入, > 以免与「金额 = 数量 × 单价」的勾稽关系对不上。 ### 实测一致性 拿一张含多行明细的电子普通发票,把本服务与腾讯云 `VatInvoiceOCR` 的返回做逐字段比对(递归展开到叶子): | 指标 | 结果 | |---|---| | 叶子字段总数 | 128 | | 键名与层级完全一致 | 128 / 128 | | 缺失的键 | 0 | | 值完全相同 | 115 / 128 | | 存在差异 | 13 | 13 处差异全部是以下三类,**没有一处是字段级缺失或结构性差异**: 1. **`Polygon` 坐标系不同**:本服务输出原图像素坐标(如 1910×2000),腾讯按内部缩放后的坐标(如 1184×1245)输出。坐标系原点与取点顺序一致,做一下等比换算即可对齐。 2. **`Angle` 浮点尾数**:本服务 `0.0`,腾讯 `-0.02`。本服务的角度由文本行上沿斜率估计,只在倾斜明显时才有非零值。 3. **个别字符的识别差异**(两个引擎各有对错,不是单向更差): - 单位 `ml` 被识别成 `m1`(本服务错,已通过后处理规则缓解) - `SOmni` 被腾讯识别成 `S0mmi`;`10X` 被腾讯识别成 `10×`(本服务对) - 某些三个字的商品名,腾讯会漏识中间一个字(本服务完整) > 说明:OCR 属于概率模型,同一张图不同次运行可能有个别字符抖动。上表是在固定样本、 > 固定版本下的实测结果,**不代表对所有票种的准确率承诺**。 --- ## 3 条 curl 示例 > 示例默认 `localhost:8010` 且 `auth.enabled=false`。开启鉴权后加 > `-H "X-API-Key: YOUR_API_KEY"`。 ### 1)URL 识别单张发票 ```bash curl -X POST http://localhost:8010/ocr/vatinvoice \ -H "Content-Type: application/json" \ -d '{ "ImageUrl": "https://example.com/invoice.png" }' ``` 返回(截取核心部分): ```json { "Response": { "VatInvoiceInfos": [ {"Name": "发票名称", "Value": "天津增值税电子普通发票"}, {"Name": "发票代码", "Value": ""}, {"Name": "发票号码", "Value": "63591128"}, {"Name": "开票日期", "Value": "2019年06月16日"}, {"Name": "合计金额", "Value": "¥87.28"}, {"Name": "合计税额", "Value": "2.62"}, {"Name": "价税合计(大写)", "Value": "捌拾玖圆玖角"}, {"Name": "小写金额", "Value": "¥89.90"}, {"Name": "销售方名称", "Value": "示例销售方有限公司"}, {"Name": "购买方名称", "Value": "示例购买方有限公司"}, {"Name": "开票人", "Value": "张三"} ], "Items": [ { "LineNo": "1", "Name": "客运服务费", "Spec": "无", "Unit": "次", "Quantity": "1", "UnitPrice": "87.28", "AmountWithoutTax": "87.28", "TaxRate": "3%", "TaxAmount": "2.62" } ], "PdfPageSize": 0, "Angle": 0.0, "RequestId": "9f2c1a3e-..." } } ``` ### 2)上传本地文件(multipart,不占用公网图床) ```bash curl -X POST http://localhost:8010/ocr/upload \ -H "X-API-Key: YOUR_API_KEY" \ -F "file=@./samples/sample_vat_invoice.png" ``` 返回结构与上面完全相同(`Response.VatInvoiceInfos` + `Response.Items`)。 适合内网环境——票据不用先传到任何外部地址。 ### 3)多页 PDF 批量识别 ```bash curl -X POST http://localhost:8010/ocr/vatpdf \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{ "ImageUrl": "https://example.com/batch.pdf", "Pages": "1-5" }' ``` ```json { "Response": { "PdfPageSize": 73, "ProcessedPages": 5, "Pages": [ {"PageNumber": 1, "Status": "ok", "VatInvoiceInfos": [...], "Items": [...]} ], "NextPages": "6-10", "RemainingPages": 68, "RequestId": "..." } } ``` `Pages` 支持 `"1-5"`、`"1,3,7"`、`"all"` 或 `[1,2,3]`。单次默认最多 10 页 (`limit.max_pdf_pages` 可调),超过会截断并返回 `NextPages`,按提示续传即可。 **批量场景请用这个接口,不要用 `EnableMultiplePage`**——后者会一次性跑完, 容易撞反向代理的读超时。 --- ## 多用户与额度管理 内置轻量用户体系(SQLite),适合小规模对外提供服务:按 Key 分配额度、限制时段、记录调用日志。 ```bash # 创建用户:2027-03-31 到期、总额度 10000 次、每天最多 500 次、只在工作日 9~18 点可用 python manage.py add --name 张三 \ --valid-to 2027-03-31 \ --quota-total 10000 --quota-daily 500 \ --weekdays 1-5 --hours 09:00-18:00 # 查看 python manage.py list python manage.py show 1 # 改额度 / 停用 / 启用 python manage.py set 1 --quota-total 20000 python manage.py disable 1 python manage.py enable 1 ``` 在 `.env` 里开启鉴权: ```bash INVOICE_AUTH__ENABLED=true INVOICE_AUTH__MODE=db # 只认用户库;both = 先静态 Key 再查库 INVOICE_AUTH__ADMIN_TOKEN=<用 openssl rand -hex 24 生成> ``` 管理接口 `/admin/users` 需要 `X-Admin-Token`。**没配 `admin_token` 时 `/admin/*` 整体关闭**, 不会裸奔。 数据在命名卷 `invoice-data` 里,备份: ```bash docker compose exec invoice-ocr python manage.py list > users-backup.txt docker run --rm -v invoice-ocr_invoice-data:/data -v "$PWD":/backup alpine \ tar czf /backup/data-backup.tar.gz -C /data . ``` > 如果用 `data/` 绑定挂载,记得把 `*.db`、`*.db-wal`、`*.db-shm` **一起**备份 > ——SQLite 在 WAL 模式下,只拷主库文件会丢最近的写入。 --- ## 实测数据 | 场景 | 环境 | 结果 | |---|---|---| | 单张电子普通发票(PNG,1800×1260) | 2 核 CPU,`max_side_len=2000` | 约 2~4s | | 73 页 PDF 连续识别(逐页) | 同上,`/ocr/vatpdf` 分批 | 275s(约 3.8s/页),73 页全部成功 | | 明细金额勾稽 | 同一份 73 页 PDF | 明细金额合计 82963.07 与票面一致,税额合计 10785.11 与票面一致,**差 0.00** | 不含 GPU,不需要 CUDA。CPU 核数越多越快,`ocr.local.intra_op_num_threads` 控制推理线程。 --- ## 已知限制 诚实列出,避免你在生产上踩坑: 1. **准确率不等于商业云 API**。复杂版式、严重倾斜、印章遮盖、低分辨率扫描件上, 商业 OCR 仍然更稳。本项目的优势是数据不出内网和零调用成本,不是绝对精度。 2. **主要适配增值税发票**(专票/普票/电子票/全电票)。火车票、出租车票、银行回单等 其它票据类型,调用 `RecognizeGeneralInvoice` 会落到 `OtherInvoice`,只返回通用文本。 3. **多页 PDF 单次上限 10 页**。这是为了避开反向代理超时,不是技术限制,可通过 `limit.max_pdf_pages` 调整(配合调大 `request_timeout` 和 nginx 的 `proxy_read_timeout`)。 4. **`Polygon` 是原图坐标**,与腾讯的缩放坐标系不同,需要等比换算。 5. **`provider=local` 时不需要任何密钥**;但一旦把服务暴露到公网,**请务必开启 `auth.enabled`**,否则等于把 CPU 免费送人。 6. **不支持手写体**,不支持严重褶皱/透视畸变的照片。 --- ## 许可与商业支持 | 内容 | 许可 | 费用 | |---|---|---| | 本仓库全部源码 `app/`、脚本、文档 | **MIT** | 免费,**含商业使用** | | 扩展包(批量队列、校对台、票种扩充) | 单独商业协议 | 按年 / 买断 | | 私有化部署、系统对接、调优、SLA | 单独商业协议 | 按项目 | **基础代码是 MIT,你可以直接拿去商用,不需要向我申请授权。** 付费买的是省时间、少踩坑和持续维护,不是代码使用权。 细节见 [COMMERCIAL.md](COMMERCIAL.md)。 ### 支持这个项目 代码永久免费,不赞助也完全不影响使用。如果它确实帮你省了几天时间: | 方式 | 成本 | 说明 | |---|---|---| | **点个 Star** | 3 秒 | 最有用的一步,能让更多需要它的人搜到 | | **[爱发电](https://ifdian.net/a/lslist666)** | 随缘 | 请我喝杯咖啡,赞助者的 Issue 优先回复 | | **私有化部署 / 定制票种** | 单独报价 | 爱发电私信,或提 Issue 标注 `商业合作` | 赞助主要花在两个地方:**新票种适配**(通行费、卷票、区块链票的版式各不相同,每一种都要重新调版面规则), 以及**字符级后处理规则的维护**(减少 `14ml` 被认成 `14m1` 这类错误)。 - 需要私有化部署 / 定制票种:爱发电私信,或在本仓库提 Issue 并标注 `商业合作` - Bug 与建议:直接提 Issue,能复现的优先处理 ### ⚠️ 第三方依赖注意 本项目的 PDF 渲染使用 **PyMuPDF**,它是 **AGPL-3.0 / 商业双授权**。 - **以源码形式分发**(本仓库的方式)没有问题; - 若你要把本服务打包成**闭源二进制**对外分发,请先阅读 [NOTICE](NOTICE), 要么买 Artifex 商业许可,要么把 `app/image_loader.py` 里的 PDF 渲染换成 `pypdfium2`(Apache-2.0 / BSD-3)。 ### 免责声明(摘要) 本工具**仅做票据文字提取与字段结构化**,识别结果**必须人工复核**。 **禁止**用于伪造、变造、篡改票据或任何违法违规用途,**使用者承担全部法律责任**。 本项目不提供发票验真、不提供纳税申报、不提供抵扣计算。 完整条款见 [DISCLAIMER.md](DISCLAIMER.md)。 --- ## 捐赠支持 如果本项目对你有帮助,可以请作者喝杯咖啡: 支付宝收款码 支付宝扫码,金额随意。想长期跟进项目也可以走 [爱发电](https://ifdian.net/a/lslist666)。 捐赠完全自愿,不影响代码的 MIT 授权与免费使用。