前端实现图片与PDF文字识别敏感信息打码方案

前端实现图片与 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。

整体流程

  1. 用户上传图片或 PDF。
  2. 如果是图片,直接交给 tesseract.js 识别。
  3. 如果是 PDF,用 pdfjs-dist 渲染每一页为 Canvas。
  4. 将 Canvas 转为图片数据,交给 tesseract.js 识别。
  5. 从识别结果中提取文字、置信度和坐标信息。
  6. 通过关键词、正则和置信度判断是否为敏感信息。
  7. 在 Canvas 上绘制黑色矩形完成打码。
  8. 导出打码后的图片或 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() 拿不到文字,可以这样组合:

  1. 用 pdfjs-dist 把 PDF 某一页渲染到 <canvas>;
  2. 把 canvas 转成图片:canvas.toDataURL('image/png');
  3. 交给 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 的文字识别和打码,但在真实业务中,还需要结合正则规则、置信度、字段位置、多语言支持和人工确认,才能在可用性和安全性之间取得平衡。

相关推荐
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队2 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端
派小心.2 小时前
A/B实验分层随机种子的稳定性验证
大数据·服务器·前端·数据分析
袋鼠云数栈UED团队2 小时前
从 Skills 到可靠的 Bugfix Agent
前端
幼儿园蛋小黑2 小时前
SPI 子系统:从原理到实践
java·前端·数据库
Data analyse4563 小时前
rrweb回放事件资源加载顺序重放校验
前端·人工智能·算法·机器学习·数据分析