مقاله فنی

کرسر سطری pull برای XLS و XLSX و ODS و CSV در Delphi

HotXLS منابع .xls و .xlsx و .xlsm و .ods و CSV و TSV را از طریق یک کرسر سطری pull تکی می‌خواند یعنی TXLSRowCursor، که FindFirst و FindNext آن هر بار یک سطر منطقی جلو می‌روند در حالی که فقط همان سطر در حافظه می‌ماند. یک ماشین حالت شش‌مقداری before-first را از EOF و cancelled و faulted جدا می‌کند، و callback reader قدیمی‌تر حالا یک آداپتور روی همان کرسر است

سناریو برای هر کسی که یک قابلیت import منتشر کرده آشناست. یک .xlsx 200 مگابایتی می‌رسد، شما یک هندلر OnCell وصل می‌کنید، و اولین نیاز بعد از «بخوانش» این است «بعد از صد posting برگشتی اول متوقف شو». حالا شکل کدتان با شما می‌جنگد: حلقه داخل کتابخانه زندگی می‌کند، هندلرتان باید یک فلگ بلند کند، هر callback بعدی همچنان شلیک می‌شود تا وقتی پارسر متوجه شود، و state انباشته — تا این لحظه چند hit، کدام ستون match شد، بعد چه کار — باید در فیلدهای کلاسی زندگی کند که فقط برای این وجود دارد که به callback جایی برای نشستن بدهد. هیچ بخشی از این یک مشکل تجزیه نیست. یک مشکل control-flow است، و همان چیزی است که یک کرسر pull حذف می‌کند

یک push callback واقعاً در 200 مگابایت چه هزینه‌ای دارد

Push کنترل را وارونه می‌کند، و وارونگی دقیقاً همان چیزی است که یک فراخوان فیلترکننده یا joinکننده نمی‌تواند تحملش کند. با یک API مبتنی بر callback کتابخانه صاحب حلقه است، پس فراخوان نمی‌تواند از Break استفاده کند، نمی‌تواند دو منبع را interleaves کند، نمی‌تواند reader را به روتینی بسپارد که انتظار رانده شدن دارد، و نمی‌تواند «نگاه کردن به سطر بعدی قبل از تصمیم» را بدون بافرینگ بیان کند. هزینه throughput نیست — یک مسیر callback با SAX خوب‌نوشته به‌خوبی استریم می‌کند — این است که هر مصرف‌کننده غیر بدیهی یک ماشین حالت کوچک خودش می‌رویاند تا حلقه‌ای را شبیه‌سازی کند که اجازه نوشتنش را نداشت. آن را در چهار قالب فایلی ضرب کنید که هر کدام به‌طور تاریخی مدخل اسکن خودش را داشته، و معناشناسی فیلتر و فرمول و خطا شروع می‌کنند بین آنها واگرا شدن، که دقیقاً همان واگرایی است که HotXLS قصد بستنش را داشت

یک کرسر pull کد فراخوانی شما را چطور عوض می‌کند؟

حلقه را به شما برمی‌گرداند، و با آن control flow معمولی پاسکال را. TXLSRowCursor.Open یک نام فایل یا یک TStream می‌پذیرد، قالب را تشخیص می‌دهد، shared stringها و متادیتای date-style را یک بار لود می‌کند، و sheet 1 را انتخاب می‌کند. SelectSheet (یک-مبنا) یا SelectSheetByName به worksheet دیگری دوباره هدف می‌گیرد و کرسر را به before-first ریست می‌کند. FindFirst و FindNext بعد روی سطر پرشده بعدی موقعیت می‌گیرند — سطرهای بدون سلول قابل دیکد رد می‌شوند، پس RowIndex می‌تواند بپرد — و سطر جاری به‌شکل CellCount و Cells[] و ValueByCol[] در معرض است، همه یک-مبنا روی محور ستون. ترک حلقه یک 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 معمولی؛ نه فلگ abort، نه سنتینل
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // دیستراکتر pass را تمام می‌کند
  end;
end;

Projection و بازه قبل از pass ست می‌شوند، نه اینکه بعداً فیلتر شوند. FirstRow و LastRow و IncludeColumn و ClearColumnProjection و IncludeFormulaText و DetectDates و DetectTextTypes همه داخل بک‌اندها رعایت می‌شوند، پس یک ستون انتخاب‌نشده اصلاً هرگز مقدارش یا رشته فرمول یا payload rich-text را تخصیص نمی‌دهد — سوئیت regression این را با فرمول‌های 16 کیلوبایتی و رشته‌های کش‌شده‌ای ثابت می‌کند که وقتی ستونشان projection نشده هرگز مادی نمی‌شوند. آن آپشن‌ها در حین فعال بودن pass عمداً منجمدند و در EOF یا روی SelectSheet یا بعد از Close دوباره قابل نوشتن می‌شوند، پس یک اسکن هرگز نمی‌تواند دو قرارداد دیکد را قاطی کند. اگر فقط inventory شیت را نیاز دارید نه سطرها را، لود فقط‌متادیتا و انتخابی شیت‌ها مدخل ارزان‌تر است

یک بک‌اند به ازای هر قالب، یک حلقه اسکن برای هر کدام

هر قالب دقیقاً یک اسکنر رو به جلو داخل HotXLS دارد، و هم کرسر pull و هم callback reader همان اسکنر را می‌رانند. TXLSXForwardRowBackend تنها ماشین حالت SAX worksheet برای بخش‌های شیت ECMA-376 Part 1 §18.3 است، که خواننده XML و جدول فرمول اشتراکی و پارسر rich-text را نگه می‌دارد، و هر بار دقیقاً تا یک مرز فیزیکی <row> جلو می‌رود. TXLSBiffForwardParser صاحب globals و انتخاب شیت و پیشروی سطر برای استریم رکورد [MS-XLS] است؛ pausable کردنش تیزترین قید کل طراحی را تولید کرد، چون یک فرمول رشته‌ای کش‌شده یک رکورد Formula است که بلافاصله رکورد String دنبالش می‌آید، پس نقطه تعلیق به ازای هر سطر هرگز نباید بین این دو فرود بیاید. TXLSForwardTextBackend یک خواننده BOM-آگاه و delimiter فعال و یک رکورد منطقی نگه می‌دارد — CSV از اولین رکورد کاما و سمی‌کالن و تب یا پایپ را می‌بوید در حالی که کاراکترهای داخل کوتیشن را نادیده می‌گیرد، و فیلدهای کوتیشن‌دار چندخطی با #10 به هم می‌پیوندند تا شماره سطر رکوردهای منطقی را دنبال کند نه newlineهای فیزیکی را. TXLSForwardOdsBackend یک قالب سطر فیزیکی تکی برای جدول‌های OpenDocument §9 نگه می‌دارد، table:number-rows-repeated را به‌شکل شمارنده باقی‌مانده می‌بیند نه یک expansion، و از سلول‌های پوشش‌داده‌شده بدون انتشار مقدار جلو می‌رود. streaming direct reader همان لودر shared-string و date-style را به اشتراک می‌گذارد

کرسر سطری pull در HotXLS که به یک اسکنر رو به جلو به ازای هر قالب dispatch می‌کند، یک بک‌اند SAX برای XLSX، یک پارسر رکورد برای BIFF، یک بک‌اند متنی بو-کننده delimiter و یک قالب سطری ODS، و callback reader روی آن به‌شکل آداپتور پیکربندی شده
هر قالب دقیقاً یک اسکنر رو به جلو دارد، و هم کرسر pull و هم callback reader همان اسکنر را می‌رانند، پس معناشناسی فیلتر و خطا نمی‌توانند واگرا شوند

چرا شش state به‌جای یک فلگ Eof؟

چون یک boolean تکی چهار وضعیت متفاوت را غیرقابل تمایز می‌کند، و فراخوان‌ها درباره همه‌شان بد حدس می‌زنند. TXLSRowCursorState آنها را صریحاً نام می‌برد

  • xrcsClosed — هیچ منبعی باز نیست
  • xrcsBeforeFirst — باز شده یا دوباره هدف‌گیری شده، هنوز سطری خوانده نشده
  • xrcsActive — ایستاده روی یک سطر معتبر
  • xrcsEof — شیت تا انتها مصرف شد
  • xrcsCancelled — فراخوان pass را عمداً متوقف کرد
  • xrcsFaulted — pass شکست خورد و استثنای اصلی پرتاب شد

همان تمایز آخر است که در پروداکشن اهمیت دارد. یک بخش worksheet غایب یا شروع ناموفق pass EReadError خودش را نگه می‌دارد و کرسر را به xrcsFaulted می‌برد؛ هرگز به یک False ساده تنزل نمی‌یابد که فراخوان آن را «این شیت خالی بود» می‌خواند. Cancel عمداً از Close باریک‌تر است: بک‌اند worksheet جاری و inflate substream آن را می‌بندد و سطر جاری را نامعتبر می‌کند، اما آرشیو ZIP یا استریم مبدأ را آزاد نمی‌کند، و صدا زدنش دو بار یک no-op است. بعد از cancel با صدا زدن صریح SelectSheet ادامه می‌دهید — کرسر به نیابت از شما pass را بی‌سروصدا ری‌استارت نمی‌کند. مالکیت استریم همان قاعده تدافعی را دنبال می‌کند: xsoBorrowed پیش‌فرض است و موقعیت استریم را هنگام بستن برمی‌گرداند، xsoOwned مالکیت را فقط بعد از موفقیت Open منتقل می‌کند، پس یک open ناموفق هرگز استریمی را که فراخوان هنوز نگه می‌دارد آزاد نمی‌کند

شش state کرسر سطری HotXLS با گذارهای بین آنها، که نشان می‌دهد Cancel یک pass فعال را به cancelled می‌برد، یک شروع ناموفق pass آن را به faulted می‌برد، و هر دو چطور از پایان شیت متمایز می‌مانند
شش state نام‌دار شیت خالی و توقف عمدی و pass ناموفق را قابل تمایز نگه می‌دارند، کاری که یک boolean تکی 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;   // فقط بک‌اند worksheet و
            Break;           // inflate substream آن را می‌بندد؛ idempotent
          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 یک سطر را بدون تکرار آرایه سلول به روتین دیگری می‌سپارد. view یک گارد مشترک نگه می‌دارد که اشاره‌گر کرسر به‌علاوه یک شمارنده نسل UInt64 را حمل می‌کند؛ پیشروی، انتخاب شیت، cancel، بستن و نابود کردن کرسر همه آن نسل را افزایش می‌دهند، و نابودی به‌علاوه صاحب گارد را پاک می‌کند. پس یک view کهنه نمی‌تواند حافظه آزادشده را بخواند: Valid یک پروب بدون استثناست که هر لحظه می‌توانید صدا بزنید، در حالی که هر عضو دیگر اول اعتبارسنجی می‌کند و EXLSRowCursorViewInvalidated پرتاب می‌کند. صادق باشید درباره اینکه این قرارداد چیست — lifetime fail-fast است، نه تضمین thread-safety، و مجوز خواندن یک سطر از thread دوم در حالی که thread اول کرسر را جلو می‌برد را نمی‌دهد

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 forward نتیجه
        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ها و رشته‌های فرمول و payloadهای rich-text سطر خروجی جاری را انباشته می‌کند و working set مخصوص هر قالب را هم در آن می‌پیچد — رکورد منطقی CSV، قالب سطر فیزیکی ODS، پیک رکورد BIFF، یا سلول خام XLSX که الان دیکد می‌شود. آن را همراه SheetPassesStarted بخوانید که می‌شمارد چند pass worksheet واقعاً شروع شد. دو هشدار این را صادق نگه می‌دارند: عدد یک برآورد است نه حساب دقیق heap، و از آخرین Open به بعد یکنواست، پس یک ابزار دیباگ و regression است نه یک گیج زنده. برای تصویر وسیع‌تر از اینکه زمان و بایت‌ها روی کتاب‌های بسیار بزرگ کجا می‌روند، کارایی کتاب‌کار بزرگ در Delphi را ببینید

مقایسه‌ای از HotXLS که نشان می‌دهد لود کل شیت هر سطر را مقیم نگه می‌دارد در مقابل کرسر pull که فقط سطر جاری به‌علاوه یک working set قالبی را نگه می‌دارد، همان چیزی که PeakRowBufferedBytes انباشته و گزارش می‌کند
PeakRowBufferedBytes سطر خروجی جاری به‌علاوه working set مخصوص قالب را انباشته می‌کند، پس حافظه عرض یک سطر را دنبال می‌کند نه تعداد سطرهای شیت را

reader push آداپتور شد، و آنچه کرسر انجام نمی‌دهد

TXLSForwardReader دیگر مدخل‌های اسکن جداگانه XLSX و BIFF و متنی حمل نمی‌کند. یک کرسر پیکربندی می‌کند، آن را می‌پیماید، و سطر جاری را به رویدادهای OnSheet و OnCell ترجمه می‌کند، و دلیلش این است که دو نما دیگر نمی‌توانند روی فیلتر و state فرمول و مدیریت خطا واگرا شوند. دو نتیجه قبل از ارتقا ارزش دانستن دارند: SheetIndex در callback حالا روی TXLSForwardReader یکنواخت یک-مبنا است (TXLSDirectReader قرارداد رویداد صفر-مبنای موجودش را نگه می‌دارد)، و OnSheet قبل از SelectSheet شلیک می‌شود، پس ست کردن SkipSheet یعنی بخش worksheet اصلاً هرگز باز یا از فشرده خارج نمی‌شود. مرزها به همان اندازه صریح‌اند: کتاب‌کار در حین فعال بودن pass نباید تغییر کند، cancel کردن ری‌استارت صریح می‌خواهد، و مسیر BIFF forward هرگز توکن‌های فرمول را دی‌کامپایل نمی‌کند، پس سلول‌های فرمول کلاسیک HasFormula را true گزارش می‌کنند با FormulaTextAvailable برابر false و نتیجه کش‌شده را به شما می‌دهند به‌جای اختراع یک رشته فرمول خالی. کرسر سطری و آداپتورش 1,298 چک را روی Delphi Win32 و Win64 به‌علاوه پکیج استاتیک C++Builder 37.0 Win64 پاس کردند

اگر در سنجش یک کرسر pull در برابر لودری که الان دارید هستید، پرسشی که باید بپرسید این نیست که کدام سریع‌تر تجزیه می‌کند بلکه این است که کدام اجازه می‌دهد شرط خروجی را که واقعاً نیاز دارید بنویسید. جزئیات کامل کامپوننت و نسخه‌های IDE پشتیبانی‌شده و لایسنس روی صفحه کامپوننت spreadsheet دلفی HotXLS است