# 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
[](https://gitee.com/li_shuai_carson/invoice-ocr/stargazers)
[](https://gitee.com/li_shuai_carson/invoice-ocr)
[](LICENSE)
[赞助支持](https://ifdian.net/a/lslist666)

**本地离线运行的增值税发票识别服务。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 授权与免费使用。