💡 完整可運作的範例可在 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# 服務路徑。

您將需要:

  • .NET 8 SDK
  • GroupDocs.Parser 26.10.0 (temporary license)
  • 一個 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());

將這些事實記錄在工作編號旁。對於範例 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 - 使用 GetText 回退 PDF

當 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。在大量夜間處理前套用授權或 temporary license。將上傳視為不可信輸入,並在隔離的工作者中解析。

在自己的檔案上試試看

Clone parse-docx-to-markdown-chunks-dotnet,還原 GroupDocs.Parser 26.10.0,然後對 documents/sample.docx 執行。接著將同樣的四個方法指向您的一本手冊,打開產生的 chunk-*.md 檔案。