Flutter三方库适配OpenHarmony【doc_text】— 字符转换、文本清洗与特殊字符处理
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net从 Word 文档中提取出来的"原始文本"并不干净——里面混着各种控制字符、空字节、Word 专用的特殊标记。做字符映射,做字符过滤,cleanText做最终清洗。这篇把这三个方法以及底层的 readU16/readU32 都讲透。\n,HT →\t,控制字符 → null:6 个 Uni
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
从 Word 文档中提取出来的"原始文本"并不干净——里面混着各种控制字符、空字节、Word 专用的特殊标记。doc_text 用了三个工具方法来处理这些问题:convertToChar 做字符映射,isValidTextChar 做字符过滤,cleanText 做最终清洗。这篇把这三个方法以及底层的 readU16/readU32 都讲透。
一、convertToChar:控制字符映射
1.1 完整实现
private convertToChar(codeUnit: number): string | null {
if (codeUnit === 0x0D || codeUnit === 0x0B || codeUnit === 0x0C) {
return "\n";
}
if (codeUnit === 0x09) {
return "\t";
}
if (codeUnit < 0x20 || codeUnit === 0x7F) {
return null;
}
return String.fromCharCode(codeUnit);
}
1.2 映射规则
| codeUnit | 十六进制 | 名称 | 映射结果 |
|---|---|---|---|
| 13 | 0x0D | 回车 (CR) | \n |
| 11 | 0x0B | 垂直制表符 (VT) | \n |
| 12 | 0x0C | 换页符 (FF) | \n |
| 9 | 0x09 | 水平制表符 (HT) | \t |
| 0-8, 10, 14-31 | 0x00-0x1F | 其他控制字符 | null(丢弃) |
| 127 | 0x7F | DEL | null(丢弃) |
| 32+ | 0x20+ | 可打印字符 | String.fromCharCode(codeUnit) |
1.3 Word 中的特殊控制字符
Word 文档中常见的控制字符:
0x0D (CR) → 段落结束标记(Word 用 CR 而不是 LF)
0x0B (VT) → 软回车(Shift+Enter)
0x0C (FF) → 分页符
0x09 (HT) → 制表符
0x07 → 单元格结束标记(表格中)
0x01 → 图片/OLE 对象占位符
0x13 → 域代码开始
0x14 → 域代码分隔
0x15 → 域代码结束
💡 Word 用 0x0D(CR)作为段落结束标记,而不是 0x0A(LF)。这是 Word 二进制格式的历史遗留。convertToChar 把 CR、VT、FF 都映射成
\n,统一了换行表示。
1.4 为什么返回 null 而不是空字符串
if (codeUnit < 0x20 || codeUnit === 0x7F) {
return null; // 不是 ""
}
// 调用方的处理
const char = this.convertToChar(codeUnit);
if (char) { // null 是 falsy,"" 也是 falsy
result += char; // 只有非空字符才拼接
}
返回 null 和返回 “” 在当前代码中效果一样(都是 falsy),但 null 语义更清晰——表示"这个字符应该被丢弃"。
二、isValidTextChar:Unicode 范围判断
2.1 完整实现
private isValidTextChar(codeUnit: number): boolean {
if (codeUnit >= 0x20 && codeUnit < 0x7F) return true;
if (codeUnit === 0x0D || codeUnit === 0x0A || codeUnit === 0x09 || codeUnit === 0x0B) return true;
if (codeUnit >= 0x4E00 && codeUnit <= 0x9FFF) return true;
if (codeUnit >= 0x3000 && codeUnit <= 0x303F) return true;
if (codeUnit >= 0xFF00 && codeUnit <= 0xFFEF) return true;
if (codeUnit >= 0x00A0 && codeUnit <= 0x00FF) return true;
if (codeUnit >= 0x2000 && codeUnit <= 0x206F) return true;
return false;
}
2.2 六个 Unicode 区间详解

2.3 未覆盖的 Unicode 区间
| 区间 | 范围 | 名称 | 是否覆盖 |
|---|---|---|---|
| 0x0100-0x024F | Latin Extended | ❌ | |
| 0x0400-0x04FF | Cyrillic | ❌ | |
| 0x0600-0x06FF | Arabic | ❌ | |
| 0x0E00-0x0E7F | Thai | ❌ | |
| 0xAC00-0xD7AF | Hangul Syllables(韩文) | ❌ | |
| 0x3040-0x309F | Hiragana(日文平假名) | ❌ | |
| 0x30A0-0x30FF | Katakana(日文片假名) | ❌ |
📌 isValidTextChar 主要覆盖了中文和英文。日文假名、韩文、阿拉伯文等没有被覆盖。这意味着包含这些文字的文档在直接提取模式下可能会丢失内容。Piece Table 模式不受影响,因为它不依赖 isValidTextChar。
2.4 isValidTextChar vs convertToChar 的关系
isValidTextChar:用于直接提取模式,判断一个 codeUnit 是否"看起来像文本"
convertToChar:用于所有模式,把 codeUnit 转换成可读字符
isValidTextChar 的范围比 convertToChar 窄:
- isValidTextChar 只接受特定的 Unicode 区间
- convertToChar 接受所有 >= 0x20 的字符
| 方法 | 使用场景 | 判断标准 |
|---|---|---|
| isValidTextChar | 直接提取(暴力探测) | 严格(6 个区间) |
| convertToChar | Piece Table + 直接提取 | 宽松(>= 0x20) |
三、cleanText:多步清洗管道
3.1 完整实现
private cleanText(text: string): string {
return text
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
.replace(/\u0000/g, "")
.replace(/\u0001/g, "")
.replace(/\u0007/g, "")
.replace(/\u0013/g, "")
.replace(/\u0014/g, "")
.replace(/\u0015/g, "")
.split("\n")
.map(line => line.trim())
.filter(line => line.length > 0)
.join("\n");
}
3.2 清洗步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | \r\n → \n |
Windows 换行统一 |
| 2 | \r → \n |
Mac 旧式换行统一 |
| 3 | 删除 \u0000 |
空字节(NULL) |
| 4 | 删除 \u0001 |
图片/OLE 占位符 |
| 5 | 删除 \u0007 |
表格单元格结束标记 |
| 6 | 删除 \u0013 |
域代码开始 |
| 7 | 删除 \u0014 |
域代码分隔 |
| 8 | 删除 \u0015 |
域代码结束 |
| 9 | 按 \n 分割 |
拆成行数组 |
| 10 | 每行 trim | 去除首尾空白 |
| 11 | 过滤空行 | 删除空行 |
| 12 | 用 \n 拼接 |
重新组合 |
3.3 Word 特殊字符的清除
\u0001 — 图片占位符
Word 中插入图片时,文本流中会有一个 0x01 字符标记位置。
提取纯文本时应该删除。
\u0007 — 单元格结束
表格中每个单元格的文本末尾有一个 0x07 字符。
提取纯文本时应该删除。
\u0013 \u0014 \u0015 — 域代码
Word 的域(Field)用这三个字符包裹:
\u0013 域代码 \u0014 域结果 \u0015
比如页码、目录、超链接等。
提取纯文本时删除这些标记字符。
3.4 清洗前后对比
清洗前:
" Hello\u0001World\r\n\r\n \u0007\n 第一段 \n\n 第二段\u0013PAGE\u0014 3 \u0015 \n"
步骤 1-2:换行统一
" Hello\u0001World\n\n \u0007\n 第一段 \n\n 第二段\u0013PAGE\u0014 3 \u0015 \n"
步骤 3-8:删除特殊字符
" HelloWorld\n\n \n 第一段 \n\n 第二段PAGE 3 \n"
步骤 9-12:分割、trim、过滤、拼接
"HelloWorld\n第一段\n第二段PAGE 3"
💡 cleanText 是 .doc 和 .docx 共用的——无论哪种格式提取出来的文本,都经过同样的清洗流程。
四、readU16 和 readU32(DocTextPlugin 版本)
4.1 实现
private readU16(bytes: Uint8Array, offset: number): number {
if (offset + 1 >= bytes.length) return 0;
return bytes[offset] | (bytes[offset + 1] << 8);
}
private readU32(bytes: Uint8Array, offset: number): number {
if (offset + 3 >= bytes.length) return 0;
return bytes[offset] |
(bytes[offset + 1] << 8) |
(bytes[offset + 2] << 16) |
(bytes[offset + 3] << 24);
}
4.2 与 OLE2Parser 版本的区别
| 区别 | DocTextPlugin | OLE2Parser |
|---|---|---|
| 数据源 | 参数传入 bytes | this.bytes |
| readU32 | 无 >>> 0 |
有 >>> 0 |
| 使用场景 | FIB、Piece Table | FAT、目录 |
4.3 缺少 >>> 0 的影响
// DocTextPlugin 的 readU32:没有 >>> 0
return bytes[offset] |
(bytes[offset + 1] << 8) |
(bytes[offset + 2] << 16) |
(bytes[offset + 3] << 24);
// 如果最高位为 1,结果可能是负数
// OLE2Parser 的 readU32:有 >>> 0
return (...) >>> 0;
// 结果总是无符号的
| 场景 | 无 >>> 0 | 有 >>> 0 |
|---|---|---|
| 值 < 0x80000000 | 正确 | 正确 |
| 值 >= 0x80000000 | 负数 | 正确的正数 |
📌 对于 FIB 中的 ccpText 和 fcClx,值通常不会超过 0x80000000,所以缺少
>>> 0在实际使用中不会出问题。但如果追求严谨,应该加上。
五、字符编码踩坑实录
5.1 GBK 文档的问题
场景:一个用 Word 2003 中文版创建的 .doc 文件
编码:ANSI 部分使用 GBK(而不是 UTF-16)
问题:
extractAnsiChars 把每个字节当作单独的字符处理
但 GBK 中文字符是双字节的
→ 中文字符被拆成两个乱码字符
5.2 示例
"你好" 在 GBK 中:[0xC4, 0xE3, 0xBA, 0xC3]
extractAnsiChars 的处理:
0xC4 → byte >= 0x80 → String.fromCharCode(0xC4) → 'Ä'
0xE3 → byte >= 0x80 → String.fromCharCode(0xE3) → 'ã'
0xBA → byte >= 0x80 → String.fromCharCode(0xBA) → 'º'
0xC3 → byte >= 0x80 → String.fromCharCode(0xC3) → 'Ã'
结果:"Äãºà" 而不是 "你好"
5.3 为什么 Piece Table 模式不受影响
Piece Table 模式下:
1. fc 位 30 = 0 → Unicode → extractUnicodeChars
→ UTF-16LE 解码,中文字符正确
2. fc 位 30 = 1 → ANSI → extractAnsiChars
→ 但现代 Word 文档中,中文通常存储为 Unicode
→ ANSI 部分通常只有纯 ASCII 文本
5.4 可能的改进
// 改进方案:检测 GBK 双字节字符
private extractAnsiChars(bytes: Uint8Array, offset: number, count: number): string {
// ...
if (byte >= 0x81 && byte <= 0xFE && i < bytes.length) {
const nextByte = bytes[i];
if (nextByte >= 0x40 && nextByte <= 0xFE) {
// GBK 双字节字符
// 需要 GBK → Unicode 转换表
}
}
// ...
}
但这需要一个 GBK 到 Unicode 的映射表(几千个条目),会显著增加代码量。当前的实现选择了简单性。
六、String.fromCharCode 的使用
6.1 基本用法
String.fromCharCode(0x0048) // → 'H'
String.fromCharCode(0x4F60) // → '你'
String.fromCharCode(0x0041) // → 'A'
6.2 BMP 范围限制
// String.fromCharCode 只能处理 BMP(基本多语言平面)字符
// 即 U+0000 到 U+FFFF
// 对于 U+10000 以上的字符(如 emoji),需要代理对
// doc_text 没有处理这种情况
// 但 Word 97-2003 文档中很少有 emoji
6.3 与 String.fromCodePoint 的区别
| 方法 | 范围 | 示例 |
|---|---|---|
| fromCharCode | U+0000 - U+FFFF | 基本够用 |
| fromCodePoint | U+0000 - U+10FFFF | 支持 emoji 等 |
七、性能考虑
7.1 字符串拼接
// 当前实现:逐字符拼接
result += char;
// 潜在问题:大量字符串拼接可能导致性能问题
// 每次 += 都会创建新的字符串对象
7.2 可能的优化
// 优化方案:使用数组收集,最后 join
const chars: string[] = [];
// ...
chars.push(char);
// ...
return chars.join("");
| 方案 | 优点 | 缺点 |
|---|---|---|
| += 拼接 | 简单 | 大文本可能慢 |
| 数组 + join | 大文本更快 | 多一步 |
实际上对于大多数 Word 文档(几千到几万字符),+= 拼接的性能是可以接受的。
7.3 cleanText 的正则性能
// 7 个 replace 调用,每个都遍历整个字符串
text
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
.replace(/\u0000/g, "")
// ...
可以合并成一个正则:
// 优化:一次正则替换
text.replace(/\r\n|\r|\u0000|\u0001|\u0007|\u0013|\u0014|\u0015/g, (match) => {
if (match === '\r\n' || match === '\r') return '\n';
return '';
});
但可读性会降低。当前的链式调用更清晰。
八、三个方法的协作关系
8.1 调用关系
extractTextFromDocx
└── parseDocxXml
└── cleanText
extractTextFromOldDoc
└── extractWordText
├── extractTextWithPieceTable
│ ├── extractUnicodeChars → convertToChar
│ └── extractAnsiChars
├── extractTextDirect
│ ├── isValidTextChar
│ └── convertToChar
└── cleanText
8.2 方法职责
| 方法 | 职责 | 输入 | 输出 |
|---|---|---|---|
| isValidTextChar | 判断是否有效文本字符 | codeUnit | boolean |
| convertToChar | 转换为可读字符 | codeUnit | string | null |
| cleanText | 清洗最终文本 | 原始文本 | 干净文本 |
总结
字符处理是 doc_text 文本提取的最后一环:
- convertToChar:CR/VT/FF →
\n,HT →\t,控制字符 → null - isValidTextChar:6 个 Unicode 区间,主要覆盖中英文
- cleanText:换行统一 + 特殊字符删除 + 空行过滤
- readU16/readU32:小端序读取,边界保护
- GBK 问题:ANSI 模式下中文可能乱码,Piece Table 模式不受影响
下一篇我们看临时文件管理——docx 解压产生的临时目录如何创建和清理。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐



所有评论(0)