مقاله فنی

دریافت سریع نام شیت‌ها در Delphi با GetSheetNames در HotXLS

گاهی تنها سؤالی که یک روتین دریافت (intake) باید پاسخ دهد، ساختاری است: آیا این ورک‌بوک شیتی به‌نام «Mapping» دارد، یا چند تب حمل می‌کند. پاسخ‌دادن به این سؤال با فراخوانی Open راه پرهزینه‌ای است. یک باز‌کردن کامل، جدول رشته‌های مشترک را متورم می‌کند، هر رکورد سبک را رمزگشایی می‌کند و سلول‌های هر worksheet را می‌پیماید، چون هیچ راهی ندارد بفهمد شما فقط فهرست مطالب را می‌خواستید. روی یک فایل بزرگ این یعنی صدها مگابایت تخصیص حافظه و چند ثانیه CPU صرف‌شده برای خواندن فهرستی که فقط چند کیلوبایت جا می‌گیرد. HotXLS، کتابخانهٔ بومی صفحه‌گستردهٔ Delphi از losLab، همان فهرست را جداگانه در اختیارتان می‌گذارد: GetSheetNames نام‌های worksheet را، به ترتیب ورک‌بوک، بدون این‌که حتی یک سلول را واقعی‌سازی کند، برمی‌گرداند

چرا خواندن فهرست ارزان است

هر دو فرمت صفحه‌گسترده فهرست مطالب خود را نزدیک ابتدای فایل می‌گذارند، و همین موضوع باعث می‌شود یک فراخوانی listing سریع باشد، نه این‌که هوشمندانه باشد. یک بستهٔ OOXML فهرست شیت‌ها را در xl/workbook.xml نگه می‌دارد، بخشی که چه ورک‌بوک ده ردیف داشته باشد چه ده میلیون ردیف، کوچک باقی می‌ماند. یک .xls از نوع BIFF8 رکوردهای BoundSheet خود را در ابتدای جریان globals ورک‌بوک، پیش از هر دادهٔ سلولی، ذخیره می‌کند. پس کاری که یک فراخوانی listing از آن پرهیز می‌کند، در برابر یک باز‌کردن کامل خطای گرد‌کردن نیست. بلکه بیشتر خود فایل است. خواندن فهرست، صرف‌نظر از تعداد ردیف، همان چند کیلوبایت هزینه دارد، در حالی که یک باز‌کردن کامل با داده مقیاس می‌گیرد، و روی یک ورک‌بوک چند مگابایتی این شکاف به چند مرتبه بزرگی هم در بایت‌های لمس‌شده و هم در حافظهٔ تخصیص‌یافته می‌رسد

GetSheetNames در HotXLS در Delphi تنها کاتالوگ برگه‌های فایل XLSX یا XLS را می‌خواند، درحالی‌که گشودن کامل از تک‌تک سلول‌ها می‌گذرد
کاتالوگ در workbook.xml یا رکوردهای BoundSheet می‌نشیند، پس فهرست‌کردن چند کیلوبایت خرج دارد در حالی که باز شدن کامل با داده مقیاس می‌یابد

همین هزینهٔ ثابت است که ارزش دارد طراحی حول آن انجام شود. یک دروازهٔ ورودی ساخته‌شده روی GetSheetNames روی یک فایل ۲۰۰ ردیفی و یک فایل ۲۰۰ مگابایتی یکسان رفتار می‌کند، پس دیگر کندترین فایل یک دسته، سرعت تصمیم‌گیری دربارهٔ ارزش پردازش یک فایل را تعیین نمی‌کند

یک فراخوانی برای .xls، .xlsx و فرمت‌های قالب

در facade مربوط به XLS، TXLSWorkbook.GetSheetNames بیش از .xls را می‌خواند. این متد فرمت‌های مبتنی بر zip یعنی .xlsx، .xlsm، .xltx و .xltm را هم می‌پذیرد و فقط workbook.xml را از آرشیو بیرون می‌کشد. برای ورودی واقعی .xls، رکوردهای BoundSheet را اسکن می‌کند و در اولین رکورد EOF زیرجریان globals متوقف می‌شود، پس یک فایل باینری بزرگ همچنان فقط به اندازهٔ کیلوبایت‌های آغازینش هزینه دارد. facade مربوط به XLSX یک تضمین دارد که برای کد سرویس طولانی‌مدت بیش از آنچه در نگاه اول به‌نظر می‌رسد اهمیت دارد: TXLSXWorkbook.GetSheetNames نمونهٔ workbook را نه ریست می‌کند و نه پر می‌کند، پس یک نمونه‌ای که از قبل یک سند باز را نگه داشته می‌تواند فایل‌های دیگر را بدون مزاحمت برای فایل در دست بررسی کند. GetODSSheetNames همین رویکرد را برای بسته‌های OpenDocument اعمال می‌کند، و هرکدام از این فراخوانی‌ها یک overload از نوع stream دارند که به شما اجازه می‌دهد یک آپلود را بدون این‌که هرگز روی دیسک بنشیند، بررسی کنید

var
  Book: TXLSXWorkbook;
  Names: TStringList;
  I: Integer;
begin
  Names := TStringList.Create;
  Book := TXLSXWorkbook.Create;
  try
    if Book.GetSheetNames('upload-7f3a.xlsx', Names) <= 0 then
      raise Exception.Create('unreadable workbook package');
    if Names.IndexOf('Mapping') < 0 then
      raise Exception.Create('required Mapping sheet is missing');
    for I := 0 to Names.Count - 1 do
      Writeln(Format('sheet %d: %s', [I, Names[I]]));
  finally
    Book.Free;
    Names.Free;
  end;
end;

همین فراخوانی برای یک دیالوگ import دسکتاپ هم مناسب است. شیت‌ها را فهرست کنید، بگذارید کاربر یکی را انتخاب کند، و فقط پس از انتخاب هزینهٔ باز‌کردن کامل را بپردازید. با یک ورک‌بوک پنجاه‌شیتی، تفاوت مشهود است: یک picker که بلافاصله ظاهر می‌شود، در برابر یکی که تا بارگذاری کل فایل در پس‌زمینه معطل می‌ماند

فایل‌های .xlsm دارای ماکرو و فرمت‌های قالب دقیقاً مانند یک .xlsx ساده فهرست می‌شوند، چون فهرست، صرف‌نظر از این‌که یک vbaProject.bin همراه بسته باشد یا نه، در همان workbook.xml قرار دارد. بنابراین یک خط‌لولهٔ intake می‌تواند شیت‌های یک ورک‌بوک ماکرودار را برای مسیریابی فهرست کند، بدون این‌که هرگز payload ماکرو را لمس کند یا کاری کند که آن را اجرا کند، و تصمیم دربارهٔ سیاست ماکرو را به مرحله‌ای واگذار کند که واقعاً فایل را باز می‌کند

خواندن مقدار بازگشتی بدون فریب خودتان

قراردادهای مقدار بازگشتی در سراسر HotXLS یکسان نیستند. برخی فراخوانی‌ها در صورت موفقیت ۱ برمی‌گردانند، برخی دیگر یک شمارش برمی‌گردانند، پس برای توابع listing تنها بررسی‌ای که همیشه درست است، در نظر‌گرفتن هر مقدار صفر یا کمتر به‌عنوان شکست است، در حالی که لیست رشته‌ها پاک می‌شود. وسوسهٔ خواندن یک لیست خالی به‌معنای «ورک‌بوکی بدون شیت» را کنار بگذارید. هم ECMA-376 و هم مشخصات BIFF8 حداقل یک شیت را در یک ورک‌بوک معتبر الزامی می‌کنند، پس صفر نام همیشه به‌معنای شکست خواندن است، هرگز به‌معنای این نیست که فایل به‌طور مشروع خالی است

یک listing ناموفق خودش یک سیگنال است که ارزش نگه‌داشتن دارد. یک فایل .xlsx که در این فراخوانی شکست می‌خورد، یکی از چند حالت مشخص است: بریده‌شده (truncated)، اصلاً یک بستهٔ OOXML واقعی نیست (خروجی‌های CSV با برچسب اشتباه از سیستم‌های دیگر مدام در همین‌جا سر و کله می‌زنند)، یا یک container رمزگذاری‌شده. تشخیص این حالت‌ها از هم کار بررسی بعدی است. ثبت‌کردن بایت‌های ابتدایی فایل ردشده در کنار خطا معمولاً یک رشتهٔ پشتیبانی را به یک پیام واحد تبدیل می‌کند

تشخیص container‌های رمزگذاری‌شده پیش از مسیریابی

یک .xlsx رمزگذاری‌شده یک zip نیست. یک فایل مرکب OLE است که جریان‌های EncryptionInfo و EncryptedPackage را در خود می‌پیچد، پس GetSheetNames نمی‌تواند داخل آن را ببیند و مانند هر فایل غیرقابل‌خواندن دیگری شکست برمی‌گرداند. CanReadEncrypted شکل این container را آزمون می‌کند، که به intake اجازه می‌دهد یک فایل رمزگذاری‌شده را عمداً مسیریابی کند، به‌جای این‌که یک خطای خواندن عمومی از جایی عمیق در یک worker را فرو ببرد:

جریان دسته‌بندی ورودی Delphi با CanReadEncrypted و GetSheetNames در HotXLS که بارگذاری‌ها را به نیازمند-گذرواژه یا ناخوانا یا عادی هدایت می‌کند
CanReadEncrypted اول اجرا می‌شود چون یک فایل OOXML رمزگذاری‌شده یک ظرف OLE است که فراخوانی‌های فهرست‌کردن نمی‌توانند داخلش را ببینند
type
  TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);

function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // OOXML رمزگذاری‌شده یک container نوع OLE است، نه zip: ابتدا بررسی کنید،
    // چون فراخوانی‌های فهرست‌کردن نمی‌توانند داخل آن را ببینند.
    if Book.CanReadEncrypted(FileName) then
      Exit(irNeedsPassword);
    if SameText(ExtractFileExt(FileName), '.ods') then
    begin
      if Book.GetODSSheetNames(FileName, Names) <= 0 then
        Exit(irUnreadable);
    end
    else if Book.GetSheetNames(FileName, Names) <= 0 then
      Exit(irUnreadable);
    Result := irNormal;
  finally
    Book.Free;
  end;
end;

رمزگذاری همان جایی است که HotXLS عمداً نامتقارن است، پس مسیریابی باید همین را رعایت کند. رمزگذاری قدیمی .xls (RC4، RC4 CryptoAPI، XOR) قابل‌خواندن است: TXLSWorkbook.Open(FileName, Password) با یک رمز عبور ذخیره‌شده رمزگشایی می‌کند، و آن فایل‌ها می‌توانند در مسیر خودکار باقی بمانند. بسته‌های OOXML رمزگذاری‌شده جهت دیگری دارند. HotXLS می‌تواند با SaveAsEncrypted یکی از آن‌ها را بنویسد، اما نمی‌تواند یکی را دوباره بخواند. OpenEncrypted وقتی یک بستهٔ رمزگذاری‌شده به آن داده می‌شود، EXlsxEncryptionNotImplemented را raise می‌کند، و به همین دلیل یک طراحی intake صادقانه، فایل‌های .xlsx رمزگذاری‌شده را نزد شخصی با اکسل می‌فرستد و .xls دارای رمز عبور را در کد نگه می‌دارد

برای کارهای دسته‌ای، این classifier جایگاه خودش را با اجرا روی کل یک پوشهٔ ورودی، پیش از این‌که هر worker پردازش واقعی را آغاز کند، به‌دست می‌آورد، چون هر probe حدود یک باز‌کردن فایل و چند کیلوبایت خواندن هزینه دارد. انجام‌دادن این کار در ابتدا، حالت شکستی را که واقعاً برای عملیات اهمیت دارد تغییر می‌دهد. به‌جای یک job ساعت سه بامداد که روی فایل ۴۱۲ از ۶۰۰ می‌میرد، ۴۱۲ فایل صف‌بندی‌شده و ۵ فایل ردشده در intake با یک دلیل ضمیمه‌شده به هرکدام خواهید داشت. همان فراخوانی‌های کتابخانه، اما داستان عملیاتی به‌مراتب بهتر

سؤال‌هایی که یک فراخوانی listing نمی‌تواند پاسخ دهد

نام‌ها و ترتیب، کل چیزی است که به‌دست می‌آورید. فراخوانی‌های listing هیچ چیزی دربارهٔ نمایانی (visibility) نمی‌گویند، پس شیت‌های hidden و very-hidden در لیست مثل بقیه ظاهر می‌شوند. این فراخوانی‌ها هیچ ابعاد used-range، هیچ شمارش سلول و هیچ property سندی گزارش نمی‌کنند. بخش docProps/core.xml هم کوچک است، اما امروز هیچ probe مخصوص property وجود ندارد، پس فراداده‌های نویسنده و عنوان همچنان به یک Open کامل نیاز دارند. راه تمیز برای زندگی با این محدودیت این است که بگذارید داده‌های ارزان همهٔ فایل‌ها را مسیریابی کنند و داده‌های پرهزینه را برای فایل‌هایی نگه دارید که از مسیریابی جان سالم به‌در می‌برند. برای فایل‌هایی که واقعاً به یک خواندن عمیق می‌رسند، یک اسکن فقط‌خواندنی روی یک .xls بزرگ با _DisableGraphics := True، که parse کردن OfficeArt را رد می‌کند، محسوسا سریع‌تر اجرا می‌شود. فقط هرگز از آن نمونه ذخیره نکنید: لایهٔ drawing‌ای که رد کرده از مدل حذف شده، و ذخیره‌کردن آن را از فایل هم حذف می‌کند

فایل‌هایی که غربالگری را پشت سر می‌گذارند معمولاً به سمت تحلیل عمیق‌تر می‌روند. میزکار ممیزی و تبدیل ورک‌بوک شمارنده‌های هر شیت را پوشش می‌دهد که ارزش جمع‌آوری دارند وقتی یک باز‌کردن کامل توجیه‌پذیر است، و راهنمای کارایی ورک‌بوک‌های بزرگ نحوهٔ سریع نگه‌داشتن همان باز‌کردن کامل را پوشش می‌دهد

HotXLS یک کتابخانهٔ بومی صفحه‌گستردهٔ Object Pascal برای Delphi و C++Builder است؛ کل سطح API، شامل فراخوانی‌های بازرسی نشان‌داده‌شده در این‌جا، در صفحهٔ محصول HotXLS Delphi Component مستند شده است