💡 完整可运行示例可在 GitHub 上获取: parse-docx-to-markdown-chunks-dotnet

过去的做法很痛苦

基于标题的 Markdown 分块是一个 .NET 检索准备路径,它在嵌入之前将 Office 和 PDF 文件转换为章节大小的文本。如果没有它,团队通常会把整个 Word 文件一次性嵌入为单个向量。封面段落和责任条款共享同一个嵌入,因此余弦搜索会把错误的章节排在前面。我在一份三页的内部指南中遇到了这种情况:每次查询都会首先返回标题块,因为索引从未看到作者已经写好的 # / ## 结构。

自行编写 DOCX 转 Markdown 转换器意味着要遍历 Open XML 样式、映射列表层级,并且在下一个上传的是 PDF 且其格式化的 Markdown 路径返回 null 时仍会失败。固定大小的 token 窗口会忽略这些标题。于是整夜的语料库会把空壳子和庞大的手册混在一起,因为没有人在一开始就根据页数或大小进行过滤。

整个文件嵌入的真实成本:错误章节的答案、在垃圾上传上浪费的提取费用,以及在出现问题的批次中运营人员无法打开的 chunk-01.md 人工制品。

有更好的方法

GroupDocs.Parser for .NET 用四个专注的调用取代了这种混乱:GetDocumentInfo、GetFormattedText(FormattedTextMode.Markdown)、当 Markdown 为 null 时的 GetText,以及一个写入 chunk-NN.md 的小标题遍历器。相同的 Parser 类型同时支持 Word DOCX 解析和 PDF 文本回退,因此你只需维护一个 C# 服务路径。

You will need:

  • .NET 8 SDK
  • GroupDocs.Parser 26.10.0(临时许可证)
  • 一个 Word DOCX(示例为 3 页 / 14820 字节),以及可选的 PDF
dotnet add package GroupDocs.Parser --version 26.10.0

新方法:检查、提取、回退、拆分

以下是切换后的相同检索准备过程。位于 parse-docx-to-markdown-chunks-dotnet 下的控制台示例会写入信息、Markdown、PDF 文本以及针对所提供的 DOCX 的四个标题块。

步骤 1 - 使用 GetDocumentInfo 进行过滤

在进行完整提取之前,先获取文件类型、页数和大小。

using var parser = new Parser(sourcePath);
var info = parser.GetDocumentInfo();

var sb = new StringBuilder();
sb.AppendLine($"Source: {Path.GetFileName(sourcePath)}");
sb.AppendLine($"File type: {info?.FileType}");
sb.AppendLine($"Pages: {info?.PageCount}");
sb.AppendLine($"Size: {info?.Size} bytes");
File.WriteAllText(outputPath, sb.ToString());

将这些信息记录在作业 ID 旁边。在示例 DOCX 上,你应该会看到 Microsoft Word Open XML Document,3 页,14820 字节。

步骤 2 - 优先使用 GetFormattedText 获取 Markdown

对于支持格式化提取的 Word 和其他格式,保留 ATX 标题以供拆分器使用。

using var parser = new Parser(sourcePath);
using TextReader? reader = parser.GetFormattedText(
    new FormattedTextOptions(FormattedTextMode.Markdown));

if (reader == null)
{
    return null;
}

string markdown = reader.ReadToEnd();
File.WriteAllText(outputPath, markdown, Encoding.UTF8);
return markdown;

null 表示需要切换路径,而不是崩溃。使用 UTF-8 编码写入,以便块工具和 git diff 保持可读。

步骤 3 - 对 PDF 使用 GetText 回退

当 Markdown 模式返回 null 时,使用纯文本层将文件继续保留在流水线中。

using var parser = new Parser(sourcePath);
using TextReader? reader = parser.GetText();
if (reader == null)
{
    return null;
}

string text = reader.ReadToEnd();
File.WriteAllText(outputPath, text, Encoding.UTF8);
return text;

预期得到 .txt,而不是人为创建的 # 标题。示例主机会为 PDF 分支写入 Result/sample-pdf.txt。

步骤 4 - 按 # 和 ## 拆分 Markdown

遍历每行;当出现新的 ATX # 或 ## 时刷新;写入 chunk-NN.md。

var chunks = new List<string>();
var current = new StringBuilder();
using (var reader = new StringReader(markdown))
{
    string? line;
    while ((line = reader.ReadLine()) != null)
    {
        bool isHeading = line.StartsWith("# ", StringComparison.Ordinal)
            || line.StartsWith("## ", StringComparison.Ordinal);
        if (isHeading && current.Length > 0)
        {
            chunks.Add(current.ToString().TrimEnd());
            current.Clear();
        }
        current.AppendLine(line);
    }
}

首先删除过时的 chunk-*.md 文件。更深的 ### 行会保留在当前章节中。示例 DOCX 会在 Result/chunks/ 下生成四个文件。

何时应使用此路径替代整文件嵌入?

当输入同时包含 Word DOCX 和 PDF 且索引需要章节大小的 Markdown 时,替换整文件嵌入。首先运行 GetDocumentInfo,对 Office 文件尝试 Markdown,若格式化读取器为 null 则调用 GetText,仅在存在 # / ## 标记时进行拆分。对未结构化的 PDF 文本保持原样,直到你添加单独的块策略。此顺序与示例控制台应用保持一致,并为运营人员明确失败模式。

对运营人员的变化

之前 之后
每个文件一个嵌入 每个标题块一个嵌入
自定义 Open XML Markdown GetFormattedText Markdown 模式
PDF 在格式化读取器为 null 时被丢弃 GetText 写入文本制品
没有廉价的预检查 页数和大小门控工作

释放每个 Parser 和 TextReader。在大批量处理前应用许可证或临时许可证。将上传视为不可信输入,并在隔离的工作进程中进行解析。

在自己的文件上尝试

克隆 parse-docx-to-markdown-chunks-dotnet,恢复 GroupDocs.Parser 26.10.0,并对 documents/sample.docx 运行。然后将同样的四个方法指向你的某本手册,打开生成的 chunk-*.md 文件。