דמיינו משימה לילית הבונה בקוד חוברת חשבוניות וכותבת אותה כ-CSV לייבוא במערכת המשך. המספרים נראים נכונים ב-Excel. קובץ ה-CSV נפתח היטב בעורך טקסט. ואז הייבוא נכשל בעמודת הסיכומים, מפני ששדה הסכום בשורה 42 מכיל =SUM(D2:D41), את הנוסחה כטקסט מילולי ולא את המספר שאליו היא אמורה להתחשב. דבר אינו מקולקל. זוהי התנהגות מתועדת, והיא הדבר הראשון שיש להבין על ייצוא מ-HotXLS: הכותב מסדר את מודל התאים בדיוק במצבו הנוכחי, ולתא נוסחה שערכו מעולם לא חושב יש רק את טקסט הנוסחה למסור
מדוע קובץ ה-CSV שלך מכיל נוסחאות במקום מספרים
HotXLS מאחסן את טקסט הנוסחה ואת הערך המחושב כשני דברים נפרדים. SaveAsCSV אינו מפעיל את מנוע החישוב בדרך החוצה, בכוונה: ייצוא לא אמור לשנות את החוברת, ולא להסתכן בהיתקעות בשרשרת נוסחאות פתולוגית. קבצים שנשמרו בידי Excel עצמו כוללים תוצאות שמורות לצד הנוסחאות, ולכן ייצוא מחדש שלהם מתנהג כפי שמצופה. המלכודת ייחודית לחוברות שהקוד שלך יצר, שבהן נכתבו נוסחאות אך הן מעולם לא חושבו. הפתרון הוא לוודא שהערכים קיימים לפני הייצוא, באמצעות אותו מנוע Calculate שפותר הפניות בין גיליונות ופונקציות מותאמות אישית:
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
R: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('invoice-run.xlsx');
Sheet := Book.Sheets[0];
// מימוש תוצאות הנוסחה בפועל כך שה-CSV יישא מספרים, לא טקסט '=...'
for R := 2 to 41 do
if Sheet.Cells[R, 4].Formula <> '' then
Sheet.Cells[R, 4].Value := Book.Calculate(Sheet.Cells[R, 4].Formula);
Book.SaveAsCSV('feed.csv', 0, ','); // גיליון 0, פסיק
Book.SaveAsCSV('feed.tsv', 0, #9); // אותו גיליון כ-TSV
finally
Book.Free;
end;
end;
שימו לב למה שהלולאה עושה בפועל: היא מחליפה את תאי הנוסחה בערכים המחושבים שלהם. זה נכון בדיוק למעבר ייצוא חד-פעמי, ושגוי אם בכוונתך לשמור את החוברת שוב כ-.xlsx לאחר מכן, מפני שהחלפת זה עתה נוסחאות חיות במספרים קפואים. ייצא מעותק, או תחום את הכתיבה החוזרת כך שתיגע רק בריצת הייצוא. המנוע שמאחורי Calculate עושה יותר מכך, כולל רישום של פונקציות משלך, וזהו נושאו של המאמר על מנוע הנוסחאות והפונקציות המותאמות אישית של HotXLS
מה מבטיח כותב הקבצים המופרדים
נתיב ה-CSV מפיק UTF-8 עם סימן סדר בתים, סופי שורות CRLF וציטוט לפי RFC 4180. כל שדה המכיל את המפריד, מרכאה או מעבר שורה נעטף, ומרכאות משובצות מוכפלות. תאריכים מוצגים כ-yyyy-mm-dd hh:nn:ss בלי קשר לתבנית התצוגה של התא. זו הבחירה הנכונה לצרכן מכונה, אף שהיא מפתיעה מי שציפה שעיצוב המסך יישמר. תאי טקסט עשיר משוטחים באמצעות שרשור המקטעים שלהם
ברירות המחדל הללו פותרות את רוב הוויכוחים עם מייבא עוד לפני שהם מתחילים, אך שתיים מהן חייבות להופיע בכל זאת בחוזה הממשק שלך. הראשונה היא ה-BOM. הוא מאפשר ל-Excel לפתוח את הקובץ עם תווים מודגשים שלמים, אך קומץ מנתחים מחמירים מתייחסים לשלושת הבתים האלה כאל נתונים; אם המנתח שלך הוא אחד מהם, הסר אותם במסירה. השנייה היא TSV. זו אינה תכונה נפרדת כלל, אלא אותו כותב הנקרא עם #9 כמפריד, לכן כל האמור לעיל חל עליה ללא שינוי. הגיליון לייצוא נבחר לפי אינדקס מבוסס-0 בגרסת העומס מרובת הארגומנטים, ואילו הקיצור SaveAsCSV(FileName) בעל ארגומנט יחיד משתמש בגיליון הפעיל
ייצוא HTML הוא תמונת מצב, לא תבנית חילופים
במקום שבו CSV משליך הכול פרט לערכים, SaveAsHTML מנסה לשמר את המראה: <table> אחד לכל גיליון, אזורים ממוזגים המיוצגים באמצעות colspan ו-rowspan, ועיצוב תאים בסיסי מוטמע כ-CSS. צבעים יחסיים לערכת הנושא מדולגים במקום להיפתר, ולכן תבנית הנשענת על חריצי ערכת נושא יוצאת פשוטה יותר מאשר ב-Excel. הגדר צבעי RGB מפורשים לכל מה שחייב לשרוד את המעבר. אובייקט האפשרויות שולט במעטפת:
var
Opts: TXLSXHtmlExportOptions;
begin
Opts := TXLSXHtmlExportOptions.Create;
try
Opts.Title := 'Weekly settlement';
Opts.TableClass := 'report-grid'; // hook עבור גיליון הסגנונות של הדף המארח
Opts.WriteDocument := True; // עמוד מלא, לא פרגמנט
if Book.SaveAsHTML('settlement.html', 0, Opts) <> 0 then
raise Exception.Create('Sheet index out of range');
finally
Opts.Free;
end;
end;
שני פרטים בקטע הזה ראויים לתשומת לב. שנה את WriteDocument ל-False והפלט יהיה מקטע טבלה חשוף במקום עמוד מלא, בדיוק מה שנחוץ להזרקת תצוגה מקדימה לפריסה קיימת: הגדר את TableClass ותן לגיליון הסגנונות של המארח לקבוע את ערכת הנושא. גם מוסכמת ההחזרה הפוכה מרוב קריאות HotXLS. SaveAsHTML מחזיר 0 בהצלחה ו--1 עבור אינדקס גיליון שגוי, לכן בדיקה מוכתבת הרגל עבור = 1 תדווח שכל ייצוא מוצלח נכשל. כאשר נחוץ אזור במקום גיליון שלם, אולי למשלוח בדואר או להטמעת בלוק יחיד, TXLSXRange.SaveAsHTML מייצא כל טווח מלבני תחת אותם כללי עיבוד
פלט RTF והמקום שבו הוא עדיין מועיל
היעד הרביעי כותב טבלאות RTF 1.6, גיליון אחד בכל קריאה באמצעות SaveAsRTF. רוחבי עמודות מקורבים בכ-96 twips לכל תו של רוחב עמודה. המגבלה המבנית שחשוב לדעת היא שתאים ממוזגים אינם נפרשים בפלט: רק תא העוגן נושא את תוכנו, והתאים המכוסים נפלטים כריקים. לכן RTF אינו מתאים לתבניות עתירות פריסה. הוא עדיין הדרך בעלת החיכוך הנמוך ביותר להכניס תוצאות טבלאיות למעבד תמלילים או למערכת ניהול מסמכים מדור קודם שקודמת לקליטת HTML
הלוך ושוב: ייבוא CSV הוא הרסני במכוון
לקריאת CSV בחזרה יש חוזה משלה. OpenCSV מנקה את החוברת כולה ובונה אותה מחדש כגיליון יחיד בשם Sheet1. במהותו זהו בנאי, לא מיזוג, ולכן לעולם אל תקרא לו על חוברת שעדיין מכילה תוכן שלא נשמר. העברת #0 כמפריד מפעילה זיהוי אוטומטי של המפריד. הדגל ADetectTypes שולט בקידום טיפוסים: כאשר הוא מופעל, מחרוזות מספריות הופכות למספרים, מחרוזות ISO-8601 הופכות לתאריכים ו-true/false הופכים לבוליאנים. כבה אותו כשההזנה מכילה מזהים עם אפסים מובילים, מיקודים או קודי מוצר, שכולם הקידום מעוות בשקט למספרים (אפס מוביל פשוט נעלם ברגע ש-00123 הופך ל-123). שתי החזיתות חושפות את אותו ייבוא. שלב אותו עם קריאות הייצוא שלמעלה ותקבל גשר פורמטים שאינו דורש התקנת Excel בשום מקום בצינור, התרחיש המתואר ב-יצירת דוחות ממסד נתונים ל-Excel עם HotXLS
ייצוא ישירות לזרם
לכל כותב כאן יש גרסת עומס של זרם לצד גרסת שם הקובץ: CSV, HTML, RTF ופורמטי החוברת עצמם. בקוד שרת, אלה גרסאות העומס שכדאי להשתמש בהן. נקודת קצה ברשת המגישה הורדת CSV יכולה לכתוב ל-TMemoryStream ולמסור אותו ישירות לאובייקט התגובה, ללא קובץ זמני, ללא משימת ניקוי וללא התנגשות בין שתי בקשות שבמקרה בחרו אותו שם שנוצר. כך גם בדחיפת ייצואים לאחסון אובייקטים או צירופם לדואר יוצא. מערכת הקבצים יוצאת לחלוטין מן התמונה
דפוס זה מצטבר לאופן שבו הספרייה נפרסת. שתי החזיתות הן קוראים וכותבים מקוריים של Object Pascal, לכן אין התקנת Excel, אין אוטומציית COM ואין צוואר בקבוק לכל תהליך המסדר בקשות בשרת. כל בקשה יכולה להחזיק אובייקט חוברת משלה, לבצע את כתיבת החישוב החוזרת מהסעיף הראשון, ולהזרים את הייצוא במקביל לשכנותיה. זיכרון הוא המשאב היחיד שיש לעקוב אחריו. מודל החוברת חי ב-RAM במשך הייצוא, לכן שירות הפותח קבצים גדולים מאוד רק כדי לפלוט אותם מחדש כ-CSV צריך להגביל משימות מקבילות או לתור את הגדולות, במקום לתת לקפיצת תעבורה לקבוע את קבוצת העבודה
כפתור קטן נוסף: הגדר IncludeBOM באפשרויות HTML כאשר המקטע יישמר כקובץ עצמאי שכלי המשך מסוים מרחרח כדי לזהות קידוד. כאשר HTML מוגש ישירות מעל HTTP, השאר את הצהרת ערכת התווים לכותרות התגובה במקום זאת
כאשר הבתים עדיין יוצאים שגויים
שאלת התמיכה הנפוצה ביותר על ייצוא CSV היא בעיית הפתיחה בתחפושת אחרת: Excel מציג ג׳יבריש במקום תווים מודגשים. הנטייה היא להאשים את הכותב, אך הוא פולט BOM של UTF-8 בדיוק מסיבה זו, והקובץ כמעט תמיד תקין כשהוא עוזב את הקוד שלך. משהו בדרך בינו לבין Excel אכל את ה-BOM. העברת FTP במצב טקסט, העתקת זרם המדלגת על שלושת הבתים הראשונים, פרוקסי המקודד מחדש במעבר: כל אחד מאלה יסיר את הסמן וישאיר את Excel לנחש את הקידוד, משימה שהוא מבצע גרוע. אבחן זאת בגבול, לא בקריאת הייצוא. פתח את הקובץ שנמסר במציג הקסדצימלי ואשר ש-EF BB BF עדיין הדבר הראשון שבו
זהו החוט המקשר בין כל ארבעת הפורמטים. קריאת הייצוא היא החלק הקל, ו-HotXLS מקבל בחירה סבירה בכל החלטה שהכותב ניצב בפניה. הכשלים חיים בתפרים, במקום שבו טקסט נוסחה פוגש מנתח שרצה מספר, שבו BOM פוגש תעבורה שאינה משמרת אותו, שבו תא ממוזג פוגש את מודל הטבלה השטוח של RTF. כל אחד מהם הוא עובדה שיש לכתוב בחוזה בין המייצא שלך לבין כל מה שצורך אותו, מפני שהצרכן אינו יכול לקרוא את כוונותיך מתוך הבתים. לרשימת השיטות המלאה בכל שתי חזיתות החוברת, דף המוצר של HotXLS Delphi Component מכיל את ההפניה המלאה