前端实现图片与 PDF 文字识别、敏感信息打码方案
背景
在证书、合同、发票、简历、身份证、营业执照等场景中,用户上传的图片和 PDF 文件里经常包含姓名、身份证号、手机号、邮箱、住址、银行卡号等敏感信息。如果这些文件需要在浏览器中预览、分享或二次流转,就需要在导出前进行文字识别和打码处理。
本文介绍一种基于 tesseract.js 和 pdfjs-dist 的前端方案:
- 对普通图片进行文字识别;
- 对 PDF 文件逐页渲染为图片后再识别;
- 根据关键词、正则规则和置信度筛选敏感区域;
- 在 Canvas 上对敏感区域进行打码;
- 支持多语言证书场景。
技术选型
| 能力 | 方案 | 说明 |
|---|---|---|
| 图片文字识别 | tesseract.js | 浏览器端 OCR,支持多语言 |
| PDF 渲染 | pdfjs-dist | 将 PDF 页面渲染为 Canvas |
| 图片打码 | Canvas | 根据识别坐标绘制遮罩 |
| 多语言支持 | tesseract.js 语言包 | 例如 eng+chi_sim+chi_tra |
tesseract.js 负责识别文字,pdfjs-dist 负责处理 PDF。对于普通图片,可以直接交给 tesseract.js;对于 PDF,需要先用 pdfjs-dist 把页面渲染成图片,再进行 OCR。
整体流程
- 用户上传图片或 PDF。
- 如果是图片,直接交给 tesseract.js 识别。
- 如果是 PDF,用 pdfjs-dist 渲染每一页为 Canvas。
- 将 Canvas 转为图片数据,交给 tesseract.js 识别。
- 从识别结果中提取文字、置信度和坐标信息。
- 通过关键词、正则和置信度判断是否为敏感信息。
- 在 Canvas 上绘制黑色矩形完成打码。
- 导出打码后的图片或 PDF 页面。
安装依赖
bash
pnpm add tesseract.js pdfjs-dist
pdfjs-dist:PDF 渲染 / 预览
pdfjs-dist 是 Mozilla PDF.js 的 npm 包,主要用于在浏览器里解析和渲染 PDF。
常见能力
- 将 PDF 渲染到
<canvas>或 SVG - 分页、缩放、翻页
- 提取 PDF 内嵌文字层
- 实现搜索、高亮、文本选择
- 适合做 PDF 预览器、文档阅读器、发票/PDF 解析
典型用法
ts
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
const loadingTask = getDocument('/sample.pdf');
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
在 Vite / Vue3 项目里,worker 路径通常需要单独处理,比较稳妥的方式是指向构建产物或 CDN 上的 pdf.worker.mjs。
封装一个 PDF 预览组件
vue
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.mjs?url'
const props = defineProps<{ src: string }>()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const totalPages = ref(0)
const currentPage = ref(1)
let pdfDoc: Awaited<ReturnType<typeof getDocument>> | null = null
watch(
() => props.src,
async (src) => {
if (!src) return
GlobalWorkerOptions.workerSrc = pdfWorker
pdfDoc = await getDocument(src).promise
totalPages.value = pdfDoc.numPages
renderPage(1)
},
{ immediate: true }
)
async function renderPage(pageNum: number) {
if (!pdfDoc) return
const page = await pdfDoc.getPage(pageNum)
const viewport = page.getViewport({ scale: 1.5 })
const canvas = canvasRef.value!
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext('2d')!
await page.render({ canvasContext: ctx, viewport }).promise
}
async function extractText(pageNum: number) {
if (!pdfDoc) return ''
const page = await pdfDoc.getPage(pageNum)
const content = await page.getTextContent()
return content.items.map((item: any) => item.str).join(' ')
}
</script>
<template>
<div>
<canvas ref="canvasRef" />
<div>
第 {{ currentPage }} / {{ totalPages }} 页
<button @click="currentPage > 1 && renderPage(--currentPage)">上一页</button>
<button @click="currentPage < totalPages && renderPage(++currentPage)">下一页</button>
<button @click="extractText(currentPage).then(console.log)">提取当前页文字</button>
</div>
</div>
</template>
关键点:
- worker 路径一定要配,Vite 项目里用
pdf.worker.mjs?url比较稳。 - 如果 PDF 本身有文字层,优先用
getTextContent()提取文本,准确率最高。 - 如果只是要预览,不想自己写翻页、缩放,也可以考虑
@tato30/vue-pdf、vue-pdf-embed、vue-pdf-preview这类封装好的组件。
tesseract.js:浏览器端 OCR
tesseract.js 是 Tesseract OCR 引擎的 JS/WASM 版本,用于从图片中识别文字。
常见能力
- 识别图片中的文字
- 支持中英文等多语言
- 返回识别文本、置信度、坐标信息
- 可运行在浏览器、Node.js、Electron 中
- 适合身份证识别、票据录入、截图文字提取
典型用法
ts
import { createWorker } from 'tesseract.js';
const worker = await createWorker('eng+chi_sim');
const { data } = await worker.recognize(imageFile);
console.log(data.text);
await worker.terminate();
需要注意的是,tesseract.js 是纯前端 OCR,中文识别对图片清晰度、分辨率、背景噪声比较敏感,复杂场景可能需要先做图像预处理,或者改用后端 OCR 服务。
封装一个图片识别组件
vue
<script setup lang="ts">
import { ref } from 'vue'
import { createWorker } from 'tesseract.js'
const text = ref('')
const loading = ref(false)
async function recognize(file: File) {
loading.value = true
text.value = ''
const worker = await createWorker('chi_sim+eng')
const { data } = await worker.recognize(file)
text.value = data.text
await worker.terminate()
loading.value = false
}
</script>
<template>
<div>
<input type="file" accept="image/*" @change="e => {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) recognize(file)
}" />
<div v-if="loading">识别中...</div>
<pre v-else>{{ text }}</pre>
</div>
</template>
关键点:
- 中文识别建议用
chi_sim+eng。 - 识别结果包含
data.text、置信度、坐标信息,可以做结构化处理。 - 识别完记得
worker.terminate(),避免内存占用持续升高。 - 如果图片模糊、分辨率低、背景噪点多,准确率会明显下降。
组合用法:扫描件 PDF 提取文字
如果 PDF 是扫描件,getTextContent() 拿不到文字,可以这样组合:
- 用
pdfjs-dist把 PDF 某一页渲染到<canvas>; - 把 canvas 转成图片:
canvas.toDataURL('image/png'); - 交给
tesseract.js识别。
伪代码:
ts
const page = await pdfDoc.getPage(1)
const viewport = page.getViewport({ scale: 2 })
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 image = canvas.toDataURL('image/png')
const worker = await createWorker('chi_sim+eng')
const { data } = await worker.recognize(image)
console.log(data.text)
await worker.terminate()
工程建议
| 场景 | 推荐方案 |
|---|---|
| 普通 PDF 预览 | pdfjs-dist 或 vue-pdf-embed / @tato30/vue-pdf |
| 提取 PDF 文字 | pdfjs-dist getTextContent() |
| 图片/截图识别 | tesseract.js |
| 扫描件 PDF 转文字 | pdfjs-dist 渲染 canvas + tesseract.js 识别 |
| 高准确率 OCR | 考虑后端 OCR 服务 |
敏感信息识别规则
单纯依靠关键词判断容易漏掉手机号、身份证号、邮箱等结构化信息。更稳妥的方式是结合多种规则:
- 正则匹配:用于识别手机号、邮箱、身份证号、银行卡号、车牌号等。
- 关键词匹配:用于识别"姓名""身份证号""电话""住址""邮箱"等字段。
- 置信度过滤:过滤掉 OCR 置信度过低的识别结果。
- 相邻词合并:将相邻识别词合并后再判断,避免"手机号:13812345678"被拆开。
- 人工确认:正式场景下建议让用户二次确认,避免误打或漏打。
哪些内容建议打码
| 类型 | 示例 | 判断方式 |
|---|---|---|
| 手机号 | 13812345678 | 正则匹配 |
| 身份证号 | 18位身份证号码 | 正则匹配 |
| 邮箱 | xxx@163.com | 正则匹配 |
| 银行卡号 | 16~19位数字 | 正则匹配 |
| 车牌号 | 浙A12345 | 正则匹配 |
| 住址 | 某省某市某小区某室 | 关键词 + 上下文 |
| 姓名 | 张三、李小明 | 关键词/OCR上下文 |
| 二维码/条形码 | 可能包含链接、身份信息 | 单独检测 |
| 公章/签名 | 合同、证明类文件 | 图像检测,OCR不够 |
敏感信息正则工具
下面是一组适合前端初筛的正则规则:
ts
export const SENSITIVE_PATTERNS = [
{
name: '手机号',
reg: /(?<!\d)1[3-9]\d{9}(?!\d)/g,
},
{
name: '邮箱',
reg: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
},
{
name: '身份证',
reg: /(?<!\d)[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx](?!\d)/g,
},
{
name: '银行卡号',
reg: /(?<!\d)\d{16,19}(?!\d)/g,
},
{
name: '车牌号',
reg: /[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{5,6}/g,
},
]
银行卡号这类连续数字需要谨慎处理,最好配合"银行卡""卡号""开户行"等关键词一起判断,避免误伤普通数字。
收集打码区域
ts
interface WordItem {
text: string
bbox: { x0: number; y0: number; x1: number; y1: number }
confidence?: number
}
function shouldMask(text: string, keywords: string[]) {
for (const p of SENSITIVE_PATTERNS) {
if (p.reg.test(text)) return true
}
const keywords = [
'身份证号',
'身份证号码',
'公民身份号码',
'手机号',
'联系电话',
'邮箱',
'住址',
'家庭地址',
'银行卡',
'开户行',
]
return keywords.some((k) => text.includes(k))
}
function collectMaskBoxes(words: WordItem[]) {
const boxes: Array<{ x0: number; y0: number; x1: number; y1: number }> = []
for (const w of words) {
if (w.confidence && w.confidence < 60) continue
if (shouldMask(w.text)) {
boxes.push(w.bbox)
continue
}
// 相邻几个词拼起来再判断,避免"手机号:138..."被拆开
const idx = words.indexOf(w)
if (idx >= 0 && idx < words.length - 1) {
const next = words[idx + 1]
const combined = w.text + next.text
if (shouldMask(combined)) {
boxes.push({
x0: Math.min(w.bbox.x0, next.bbox.x0),
y0: Math.min(w.bbox.y0, next.bbox.y0),
x1: Math.max(w.bbox.x1, next.bbox.x1),
y1: Math.max(w.bbox.y1, next.bbox.y1),
})
}
}
}
return boxes
}
在 Canvas 上打码
ts
function drawRedactBoxes(
ctx: CanvasRenderingContext2D,
boxes: Array<{ x0: number; y0: number; x1: number; y1: number }>
) {
ctx.fillStyle = 'black'
for (const b of boxes) {
ctx.fillRect(b.x0, b.y0, b.x1 - b.x0, b.y1 - b.y0)
}
}
如果需要更明显的打码效果,也可以使用马赛克、高斯模糊或半透明遮罩。
图片识别与打码组件
vue
<script setup lang="ts">
import { ref } from 'vue'
import { createWorker } from 'tesseract.js'
const previewUrl = ref('')
const recognizedText = ref('')
const loading = ref(false)
let canvas: HTMLCanvasElement | null = null
async function handleImage(file: File) {
previewUrl.value = URL.createObjectURL(file)
await recognizeAndRedact(file)
}
async function recognizeAndRedact(source: File | string) {
loading.value = true
recognizedText.value = ''
const worker = await createWorker('chi_sim+eng')
const result = await worker.recognize(source)
recognizedText.value = result.data.text
const words = result.data.words as Array<{
text: string
bbox: { x0: number; y0: number; x1: number; y1: number }
confidence: number
}>
await worker.terminate()
const img = await loadImage(source)
canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')!
ctx.drawImage(img, 0, 0, img.width, img.height)
// 示例:对包含"姓名""身份证""手机号"的区域打码
const sensitiveKeywords = ['姓名', '身份证', '手机号', '电话', '地址']
for (const word of words) {
if (sensitiveKeywords.some((k) => word.text.includes(k))) {
const { x0, y0, x1, y1 } = word.bbox
ctx.fillStyle = 'black'
ctx.fillRect(x0, y0, x1 - x0, y1 - y0)
}
}
loading.value = false
}
function loadImage(source: File | string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = reject
img.src = typeof source === 'string' ? source : URL.createObjectURL(source)
})
}
function download() {
if (!canvas) return
const link = document.createElement('a')
link.download = 'redacted.png'
link.href = canvas.toDataURL('image/png')
link.click()
}
</script>
<template>
<div>
<input type="file" accept="image/*" @change="(e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) handleImage(file)
}" />
<div v-if="loading">识别并打码中...</div>
<img v-if="previewUrl" :src="previewUrl" style="max-width: 100%" />
<div v-if="recognizedText">
<h4>识别文本</h4>
<pre>{{ recognizedText }}</pre>
</div>
<button :disabled="!canvas" @click="download">下载打码图片</button>
</div>
</template>
PDF 识别与打码组件
PDF 文件需要先渲染成图片,再进行 OCR 和打码。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.mjs?url'
import { createWorker } from 'tesseract.js'
const loading = ref(false)
const pages = ref<{ page: number; url: string }[]>([])
GlobalWorkerOptions.workerSrc = pdfWorker
async function handlePdf(file: File) {
loading.value = true
pages.value = []
const arrayBuffer = await file.arrayBuffer()
const pdf = await getDocument({ data: arrayBuffer }).promise
const worker = await createWorker('chi_sim+eng')
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const viewport = page.getViewport({ scale: 2 })
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
// 先把 PDF 页转成图片,再交给 tesseract.js 识别
const image = canvas.toDataURL('image/png')
const result = await worker.recognize(image)
const words = result.data.words as Array<{
text: string
bbox: { x0: number; y0: number; x1: number; y1: number }
}>
const sensitiveKeywords = ['姓名', '身份证', '手机号', '电话', '地址']
for (const word of words) {
if (sensitiveKeywords.some((k) => word.text.includes(k))) {
const { x0, y0, x1, y1 } = word.bbox
ctx.fillStyle = 'black'
ctx.fillRect(x0, y0, x1 - x0, y1 - y0)
}
}
pages.value.push({
page: i,
url: canvas.toDataURL('image/png'),
})
}
await worker.terminate()
loading.value = false
}
</script>
<template>
<div>
<input type="file" accept="application/pdf" @change="(e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) handlePdf(file)
}" />
<div v-if="loading">PDF 识别并打码中,页数较多时可能较慢...</div>
<div v-for="item in pages" :key="item.page">
<h4>第 {{ item.page }} 页</h4>
<img :src="item.url" style="max-width: 100%" />
<a :href="item.url" :download="`page-${item.page}-redacted.png`">下载该页</a>
</div>
</div>
</template>
图片和 PDF 统一组件
图片和 PDF 可以合并成一个组件,上传文件后自动判断类型走不同分支,最后统一展示打码结果和下载入口。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { createWorker } from 'tesseract.js'
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.mjs?url'
GlobalWorkerOptions.workerSrc = pdfWorker
const loading = ref(false)
const recognizedText = ref('')
const results = ref<{ label: string; url: string }[]>([])
// ---------- 敏感信息规则 ----------
interface WordItem {
text: string
bbox: { x0: number; y0: number; x1: number; y1: number }
confidence?: number
}
const SENSITIVE_PATTERNS = [
{ name: '手机号', reg: /(?<!\d)1[3-9]\d{9}(?!\d)/g },
{ name: '邮箱', reg: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
{
name: '身份证',
reg: /(?<!\d)[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}\dXx/g,
},
{ name: '银行卡号', reg: /(?<!\d)\d{16,19}(?!\d)/g },
{
name: '车牌号',
reg: /[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{5,6}/g,
},
]
function shouldMask(text: string, keywords: string[]) {
for (const p of SENSITIVE_PATTERNS) {
if (p.reg.test(text)) return true
}
return keywords.some((k) => text.includes(k))
}
function collectMaskBoxes(words: WordItem[], keywords: string[]) {
const boxes: Array<{ x0: number; y0: number; x1: number; y1: number }> = []
for (const w of words) {
if (w.confidence && w.confidence < 40) continue
if (shouldMask(w.text, keywords)) {
boxes.push(w.bbox)
continue
}
const idx = words.indexOf(w)
if (idx >= 0 && idx < words.length - 1) {
const next = words[idx + 1]
const combined = w.text + next.text
if (shouldMask(combined, keywords)) {
boxes.push({
x0: Math.min(w.bbox.x0, next.bbox.x0),
y0: Math.min(w.bbox.y0, next.bbox.y0),
x1: Math.max(w.bbox.x1, next.bbox.x1),
y1: Math.max(w.bbox.y1, next.bbox.y1),
})
}
}
}
return boxes
}
function drawRedactBoxes(
ctx: CanvasRenderingContext2D,
boxes: Array<{ x0: number; y0: number; x1: number; y1: number }>
) {
ctx.fillStyle = 'black'
for (const b of boxes) {
ctx.fillRect(b.x0, b.y0, b.x1 - b.x0, b.y1 - b.y0)
}
}
// ---------- 通用处理 ----------
const sensitiveKeywords = [
'姓名', 'Name', '身份证', 'ID No', '身份证号码',
'公民身份号码', '手机号', 'Phone', 'Tel', '电话',
'邮箱', 'Email', 'Address', '住址', '出生日期',
'Date of Birth', '证书编号', 'Certificate No',
'发证日期', 'Issue Date',
]
async function processCanvas(
canvas: HTMLCanvasElement,
worker: Tesseract.Worker
) {
const image = canvas.toDataURL('image/png')
const result = await worker.recognize(image)
const words = result.data.words as Array<{
text: string
bbox: { x0: number; y0: number; x1: number; y1: number }
confidence: number
}>
recognizedText.value += result.data.text + '\n'
const boxes = collectMaskBoxes(words, sensitiveKeywords)
const ctx = canvas.getContext('2d')!
drawRedactBoxes(ctx, boxes)
return canvas.toDataURL('image/png')
}
async function handleFile(file: File) {
loading.value = true
recognizedText.value = ''
results.value = []
const worker = await createWorker('eng+chi_sim')
try {
if (file.type === 'application/pdf') {
const arrayBuffer = await file.arrayBuffer()
const pdf = await getDocument({ data: arrayBuffer }).promise
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const viewport = page.getViewport({ scale: 2 })
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 url = await processCanvas(canvas, worker)
results.value.push({ label: `第 ${i} 页`, url })
}
} else if (file.type.startsWith('image/')) {
const img = await loadImage(URL.createObjectURL(file))
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')!
ctx.drawImage(img, 0, 0)
const url = await processCanvas(canvas, worker)
results.value.push({ label: '图片', url })
}
} finally {
await worker.terminate()
loading.value = false
}
}
function loadImage(src: string) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = reject
img.src = src
})
}
function downloadAll() {
results.value.forEach((item, index) => {
const a = document.createElement('a')
a.href = item.url
a.download = `redacted-${index + 1}.png`
a.click()
})
}
</script>
<template>
<div>
<input
type="file"
accept="image/*,application/pdf"
@change="(e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) handleFile(file)
}"
/>
<div v-if="loading">识别并打码中...</div>
<div v-if="recognizedText">
<h4>识别文本</h4>
<pre>{{ recognizedText }}</pre>
</div>
<div v-for="item in results" :key="item.label">
<h4>{{ item.label }}</h4>
<img :src="item.url" style="max-width: 100%" />
</div>
<button :disabled="results.length === 0" @click="downloadAll">
下载全部打码结果
</button>
</div>
</template>
这样就不用拆成两个组件了,图片和 PDF 走同一个流程:上传 → 判断类型 → 渲染成 Canvas → OCR → 敏感信息打码 → 输出结果。
多语言证书场景
对于多语言证书,例如中英双语学历证、职业资格证、营业执照等,可以给 tesseract.js 配置多个语言包。
语言包怎么选
tesseract.js 的语言代码和 ISO 639-2/3 对应,常见组合:
| 证书类型 | 推荐语言组合 |
|---|---|
| 中英双语证书 | eng+chi_sim |
| 中日韩混合 | eng+chi_sim+jpn+kor |
| 中英+数字 | eng+chi_sim(数字默认 eng 就能识别) |
| 繁体中文 | chi_tra 或 chi_sim+chi_tra |
| 中英+日文 | eng+chi_sim+jpn |
语言包首次加载会下载,体积不小(一个语言包几十 MB),所以不要无脑全装,按业务场景选。
多语言识别示例
ts
import { createWorker } from 'tesseract.js'
// 中英双语证书
const worker = await createWorker('eng+chi_sim')
// 如果需要繁体
// const worker = await createWorker('eng+chi_sim+chi_tra')
const result = await worker.recognize(imageSource)
console.log(result.data.text)
await worker.terminate()
多语言 + 打码的完整流程
ts
async function recognizeAndRedact(
imageSource: File | string,
langs = 'eng+chi_sim'
) {
const worker = await createWorker(langs)
const result = await worker.recognize(imageSource)
const words = result.data.words as Array<{
text: string
bbox: { x0: number; y0: number; x1: number; y1: number }
confidence: number
}>
await worker.terminate()
// 敏感词库也要覆盖多语言
const sensitiveKeywords = [
'姓名', 'Name', '身份证', 'ID No', '身份证号码',
'公民身份号码', 'Phone', 'Tel', '电话', '邮箱',
'Email', 'Address', '住址', '出生日期', 'Date of Birth',
'证书编号', 'Certificate No', '发证日期', 'Issue Date',
]
const boxes = collectMaskBoxes(words, sensitiveKeywords)
return { text: result.data.text, boxes }
}
多语言场景实际建议
- 语言包顺序 :
eng放前面通常更稳,因为数字、标点、英文字段在证书里很常见。 - 繁体证书 :如果证书是港澳台或海外繁体,加
chi_tra,否则繁体字可能识别成一堆乱码。 - 日文/韩文 :如果证书含日文汉字,
jpn语言包也能识别部分汉字,但和chi_sim混用时要注意误识别。 - 性能 :每多一个语言包,识别速度会明显下降。能用
eng+chi_sim解决的,不要加jpn+kor+deu+fra。 - 置信度过滤 :多语言混排时,OCR 对某些词的置信度会偏低,可以适当放宽阈值,比如
confidence >= 40才参与敏感词匹配。 - 字段位置:证书版式通常固定,如果能确定"姓名"在第几行、"身份证号"在某个区域,可以用区域裁剪 + 定向识别提升准确率,比全图识别更可靠。
更稳的工程策略
- 不要只信 OCR 文本:tesseract.js 对中文、复杂背景、倾斜图片容易误识别。
- 敏感信息建议默认保守:宁可多打一点,也不要漏打。
- 合同、身份证、发票、病历、快递单这类文件,最好加人工确认。
- 二维码、签名、公章不能只靠文字识别,需要单独检测。
- 银行卡号正则要谨慎:连续数字很多,容易误伤,可以配合"银行卡、卡号、开户行"等关键词一起判断。
- 如果是正式业务场景,建议把 OCR 和敏感信息识别放到后端,用更稳定的 OCR 和规则引擎。
可以把它理解成:代码不难,难的是规则边界。你现在这个更适合先做成"辅助打码工具",识别出疑似敏感区域后,让用户确认再导出。
注意事项
- tesseract.js 是浏览器端 OCR,中文识别对图片清晰度、分辨率和背景噪声比较敏感。
- 银行卡号、连续数字、地址等字段容易误判,建议配合关键词和人工确认。
- 二维码、条形码、签名、公章不能只依赖文字识别,需要单独处理。
- PDF 页数较多时,建议分页处理,避免页面卡顿。
- 正式业务场景下,建议将 OCR 和敏感信息识别放到后端,使用更稳定的 OCR 服务和规则引擎。
- 本方案更适合做成"辅助打码工具",识别结果导出前最好由用户二次确认。
小结
这个方案的核心不是代码复杂度,而是敏感信息识别的边界。前端可以通过 tesseract.js 和 pdfjs-dist 快速实现图片与 PDF 的文字识别和打码,但在真实业务中,还需要结合正则规则、置信度、字段位置、多语言支持和人工确认,才能在可用性和安全性之间取得平衡。