当前位置: 首页 > news >正文

纯前端实现发票二维码批量识别——PDF.js + jsQR 实战

最近在做财务报销相关的需求,需要从大量电子发票 PDF 中提取发票号码、购买方、销售方、金额等信息。手动复制粘贴效率极低,于是研究了一下能不能纯前端实现自动识别。

折腾了几天,最终用PDF.js + jsQR + Tesseract.js实现了一个完全在浏览器端运行的发票批量识别工具,不需要后端,不上传文件,隐私安全。本文记录一下核心实现思路。


技术选型

功能说明
PDF 解析pdf.jsMozilla 出品,浏览器端解析 PDF
二维码识别jsQR纯 JS 二维码解码
图片 OCRTesseract.jsGoogle Tesseract 的 WebAssembly 版本

核心流程

1. PDF 文字层提取

电子发票 PDF 本身带有文字层,pdf.js 可以直接提取,不需要 OCR,准确率高:

async function extractPdfText(pdf) { let fullText = ''; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const textContent = await page.getTextContent(); // items.str 就是每个文字块的内容 const pageText = textContent.items.map(item => item.str).join(' '); fullText += pageText + '\n'; } return fullText; }


2. 扫描发票二维码

把 PDF 页面渲染到离屏 canvas,再用 jsQR 扫描像素数据:

async function scanPageForQR(pdf, pageNum, scale = 2.0) { const page = await pdf.getPage(pageNum); const viewport = page.getViewport({ scale }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); await page.render({ canvasContext: ctx, viewport }).promise; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' }); return code ? code.data : null; }

注意 scale 建议设 2.0 以上,分辨率太低二维码识别率会下降。

3. 解析二维码内容

增值税发票二维码格式是逗号分隔的字符串:

01,32,,26512000001023417645,38.69,20260317,,7147


字段顺序:版本, 类型码, 发票代码, 发票号码, 金额, 日期, 校验码

function parseInvoiceQR(qrText) { const parts = qrText.split(','); const invoiceTypeMap = { '01': '增值税专用发票', '04': '增值税普通发票', '32': '电子发票(普通发票)', // ... }; const typeCode = parts[1].trim(); return { invoiceType: invoiceTypeMap[typeCode] || typeCode, invoiceNumber: parts[3].trim(), invoiceDate: formatDate(parts[5].trim()), // 注意:parts[4]=金额,parts[5]=日期 amount: parts[4].trim(), }; }

踩坑:不同版本发票二维码字段顺序不同,新版电子发票 parts[4] 是金额,parts[5] 才是日期,不要搞反。

4. 从 PDF 文本中提取结构化字段

这是最麻烦的部分。不同发票 PDF 的文本提取顺序差异很大:

  • 新版电子发票:标签和值分离,"名称:"标签在前半段,公司名在后半段
  • 旧版专票:标签值紧邻,名称: XXX科技有限公司
  • 部分发票:销售方在购买方前面出现(布局不同)

针对这些差异,我的处理策略:

// 优先匹配"名称: XXX公司"紧邻格式 const nameInline = [...t.matchAll( /名称[::]\s*([\u4e00-\u9fa5()()][^\d::\n]{2,40}?(?:有限公司|股份公司|分公司|个体工商户|餐厅))/g )]; // 通过"购买方"/"销售方"标签位置判断顺序 const buyerLabelPos = t.search(/购\s*买\s*方/); const sellerLabelPos = t.search(/销\s*售\s*方/); const sellerFirst = buyerLabelPos > sellerLabelPos; // 旧版专票销售方在前 if (nameInline.length >= 2) { result.buyerName = sellerFirst ? nameInline[1][1] : nameInline[0][1]; result.sellerName = sellerFirst ? nameInline[0][1] : nameInline[1][1]; }

税号同理,通过标签位置判断哪个是购买方哪个是销售方:

const taxRe = /\b([0-9A-Z]{15,20})\b/g; const taxes = [...t.matchAll(taxRe)]; if (buyerPos > sellerPos) { // 销售方先出现 result.sellerTaxId = taxes[0][1]; result.buyerTaxId = taxes[1][1]; } else { result.buyerTaxId = taxes[0][1]; result.sellerTaxId = taxes[1][1]; }

5. 图片发票走 OCR

图片发票没有文字层,用 Tesseract.js 识别:

const worker = await Tesseract.createWorker('chi_sim+eng', 1, {}); const { data: { text } } = await worker.recognize(canvas); await worker.terminate();

OCR 识别出来的文本每个字之间有空格,所以正则要先 text.replace(/\s+/g, '') 去掉所有空白再匹配。

批量处理

核心是维护一个文件队列,逐一处理:

async function startBatchScan() { const pending = fileQueue.filter(q => q.status === 'pending'); for (let i = 0; i < pending.length; i++) { const item = pending[i]; item.status = 'processing'; try { const data = await processFile(item.file, item.type); item.status = 'done'; results.push({ fileName: item.file.name, data }); } catch (e) { item.status = 'error'; results.push({ fileName: item.file.name, error: e.message }); } renderResultTable(); // 每识别一张就更新表格 } }

效果

实测下来,新版电子发票 PDF 识别准确率很高,发票号码、金额、购买方/销售方基本都能正确提取。图片识别因为 OCR 精度问题,税号偶尔会有字符识别错误。

识别结果支持导出 CSV,带 BOM 头,Excel 直接打开中文不乱码:

const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' });

总结

整个方案完全在浏览器端运行,核心依赖:

  • pdf.js 解析 PDF 文字层和渲染页面
  • jsQR 扫描二维码
  • Tesseract.js 处理图片 OCR

最麻烦的是不同发票格式的兼容,需要针对各种布局写不同的正则策略。如果你也有类似需求,可以参考这个思路,或者直接用我做好的在线工具试试效果:发票批量识别工具。


如有问题欢迎评论区交流。

http://www.jsqmd.com/news/652404/

相关文章:

  • 图像质量评价指标全解析:SROCC、PLCC、KROCC到底怎么选?
  • 零基础学化妆|6家合规化妆培训参考,新手择校避坑指南 - 品牌测评鉴赏家
  • claude 安装配置手册
  • linux系统中简单统计python项目代码行数信息
  • NSCT图像分解实战:用Matlab+NSCT_TOOLBOX处理医学影像(附完整代码)
  • Markdown编辑器
  • 2026化妆培训十大机构盘点!零基础小白择校不踩坑! - 品牌测评鉴赏家
  • Python自动化查询DELL服务器保修信息:从SN号到完整报告(附多线程优化)
  • 2025届毕业生推荐的降AI率工具推荐
  • 别再瞎调参数了!GPT-5的reasoning_effort和verbosity到底怎么设?实战避坑指南
  • 避免数据丢失!双系统Ubuntu扩容必知的5个关键步骤与常见错误
  • OpenIPC固件在君正T31ZX平台烧录失败故障排查与彻底解决方案
  • OpenCL SDK技术深度解析与高性能计算实战指南
  • 电力负荷预测实战:用AAAI最佳论文Informer模型搞定未来72小时用电量
  • 告别IE内核:在WPF中集成CefSharp构建现代化Web视图
  • 情绪价值拉满:用 ArkUI 写个“马屁精”APP,点一下屏幕换着花样疯狂夸你
  • OpenClaw v2026.4.5 源码安装
  • 12 - Go Slice:底层原理、扩容机制与常见坑位
  • 项目实训(三):安全分析引擎迭代——统一 Source 模型、SQL 形态识别与污点传播重构
  • 为什么92%的AI项目在Q3财报前暴雷成本超支?揭秘生成式AI分摊模型中被忽略的3个隐性因子
  • Python自动化数据备份:守护你的数据安全
  • 仅限首批200家AI原生企业开放的CI/CD流水线模板库(含Phi-3/Qwen2/Llama3全栈适配):生成式AI应用交付效率提升3.8倍的终极配置清单
  • CSS 提示工具:高效提升网页设计效率的利器
  • 伺服驱动器编码器信号(A+/A-,B+/B-,Z+/Z-)差分接线详解:从高创CDHD2到雷赛L8EC
  • Python面试30分钟突击掌握
  • 美妆学习避坑指南:如何从三个维度判断化妆教学团队的专业度 - 品牌测评鉴赏家
  • 长推理不一定更强:北航 × 字节提出SAGE-RL,挖出大模型隐藏天赋
  • SAP SD实战解析:从出荷点到纳入日,构建高效订单履行流程
  • compose_skill 和 android skills,对 Android 项目提升巨大的专家 AI Skills
  • 2026年化妆学校择校参考:零基础入门与技能提升指南 - 品牌测评鉴赏家