مقاله فنی

خروجی گرفتن از ورک‌بوک‌های اکسل به CSV، TSV، HTML و RTF در Delphi با HotXLS

یک کار شبانه را تصور کنید که یک کتابچهٔ کاریِ فاکتور را در کد می‌سازد و آن را به‌صورت CSV برای وارد کردن توسط یک سیستم پایین‌دستی می‌نویسد. اعداد در Excel درست به نظر می‌رسند. CSV هم در یک ویرایشگر متنی تمیز باز می‌شود. سپس importer روی ستون جمع‌ها خفه می‌شود، چون فیلد مبلغ برای سطر 42 =SUM(D2:D41) می‌خواند، یعنی فرمول به‌صورت متن لفظی، نه رقمی که باید به آن محاسبه شود. هیچ‌چیز خراب نیست. این یک رفتار مستندشده است، و اولین چیزی است که باید دربارهٔ خروجی گرفتن از HotXLS فهمید: نویسنده مدل سلولی را دقیقاً همان‌طور که هست سریالایز می‌کند، و یک سلول فرمولی که مقدارش هرگز محاسبه نشده، فقط متن فرمول خود را دارد که تحویل دهد

چرا CSV شما به‌جای اعداد فرمول دارد

HotXLS متن فرمول و مقدار محاسبه‌شده را به‌عنوان دو چیز جداگانه ذخیره می‌کند. SaveAsCSV به‌طور عمدی موتور محاسبه را در مسیر خروج اجرا نمی‌کند: یک خروجی‌گیری نباید کتابچهٔ کاری را جهش دهد، و نباید ریسک گیر افتادن روی یک زنجیرهٔ فرمول پاتولوژیک را بپذیرد. فایل‌هایی که خودِ Excel ذخیره کرده نتایج کش‌شده را کنار فرمول‌ها حمل می‌کنند، پس خروجی‌گیری مجدد از آن‌ها همان‌طور که انتظار دارید رفتار می‌کند. این تله مختص کتابچه‌های کاری‌ای است که کد خودتان تولید کرده، جایی‌که فرمول‌ها نوشته شده‌اند اما هرگز ارزیابی نشده‌اند. راه‌حل این است که پیش از خروجی گرفتن، مقدارها را موجود کنید، با استفاده از همان موتور Calculate که ارجاع‌های میان‌شیتی و توابع سفارشی را حل می‌کند:

دیاگرام سلول کتاب‌کار HotXLS در Delphi که تا Book.Calculate مقدار را حساب کند تنها متن فرمول نگه می‌دارد، تا برون‌بری CSV به‌جای متن =SUM عدد صادر کند
SaveAsCSV مدل سلول را همان‌طور که هست سریال می‌کند — بدون Calculate فیلد مقدار متن literal فرمول را حمل می‌کند و ایمپورت‌کننده آن را رد می‌کند
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('invoice-run.xlsx');
    Sheet := Book.Sheets[0];

    // نتایج فرمول را واقعیت ببخشید تا CSV اعداد را حمل کند، نه متن '=...'
    for R := 2 to 41 do
      if Sheet.Cells[R, 4].Formula <> '' then
        Sheet.Cells[R, 4].Value := Book.Calculate(Sheet.Cells[R, 4].Formula);

    Book.SaveAsCSV('feed.csv', 0, ',');    // برگه 0، کاما
    Book.SaveAsCSV('feed.tsv', 0, #9);     // همان برگه به‌صورت TSV
  finally
    Book.Free;
  end;
end;

به این توجه کنید که این حلقه واقعاً چه کاری می‌کند: سلول‌های فرمولی را با مقدارهای محاسبه‌شده‌شان بازنویسی می‌کند. این برای یک گذر خروجی‌گیریِ یک‌بارمصرف کاملاً درست است و اگر قصد دارید بعداً کتابچهٔ کاری را دوباره به‌صورت .xlsx ذخیره کنید غلط است، چون تازه فرمول‌های زنده را با اعداد منجمد جایگزین کرده‌اید. از یک کپی خروجی بگیرید، یا بازنویسی را محدود کنید که فقط اجرای خروجی‌گیری را لمس کند. موتور پشت Calculate فراتر از این هم می‌رود، از جمله ثبت توابع سفارشی خودتان، که موضوع موتور فرمول HotXLS و توابع سفارشی است

نویسندهٔ delimited چه چیزی را تضمین می‌کند

مسیر CSV یک UTF-8 با byte order mark، پایان‌سطرهای CRLF و نقل‌قول‌گذاری RFC 4180 تولید می‌کند. هر فیلدی که شامل جداکننده، نقل‌قول یا شکست سطر باشد پیچیده می‌شود، و نقل‌قول‌های تعبیه‌شده دوتایی می‌شوند. تاریخ‌ها صرف‌نظر از فرمت نمایش سلول به‌صورت yyyy-mm-dd hh:nn:ss رندر می‌شوند. این تصمیم درستی برای یک مصرف‌کنندهٔ ماشینی است، هرچند هرکسی که انتظار داشته فرمت‌دهی روی‌صفحه منتقل شود را غافلگیر می‌کند. سلول‌های rich text با به‌هم‌چسباندن run‌هایشان مسطح می‌شوند

دیاگرام نویسندهٔ یگانهٔ جداشدهٔ HotXLS در Delphi که CSV را با کاما و TSV را با #9 تولید می‌کند؛ هر دو خروجی BOM با UTF-8 و پایان‌های CRLF و نقل‌قول مطابق RFC 4180 دارند
CSV و TSV از یک نویسنده می‌آیند، پس BOM با UTF-8 و پایان‌های CRLF و نقل‌قول RFC 4180 بدون تغییر برای هر دو اعمال می‌شود

این پیش‌فرض‌ها بیشتر بحث‌ها با یک importer را پیش از شروع حل می‌کنند، اما دوتای آن‌ها به‌هرحال باید در قرارداد رابط شما ذکر شوند. اولی BOM است. همان چیزی است که به Excel اجازه می‌دهد فایل را با کاراکترهای accent‌دار سالم باز کند، با این‌حال تعدادی از parserهای سخت‌گیر آن سه بایت را به‌عنوان داده در نظر می‌گیرند؛ اگر مال شما یکی از آن‌هاست، آن‌ها را در تحویل حذف کنید. دومی TSV است. این اصلاً یک ویژگی جداگانه نیست، فقط همان نویسنده است که با #9 به‌عنوان جداکننده فراخوانی شده، پس همهٔ چیزهای بالا بدون تغییر روی آن اعمال می‌شود. شیتی که خروجی گرفته می‌شود در اُورلود چند-آرگومانی با ایندکس 0-پایه انتخاب می‌شود، در حالی که میان‌بر تک-آرگومانیِ SaveAsCSV(FileName) شیت فعال را می‌گیرد

خروجی HTML یک اسنپ‌شات است، نه یک قالب تبادلی

جایی‌که CSV هر چیزی به‌جز مقدارها را دور می‌ریزد، SaveAsHTML تلاش می‌کند ظاهر را نگه دارد: یک <table> برای هر شیت، نواحی ادغام‌شده به‌صورت colspan و rowspan بیان می‌شوند، استایل‌دهی پایهٔ سلولی به‌صورت CSS اینلاین می‌شود. رنگ‌های وابسته به تم به‌جای حل‌شدن، نادیده گرفته می‌شوند، پس قالبی که به اسلات‌های تم تکیه می‌کند ساده‌تر از چیزی که در Excel به نظر می‌رسد بیرون می‌آید. روی هرچیزی که باید این سفر را زنده بماند رنگ‌های RGB صریح تنظیم کنید. شیء options پاکت را کنترل می‌کند:

var
  Opts: TXLSXHtmlExportOptions;
begin
  Opts := TXLSXHtmlExportOptions.Create;
  try
    Opts.Title := 'Weekly settlement';
    Opts.TableClass := 'report-grid';     // نقطه اتصال برای stylesheet صفحه میزبان
    Opts.WriteDocument := True;           // صفحه کامل، نه یک قطعه
    if Book.SaveAsHTML('settlement.html', 0, Opts) <> 0 then
      raise Exception.Create('Sheet index out of range');
  finally
    Opts.Free;
  end;
end;

دو جزئیات در آن قطعه‌کد ارزش توجه دارند. WriteDocument را به False برگردانید و خروجی به‌جای یک صفحهٔ کامل، یک قطعهٔ جدول خام می‌شود، که همان چیزی است که وقتی یک پیش‌نمایش را داخل یک چیدمان موجود تزریق می‌کنید می‌خواهید: TableClass را تنظیم کنید و بگذارید stylesheet میزبان تم‌دهی را انجام دهد. قرارداد بازگشتی هم برعکس بیشتر فراخوانی‌های HotXLS است. SaveAsHTML در موفقیت 0 و برای یک ایندکس شیت نامعتبر -1 برمی‌گرداند، پس یک بررسی عادت‌محورِ = 1 هر خروجی‌گیری موفق را به‌عنوان شکست گزارش می‌کند. وقتی به یک ناحیه به‌جای کل شیت نیاز دارید، شاید برای ایمیل یا جاسازی یک بلوک تکی، TXLSXRange.SaveAsHTML هر محدودهٔ مستطیلی را تحت همان قواعد رندرینگ خروجی می‌دهد

خروجی RTF و جایی‌که هنوز جای خودش را دارد

هدف چهارم جدول‌های RTF 1.6 را می‌نویسد، یک شیت در هر فراخوانی از طریق SaveAsRTF. عرض ستون‌ها تقریباً 96 twip به‌ازای هر کاراکتر از عرض ستون تخمین زده می‌شود. محدودیت ساختاری‌ای که باید بدانید این است که سلول‌های ادغام‌شده در خروجی span نمی‌شوند: فقط سلول لنگر محتوایش را حمل می‌کند، و سلول‌های پوشش‌داده‌شده به‌صورت خالی صادر می‌شوند. این RTF را برای قالب‌های سنگین از نظر چیدمان کنار می‌گذارد. با این‌حال جایگاه خودش را به‌عنوان مسیر کم‌مقاومت‌ترین برای انداختن نتایج جدولی داخل یک واژه‌پرداز یا یک سیستم مدیریت اسناد قدیمی که پیش از پذیرش HTML بوده حفظ می‌کند

رفت‌وبرگشت: وارد کردن CSV به‌طور طراحی‌شده مخرب است

خواندن دوبارهٔ CSV قرارداد خودش را دارد. OpenCSV کل کتابچهٔ کاری را پاک می‌کند و آن را به‌عنوان یک شیت واحد به نام Sheet1 بازمی‌سازد. این از نظر روحیه یک سازنده (constructor) است، نه یک ادغام، پس هرگز آن را روی یک کتابچهٔ کاری که هنوز محتوای ذخیره‌نشده دارد فراخوانی نکنید. دادن #0 به‌عنوان جداکننده تشخیص خودکار جداکننده را فعال می‌کند. فلگ ADetectTypes ترفیع نوع را کنترل می‌کند: وقتی روشن باشد، رشته‌های عددی به عدد تبدیل می‌شوند، رشته‌های ISO-8601 به تاریخ، و true/false به بولین. آن را وقتی feed شامل شناسه‌هایی با صفرهای پیشرو، کدپستی‌ها یا کدهای محصول است خاموش کنید، که همگی ترفیع بی‌سروصدا آن‌ها را به عدد تبدیل می‌کند (یک صفر پیشرو دقیقاً همان لحظه‌ای که 00123 به 123 تبدیل می‌شود از بین می‌رود). هر دو نما همین import را در معرض قرار می‌دهند. آن را با فراخوانی‌های خروجی‌گیریِ بالا جفت کنید و یک پل قالبی خواهید داشت که به هیچ نصب Excel‌ای در هیچ‌جای pipeline نیاز ندارد، سناریویی که در تولید گزارش پایگاه‌داده-به-اکسل با HotXLS پوشش داده شده

خروجی گرفتن مستقیم داخل یک stream

هر نویسنده‌ای اینجا یک اُورلود streamی دارد که کنار نسخهٔ نام-فایلی نشسته: CSV، HTML، RTF، و خودِ قالب‌های کتابچهٔ کاری. در کد سمت سرور، این اُورلودها همان‌هایی هستند که باید به سراغشان رفت. یک endpoint وب که یک دانلود CSV را سرو می‌کند می‌تواند داخل یک TMemoryStream بنویسد و آن را مستقیم به شیء response بدهد، بدون هیچ فایل موقتی، بدون کار پاک‌سازی، و بدون برخورد میان دو درخواست که اتفاقاً همان نام تولیدشده را انتخاب کرده‌اند. همین موضوع برای هل دادن خروجی‌ها داخل blob storage یا پیوست کردن آن‌ها به ایمیل خروجی هم برقرار است. سیستم فایل کاملاً از تصویر بیرون می‌افتد

این الگو با نحوهٔ استقرار کتابخانه ترکیب می‌شود. هر دو نما خواننده و نویسندهٔ نیتیو Object Pascal هستند، پس هیچ نصب Excel‌ای، هیچ اتوماسیون COM‌ای، و هیچ گلوگاه هر-فرآیندی که درخواست‌ها را روی سرور سریالایز کند وجود ندارد. هر درخواست می‌تواند شیء کتابچهٔ کاری خودش را داشته باشد، بازنویسیِ محاسبه از بخش اول را اجرا کند، و خروجی‌اش را موازی با همسایه‌هایش استریم کند. حافظه تنها منبعی است که باید مراقبش بود. مدل کتابچهٔ کاری برای مدت خروجی‌گیری در RAM زندگی می‌کند، پس سرویسی که فایل‌های بسیار بزرگ را فقط برای صدور دوبارهٔ آن‌ها به‌صورت CSV باز می‌کند باید کارهای هم‌زمان را سقف بگذارد، یا آن‌هایی که بیش‌ازحد بزرگ‌اند را در صف بگذارد، به‌جای این‌که بگذارد یک جهش ترافیک working set را تعیین کند

یک دکمهٔ کوچک‌تر: IncludeBOM را روی گزینه‌های HTML تنظیم کنید وقتی قطعه به‌صورت یک فایل مستقل ذخیره می‌شود که یک ابزار پایین‌دستی برای تشخیص رمزگذاری آن را می‌بوید. وقتی HTML را مستقیم روی HTTP سرو می‌کنید، در عوض اعلان charset را به هدرهای response بسپارید

وقتی بایت‌ها هنوز اشتباه بیرون می‌آیند

رایج‌ترین سؤال پشتیبانی دربارهٔ خروجی CSV همان مشکل ابتدای مقاله است با یک لباس متفاوت: Excel به‌جای کاراکترهای accent‌دار mojibake نشان می‌دهد. غریزه این است که نویسنده را مقصر بدانید، اما دقیقاً به همین دلیل یک UTF-8 BOM صادر می‌کند، و فایل تقریباً همیشه وقتی از کد شما خارج می‌شود درست است. چیزی میان آنجا و Excel، BOM را خورده. یک انتقال FTP در حالت متنی، یک کپی stream که سه بایت اول را رد می‌کند، یک پروکسی که در مسیر عبور دوباره رمزگذاری می‌کند: هرکدام از این‌ها نشانگر را حذف می‌کنند و Excel را مجبور می‌کنند رمزگذاری را حدس بزند، کاری که بد انجام می‌دهد. این را در مرز تشخیص دهید، نه در فراخوانی خروجی‌گیری. فایل تحویل‌داده‌شده را در یک hex viewer باز کنید و تأیید کنید EF BB BF هنوز اولین چیز درون آن است

دیاگرام ردیابی اینکه BOM درست UTF-8 نوشته‌شدهٔ برون‌بری CSV در HotXLS در Delphi چگونه با انتقال متنی FTP یا پراکسی بازکدگذاری برده می‌شود و اکسل را به نمایش نویسه‌های خراب می‌گذارد
نویسنده EF BB BF را درست صادر می‌کند — mojibake فقط پس از آن ظاهر می‌شود که یک انتقال، نشانگر را کنده باشد، پس بایت‌های تحویل‌شده را در یک hex viewer تشخیص دهید

این همان نخ رابط برای هر چهار قالب است. فراخوانی خروجی‌گیری بخش آسان است، و HotXLS در هر تصمیمی که نویسنده با آن روبه‌رو می‌شود یک انتخاب قابل‌دفاع می‌کند. شکست‌ها در درزها زندگی می‌کنند، جایی‌که متن فرمول با یک parser که یک عدد می‌خواست روبه‌رو می‌شود، جایی‌که یک BOM با یک ترابرد که آن را حفظ نمی‌کند روبه‌رو می‌شود، جایی‌که یک سلول ادغام‌شده با مدل جدول مسطح RTF روبه‌رو می‌شود. هرکدام از این‌ها واقعیتی است که باید در قرارداد میان exporter شما و هرچه که آن را مصرف می‌کند نوشته شود، چون مصرف‌کننده نمی‌تواند نیت شما را از بایت‌ها بخواند. برای فهرست کامل متدها در هر دو نمای کتابچهٔ کاری، صفحهٔ محصول HotXLS Delphi Component مرجع کامل را حمل می‌کند