纯前端实现发票二维码批量识别——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_simeng, 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;charsetutf-8 });总结整个方案完全在浏览器端运行核心依赖pdf.js 解析 PDF 文字层和渲染页面jsQR 扫描二维码Tesseract.js 处理图片 OCR最麻烦的是不同发票格式的兼容需要针对各种布局写不同的正则策略。如果你也有类似需求可以参考这个思路或者直接用我做好的在线工具试试效果发票批量识别工具。如有问题欢迎评论区交流。