HarmonyOS 相机 + 文字识别实现拍照识字 38 结构化文本识别与关键信息提取
38 结构化文本识别与关键信息提取
引言
前面的文章解决了"把图片里的文字识别出来并展示"。但很多真实需求不止于此:扫一张名片要自动存下姓名和电话,扫一张发票要自动记账金额,扫一张身份证要提取证件号。这类需求的核心是"结构化提取"——从一整段识别文本中,精准捞出关键字段。HarmonyOS 的 textRecognition 结果并非只有一段拼好的字符串,它还保留了段落、行、单词的层级结构与坐标信息。本文先讲清 TextRecognitionResult 的数据结构,再给出基于正则和逐行匹配的两套关键信息提取方案,并讨论 result.value 与逐行结果的差异与局限。
正文知识点
1. TextRecognitionResult 的层级结构
textRecognition.recognizeText 返回的 TextRecognitionResult 包含两级内容:
value: string—— 识别出的全文文本(工程中展示用的就是这个字段);blocks: Array<TextBlock>—— 文本块(段落)数组,每个TextBlock有value(段落文本)和lines数组;- 每个
TextLine(文本行)有value(行文本)、cornerPoints(行外框四点坐标,顺时针)和words数组; - 每个
TextWord(单词)有value(单词文本)和cornerPoints(单词外框坐标)。
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.ets 的 recognizeImage):
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认成O、1认成l,身份证/金额提取前可先做归一化(全角转半角、去空格);正则写宽一点([0-90-9])能显著提高召回。 - 行序不保证 100% 可靠:
blocks的顺序大体按版面自上而下,但多栏排版时可能交错;做"上一行是姓名、下一行是电话"的配对前,优先用坐标判断位置关系。 - 没有置信度字段:不要试图读取
confidence(当前版本不存在),也不要用手写体测试效果——官方约束明确主要支持印刷体。 - 清理资源:结构化识别同样要释放
pixelMapInstance和imageResource(见示例三finally),否则高频扫描会累积内存。
总结
TextRecognitionResult 的"全文 value + 段落/行/词 + 坐标"结构,决定了提取方案的选择:纯文本字段(电话、邮箱、金额)用 value 加正则最省事;需要按行、按位置关联的字段(姓名与电话配对、表格式单据)用 blocks/lines 遍历更可靠;两者结合、逐行优先、全文兜底,是工程上最稳妥的姿势。记住当前版本没有置信度、只认印刷体这两条边界,方案设计就不会跑偏。
*本系列文章基于 HarmonyOS 5.0.5 SDK(API 12)与示例工程 aicharacter-recognition 编写,文中接口以对应版本 API 参考为准。*
更多推荐



所有评论(0)