HotXLS קורא מקורות .xls, .xlsx, .xlsm, .ods, CSV ו־TSV דרך סמן שורות pull אחד, TXLSRowCursor, שה־FindFirst וה־FindNext שלו מתקדמים שורה לוגית אחת בכל פעם בזמן שרק אותה שורה נשארת בזיכרון. מכונת מצבים בת שישה ערכים מפרידה בין before-first ל־EOF, ל־cancelled ול־faulted, וקורא הקולבק הישן הוא כעת מתאם מעל אותו סמן
התרחיש מוכר לכל מי ששיווק פעם תכונת יבוא. מגיע קובץ .xlsx של 200 MB, אתם חוברים מטפל OnCell, והדרישה הראשונה אחרי "קרא אותו" היא "עצור אחרי מאה הרישומים ההפוכים הראשונים". עכשיו צורת הקוד שלכם נלחמת בכם: הלולאה חיה בתוך הספרייה, המטפל שלכם חייב להרים דגל, כל קולבק שלאחר מכן עדיין נורה עד שהמנתח שם לב, והמצב המצטבר — כמה פגיעות עד עתה, איזו עמודה נתאימה, מה לעשות הלאה — חייב לשבת בשדות של מחלקה שקיימת רק כדי לתת לקולבק מקום לשבת בו. שום דבר מזה אינו בעיית ניתוח. זו בעיית זרימת בקרה, והיא בדיוק מה שסמן pull מסיר
מה קולבק push עולה באמת ב־200 MB
push הופך את הבקרה, והיפוך הוא בדיוק מה שקורא מסנן או מצרף אינו יכול להרשות לעצמו. עם API של קולבקים הספרייה מחזיקה בלולאה, ולכן הקורא אינו יכול להשתמש ב־Break, אינו יכול לשזור שני מקורות, אינו יכול למסור את הקורא לשגרה שמצפה שתנהלו אותה, ואינו יכול לבטא "הציצו לשורה הבאה לפני ההחלטה" בלי חיצוץ. העלות אינה קצב — נתיב קולבק SAX כתוב היטב זורם יפה — היא שכל צרכן לא טריוויאלי מגדל מכונת מצבים קטנה משלו כדי לדמות את הלולאה שלא הותר לו לכתוב. הכפילו את זה בארבעה פורמטי קבצים, שלכל אחד מהם היסטורית נקודת כניסת סריקה משלו, והסמנטיקה של הסינון, הנוסחאות והשגיאות מתחילה להתפצל ביניהם — בדיוק הסטייה ש־HotXLS יצא לסגור
איך סמן pull משנה את קוד הקריאה שלכם?
הוא מחזיר לכם את הלולאה, ואיתה את זרימת הבקרה הפשוטה של Pascal. TXLSRowCursor.Open מקבל שם קובץ או TStream, מזהה את הפורמט, טוען מחרוזות משותפות ומטא־נתוני סגנון תאריך פעם אחת, ובוחר בגיליון 1. SelectSheet (בבסיס 1) או SelectSheetByName מכוונים מחדש לגיליון אחר ומאפסים את הסמן ל־before-first. FindFirst ו־FindNext ממקמים אז על השורה המאוכלסת הבאה — שורות בלי תאים שניתנים לפענוח מדולגות, כך ש־RowIndex יכול לקפוץ — והשורה הנוכחית חשופה כ־CellCount, Cells[] ו־ValueByCol[], כולן בבסיס 1 בציר העמודות. יציאה מהלולאה היא Break
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // מדלגים על רצועת הכותרת
Cursor.IncludeColumn(1); // מפענחים רק את שתי העמודות האלה
Cursor.IncludeColumn(7);
if not Cursor.Open('postings-200mb.xlsx') then
Exit;
if not Cursor.SelectSheetByName('Ledger') then
Exit;
Hits := 0;
if Cursor.FindFirst then
repeat
if VarToStr(Cursor.ValueByCol[7]) = 'REVERSED' then
begin
Inc(Hits);
if Hits = 100 then
Break; // Break רגיל; בלי דגל ביטול, בלי סנטינל
end;
until not Cursor.FindNext;
finally
Cursor.Free; // ההורס מסיים את המעבר
end;
end;היטל וטווח נקבעים לפני המעבר, לא מסוננים אחריו. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates ו־DetectTextTypes כולם מכובדים בתוך הבקאנדים, כך שעמודה שלא נבחרה לעולם לא מקצה את הערך שלה, מחרוזת הנוסחה שלה או מטען הטקסט העשיר שלה מלכתחילה — סוויטת הרגרסיה מוכיחה זאת עם נוסחאות של 16 KiB ומחרוזות שמורות שלעולם לא מתממשות כשהעמודה שלהן אינה נכללת בהיטל. האפשרויות האלה קפואות בכוונה בזמן שמעבר פעיל וחוזרות להיות ניתנות לכתיבה ב־EOF, ב־SelectSheet, או אחרי Close, כך שסריקה אחת לעולם לא יכולה לערבב שני חוזי פענוח. אם אתם צריכים רק את מלאי הגיליונות ולא את השורות, טעינת מטא־נתונים בלבד וטעינת גיליונות סלקטיבית היא נקודת הכניסה הזולה יותר
בקאנד אחד לכל פורמט, לולאת סריקה אחת לכל אחד
לכל פורמט יש סורק קדימה אחד בדיוק בתוך HotXLS, וגם סמן ה־pull וגם קורא הקולבק מניעים את אותו סורק. TXLSXForwardRowBackend הוא מכונת המצבים ה־SAX היחידה של גיליונות עבור חלקי גיליון של ECMA-376 Part 1 §18.3, מחזיק את קורא ה־XML, את טבלת הנוסחאות המשותפות ואת מנתח הטקסט העשיר, ומתקדם בדיוק לגבול פיזי אחד של <row> בכל קריאה. TXLSBiffForwardParser מחזיק את הגלובלים, את בחירת הגיליון ואת התקדמות השורה עבור זרם הרשומות של [MS-XLS]; הפיכתו לניתן להשהיה הניבה את האילוץ החד ביותר בעיצוב כולו, כי נוסחת מחרוזת שמורה היא רשומת Formula שמיד אחריה רשומת String, ולכן נקודת השהיה לכל שורה חייבת לעולם לא ליפול בין השתיים. TXLSForwardTextBackend מחזיק קורא מודע־BOM, את המפריד הפעיל ורשומה לוגית אחת — CSV מרחרח פסיק, נקודה־פסיק, טאב או צינור מהרשומה הראשונה תוך התעלמות מתווים מצוטטים, ושדות מצוטטים מרובי שורות מחוברים עם #10 כך שמספר השורה עוקב אחר רשומות לוגיות ולא אחר ירידות שורה פיזיות. TXLSForwardOdsBackend מחזיק תבנית שורה פיזית אחת עבור טבלאות §9 של OpenDocument, מתייחס ל־table:number-rows-repeated כמניין נותר ולא כהרחבה, ומתקדם מעבר לתאים מכוסים בלי לפלוט ערכים. קורא ה־streaming הישיר חולק את אותו טוען מחרוזות משותפות וסגנון תאריך
למה שישה מצבים במקום דגל Eof אחד?
כי בוליאני יחיד הופך ארבעה מצבים שונים לבלתי ניתנים להבחנה, והקוראים מנחשים לא נכון בכולם. TXLSRowCursorState נותן להם שמות מפורשים
xrcsClosed— אף מקור אינו פתוחxrcsBeforeFirst— נפתח או כוון מחדש, עדיין לא נקראה שורהxrcsActive— עומד על שורה תקפהxrcsEof— הגיליון נצרך עד הסוףxrcsCancelled— הקורא עצר את המעבר במכווןxrcsFaulted— המעבר נכשל והחריגה המקורית נזרקה
ההבחנה האחרונה הזאת היא זו שחשובה בייצור. חלק גיליון חסר או תחילת מעבר שנכשלה שומרים על ה־EReadError שלהם ומעבירים את הסמן ל־xrcsFaulted; זה לעולם לא מודרג ל־False פשוט שקורא יפרש כ"הגיליון הזה היה ריק". Cancel מכווץ בכוונה יותר מ־Close: הוא סוגר את בקאנד הגיליון הנוכחי ואת תת־הזרם של הניפוח שלו ופוסל את השורה הנוכחית, אבל אינו משחרר את ארכיון ה־ZIP או את זרם המקור, וקריאה כפולה אינה עושה דבר. אחרי ביטול תחדשו בקריאה מפורשת ל־SelectSheet — הסמן לא יפעיל מעבר מחדש בשקט מטעמכם. בעלות על זרמים נוהגת לפי אותו כלל הגנתי: xsoBorrowed הוא ברירת המחדל ומשחזר את מיקום הזרם בסגירה, ו־xsoOwned מעביר בעלות רק אחרי ש־Open כבר הצליח, כך שפתיחה שנכשלה לעולם לא משחררת זרם שהקורא עדיין מחזיק
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: הסמן לעולם לא משחרר את Src, ו־Close משחזר את
// המיקום שהיה לזרם כשנקרא Open
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // סוגר רק את בקאנד הגיליון ואת תת־זרם
Break; // הניפוח שלו; אידמפוטנטי
end;
until not Cursor.FindNext;
case Cursor.State of
xrcsEof: Log('sheet consumed to the end');
xrcsCancelled: Log('stopped by the operator');
xrcsFaulted: Log('pass failed; the EReadError was already raised');
end;
finally
Cursor.Free;
end;
finally
Src.Free; // עדיין שלנו, עדיין תקף, המיקום משוחזר
end;
end;שאילת השורה הנוכחית בלי להעתיק אותה
IXLSRowCursorView מוסר שורה לשגרה אחרת בלי לשכפל את מערך התאים. התצוגה שומרת שומר משותף שמחזיק את מצביע הסמן ועוד מונה דור UInt64; התקדמות, בחירת גיליון, ביטול, סגירה והריסת הסמן מגדילים כולם את הדור הזה, וההריסה בנוסף מנקה את בעל השומר. כך תצוגה מיושנת אינה יכולה לקרוא זיכרון משוחרר: Valid הוא בדיקה נטולת חריגות שאפשר לקרוא לה בכל עת, בעוד כל חבר אחר מאמת תחילה וזורק EXLSRowCursorViewInvalidated. היו כנים לגבי מה שהחוזה הזה הוא — זה כשל מהיר של משך חיים, לא ערובה לבטיחות תהליכונים, והוא אינו מאשר קריאת שורה מתהליכון שני בזמן שהראשון מקדם את הסמן
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // שאילה; לא מועתק מערך תאים
for I := 0 to View.CellCount - 1 do
begin
Cell := View.Cells[I];
if Cell.HasFormula and not Cell.FormulaTextAvailable then
UseCachedResult(Cell.Value) // קריאות BIFF קדימה שומרות את
else if Cell.Kind = xdkEmpty then // התוצאה השמורה, לא את האסימונים
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank הם תאים אמיתיים
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// הממשק שורד את הלולאה, אבל השורה מאחוריו לא
if not View.Valid then // Valid לעולם לא זורק; Cells[] כעת היה זורק
View := nil; // EXLSRowCursorViewInvalidated
end;PeakRowBufferedBytes, ומה מותר לו להוכיח
PeakRowBufferedBytes קיים כדי להפגין שהזיכרון עוקב אחר רוחב שורה ולא אחר מניין שורות. הוא מצטבר את רשומות התאים, ה־Variantים, מחרוזות הנוסחאות ומטעני הטקסט העשיר של שורת הפלט הנוכחית ומקפל פנימה את קבוצת העבודה הייחודית לפורמט — הרשומה הלוגית של CSV, תבנית השורה הפיזית של ODS, שיא הרשומות של BIFF, או התא הגולמי של XLSX שמפוענח כרגע. קראו אותו יחד עם SheetPassesStarted, שמונה כמה מעברי גיליון באמת החלו. שתי אזהרות שומרות על כנותו: הספרה היא אומדן, לא חשבונאות ערימה מדויקת, והוא מונוטוני מאז ה־Open האחרון, ולכן הוא כלי ניפוי ורגרסיה ולא מד חי. לתמונה הרחבה יותר של היכן זמן ובתים הולכים בחוברות ענק, ראו ביצועי חוברת גדולה ב־Delphi
קורא ה־push הפך למתאם, ומה הסמן לא יעשה
TXLSForwardReader אינו נושא עוד נקודות כניסה נפרדות לסריקת XLSX, BIFF וטקסט. הוא מגדיר סמן, הולך בעקבותיו, ומתרגם את השורה הנוכחית לאירועי OnSheet ו־OnCell — ומכאן ששתי החזיתות כבר לא יכולות להתפצל על סינון, מצב נוסחאות או טיפול בשגיאות. שתי תוצאות שוות לדעת לפני השדרוג: SheetIndex של הקולבק הוא כעת באופן אחיד בבסיס 1 על TXLSForwardReader (TXLSDirectReader שומר על חוזה האירועים הקיים שלו בבסיס 0), ו־OnSheet נורה לפני SelectSheet, כך שקביעת SkipSheet פירושה שחלק הגיליון לעולם לא נפתח או נפרק כלל. הגבולות מפורשים באותה מידה: אין לשנות את החוברת בזמן שמעבר פעיל, ביטול דורש הפעלה מחדש מפורשת, ונתיב BIFF הקדימה לעולם לא מפרק אסימוני נוסחה, כך שתאי נוסחה קלאסיים מדווחים HasFormula true עם FormulaTextAvailable false ומוסרים לכם את התוצאה השמורה במקום להמציא מחרוזת נוסחה ריקה. סמן השורות והמתאם שלו עברו 1,298 בדיקות על Delphi Win32 ו־Win64 ועוד החבילה הסטטית של C++Builder 37.0 Win64
אם אתם שוקלים סמן pull מול הטוען שיש לכם עכשיו, השאלה שיש לשאול אינה מי מנתח מהר יותר אלא מי מאפשר לכם לכתוב את תנאי היציאה שאתם באמת צריכים. פרטי הרכיב המלאים, גרסאות ה־IDE הנתמכות והרישוי נמצאים בדף רכיב הגיליונות HotXLS ל־Delphi