前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

拿到了 WordDocument 流的数据,接下来要从中提取文本。这不是简单的"读字节"——Word 文档的文本存储方式相当复杂,涉及 FIB(File Information Block)、Piece TableCLX 结构。doc_text 的 extractWordText 方法就是在处理这些东西。这篇是整个系列技术含量最高的一篇。

一、extractWordText 方法入口

1.1 源码

private extractWordText(wordBytes: Uint8Array, ole: OLE2Parser): string | null {
  if (wordBytes.length < 0x200) {
    return null;
  }

  // 读取 FIB
  const flags = this.readU16(wordBytes, 0x0A);
  const isEncrypted = (flags & 0x0100) !== 0;

  if (isEncrypted) {
    return "[加密文档,无法读取]";
  }

  // 获取文本长度
  const ccpText = this.readU32(wordBytes, 0x4C);
  if (ccpText <= 0 || ccpText > 10000000) {
    return null;
  }

  // 获取 CLX 信息
  const fcClx = this.readU32(wordBytes, 0x1A2);
  const lcbClx = this.readU32(wordBytes, 0x1A6);

  // 查找 Table 流
  let tableData: Uint8Array | null = null;
  let tableEntry = ole.findEntry("1Table");
  if (!tableEntry) {
    tableEntry = ole.findEntry("0Table");
  }
  if (tableEntry) {
    tableData = ole.readEntryData(tableEntry);
  }

  // 使用 piece table 提取文本
  if (tableData && fcClx > 0 && lcbClx > 0) {
    const text = this.extractTextWithPieceTable(wordBytes, tableData, fcClx, lcbClx, ccpText);
    if (text) {
      return this.cleanText(text);
    }
  }

  // 回退:直接提取
  return this.extractTextDirect(wordBytes, ccpText);
}

1.2 执行流程

extractWordText(wordBytes, ole)
    │
    ├── 1. 检查最小长度(0x200 = 512 字节)
    │
    ├── 2. 读取 FIB flags → 检查加密
    │
    ├── 3. 读取 ccpText → 文本字符数
    │
    ├── 4. 读取 fcClx / lcbClx → CLX 位置和大小
    │
    ├── 5. 查找 1Table / 0Table 流
    │
    ├── 6. 尝试 Piece Table 提取
    │       │
    │       ├── 成功 → cleanText → 返回
    │       │
    │       └── 失败 ↓
    │
    └── 7. 回退:extractTextDirect

二、FIB(File Information Block)

2.1 什么是 FIB

FIB 是 WordDocument 流的头部,包含了文档的各种元信息。它从偏移 0 开始,长度可变(几百到上千字节)。

2.2 doc_text 读取的 FIB 字段

偏移 长度 字段 代码 说明
0x0A 2 flags readU16(wordBytes, 0x0A) 文档标志位
0x4C 4 ccpText readU32(wordBytes, 0x4C) 正文字符数
0x1A2 4 fcClx readU32(wordBytes, 0x1A2) CLX 在 Table 流中的偏移
0x1A6 4 lcbClx readU32(wordBytes, 0x1A6) CLX 的大小

2.3 加密检测

const flags = this.readU16(wordBytes, 0x0A);
const isEncrypted = (flags & 0x0100) !== 0;
flags 的位布局:
位 0-7: 各种标志
位 8 (0x0100): fEncrypted — 文档是否加密

flags & 0x0100:
如果位 8 为 1 → 结果非零 → isEncrypted = true
如果位 8 为 0 → 结果为零 → isEncrypted = false

2.4 加密文档的处理

if (isEncrypted) {
  return "[加密文档,无法读取]";
}
处理方式 说明
返回提示文字 告诉用户文档是加密的
不尝试解密 解密需要密码,超出插件范围
不返回 null 区别于"无法解析"的情况

💡 返回 “[加密文档,无法读取]” 而不是 null——这样调用者可以区分"文件损坏"(null)和"文件加密"(有提示文字)。

三、ccpText:文本长度

3.1 读取

const ccpText = this.readU32(wordBytes, 0x4C);
if (ccpText <= 0 || ccpText > 10000000) {
  return null;
}

3.2 ccpText 的含义

ccpText = Character Count of Plain Text
即文档正文的字符数(不包括页眉页脚、脚注等)

3.3 上限检查

if (ccpText > 10000000) {
  return null;  // 超过 1000 万字符,可能是格式错误
}
检查 原因
ccpText <= 0 无文本 空文档或格式错误
ccpText > 10000000 超大 可能是格式错误导致的异常值

📌 10000000(一千万)是一个安全上限。正常的 Word 文档不会有这么多字符。如果 ccpText 超过这个值,说明 FIB 解析可能出了问题。

四、CLX 定位

4.1 什么是 CLX

CLX(Complex)是 Table 流中的一个结构,包含了 Piece Table——告诉我们文本的每一段存储在 WordDocument 流的什么位置、用什么编码。

4.2 fcClx 和 lcbClx

const fcClx = this.readU32(wordBytes, 0x1A2);  // CLX 在 Table 流中的偏移
const lcbClx = this.readU32(wordBytes, 0x1A6);  // CLX 的大小(字节)
字段 含义 示例
fcClx File offset of CLX 0x1234(CLX 从 Table 流偏移 0x1234 开始)
lcbClx Length of CLX in bytes 0x100(CLX 长度 256 字节)

4.3 有效性检查

if (tableData && fcClx > 0 && lcbClx > 0) {
  // CLX 信息有效,尝试 Piece Table 提取
}

如果 fcClx 或 lcbClx 为 0,说明文档没有 CLX 结构(可能是非常旧的格式),需要用回退策略。

五、Table 流查找

5.1 代码

let tableData: Uint8Array | null = null;
let tableEntry = ole.findEntry("1Table");
if (!tableEntry) {
  tableEntry = ole.findEntry("0Table");
}
if (tableEntry) {
  tableData = ole.readEntryData(tableEntry);
}

5.2 1Table vs 0Table 的选择

Word 文档的 FIB 中有一个标志位 fWhichTblStm:
- fWhichTblStm = 1 → 使用 1Table
- fWhichTblStm = 0 → 使用 0Table

doc_text 的简化策略:
- 先试 1Table(大多数文档用这个)
- 找不到再试 0Table
策略 准确性 复杂度
读取 FIB 标志位
先 1Table 后 0Table 高(实际上几乎所有文档用 1Table)

六、Piece Table 提取入口

6.1 调用

if (tableData && fcClx > 0 && lcbClx > 0) {
  const text = this.extractTextWithPieceTable(wordBytes, tableData, fcClx, lcbClx, ccpText);
  if (text) {
    return this.cleanText(text);
  }
}

6.2 参数说明

参数 类型 来源 说明
wordBytes Uint8Array WordDocument 流 文本数据在这里
tableBytes Uint8Array 1Table/0Table 流 Piece Table 在这里
fcClx number FIB 偏移 0x1A2 CLX 在 Table 流中的偏移
lcbClx number FIB 偏移 0x1A6 CLX 的大小
ccpText number FIB 偏移 0x4C 文本字符数

6.3 成功与回退

Piece Table 提取
    │
    ├── 成功(text 不为 null)→ cleanText → 返回
    │
    └── 失败(text 为 null)→ 回退到 extractTextDirect

七、回退策略

7.1 代码

// 回退:直接提取
return this.extractTextDirect(wordBytes, ccpText);

7.2 触发条件

条件 说明
tableData 为 null 找不到 Table 流
fcClx <= 0 CLX 偏移无效
lcbClx <= 0 CLX 大小无效
extractTextWithPieceTable 返回 null Piece Table 解析失败

7.3 回退策略的价值

正常流程(Piece Table):
准确性高,能正确处理编码

回退流程(直接提取):
准确性低,但能处理一些异常格式的文档

💡 双重策略是 doc_text 的一个重要设计——先用精确方法,失败了再用暴力方法。这样可以最大化成功率。

八、完整的 .doc 文本提取流程

8.1 流程图

.doc 文件
    ↓
OLE2Parser → FAT + 目录
    ↓
findEntry("WordDocument") → wordData
findEntry("1Table") → tableData
    ↓
extractWordText(wordData, ole)
    ├── FIB 解析 → flags, ccpText, fcClx, lcbClx
    ├── 加密检查 → "[加密文档]" 或继续
    ├── Piece Table 提取
    │   ├── CLX 定位 → tableData[fcClx..fcClx+lcbClx]
    │   ├── 解析 piece descriptors
    │   ├── 根据编码标志提取 Unicode/ANSI 文本
    │   └── 成功 → cleanText → 返回
    └── 直接提取(回退)
        ├── 多偏移量探测
        ├── Unicode/ANSI 双通道
        └── 取最长有效文本

8.2 各步骤的数据流

步骤 输入 输出 可能失败
OLE2 解析 文件字节 FAT + 目录 ✅ 魔数不匹配
流查找 目录 wordData + tableData ✅ 流不存在
FIB 解析 wordData flags + ccpText + CLX ✅ 数据不足
Piece Table wordData + tableData 文本 ✅ CLX 无效
直接提取 wordData 文本 ✅ 无有效文本

总结

FIB 解析与 Piece Table 提取是 .doc 文本提取的核心:

  1. FIB:从 WordDocument 流头部读取 flags、ccpText、fcClx、lcbClx
  2. 加密检测:flags 位 8 判断,加密文档返回提示文字
  3. ccpText 校验:上限 1000 万字符,防止异常值
  4. Table 流查找:先 1Table 后 0Table
  5. 双重策略:Piece Table 优先,失败回退到直接提取

下一篇我们深入 Piece Table 的内部结构——CLX、CP 数组、PCD 数组、编码判断。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

FIB 结构
Word 文档 FIB 与 Piece Table 关系

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐