文字识别 文档智能|OCR 助力 OA,如何实现报销两天到账

第一次接触 OCR,还是在18年刚毕业的时候。那时候热衷于爬虫,在爬取数据的过程中总会遇到形形色色的验证码问题。虽然有很多打码平台可以解决这个问题,但是我还是趁着这个机会去学习了 OCR 的知识。

相关产品
文字识别 产品页,含版本规格、价格与购买入口。

一、OCR 应用

时隔多年,再来回顾 OCR ,当时是使用 PIL 和 pytesseract 来简单的实现图片文字的识别。

from PIL import Imageimport pytesseract# 如果 Tesseract 未配置在系统路径中,指定其路径# pytesseract.pytesseract.tesseract_cmd = r'C:\\Program Files\\Tesseract- OCR \\tesseract.exe'# 打开图片image = Image.open("example_image.png")# 使用 Tesseract OCR 进行文字识别text = pytesseract.image_to_string(image, lang="eng")print("识别结果:", text)

以上代码仅仅是对于文字的识别,而对于发票这种结构化的识别,仅仅这样是完成不了的。所以后来在OA系统的财务报销模块改造升级的时候,果断使用了第三方的接口来实现发票的 OCR 。

二、OA 报销流程

最初公司在 OA 上的报销流程甚是复杂。首先我们需要从 OA 上提交报销工单,除了需要填写项目编号之外,就是填写报销金额,这个金额是需要根据手里需要报销的发票(电子发票或者纸质发票)自行计算。然后经过各级领导审批之后,财务在最终确认报销单,然后进入到贴发票环节。

将财务确认的报销单打印签字之后,需要将一张张发票用胶水粘到一张A4纸上,电子发票需要打印在纸上再粘,然后与报销单一起交给前台,等到到固定日期统一邮寄到总公司的财务处。然后财务收到你的报销单和发票之后进行核对校验,审核通过之后再等待财务出纳日,报销款才能到我的工资卡。

如果发票贴错了,或者报销金额与发票金额不对等怎么办?那么恭喜你,财务不仅会把发票寄还给你,你还要重新贴好发票或者修改报销单之后,等待前台邮寄、财务审核…

我当时经常加班打车,出租车票经常一攒一大堆,然后就用计算器挨张累加金额,反复计算好几次,生怕填错了,电子发票也经常搞得混乱不堪,所以每次报销对于我来说都是很累的事情。

三、OCR 在 OA 中的应用

后来,公司推行各种智能化,包括商旅、打车等,OA 接入订票平台和打车平台等,这样出差和打车就不用员工自己垫付。然后顺便也把报销平台进行了改造。OA 新增了发票夹功能。

将电子发票或者纸质发票的照片上传到发票夹,然后调用 OCR 接口,识别出来发票金额等信息之后,我们在提交报销单的时候,就能直接关联发票,同时发票金额也是自动计算填入报销单,再也不用我们自己再去挨个计算。

同时,报销平台除了接入 OCR,同时也接入了发票核验功能,报销单和电子发票也不用打印出来邮寄,全程实现了无纸化,报销周期也从之前的两个星期缩减到了现在的2天,极大提高了报销效率。

在之前 Tessera 的 OCR 代码中,只能对纯文本进行识别。如果想要识别发票这种结构化的表格信息,需要结合区域检测或专用 OCR 工具,所以本篇文章主要是对腾讯云的 OCR 产品文档智能的发票 OCR 能力来做一个深入探究。使用腾讯云文档智能来实现OA中的发票夹,并验证识别准确性是否满足 OA 发票识别的需求。

四、文档智能

文档智能(Document AI)融合了业界领先的深度学习技术、图像检测技术以及 OCR 大模型能力,能够实现不限版式的结构化信息抽取。支持智能提取各类证照、票据、表单、合同等结构化场景的 key:value 字段信息,并支持提取表格信息的 key:value 组的结构化。

在产品页面/文档:文档智能中,我们可以通过点击页面的开通服务按钮,开通文档智能的服务。

开通之后,进入 OCR 控制台,在资源包管理菜单下,我们可以看到腾讯云赠送的文档抽取(基础版)和文档抽取(多模态版)各1000次的调用次数。

除了每个月会免费赠送文档抽取的资源包之外,还会赠送银行卡识别、英文识别的资源包等。

五、产品体验

腾讯云提供了文档抽取(多模态版)的产品 demo 体验,点击 OCR Demo,在产品 demo 中识别的是国际物流的单据,可以看到精准识别了物流单据中的结构化信息。

我们也可以上传一些本地的单据,点击开始识别就可以调用 OCR 能力,有兴趣的小伙伴可以试用一下。

六、版本对比

文档智能一共提供了基础版和多模态版的识别接口调用。

文档抽取(多模态版)不限定版式,支持海外票据、提单、运单、进出口报关单、装箱单、过磅单、采购单等物流单据,识别率高且有更大的模型参数算力支撑。

而基础版只支持固定版式的识别,例如网约车行程单、驾驶证、运输证、房产证、不动产证、毕业证等各类证件单据,这种固定版式识别相对于简单,这也意味着相对有高级版来说识别速度更快。

我在这里选择了文档抽取(多模态版),用来开发发票夹,在开通和体验完文档抽取之后,就开始发票夹应用的开发环节。

七、后端开发

在开发之前首先要确定开发架构,考虑到想要做一个多端的适配,且需要对用户的发票和请求做一个管理,所以这里我选择使用 uni-app 为开发底座,vue3+springBoot 前后端的架构来实现这个发票夹。

八、接口调用

腾讯云产品提供了两种接口调用的方式。一是自己是实现接口请求,但是需要使用 签名方法v3 生成请求凭证。

第二种方式是使用腾讯云提供的 tencentcloudapi SDK,这样就不要自己封装签名,通过 SDK 就可以直接调用文档抽取(多模态版)的接口。

<dependency> <groupId>com.tencentcloudapi</groupId> <artifactId>tencentcloud-sdk-java</artifactId> <version>3.1.322</version></dependency>

九、初始化客户端

实现 OcrService,初始化 tencentcloudapi 的请求客户端 CommonClient,代码如下:

public class OcrService { private final CommonClient client; public OcrService(OcrConfig ocrConfig) { // 实例化一个认证对象 Credential cred = new Credential(ocrConfig.getSecretId(), ocrConfig.getSecretKey()); // 实例化一个 HTTP 选项 HttpProfile httpProfile = new HttpProfile(); httpProfile.setEndpoint(ocrConfig.getEndpoint()); // 实例化一个客户端配置对象 ClientProfile clientProfile = new ClientProfile(); clientProfile.setHttpProfile(httpProfile); // 实例化要请求产品的client对象 this.client = new CommonClient("ocr", "2018-11-19", cred, ocrConfig.getRegion(), clientProfile); }}

在初始化请求的时候,除了要填写必要的请求参数,还要设置访问密钥:SecretId和SecretKey。在控制台就可以生成密钥。

十、构造 OCR 请求

调用文档抽取(多模态版)接口时,可以将图片上传到公网上,然后使用 ImageUrl 数调用接口。我使用的是第二种方法,现将图片转换成 Base64 格式,然后通过 ImageBase64 将图片发送到接口。

public String recognizeInvoice(String imageBase64) throws TencentCloudSDKException { try { // 构造请求参数 String params = String.format("{\\"ImageBase64\\":\\"%s\\"}", imageBase64); // 发起请求 log.info("开始调用OCR识别服务…"); String response = client.call("SmartStructuralPro", params); log.info("OCR识别完成: {}", response); return response; } catch (TencentCloudSDKException e) { log.error("OCR识别失败: {}", e.getMessage(), e); throw e; }}

核心代码就是使用 client.call 调用文档抽取(多模态版)接口,然后将接口的响应原样返回。

十一、Controller

实现 Controller 对外暴露 /api/ocr/recognize 接口,通过调用service的方法,实现前端页面与高级版 OCR 接口的连通。

@Slf4j@RestController@RequestMapping("/api/ocr")@CrossOriginpublic class OcrController { private final OcrService ocrService; public OcrController(OcrService ocrService) { this.ocrService = ocrService; } @PostMapping("/recognize") public OcrResponse<String> recognizeInvoice(@RequestBody OcrRequest request) { try { log.info("收到OCR识别请求, 图片大小: {} bytes", request.getImageBase64().length()); String response = ocrService.recognizeInvoice(request.getImageBase64()); return OcrResponse.success(response); } catch (TencentCloudSDKException e) { log.error("OCR识别失败: {}", e.getMessage(), e); return OcrResponse.error(e.getMessage()); } }}

至此,后端开发接口服务已经开发完成。接下来就是开发前端页面,与将后端接口集成到前端中实现发票的 OCR 调用。

十二、前端开发

前端使用 uni-app 和 vue3 完成开发,这里以 OA 系统中发票夹为中心进行开发,实现用户上传发票、识别发票和保存发票的功能,详细功能点如下:

1. 发票 OCR 识别;

2. 发票导入重复检查;

3. 发票列表展示、详情展示;

4. 发票排序;

5. 发票删除;

6. 发票搜索。

在发票夹的首页,实现了发票列表展示、筛选、搜索、删除的功能,同时也是用户添加发票的入口。

员工需要先添加发票,才能实现筛选、搜索、删除等功能,所以这里我们先看看添加、实现发票的功能是如何实现的,然后再探索其他的功能模块。

十三、添加发票

点击添加发票按钮,会弹出拍照/相册选择的提示框,这里点击相册,就会跳转到发票添加页面。

发票上传页面的实现就是简单的布局:

而页面除了需要实现 OCR 发票识别功能,还需要实现发票图片上传、发票查重、预览等功能。

1. 发票上传

点击页面,进入到相册选择发票。

选择发票之后,点击确认上传,就会调用 OCR 接口上传。

2. 电子发票识别

在点击确认上传之后,开始触发 OCR 识别,这时候就会调用后端服务中 /api/ocr/recognize 接口对发票进行识别。这里封装了 call OCR API 方法来实现接口的调用。

const API_CONFIG = { baseUrl: 'http://localhost:8080/api/ocr' // Java 后端服务地址};// 调用 OCR APIasync function callOCRAPI(imagePath) { try { // 将本地图片转为 base64 const base64 = await new Promise((resolve, reject) => { uni.getFileSystemManager().readFile({ filePath: imagePath, encoding: 'base64', success: res => resolve(res.data), fail: err => reject(err) }); }); // 发起请求 return await new Promise((resolve, reject) => { uni.request({ url: `${API_CONFIG.baseUrl}/recognize`, method: 'POST', header: { 'Content-Type': 'application/json' }, data: { imageBase64: base64 }, success: (res) => { if (res.statusCode === 200 && res.data?.code === 0) { resolve(res.data); } else { reject(new Error(res.data?.message || '识别失败')); } }, fail: (err) => { reject(err); } }); }); } catch (error) { throw error; }}

在上面的代码中主要实现了两个功能,首先实现图片转base64,然后发起后端服务的请求。

发票识别成功之后,发票就会添加到到首页。同时在控制台可以看到后台请求返回的数据,以及解析到的数据。

从后台也可以看到 OCR 请求的调用和返回日志。

3. 纸质发票识别

这里同样需要实现纸质发票的识别,这里也相当于测试了相机拍照识别发票的功能,我们先选择一张纸质发票。

可以看到纸质发票比电子发票的结构更为复杂,而且照片中也有其他的干扰因素,所以识别起来应该更困难一些。

确认上传,可以看到纸质发票被识别成功。长按可以看到详细的发票信息:

进入详情页也可以预览发票图片:

4. 发票去重

在识别发票的模块中,为了避免一张发票多次添加到发票夹中,这里对发票做了一个排重的处理。在生产中需要在后台实现识别重复的逻辑,使用发票编号作为查找条件,去数据库中查找该用户是否已经存此张发票。

这里我在前端页面简单实现了去重的逻辑:

// 发票查重实现checkInvoiceExists(number) { try { const savedInvoices = uni.getStorageSync('invoices'); if (savedInvoices) { const invoices = JSON.parse(savedInvoices); return invoices.some(invoice => invoice.number === number); } return false; } catch (error) { console.error('检查发票失败:', error); return false; }}// 查重处理if (this.checkInvoiceExists(response.number)) { uni.hideLoading(); uni.showModal({ title: '提示', content: '该发票已存在,请勿重复添加', showCancel: false, success: () => { this.loading = false; } }); return;}

使用 number 发票号码作为去重条件,最终展现效果:

十四、数据解析

发票详细信息的展示,主要是对文档抽取(多模态版)接口返回信息的解析,结构将识别每条发票信息都会以相同的结构返回:

如图,响应数据中 key 的 AutoName 字段是发票中每一项的 key,value 中的 AutoContent 字段是发票中每一项的值。

1. 结构定义

这里我定义了字段结构,解析响应数据,将数据存储在 invoiceData 变量中:

{ id: String, // 发票唯一标识 imageKey: String, // 发票图片的存储key number: String, // 发票号码 type: String, // 发票类型(增值税专用发票/普通发票/电子发票) amount: String, // 发票金额 date: String, // 开票日期 seller: String, // 销售方名称 sellerTaxNo: String, // 销售方纳税人识别号 buyer: String, // 购买方名称 buyerTaxNo: String, // 购买方纳税人识别号 items: [ // 发票商品明细 { name: String, // 商品名称 spec: String, // 规格型号 unit: String, // 单位 quantity: String,// 数量 price: String, // 单价 amount: String, // 金额 taxRate: String, // 税率 tax: String // 税额 } ]}

2. 结构分析

在调用文档抽取(多模态版)接口识别时,因为是是对 key:value 进行结构化识别,但是不同的发票之间相同的 value,key 是不一样的,例如下面两张电子发票的纳税人识别号字段的key就不一样:

发票1:

发票2:

发票1返回的纳税人识别号结构:

发票2返回的纳税人识别号结构:

对于纳税人的 key,一个是销售方信息统一社会信用代码/纳税人识别号,一个是销售方纳税人识别号。当时我是按照发票1的结构开发的解析程序,所以就导致发票2的许多字段没有解析出来。

3. 代码开发

所以,这里需要通过对不同的发票数据结构进行判断提取:

switch (AutoName) { case '标题': invoiceData.type = AutoContent || '电子发票'; break; case '发票号码': case '号码': invoiceData.number = AutoContent || ''; break; case '开票日期': invoiceData.date = AutoContent ? AutoContent.replace('年', '-').replace('月', '-').replace('日', '') : ''; break; case '销售方名称': case '销售方信息名称': invoiceData.seller = AutoContent || ''; break; case '销售方纳税人识别号': case '销售方信息统一社会信用代码/纳税人识别号': invoiceData.sellerTaxNo = AutoContent || ''; break; case '购买方名称': case '购买方信息名称': invoiceData.buyer = AutoContent || ''; break; case '价税合计(小写)': invoiceData.amount = AutoContent ? AutoContent.replace('¥', '').replace('¥', '') : '0'; break; }// …省略部分代码

这样,不同格式的发票也成功识别了。

同样也要将增值税专用发票、纸质发票结构的key考虑进去。

十五、发票展示

发票识别成功之后,就会展示在首页。

1. 发票详情弹出层

这里主要实现了两个发票的详情展示。第一个是长按列表中的发票,就会弹出发票的信息,包括发票号码等。绑定长按事件:

<view class="invoice-item" @tap="goToDetail(invoice)" @longpress="showInvoicePopup(invoice)">

showInvoicePopup会修改showPopup变量,用来控制弹出层显示。

最终效果如下:

2. 发票详情页面

第二个就是点击发票,跳转发票详情页面:

在这个页面可以预览发票图片:

整体就是一个页面展示,主要是定义图片的预览事件 previewImage。

<image class="invoice-image" :src="imageUrl" mode="aspectFit" @tap="previewImage"/>

当触发点击事件(tap)时,通过uni.previewImage就可以实现图片预览。

previewImage() { if (this.imageUrl) { uni.previewImage({ urls: [this.imageUrl] }); }}

3. 发票筛选

发票列表支持按日期范围筛选。

支持按时间正序/倒序排序。

提供快速重置筛选条件功能。

这个功能逻辑比较简单,主要是对发票时间进行一个排序和筛选,代码如下:

filteredInvoices() { let result = […this.invoices]; // 时间区间筛选 if (this.startDate || this.endDate) { result = result.filter(invoice => { const invoiceDate = new Date(invoice.date); let isValid = true; if (this.startDate) { const start = new Date(this.startDate); isValid = isValid && invoiceDate >= start; } if (this.endDate) { const end = new Date(this.endDate); isValid = isValid && invoiceDate <= end; } return isValid; }); } // 时间排序 result.sort((a, b) => { const dateA = new Date(a.date); const dateB = new Date(b.date); return this.sortDesc ? dateB – dateA : dateA – dateB; }); return result;}

4. 发票搜索

支持实时搜索,可按销售方名称、发票号码、金额搜索。

并使用防抖处理优化性能。

debounce(fn, delay = this.debounceDelay) { return (…args) => { if (this.searchDebounceTimer) { clearTimeout(this.searchDebounceTimer); } this.searchDebounceTimer = setTimeout(() => { fn.apply(this, args); }, delay); };}

5. 发票删除

实现了左滑显示删除按钮、删除前二次确认,当用户点击确认之后,发票才会被删除。

实现代码如下:

onDeleteTap(event, invoice) { if (event) { event.stopPropagation(); } uni.showModal({ title: '确认删除', content: '是否确认删除该发票?', confirmColor: '#ff4444', showCancel: true, success: (res) => { if (res.confirm) { this.invoices = this.invoices.filter(item => item.id !== invoice.id); uni.showToast({ title: '删除成功', icon: 'success' }); this.saveInvoices(); } this.$nextTick(() => { invoice.x = 0; }); } });}

十六、结语

本篇文章主要使用腾讯云的文档抽取(多模态版)接口,实现了 OA 系统中的发票识别模块。在整体的开发过程中,文档抽取(多模态版)使用起来比较方便,通过 SDK 几行代码就能接入到系统中。

通过对各种版式的电子发票,以及纸质增值税专用发票的识别结果来看,文档抽取(多模态版)有着精准的识别能力,适合于各种版式的单据识别。

赞 (0)
上一篇 4天前
下一篇 4天前