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 را به اشتراک میگذارد
چرا شش 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 ناموفق هرگز استریمی را که فراخوان هنوز نگه میدارد آزاد نمیکند
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 را ببینید
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 است