將 DOCX、XLSX 與 PDF 合併成單一綑綁檔 — 示範執行

讓我每個星期五都被吃掉的事

每個星期五的下午,大約持續了一年,我都有同樣的小儀式。合約會以三個檔案的形式送來——Word 的主協議、Excel 的價格附件,以及合作夥伴的條款表格(PDF)。我必須把它們合併成一個乾淨的 PDF。這並不難。打開 Word,匯出為 PDF;打開 Excel,匯出為 PDF;打開某個免費的 PDF 合併工具,拖入三個檔案,檢查順序,儲存。

這大約需要八分鐘。每週十五份合約算起,就因為移動滑鼠而浪費了兩小時。更糟的是,每隔幾週就會有人把附件放在第一頁,因為合併工具會依檔名的字母順序排序。

如果這聽起來很熟悉,接下來的內容就是我在某個下午最終用程式碼取代這個儀式的過程。

真正的成本不是時間——而是每五十份合約中就有一份頁面順序錯誤,且在客戶簽署錯誤版本之前沒人發現。

我真正想要的

不是「華麗的文件流程」。只有三件事:

  1. 提供一個方法接受檔案清單(任意組合的 DOCX、XLSX、PDF),回傳一個 PDF。
  2. 將相同的邏輯指向一個資料夾,讓它自行找出檔案清單。
  3. 從完成的合併檔中抽取頁碼範圍,而不必重新執行整個合併。

這就是全部需求。如果函式庫無法乾淨利落地完成這三件事,我就不想知道它的存在。

設定

  • .NET 6.0 或更新版本
  • GroupDocs.Merger for .NET 24.10 以上(取得臨時授權 以免輸出帶有評估浮水印)
  • 一個資料夾,內含你平時手動合併的各種文件組合
dotnet add package GroupDocs.Merger

依賴就這麼簡單。沒有外部轉換器、沒有無頭 Office 安裝、也不需要額外的 PDF 操作函式庫。

步驟 1 — 讓資料夾作為輸入

我總是從這裡開始,因為這是最實際的切入點。實際上,其他流程(上傳處理程式、電子郵件匯入工作、財務的夜間匯出)會把一堆檔案放入目錄,我的程式必須處理它們。

// Pick up every supported file in the drop folder; the PDF wins
// the tie-break for position 0 so the merger keeps the output
// as a PDF regardless of how files are named.
string[] extensions = { ".pdf", ".docx", ".xlsx" };
var files = Directory.EnumerateFiles(folderPath)
    .Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
    .OrderBy(f => Path.GetExtension(f).ToLowerInvariant() == ".pdf" ? 0 : 1)
    .ThenBy(f => f)
    .ToArray();

if (files.Length == 0)
    throw new InvalidOperationException(
        $"No supported documents found in '{folderPath}'.");

OrderBy 的技巧是關鍵。GroupDocs.Merger 會根據你最先開啟的檔案決定輸出格式——如果我先給它一個 DOCX 作為主要文件,輸出就會是 DOCX。因為我的流程始終需要 PDF 輸出,我確保資料夾中任何現有的 PDF 取得第 0 位。

有兩件事值得一提:

  • ToLowerInvariant():因為合作夥伴可能會傳送 REPORT.PDF,若只使用小寫過濾會悄悄把它排除。
  • ThenBy(f) 只用來保證輸出具有決定性。若省略它,同一資料夾的兩次執行可能因檔案系統的順序而不同。

步驟 2 — 合併本身

取得排序好的路徑清單後,合併的程式碼比合併的說明還要簡短。

Console.WriteLine($"Primary source: {sourcePaths[0]}");
using var merger = new Merger(sourcePaths[0]);

var joinOptions = new JoinOptions();
for (int i = 1; i < sourcePaths.Length; i++)
{
    Console.WriteLine($"Joining: {sourcePaths[i]}");
    merger.Join(sourcePaths[i], joinOptions);
}

merger.Save(outputPath);
Console.WriteLine($"Unified PDF binder saved to: {Path.GetFullPath(outputPath)}");

以下是我在實際使用中得到的幾點注意事項:

  • using 很重要。Merger 會保留來源檔案的檔案句柄;如果忘記釋放,負責處理資料夾的工作程式最終會無法刪除自己的輸入檔案。
  • JoinOptions 這裡是空的,因為預設值已滿足我 95% 的需求。若需要自訂,頁碼範圍、旋轉與插入位置都在此設定。
  • 當 Excel 檔案加入合併檔時,工作表到頁面的版面配置由來源活頁簿的列印區域決定。如果你的 XLSX 最終產生 38 頁而你只想要三頁,必須在試算表本身調整,而不是在 JoinOptions 中。

我總是在儲存後立即加入的一個 sanity check:

using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");

這兩秒的程式碼捕捉到的「悄悄遺失附件」錯誤,比我寫過的任何測試都多。

步驟 3 — 之後抽取一段

我每次都會收到的後續需求:「能只給我封面頁嗎?」或「客戶只要簽名頁。」重新組合整個合併檔只為了兩頁實在太蠢——直接抽取即可。

using var merger = new Merger(binderPath);
merger.ExtractPages(new ExtractOptions(pages));
merger.Save(outputPath);
Console.WriteLine($"Extracted pages [{string.Join(",", pages)}] to " +
    Path.GetFullPath(outputPath));

pages 是一個 int[],包含你想保留的 1 起始頁碼。其他頁面全部捨棄。速度快是因為結果已經是 PDF——不需要再做轉換。

前後對比,說實話

我以前的做法 使用 Merger.Join
每份合約所需時間 點擊 5–10 分鐘 端到端不到 30 秒
常見失敗 頁面順序錯誤,無人察覺 依檔案清單的順序,且可重複執行
每日 100 份的擴充性 做不到——需要雇人 只需一個工作者,大部分時間都很閒
需要維護的程式碼 一頁名為「Binder Process v4」的 Confluence 文件 一個類別,約 70 行
輸出 三個 PDF 加上祈禱 一個合併檔,且可記錄頁數

我最在意的是「失敗」那一行。手動合併會悄悄失敗;而程式碼若記錄頁數則會明顯失敗。

來自小型法律科技團隊的真實故事

我曾合作的兩人新創公司有一名法律助理,她的早晨從合約組裝開始。Word 合約、Excel 價格表、PDF 附錄,透過應用程式串接,然後上傳至 DocuSign。每個套件大約八分鐘,若一天處理 30 套,基本上佔滿她整個早晨。

他們把資料夾掃描的方式放入已經監控收件郵件的後端服務。每個套件只要二十秒,外加一行記錄頁數的日誌。法律助理因此改為審閱合約,而不是組裝。再也沒有人寄出順序錯誤的合併檔——不是因為函式庫有魔法,而是檔案清單在程式碼中明確,且可以比對差異。

string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";

var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");

這就是完整的整合。所有上游(郵件監聽器、儲存路徑)已經就緒。

我今天沒用到但明天會用的東西

同一個函式庫還能做許多我此篇未提及的功能,因為文章篇幅限制。大致上我使用的順序如下:

  • 在輸出上加上浮水印,例如在未簽署的副本上加上「DRAFT」標記。
  • 頁面旋轉,針對側向掃描的檔案。
  • 自訂頁面順序,當來源順序與交付順序不一致時。
  • PDF 加密,用於傳送給外部對象的文件。

所有功能都在同一個 Merger API 之下。完整清單請參考 文件——我只是想說「合併」是入門的低成本功能,其他功能在需要時也都可用。

我想對過去的自己說的話

如果你正打算自行寫一個 DOCX 轉 PDF 的步驟,因為「只是一個方法」就想動手,請停下。轉換是最容易悄悄退化的部分——新 Office 功能、掃描圖像處理、內嵌字型等。讓其他工具負責這層面,將你的星期五下午花在不是檔名排序的事上。

接下來可以做什麼:

有用的連結