The thing that kept eating my Fridays
כל אחר צהריים של שישי, במשך כשנה, היה לי אותו טקס קטן. חוזה היה מגיע בשלושה קבצים — ההסכם הראשי ב‑Word, נספח תמחור ב‑Excel, וגיליון תנאי השותף כ‑PDF — והייתי צריך לשלב אותם לקובץ PDF אחד נקי. זה לא היה קשה. פותחים Word, מייצאים ל‑PDF. פותחים Excel, מייצאים ל‑PDF. פותחים אפליקציית מיזוג PDF חינמית, גוררים שלושה קבצים, בודקים את הסדר, ושומרים.
זה לקח אולי שמונה דקות. אם מכפילים זאת בחמש עשרה חוזים בשבוע, מאבדים שעתיים בתזוזת העכבר. גרוע יותר, כל כמה שבועות מישהו שלח מאגד עם הנספח בעמוד הראשון מכיוון שהשמות ממוינים אלפביתית באפליקציית המיזוג.
אם זה מוכר, שאר הפוסט מתאר את אחר הצהריים שבו סוף סוף החלפתי את הטקס בקוד.
העלות האמיתית איננה בזמן — אלא החוזה האחד מתוך חמישים שבו העמודים מגיעים בסדר הלא נכון ואף אחד לא שם לב עד שהלקוח חותם על הגרסה השגויה.
What I actually wanted
לא “צינור מסמכים מתוחכם”. רק שלושה דברים:
- להעביר למתודה רשימת קבצים (כל שילוב של DOCX, XLSX, PDF) ולקבל חזרה קובץ PDF אחד.
- להפנות את הלוגיקה לאותו קובץ לתיקייה ולתת לה לקבוע את רשימת הקבצים בעצמה.
- לחלץ טווח עמודים מהמאגד המוגמר מבלי לבצע מחדש את כל המיזוג.
זה כל המשימה. אם הספרייה לא יכולה לבצע את שלושת הדברים בצורה נקייה, איני רוצה לדעת על כך.
Setup
- .NET 6.0 או גרסה מאוחרת יותר
- GroupDocs.Merger for .NET 24.10+ (grab a temporary license כדי שלא תשלח את סימן המים של ההערכה)
- תיקייה עם כל שילוב של מסמכים שהיית רגיל למזג ידנית
dotnet add package GroupDocs.Merger
זהו כל מה שנדרש כתלות. אין ממיר חיצוני, אין התקנת Office ללא ממשק, ואין ספריית מניפולציית PDF מעל.
Step 1 — Let a folder be the input
תמיד מתחילים כאן מכיוון שזה נקודת הכניסה הריאלית. בפועל, משהו אחר (מטפל העלאות, משימת קבלת אימיילים, ייצוא לילה מהמחלקה הפיננסית) משאיר חבילה של קבצים בתיקייה, וקוד שלי צריך להתמודד עם מה שמוצא.
// 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)קיים רק כדי להפוך את הפלט לדטרמיניסטי. ללא זה, שני ריצות על אותה תיקייה יכולות להשתנות בהתאם למצב מערכת הקבצים.
Step 2 — The merge itself
כאשר יש לי רשימת נתיבים מסודרת, המיזוג קצר יותר מהתיאור של המיזוג.
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.
בדיקת תקינות אחת שאני תמיד מוסיף מיד אחרי השמירה:
using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");
שתי שניות קוד שתפסו יותר באגים של “נספח שנפל בשקט” מאשר כל בדיקה שכתבתי.
Step 3 — Extract a slice later
הבקשה המשנית שאני מקבל בכל פעם: “אתה יכול לשלוח לי רק את דף הכיסוי?” או “הלקוח רוצה רק את החתימות.” לבנות מחדש את כל המאגד כדי להעביר שני עמודים זה טיפשי — Extract עושה זאת ישירות.
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 — ללא סיבוב המרה.
Before vs. after, honestly
| מה שהייתי עושה לפני | עם Merger.Join |
|
|---|---|---|
| זמן לכל חוזה | 5–10 דקות של לחיצות | פחות מ‑30 שניות מקצה לקצה |
| כשל טיפוסי | עמודים בסדר הלא נכון, אף אחד לא שם לב | הסדר שהרשימה מציינת, באופן קבוע |
| הרחבה ל‑100 ביום | לא מתאפשר — צריך לשכור אדם | עובד אחד, משועמם רוב הזמן |
| קוד שאתה מתחזק | דף Confluence בשם “Binder Process v4” | מחלקה אחת, כ‑70 שורות |
| פלט | שלושה PDFים ותפילה | מאגד אחד, עם ספירת עמודים שניתן לתעד |
השורה שמעניינת אותי ביותר היא שורת ה"כשל". מיזוג ידני נכשל בשקט; קוד שרושם ספירת עמודים נכשל בקול רם.
A real story from a tiny legal-tech team
סטארט‑אפ של שני אנשים שעבדתי איתו היה לו עובד משפטי שהבוקר שלו התחיל בהרכבת חוזים. הסכם ב‑Word, תמחור ב‑Excel, נספח PDF, משולבים באפליקציה, מועלים ל‑DocuSign. כ‑שמונה דקות לחבילה, וב‑30 חבילות ביום זה היה למעשה כל בוקר שלה.
הם הטמיעו את שיטת סריקת התיקייה בשירות ה‑backend שכבר ציפה למיילים הנכנסים. עשרים שניות לכל חבילה, ועוד שורת לוג עם ספירת העמודים. העובד המשפטי עבר לבחינת חוזים במקום להרכיבם. אף אחד לא שלח יותר מאגד מסודר באופן שגוי — לא בגלל שהספרייה קסומה, אלא מכיוון שרשימת הקבצים מפורשת בקוד וניתן לבצע diff.
string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";
var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");
זהו כל האינטגרציה. כל מה שמקדים (מאזין המיילים, נתיב האחסון) כבר היה במקום.
Stuff I didn’t need today but will tomorrow
הספרייה הזו עושה ערימה של דברים שלא כיסיתי מכיוון שהמאמר היה מתארך. בקירוב לפי הסדר שבו השתמשתי בהם:
- סימני מים על הפלט עבור חותמות “טיוטה” על עותקים לפני חתימה.
- סיבוב עמודים עבור סריקות שמגיעות בצד.
- סדר עמודים מותאם כאשר סדר המקור אינו סדר המסירה.
- הצפנת PDF לכל דבר שנשלח לצד חיצוני.
הכל נמצא מאחורי אותו API של Merger. ה-docs מכילים את הרשימה המלאה — רק רציתי לציין ש"מיזוג" הוא הפתרון הבסיסי והשאר זמין כשצריך.
What I’d tell past-me
אם אתה עומד לכתוב שלב DOCX‑to‑PDF משלך מכיוון ש"זה רק מתודה אחת", עצור. ההמרה היא החלק שמחליד בשקט — תכונות Office חדשות, טיפול בתמונות סרוקות, גופנים משובצים, ועוד. תן למשהו אחר לטפל במשטח הזה, והקדש את אחר הצהריים של שישי למשהו שאינו מיון קבצים.
היכן ללכת הלאה:
- Temporary license — נדרש לפלט ללא סימן מים
- Advanced merging options — JoinOptions, אפשרויות שמירה, דחיסה
- Supported formats — הרבה מעבר לשלושה שהצגתי כאן
- Sample projects on GitHub — כולל את זה