מאמר טכני

סמן שורות pull עבור XLS, XLSX, ODS ו־CSV ב־Delphi

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 הישיר חולק את אותו טוען מחרוזות משותפות וסגנון תאריך

סמן שורות ה־pull של HotXLS מנתב לסורק קדימה אחד לכל פורמט: בקאנד SAX עבור XLSX, מנתח רשומות עבור BIFF, בקאנד טקסט מרחרח מפרידים ותבנית שורה של ODS, כשקורא הקולבק מוגדר מעל כמתאם
לכל פורמט סורק קדימה אחד בדיוק, וגם סמן ה־pull וגם קורא הקולבק מניעים את אותו סורק, כך שסמנטיקת הסינון והשגיאות אינה יכולה להתפצל

למה שישה מצבים במקום דגל Eof אחד?

כי בוליאני יחיד הופך ארבעה מצבים שונים לבלתי ניתנים להבחנה, והקוראים מנחשים לא נכון בכולם. TXLSRowCursorState נותן להם שמות מפורשים

  • xrcsClosed — אף מקור אינו פתוח
  • xrcsBeforeFirst — נפתח או כוון מחדש, עדיין לא נקראה שורה
  • xrcsActive — עומד על שורה תקפה
  • xrcsEof — הגיליון נצרך עד הסוף
  • xrcsCancelled — הקורא עצר את המעבר במכוון
  • xrcsFaulted — המעבר נכשל והחריגה המקורית נזרקה

ההבחנה האחרונה הזאת היא זו שחשובה בייצור. חלק גיליון חסר או תחילת מעבר שנכשלה שומרים על ה־EReadError שלהם ומעבירים את הסמן ל־xrcsFaulted; זה לעולם לא מודרג ל־False פשוט שקורא יפרש כ"הגיליון הזה היה ריק". Cancel מכווץ בכוונה יותר מ־Close: הוא סוגר את בקאנד הגיליון הנוכחי ואת תת־הזרם של הניפוח שלו ופוסל את השורה הנוכחית, אבל אינו משחרר את ארכיון ה־ZIP או את זרם המקור, וקריאה כפולה אינה עושה דבר. אחרי ביטול תחדשו בקריאה מפורשת ל־SelectSheet — הסמן לא יפעיל מעבר מחדש בשקט מטעמכם. בעלות על זרמים נוהגת לפי אותו כלל הגנתי: xsoBorrowed הוא ברירת המחדל ומשחזר את מיקום הזרם בסגירה, ו־xsoOwned מעביר בעלות רק אחרי ש־Open כבר הצליח, כך שפתיחה שנכשלה לעולם לא משחררת זרם שהקורא עדיין מחזיק

ששת המצבים של סמן השורות של HotXLS והמעברים ביניהם, מראים כיצד Cancel מעביר מעבר פעיל ל־cancelled, תחילת מעבר שנכשלה מעבירה אותו ל־faulted, וכיצד שניהם נשארים מובחנים מסוף גיליון
שישה מצבים בעלי שם שומרים גיליון ריק, עצירה מכוונת ומעבר שנכשל ניתנים להבחנה — מה שבוליאני Eof יחיד אינו יכול
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

השוואה של HotXLS המציגה טעינת גיליון מלאה שמשאירה כל שורה תושבת מול סמן ה־pull שמחזיק רק את השורה הנוכחית ועוד קבוצת עבודה אחת לפורמט — מה ש־PeakRowBufferedBytes מצטבר ומדווח
PeakRowBufferedBytes מצטבר את שורת הפלט הנוכחית בתוספת קבוצת העבודה הייחודית לפורמט, כך שהזיכרון עוקב אחר רוחב שורה ולא אחר מניין השורות בגיליון

קורא ה־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