前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >智能结构体 | OCR助力OA,如何实现报销两天到账

智能结构体 | OCR助力OA,如何实现报销两天到账

原创
作者头像
叫我阿柒啊
修改2025-01-07 15:53:57
修改2025-01-07 15:53:57
23060
代码可运行
举报
运行总次数:0
代码可运行

前言

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

OCR应用

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

代码语言:python
代码运行次数:0
复制
from PIL import Image
import 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天,极大提高了报销效率。

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

智能结构化

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

在产品官网/文档:智能结构化OCR定制模板OCR自定义文字识别中,我们可以通过点击页面的开通服务按钮,开通智能结构化的服务。

开通之后,进入OCR控制台,在资源包管理菜单下,我们可以看到腾讯云赠送的智能结构化基础版和高级版各1000次的调用次数。

除了每个月会免费赠送智能结构化的资源包之外,还会赠送银行卡识别、英文识别的资源包等。

产品体验

腾讯云提供了智能结构化高级版的产品demo体验,点击OCR Demo,在产品demo中识别的是国际物流的单据,可以看到精准识别了物流单据中的结构化信息。

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

版本对比

智能结构化一共提供了基础版和高级版的识别接口调用。

智能结构化高级版不限定版式,支持海外票据、提单、运单、进出口报关单、装箱单、过磅单、采购单等物流单据,识别率高且有更大的模型参数算力支撑。

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

我在这里选择了智能结构化高级版,用来开发发票夹,在开通和体验完智能结构化之后,就开始发票夹应用的开发环节。

后端开发

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

接口调用

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

第二种方式是使用腾讯云提供的tencentcloudapi SDK,这样就不要自己封装签名,通过SDK就可以直接调用智能结构化的接口。

代码语言:xml
复制
<dependency>
    <groupId>com.tencentcloudapi</groupId>
    <artifactId>tencentcloud-sdk-java</artifactId>
    <version>3.1.322</version>
</dependency>

Service

初始化客户端

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

代码语言:java
复制
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);
    }
} 

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

构造OCR请求

调用智能结构化OCR接口时,可以将图片上传到公网上,然后使用ImageUrl参数调用接口。我使用的是第二种方法,现将图片转换成Base64格式,然后通过ImageBase64将图片发送到接口。

代码语言:java
复制
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

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

代码语言:java
复制
@Slf4j
@RestController
@RequestMapping("/api/ocr")
@CrossOrigin
public 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 接口对发票进行识别。这里封装了 callOCRAPI 方法来实现接口的调用。

代码语言:javascript
代码运行次数:0
复制
const API_CONFIG = {
  baseUrl: 'http://localhost:8080/api/ocr'  // Java 后端服务地址
};

// 调用 OCR API
async 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. 发票去重

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

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

代码语言:javascript
代码运行次数:0
复制
// 发票查重实现
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变量中:

代码语言:json
复制
{
  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. 代码开发

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

代码语言:javascript
代码运行次数:0
复制
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. 发票详情弹出层

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

代码语言:javascript
代码运行次数:0
复制
<view 
  class="invoice-item"
  @tap="goToDetail(invoice)"
  @longpress="showInvoicePopup(invoice)"
>

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

最终效果如下:

2. 发票详情页面

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

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

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

代码语言:html
复制
<image 
  class="invoice-image" 
  :src="imageUrl" 
  mode="aspectFit"
  @tap="previewImage"
/>

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

代码语言:JavaScript
复制
previewImage() {
  if (this.imageUrl) {
    uni.previewImage({
      urls: [this.imageUrl]
    });
  }
}
3. 发票筛选

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

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

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

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

代码语言:javascript
代码运行次数:0
复制
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. 发票搜索

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

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

代码语言:javasript
复制
debounce(fn, delay = this.debounceDelay) {
  return (...args) => {
    if (this.searchDebounceTimer) {
      clearTimeout(this.searchDebounceTimer);
    }
    this.searchDebounceTimer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

5. 发票删除

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

实现代码如下:

代码语言:javascript
代码运行次数:0
复制
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;
      });
    }
  });
}

结语

本篇文章主要使用腾讯云的智能结构化高级版OCR接口,实现了OA系统中的发票识别模块。在整体的开发过程中,智能结构化使用起来比较方便,通过SDK几行代码就能接入到系统中。

通过对各种版式的电子发票,以及纸质增值税专用发票的识别结果来看,智能结构化有着精准的识别能力,适合于各种版式的单据识别。


我是阿柒,本期结束咱们下次再见👋~

关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • OCR应用
    • OA报销流程
    • OCR在OA中的应用
  • 智能结构化
    • 产品体验
    • 版本对比
  • 后端开发
    • 接口调用
    • Service
      • 初始化客户端
      • 构造OCR请求
    • Controller
  • 前端开发
    • 添加发票
      • 1. 发票上传
      • 2. 电子发票识别
      • 3. 纸质发票识别
      • 4. 发票去重
    • 数据解析
      • 1. 结构定义
      • 2. 结构分析
      • 3. 代码开发
    • 发票展示
      • 1. 发票详情弹出层
      • 2. 发票详情页面
      • 3. 发票筛选
      • 4. 发票搜索
    • 5. 发票删除
  • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档