
简介面向前端开发者的轻量级 OCR 工具包基于 JavaScript/ECMAScript 编写无需后端服务即可在浏览器中直接识别图片中的英文和数字适合验证码识别、发票自动填写等快速文本提取场景也适合需要定制 OCR 功能的中高级前端开发者学习和二次开发。资源包为 rar 压缩包大小仅 536KB共 5 个文件包含 1 个 JS 核心库、1 个 HTML 示例页面以及 3 张测试图片2 张 PNG、1 张 JPG目录结构清晰简洁整体上手门槛很低。目前已有 5383 人学习下载不少开发者将其用于前端工具链或自动化脚本中。借助附带的示例页面和测试图片可以快速验证识别效果并对照源码理解算法思路便于按需裁剪和优化官方已计划后续将支持汉字识别届时在中文文档、名片识别、票据录入等场景中将会变得更加实用。1. 前端 ocr.js 识别图片器把识别引擎搬进浏览器纯前端完成文字提取标题里写的 ocr.js不是某个固定的 npm 包名而是“纯前端图片文字识别”这一类实现的统称落地时最常见的是 Tesseract.js。它把 C 写的 Tesseract OCR 引擎编译成 WebAssembly在浏览器 Web Worker 里执行主线程不卡语言包按需加载简体中文、繁体、英文和数字都能认。相比“图片传后端 → 调接口 → 返回文本”的老链路纯前端方案没有服务器费用图片不出浏览器还能离线部署特别适合表单录入、证件抽号、截图文字提取这类场景。第一次接触前端 OCR 的开发者第三章给的最小代码可以直接跑通已经在用 Tesseract.js 的后面预处理参数、坐标映射、worker 复用这几部分更值得看。2. ocr.js 选型与前端的边界什么场景值得不开后端2.1 Tesseract.js 的三段加载链路core、语言包、worker 脚本Tesseract.js 发布在 npm 上但真正执行识别计算的不是那几百行 JavaScript而是 C 编译的 WASM 二进制。浏览器支持 WebAssembly 之后Tesseract 这种重型计算引擎才有机会跑在标签页里。整个识别过程由三个部分组成coreTesseract 引擎的 WASM 编译产物负责图像分析、LSTM 神经网络推理、字符输出。language data识别引擎专用的训练数据每种语言对应一个 traineddata 文件简体中文的体积通常明显大于英文。worker 脚本JS 层和 WASM 层之间通信的包装Tesseract.js 会把识别任务派发到独立 Web Worker。第一次调用 createWorker 并不是“创建一个对象”这么简单。内部动作是注册 Worker 线程 → 下载 core 二进制 → 下载语言包 → 初始化识别引擎全部完成之后才能接收图片。这也是很多新手感觉“第一次识别特别慢”的原因从第二张图片开始才进入纯识别耗时。// 三段加载体现在一条初始化语句里 const worker await Tesseract.createWorker( chi_sim, // 语言包简体中文 1, // OEM使用 LSTM 引擎 { logger: m console.log(m.status, m.progress) } );语言包默认从 jsDelivr 拉取core 和 worker 脚本同理。你在初始化时把 logger 传进去控制台能看到 loading tesseract core、initializing api、loading language traineddata、recognizing text 四段状态。实际计算都发生在 WASM 层JS 只是壳这是理解 Tesseract.js 性能特征的前提。2.2 从性能、隐私、成本三个维度看纯前端方案先看性能。Tesseract 对印刷体文字的识别效果在干净图片上已经足够日常使用。浏览器端跑 LSTM 模式单张识别在秒级对表单录入这类低频场景完全可接受。首次加载语言包有网络开销但语言包下载过一次之后会被浏览器缓存之后刷新页面直接命中缓存。第二是隐私。图片不出浏览器对内网、敏感数据场景是很大的优势。后端方案意味着图片要上传多一次网络往返还会引入数据落地和权限管理的问题。纯前端方案不需要存原始图片识别完把文本入库或直接展示原始图随手释放。第三是成本。前端方案没有按次计费的 API 调用费用不需要在服务器上维护 OCR 服务进程一个静态资源目录就能承载整个识别能力。对中小型内部系统来说这是很现实的选择也是“前后端分离”做到极致的一种体现。2.3 和后端 OCR API 放在一起怎么选对比维度前端 ocr.jsTesseract.js 等后端 OCR API 或自建服务图片是否上传不出浏览器必须上传首次识别耗时语言包下载可能数秒主要看网络往返后续单张识别秒级受终端 CPU 影响秒级受服务负载影响复杂光照/模糊场景准确率偏低依赖前端预处理云厂商特定模型更强API 费用无按调用次数或资源计费部署依赖纯静态资源后端服务、存储、日志选型时有一条经验线拍摄环境可控、图片质量基本稳定的业务前端方案足够手机随手拍、图片来源复杂、对准确率有硬要求的场景后端或“前端先识别、低置信度转后端兜底”的混合方案更稳。2.4 不适合硬上纯前端的三种情况第一种是大量高并发识别每天几万张进件全在浏览器端跑终端性能不可控模型版本也没法统一升级。第二种是对识别质量有硬性要求且需要持续调模型Tesseract 的训练数据更新不如商业方案频繁浏览器端能做到的“调模型”基本只有换语言包。第三种是终端设备过弱低端手机跑 WASM 推理的耗时可能比后端方案慢一个数量级。遇到这三种场景不要急着写识别页面。3. 最小可运行用 ocr.js 从 npm 安装到识别出第一行文字3.1 安装与一段能在本地跑起来的完整代码npm install tesseract.js装完之后写一个最简页面把input typefile选中的图片直接丢给识别引擎import Tesseract from tesseract.js; const fileInput document.querySelector(#fileInput); fileInput.addEventListener(change, async (e) { const file e.target.files[0]; const worker await Tesseract.createWorker(chi_sim); const { data } await worker.recognize(file); console.log(识别文本, data.text); console.log(整体置信度, data.confidence); await worker.terminate(); });逻辑不复杂先 createWorker 并传语言代码 chi_sim让引擎加载简体中文语言包再调用 recognize 传入文件返回的 data 里带 text、confidence、blocks 等字段最后 terminate 释放 worker。主要容易忘的是 terminate不调用的话浏览器会一直占着一个 worker 线程和语言包内存。worker.recognize()的第一个参数不仅接受 File还支持 Blob、Canvas 元素、带 data URL 的字符串以及一个远程图片 URL。最常见的是 file 选中后直接交出去不需要先把图片插入 DOM。3.2 识别进度怎么读从 loading 到 recognizing如果初始化时传了 logger控制台会持续输出状态对象。典型用途是把它接到底部进度条const worker await Tesseract.createWorker(chi_sim, 1, { logger: info { // 只有 recognizing text 阶段的 progress 适合做进度条 if (info.status recognizing text) { bar.style.width ${Math.round(info.progress * 100)}%; } } });常见状态依次是 loading tesseract core、initializing api、loading language traineddata、recognizing text。前三个只在首次创建 worker 时出现识别多张图时只有 recognizing text 会重复。做进度条时不要监听全部状态否则加载语言包那几十秒会一直被当成“识别中”。3.3 中英文同时识别怎么传参多语言识别用数组const worker await Tesseract.createWorker([chi_sim, eng]);语言代码有对应关系不要写成中文名语言代码含义典型用途chi_sim简体中文中文文档、表单chi_tra繁体中文港澳台资料eng英文英文单据、数字串osd方向与脚本检测自动判断图片是否旋转加载的语言包数量越多初始化越慢识别时间也会变长。只识别中文和数字却把中英繁三种都传进去属于典型的参数误用。3.4 第一次运行必踩的两个坑跨域与 worker 路径直接用浏览器双击 HTML 文件打开大概率报跨域错误Tesseract.js 内部的 worker 和 WASM 加载依赖 fetch。本地开发要起一个 http 服务比如npx serve .或python3 -m http.server 8000。另一个高频坑在打包工具。Vite 会把 worker 按 ES Module 格式单独打包Tesseract.js 默认注册的 worker 路径可能和打包产物对不上表现是页面一直停留在 loading 阶段。常见解法是把 worker 脚本放进 public 目录用 workerPath 手动指过去const worker await Tesseract.createWorker(chi_sim, 1, { workerPath: /tess/worker.min.js, corePath: /tess/tesseract-core.wasm.js, langPath: /tess/lang, });三个参数的分工可以记成workerPath 对应 JS 包装脚本corePath 对应 WASM 识别引擎langPath 对应语言包目录。统一指到自己的静态目录之后离线环境也能跑这正好是下一节里生产部署的前置准备。4. 识别率的命门用 canvas 做灰度、缩放和二值化4.1 为什么截屏识别率高手机拍的就差Tesseract 引擎擅长处理干净的印刷体它隐含的假设是“背景干净、文字和背景对比度足够”。手机拍的图片往往有摩尔纹、阴影、偏色直接丢给引擎笔画会被当成背景噪声滤掉。最直接的提升手段不是换引擎而是做预处理彩色图转灰度、小字放大、灰度图变成黑白分明的二值图。如果图片本身是“白纸黑字”这类二值化几乎是免费的准确率提升。彩色印刷品和照片需要调参下面给的参数表可以作为默认起点。4.2 一份可以直接复用的 canvas 预处理函数function preprocessImage(inputCanvas, { scale 2, threshold 150 } {}) { const output document.createElement(canvas); output.width inputCanvas.width * scale; output.height inputCanvas.height * scale; const ctx output.getContext(2d, { willReadFrequently: true }); // 先按 scale 缩放放大笔画能缓解文字断线问题 ctx.drawImage(inputCanvas, 0, 0, output.width, output.height); const imageData ctx.getImageData(0, 0, output.width, output.height); const d imageData.data; for (let i 0; i d.length; i 4) { // 加权灰度公式亮度权重对应人眼对 RGB 的敏感度 const gray 0.299 * d[i] 0.587 * d[i 1] 0.114 * d[i 2]; // 大于阈值判为白底低于阈值判为黑字 const binary gray threshold ? 255 : 0; d[i] d[i 1] d[i 2] binary; } ctx.putImageData(imageData, 0, 0); return output; }willReadFrequently: true是给 getImageData 的优化提示告诉浏览器这个 canvas 需要频繁读像素不要走 GPU 加速路径。循环里把灰度压成 0 或 255得到高对比度二值图。threshold 影响极大太小会把灰色笔画判成背景字变残缺太大则把噪点全判成前景。先用 150 跑一遍看识别文本是缺字还是多字再上下调整 10 到 20。File 对象要先进 canvas比较顺手的做法是 createImageBitmapconst bitmap await createImageBitmap(file); const canvas document.createElement(canvas); canvas.width bitmap.width; canvas.height bitmap.height; canvas.getContext(2d).drawImage(bitmap, 0, 0);4.3 三个最值得调的预处理参数参数建议范围影响scale1 到 3放大让笔画更完整超过 3 会同时放大噪点threshold120 到 180越低字越粗越容易糊越高背景越干净但容易丢笔画输出宽度不超过 2000px过大拖慢识别且引入边缘黑边如果原始图特别小比如 320px 宽的手机截图scale 用 3 提升明显原图超过 1000px 时保持 1 或 2 即可。更稳的经验值是最终交给引擎时文字高度落在 30 到 60 像素区间识别效果最均衡。4.4 用 confidence 决定要不要二次预处理Tesseract.js 会给整体置信度也能细到每个 word。整体置信度适合做流程分支const { data } await worker.recognize(preprocessedCanvas); if (data.confidence 60) { // 置信度偏低换一组阈值重新识别 const retry preprocessImage(inputCanvas, { scale: 2, threshold: 130 }); const retryData await worker.recognize(retry); console.log(retryData.text); } else { console.log(data.text); }这种“先识别一次置信度不够就换参数再跑”的逻辑比一次性调出完美参数更可靠。注意 confidence 是 0 到 100 的数值不是 0 到 1data.confidence 92表示可靠直接和 60 比较就行。5. 识别结果不止是文字坐标映射、正则抽字段与表单回填5.1 data 结构里值得关注的信息层级识别结果不是只有一串 textdata.blocks里保留了排版信息结构层级字段路径典型用途块data.blocks整页区域划分段落block.paragraphs段落边界行paragraph.lines一行文字的位置最适合画框词line.words单个词汇点选实际返回的数据可能嵌套较深中文文本有时一个段落只有一行一行里 words 也可能只有一项。bbox 字段在各层都存在取哪层取决于粒度需求定位大致区域用 block画一行框用 line点选具体词汇用 words。5.2 把识别出的文字行画回原图做可视化验证识别批次多了之后最需要的是“看到机器读到了什么、位置在哪”。把 lines 的 bbox 画回 canvas是前端最直观的验收方式function drawBounds(canvas, data) { const ctx canvas.getContext(2d); for (const block of data.blocks || []) { for (const paragraph of block.paragraphs || []) { for (const line of paragraph.lines || []) { const r line.bbox; ctx.strokeStyle #00c853; ctx.lineWidth 2; ctx.strokeRect(r.x0, r.y0, r.x1 - r.x0, r.y1 - r.y0); } } } }坐标基准有一个关键点识别时喂给 worker 的图是什么尺寸bbox 坐标就落在哪个尺寸坐标系里。如果先对原图缩放再识别画框要用缩放过后的画布当底直接画回原始图会整体错位。最稳妥的做法是把预处理画布临时保留绘制时用它当背景。5.3 从识别文本里正则抽取身份证和手机号拿到整段文本后常见需求是抽字段。前端场景用正则收敛就够不需要上模型const text data.text.replace(/\s/g, ); const idCardPattern /(\d{6}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx])/; const phonePattern /(1[3-9]\d{9})/; const idCard text.match(idCardPattern)?.[1]; const phone text.match(phonePattern)?.[1]; console.log(识别到的身份证号, idCard); console.log(识别到的手机号, phone);身份证正则先校验了出生日期段避免一长串普通数字被误命中拿到之后还要判断String.length 18。OCR 偶尔把某个字符识别成相近数字前端能挡住的是明显错误最终入库建议再走一次后端校验。5.4 表单回填场景怎么做才不出错录入场景里识别结果要落到表单项const fullName text.match(/姓名[:]\s*([\u4e00-\u9fa5·]{2,4})/)?.[1]; if (fullName) document.querySelector(#name).value fullName; if (idCard) document.querySelector(#idCard).value idCard; if (phone) document.querySelector(#phone).value phone;关键在别拿整块识别文本直接填进输入框先按行拆分、按关键词定位、再取值。识别错一两个字是常态正则和长度校验能在入库前挡住一部分明显错误。整体准确率可能比后端方案低几个点但图片不出浏览器、没有链路成本对内部管理系统这类场景已经够用。6. 生产化技巧worker 并发、离线语言包与内存回收6.1 用 createScheduler 批量识别而不是循环开 worker一个常见误用是循环里 createWorker → recognize → terminate每张图都付出一次完整的初始化代价。正确做法是用 Tesseract.createScheduler 维护 worker 池const scheduler Tesseract.createScheduler(); // 创建 4 个 worker 放进调度器 for (let i 0; i 4; i 1) { const worker await Tesseract.createWorker(chi_sim); scheduler.addWorker(worker); } const results await Promise.all( images.map(image scheduler.addJob(recognize, image)) ); results.forEach(r console.log(r.data.text)); scheduler.terminate();池子大小建议取终端逻辑核心数减一开太多线程反而互相抢 CPU。addJob 会自动把任务分配给空闲 worker这是纯前端批量识别最省事的并发方式。6.2 语言包本地化适配离线与内网环境Tesseract.js 默认从 jsDelivr 拉语言包公网环境没问题内网和国产化环境往往连不上外网。前面提到的三个 path 就是为这种场景准备的const worker await Tesseract.createWorker(chi_sim, 1, { workerPath: ./tess/worker.min.js, corePath: ./tess/tesseract-core.wasm.js, langPath: ./tess/lang/chi_sim.traineddata.gz, });把这三个文件拷进静态资源目录后整个识别链路不再发任何外部请求刷新页面也能直接跑。注意后缀.traineddata.gz不要解压也不要改名Tesseract 内部自带解压逻辑。内网系统建议一开始就把这三个路径收敛到公共配置常量里而不是散落各处依赖默认 CDN。6.3 识别完不立即 terminate 的合理理由单张识别就 terminate 是一种误用。页面后续还会出现新图时worker 应该留在内存里复用直到页面卸载或者用户明确退出识别功能时才清理。使用 Scheduler 时统一在退出时调用一次scheduler.terminate()即可。window.addEventListener(beforeunload, () { if (scheduler) scheduler.terminate(); });识别耗时如果突然变长优先检查是不是之前并发跑着没回收的 worker 在抢占 CPU。这类问题不体现在报错里只体现在时间上。把“worker 复用、池化调度、退出回收”这三件事做好才是把 ocr.js 从 demo 推到生产环境的关键一步。本文还有配套的精品资源点击获取