38 结构化文本识别与关键信息提取

引言

前面的文章解决了"把图片里的文字识别出来并展示"。但很多真实需求不止于此:扫一张名片要自动存下姓名和电话,扫一张发票要自动记账金额,扫一张身份证要提取证件号。这类需求的核心是"结构化提取"——从一整段识别文本中,精准捞出关键字段。HarmonyOS 的 textRecognition 结果并非只有一段拼好的字符串,它还保留了段落、行、单词的层级结构与坐标信息。本文先讲清 TextRecognitionResult 的数据结构,再给出基于正则和逐行匹配的两套关键信息提取方案,并讨论 result.value 与逐行结果的差异与局限。

正文知识点

1. TextRecognitionResult 的层级结构

textRecognition.recognizeText 返回的 TextRecognitionResult 包含两级内容:

  • value: string —— 识别出的全文文本(工程中展示用的就是这个字段);
  • blocks: Array<TextBlock> —— 文本块(段落)数组,每个 TextBlockvalue(段落文本)和 lines 数组;
  • 每个 TextLine(文本行)有 value(行文本)、cornerPoints(行外框四点坐标,顺时针)和 words 数组;
  • 每个 TextWord(单词)有 value(单词文本)和 cornerPoints(单词外框坐标)。
层级关系是:全文 value → blocks(段落)→ lines(行)→ words(单词),且行、词都带像素坐标。坐标的价值在于:可以判断两个字段在画面中的相对位置(谁在左、谁在上),这在"姓名/电话"这类字段的配对上有奇效。

2. result.value 与逐行结果的区别与局限

  • result.value 是把识别文本按顺序拼接的纯文本,直接、够用,但丢失了排版结构——哪些字是一行的、字段的物理位置在哪,全都没有了;而且不同引擎对换行的拼接策略不同,用它做"按行解析"不可靠。
  • blocks / lines / words 保留了结构信息:行与行的边界是明确的,每个词都有坐标。缺点是需要多写遍历代码,且引擎对"块"的切分并不总是符合人眼直觉。
  • 共同局限:没有置信度字段。当前版本的行/词对象里没有 confidence(置信度)属性,无法像某些平台那样按分数过滤低质量识别;且识别针对印刷体,手写体、艺术字体效果差。
因此建议的组合策略是:需要全文value,需要按行/按位置解析用 blocks/lines

3. 正则提取常见结构化字段

value 或每一行做正则匹配,是提取"手机号、身份证、日期、金额"最轻量的方式。常用模式:

字段 正则(示例) 说明

手机号 1[3-9]\d{9} 大陆手机号 11 位
身份证号 \d{17}[\dXx] 18 位,末位可为 X
日期 \d{4}[-/年]\d{1,2}[-/月]\d{1,2}日? 覆盖常见分隔符
金额 (?:¥\|¥\|RMB)?\s?\d+(?:\.\d{2})? 带或不带货币符号
邮箱 [\w.-]+@[\w.-]+\.\w+ 简单邮箱模式

注意 OCR 会把全角字符、空格等原样带出,正则里要预留容错(如 [0-90-9]\s*)。

代码示例

示例一:基于 result.value 提取名片关键信息

先沿用工程的识别流程拿到 value,再做正则提取(识别部分可复用 Camera.etsrecognizeImage):

interface ContactInfo {
  name: string;
  phone: string;
  email: string;
}

// 输入识别全文,提取名片关键字段
function extractContact(rawText: string): ContactInfo {
  const info: ContactInfo = { name: '', phone: '', email: '' };
  // 手机号:支持 138-1234-5678、138 1234 5678 等带分隔符写法
  const phoneMatch: RegExpMatchArray | null = rawText.match(/1[3-9][-\s]?\d{4}[-\s]?\d{4}/g);
  if (phoneMatch && phoneMatch.length > 0) {
    info.phone = phoneMatch[0].replace(/[-\s]/g, '');
  }
  // 邮箱
  const emailMatch: RegExpMatchArray | null = rawText.match(/[\w.-]+@[\w.-]+\.\w+/g);
  if (emailMatch && emailMatch.length > 0) {
    info.email = emailMatch[0];
  }
  // 姓名:取"姓名/名字/Name"关键字所在行,剥离关键字后作为姓名
  const lines: string[] = rawText.split('\n');
  for (const line of lines) {
    const nameLine = line.match(/(?:姓名|名字|Name)[::\s]*(.+)/i);
    if (nameLine) {
      info.name = nameLine[1].trim();
      break;
    }
  }
  return info;
}

示例二:基于 lines 逐行解析,坐标辅助配对

当名片排版是"姓名一行、电话一行"时,直接遍历 blocks/lines 比全文正则更稳(此示例演示遍历 TextRecognitionResult.blocks 结构):

import { textRecognition } from '@kit.CoreVisionKit';

function parseByLines(result: textRecognition.TextRecognitionResult): ContactInfo {
  const info: ContactInfo = { name: '', phone: '', email: '' };
  for (const block of result.blocks) {
    for (const line of block.lines) {
      const text: string = line.value;
      // 逐行按关键字归类
      if (!info.name && /(?:姓名|名字|Name)/i.test(text)) {
        info.name = text.replace(/(?:姓名|名字|Name)[::]?/i, '').trim();
      } else if (!info.phone && /1[3-9]\d{9}/.test(text.replace(/[-\s]/g, ''))) {
        info.phone = text.replace(/[-\s]/g, '');
      } else if (!info.email && /[\w.-]+@[\w.-]+\.\w+/.test(text)) {
        info.email = text;
      }
    }
  }
  return info;
}

若一行里同时有"姓名"和"电话",而二者又需要绑定关系,可利用 line.cornerPoints 的坐标判断谁在左谁在右:取两行外框左上角点的 x/y 比较即可,示例略。

示例三:接入工程识别流程

// 在 Camera.ets 中新增:识别并结构化
async recognizeContact(buffer: ArrayBuffer): Promise<ContactInfo> {
  let imageResource = image.createImageSource(buffer);
  let pixelMapInstance = await imageResource.createPixelMap();
  let visionInfo: textRecognition.VisionInfo = { pixelMap: pixelMapInstance };
  let textConfiguration: textRecognition.TextRecognitionConfiguration = {
    isDirectionDetectionSupported: true
  };
  let contact: ContactInfo = { name: '', phone: '', email: '' };
  try {
    if (canIUse('SystemCapability.AI.OCR.TextRecognition')) {
      const result = await textRecognition.recognizeText(visionInfo, textConfiguration);
      contact = parseByLines(result);
      if (contact.phone === '' && result.value !== '') {
        // 逐行没解析出来,退回全文正则兜底
        contact = extractContact(result.value);
      }
    }
  } catch (error) {
    let err = error as BusinessError;
    hilog.error(0x0000, 'Camera', `recognizeContact failed. code=${err.code}, message=${err.message}`);
  } finally {
    pixelMapInstance.release();
    imageResource.release();
  }
  return contact;
}

运行效果与注意事项

  • 先按行、后全文兜底:示例三展示了双保险——优先用 lines 结构解析,解析不出关键字段再退回 value 正则。实测中"关键字+冒号"的印刷体名片,逐行解析的准确率更高。
  • 正则容错:OCR 常见把 0 认成 O1 认成 l,身份证/金额提取前可先做归一化(全角转半角、去空格);正则写宽一点([0-90-9])能显著提高召回。
  • 行序不保证 100% 可靠blocks 的顺序大体按版面自上而下,但多栏排版时可能交错;做"上一行是姓名、下一行是电话"的配对前,优先用坐标判断位置关系。
  • 没有置信度字段:不要试图读取 confidence(当前版本不存在),也不要用手写体测试效果——官方约束明确主要支持印刷体。
  • 清理资源:结构化识别同样要释放 pixelMapInstanceimageResource(见示例三 finally),否则高频扫描会累积内存。

总结

TextRecognitionResult 的"全文 value + 段落/行/词 + 坐标"结构,决定了提取方案的选择:纯文本字段(电话、邮箱、金额)用 value 加正则最省事;需要按行、按位置关联的字段(姓名与电话配对、表格式单据)用 blocks/lines 遍历更可靠;两者结合、逐行优先、全文兜底,是工程上最稳妥的姿势。记住当前版本没有置信度、只认印刷体这两条边界,方案设计就不会跑偏。


*本系列文章基于 HarmonyOS 5.0.5 SDK(API 12)与示例工程 aicharacter-recognition 编写,文中接口以对应版本 API 参考为准。*

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐