یک کار نرمالسازی انبوه صفحهگسترده، سه مسئله است که یک لباس مشترک پوشیدهاند. شما آرشیوی از فرمتهای درهم دارید: فایلهای .xls از دوران BIFF، فایلهای .xlsx مدرن، تعدادی .ods پراکنده از یک آزمایش LibreOffice، و چند فایلی که هیچکس نمیتواند باز کند چون رمز عبورشان همراه یک کارمند سابق از شرکت بیرون رفته است. هدف این است که همهچیز به XLSX و CSV تبدیل شود. نسخهای از این کار که بیشتر افراد مینویسند حلقهای است که هر فایل را باز میکند و با پسوندی تازه ذخیره میکند، و درست تا لحظهای کار میکند که کسی بپرسد کدام فایلها نمودارهایشان را از دست دادند، ماکروهایشان را انداختند، یا اصلاً باز نشدند. حلقه پاسخی ندارد، چون تبدیلِ تنها هیچ سابقهای نگه نمیدارد. یک میز کار (workbench) نگه میدارد: نخست فهرستبرداری میکند، دوم تبدیل میکند، و سوم راستیآزمایی میکند، و این سه مرحله باید اطلاعات را با هم به اشتراک بگذارند تا هر یک از آنها قابل اعتماد باشد
سرهمکردن آن میز کار در دلفی یا C++Builder یعنی سیمکشی چهار قابلیت HotXLS به یکدیگر، که هیچکدام در هیچ نقطهای از خط لوله به نصب اکسل نیاز ندارند. دو موتور بومی وجود دارد: یک نمای (facade) BIFF8 برای .xls و یک نمای OOXML برای .xlsx و .ods. فراخوانیهای کاوش ارزانی هستند که فراداده را بدون تجزیه کل فایل میخوانند. شمارندههای ممیزی به ازای هر برگ هستند که به شما میگویند یک کارپوشه واقعاً چه چیزی در خود دارد. و یک ماتریس تبدیل هست با یک نمایه وفاداری مستند برای هر مسیر. کار اصلی در دانستن این است که هر کدام از اینها کجا لبه تیزی دارد، چون همهشان دارند، و همین لبهها دقیقاً چیزهایی هستند که یک دستهکار شبانه تمیز را به یک حادثه صبح دوشنبه تبدیل میکنند
پیش از بارگذاری کاوش کنید: نام برگها و تشخیص رمزگذاری
بازکردن یک کارپوشه ۲۰۰ مگابایتی فقط برای اینکه بفهمید رمزگذاری شده، به ازای هر فایل دقیقهها هدر میدهد، و ضربشده در یک آرشیو بزرگ، روزها هدر میدهد. هر دو نما GetSheetNames را افشا میکنند، که فراداده برگها را بدون پرکردن کارپوشه میخواند. پیادهسازی BIFF فقط رکوردهای BoundSheet در ابتدای جریان را پویش میکند؛ پیادهسازی OOXML فقط workbook.xml درون zip را میخواند. در کنار آن، CanReadEncrypted یک ظرف رمزگذاری را بدون تلاش برای رمزگشایی تشخیص میدهد:
var
Probe: TXLSXWorkbook;
Names: TStringList;
begin
Names := TStringList.Create;
Probe := TXLSXWorkbook.Create;
try
if Probe.CanReadEncrypted(FileName) then
begin
Writeln(FileName + ': encrypted container - route to manual handling');
Exit;
end;
if Probe.GetSheetNames(FileName, Names) <= 0 then
Writeln(FileName + ': unreadable - quarantine')
else
Writeln(Format('%s: %d sheet(s), first "%s"',
[FileName, Names.Count, Names[0]]));
finally
Probe.Free;
Names.Free;
end;
end;
دو جزئیات عملیاتی این حلقه را ارزان میکنند. GetSheetNames نمونه کارپوشه را بازنشانی یا پر نمیکند، پس یک شیء کاوشگر واحد میتواند هزاران فایل را بدون بازسازیشدن طبقهبندی کند. و نسخه همین فراخوانی در نمای XLS بستههای .xlsx را نیز میفهمد، که آن را به کاوشگر واحد مناسبی تبدیل میکند وقتی نمیتوان به پسوند فایلها اعتماد کرد، و در آرشیوی به آن قدمت بهندرت میتوان اعتماد کرد. تریاژ پیش از بارگذاری شایسته بحثی جداگانه است؛ سازوکار بازرسی سبکوزن در مقاله ما درباره فهرستکردن برگها و بازرسی سبکوزن کارپوشه آمده است
شمارش آنچه یک کارپوشه واقعاً در خود دارد
وقتی فایلی از تریاژ گذشت، گذر ممیزی مسیر تبدیل آن را تعیین میکند. نمای XLSX برای هر خانواده ویژگی که بر یک تصمیم وفاداری اثر میگذارد یک شمارنده افشا میکند: سلولهای ادغامشده، نمودارها، تصاویر، قالبهای شرطی، اعتبارسنجیهای داده، جدولها، ابرپیوندها و یادداشتها، بهعلاوه پرچمهای سطح کارپوشه برای ماکروها، محافظت و فرمت منبع. مسیر تبدیل یک فایل تقریباً بهتمامی به این بستگی دارد که کدامیک از اینها غیرصفر برمیگردند
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) <> 1 then Exit;
for I := 0 to Book.Sheets.Count - 1 do
begin
Sheet := Book.Sheets[I];
Writeln(Format('%s: cells=%d merges=%d charts=%d cf=%d dv=%d protected=%s',
[Sheet.Name, Sheet.Cells.Count, Sheet.MergedCells.Count,
Sheet.Charts.Count, Sheet.ConditionalFormats.Count,
Sheet.DataValidations.Count, BoolToStr(Sheet.IsProtected, True)]));
end;
if Book.HasVbaProject then
Writeln(' contains VBA project - macro policy applies');
if Book.ExternalLinks.Count > 0 then
Writeln(Format(' %d external link(s)', [Book.ExternalLinks.Count]));
finally
Book.Free;
end;
end;
Cells.Count را با یک هشدار در ذهن بخوانید. انبار سلولها تُنُک است، پس این عدد سلولهای نمونهسازیشده را میشمارد، نه مساحت مستطیلی بازه استفادهشده را. برگی با یک مقدار در A1 و مقداری دیگر در ZZ9999 دو سلول گزارش میکند، نه حدود یک میلیون سلولی که میان آنها قرار دارد. پویش معادل در سمت BIFF از کرانهای UsedRange همراه با ForEachCell استفاده میکند، و همان خطای یکواحدی را با خود دارد که تقریباً همه را بار اول زمین میزند: UsedRange.FirstRow و همخانوادههایش ۰-مبنا هستند، درحالیکه Cells.Item[Row, Col] ۱-مبناست. پیمایشی که فراموش کند به هر کران یک واحد بیفزاید، مستطیل اشتباهی را ممیزی میکند و هرگز چیزی نمیگوید
دو اهرم هزینه یک گذر صرفاً ممیزی روی فایلهای قدیمی بزرگ را پایین میآورند. تنظیم _DisableGraphics روی true پیش از بازکردن یک .xls، تجزیه لایه ترسیمی OfficeArt را بهکلی رد میکند، که در کارپوشههای پر از شکل زمان واقعی صرفهجویی میکند. با این حال، این اکیداً یک بهینهسازی فقطخواندنی است: ذخیرهکردن از نمونهای که اینگونه باز شده، ترسیمهایی را که هرگز تجزیه نکرده حذف میکند، پس این پرچم فقط به مسیرهایی تعلق دارد که هرگز فایل را دوباره نمینویسند. وقتی ممیزی به محتوای تکتک سلولها نیاز دارد نه به شمارش، فراخوان بازگشتی (callback) ForEachCell مستقیماً روی سلولهای پرشده میپیماید و از سربار Variant به ازای هر دسترسی که ویژگیهای سلولی اندیسدار در هر خواندن میپردازند دور میزند، سرباری که در میلیونها سلول بهسرعت جمع میشود
کدهای بازگشتی ناسازگار را زود نرمال کنید
فراخوانیهای ورودی/خروجی HotXLS خطاها را از طریق نتایج عدد صحیح گزارش میکنند نه استثنا، و این قراردادها در سراسر API یکنواخت نیستند. بیشتر فراخوانیهای باز و ذخیره در موفقیت 1 و در شکست -1 برمیگردانند. GetSheetNames تعداد برگها را برمیگرداند، یا -1 را همراه با فهرست پاکشده. SaveAsHTML در XLSX دوباره الگو را میشکند و برای موفقیت 0 و برای اندیس برگ خارج از بازه -1 برمیگرداند. میز کاری که همهجا = 1 را آزمایش کند، فراخوانیهایی را که موفقیت را به شکل دیگری اعلام میکنند بیسروصدا اشتباه طبقهبندی میکند، و میز کاری که <> -1 را آزمایش کند، آنهایی را که با کد دیگری شکست میخورند میبلعد
قاعدهای که از برخورد با کل API جان سالم به در میبرد باریکتر از آن است که به نظر میرسد: برای فراخوانیهای شمارشبرگردان <= 0 را شکست تلقی کنید، برای هر روال ذخیرهای که واقعاً استفاده میکنید مقدار موفقیت مستند را بررسی کنید، و هر دو را پشت یک تابع کوچک بررسی نتیجه بگذارید تا قرارداد دقیقاً در یک جا زندگی کند. خطوط لوله دستهای بسیار بیشتر از هر باگ عجیب تجزیهگر، از انباشت آهسته کدهای بازگشتی بررسینشده شکست میخورند، و هزینه اشتباه در این مورد چهل هزار فایل بعد سر میرسد، وقتی هیچکس یادش نمیآید کدام تبدیلها واقعاً انجام شدند
ماتریس تبدیل و اینکه هر مسیر کجا داده از دست میدهد
دو نما کار تبدیل را میان خود تقسیم میکنند. TXLSXWorkbook فایلهای XLSX، ODS و CSV را باز میکند، و XLSX، ODS، CSV، HTML، RTF و XLSX رمزگذاریشده با AES را ذخیره میکند. TXLSWorkbook فایلهای BIFF را باز و ذخیره میکند، و HTML، RTF و CSV صادر میکند. نکته مفید این است که هر مسیر با یک نمایه وفاداری مستند میآید، نه یک وعده مبهم درستی، پس میتوانید از پیش تصمیم بگیرید کدام مسیرها برای کدام فایلها امن هستند
صادرات CSV، UTF-8 با BOM، پایان خط CRLF و نقلقولگذاری RFC 4180 مینویسد. کاری که نمیکند ارزیابی فرمولهاست: سلولی که =SUM(...) در خود دارد بهصورت متن لفظی فرمول صادر میشود، پس یک برگ پر از فرمول به یک برگ پر از رشته تبدیل میشود مگر اینکه ابتدا مقادیر را محاسبه کنید. صادرات HTML یک جدول واحد تولید میکند، که در آن colspan و rowspan جای سلولهای ادغامشده را میگیرند و استایلهای پایه درونخطی میشوند. صادرات RTF محدودیت تیزتری دارد: نمیتواند سلولهای ادغامشده را در عرض ستونها بگستراند، پس سلولهای ادامه یک ادغام خالی درمیآیند. واردکردن ODS بهعمد سبکوزن است، به گفته مستندات خود کتابخانه. مقادیر اسکالر و نتایج کششده فرمولها میرسند؛ استایلها، عبارتهای فرمول زنده ODF و ترسیمها نمیرسند. این موضوع همان لحظهای اهمیت پیدا میکند که آرشیو حاوی فایلهای واقعی OpenDocument تحت حاکمیت OASIS ODF 1.3 باشد، جایی که هر چیزی نزدیک به یک تبدیل وفادار بصری بیش از آن چیزی میخواهد که این مسیر واردکردن برای حمل آن ساخته شده، و گذر ممیزی همان چیزی است که پیش از آنکه دستهکار بیسروصدا آن فایلها را مسطح کند، وجودشان را به شما میگوید
SaveXLSWorkbookAsXLSX یک پل داده است، نه پل چیدمان
نمای BIFF نمیتواند مستقیماً OOXML بنویسد، پس گذر از .xls به .xlsx از طریق تابع SaveXLSWorkbookAsXLSX در یونیت lxXlsxExport انجام میشود. وفاداری این پل ارزش آن را دارد که صریح بیان شود، چون نامش بیش از آنچه انجام میدهد وعده میدهد. مقادیر، فرمولها، قالبهای عددی، رنگهای پرکننده، ویژگیهای اصلی فونت، عرض ستونها و تنظیمات نما مانند خطوط شبکه را کپی میکند. حاشیهها، بازههای ادغامشده، یادداشتها، نمودارها یا قالبهای شرطی را کپی نمیکند. برای نرمالسازی در سطح داده، جایی که سیستمهای پاییندستی نتیجه را تجزیه میکنند و هیچکس به قالببندی نگاه نمیکند، این دقیقاً کافی است و چیزی که کسی به آن نیاز داشته باشد از دست نمیرود. برای یک گزارش هیئتمدیره قالببندیشده که قرار است یک انسان آن را بخواند، کافی نیست، و دقیقاً همینجاست که شمارندههای ممیزی جای خود را مییابند: فایلی که ممیزی آن را حامل نمودار و قالب شرطی علامت زده باید به یک صف دستی هدایت شود، نه از پلی بگذرد که هر دو را بیهیچ حرفی میاندازد
var
Legacy: IXLSWorkbook; // ارجاع واسط: Free نکنید
Modern: TXLSXWorkbook;
begin
if SameText(ExtractFileExt(FileName), '.xls') then
begin
Legacy := TXLSWorkbook.Create;
if Legacy.Open(FileName) <= 0 then Exit;
if SaveXLSWorkbookAsXLSX(Legacy,
ChangeFileExt(FileName, '.xlsx')) <= 0 then
Writeln('bridge failed: ' + FileName);
end
else
begin
Modern := TXLSXWorkbook.Create;
try
Modern.StreamingWrite := True; // XML برگ را مستقیم به zip جریان بده
if Modern.Open(FileName) = 1 then
Modern.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Modern.Free;
end;
end;
end;
حلقه بالا اهرم توان عملیاتی سمت OOXML را هم نشان میدهد. تنظیم StreamingWrite روی true، XML کاربرگ را مستقیماً به بسته خروجی جریان میدهد بهجای آنکه آن را بهصورت یک رشته غولپیکر در حافظه نگه دارد، و این همان تفاوت میان یک اجرای راحت و یک کرش کمبود حافظه است وقتی فایلها به صدها هزار سطر میرسند. اندازهگذاری و رفتار حافظه در آن حالت بحث جداگانه خود را در مقاله ما درباره نوشتن جریانی برای کارهای دستهای سرور دارند. یک ویژگی دیگر برای دستهکاری که میخواهد از همه هستهها استفاده کند اهمیت دارد: هیچیک از دو نما امن برای نخ (thread-safe) نیستند، ولی هیچکدام حالت سراسری هم به اشتراک نمیگذارند، پس الگوی پشتیبانیشده برای تبدیل موازی یک نمونه کارپوشه به ازای هر نخ کارگر است، بدون هیچ قفلی میان آنها
فایلهای رمزدار، و اینکه با آنها چه باید کرد
فایلهای قفلشده آرشیو بهروشنی بر اساس فرمت تقسیم میشوند، و همین تقسیم تعیین میکند به کجا بروند. رمزگذاری .xls قدیمی، چه RC4، چه RC4 روی CryptoAPI و چه مبهمسازی قدیمی XOR، خواندنی است: رمز عبور را به Open بدهید و فایل مانند هر فایل دیگری تبدیل میشود. بستههای .xlsx رمزگذاریشده داستان دیگری دارند. HotXLS آنها را با CanReadEncrypted تشخیص میدهد ولی نمیتواند رمزگشاییشان کند، پس تنها حرکت صادقانه هدایت آنها به صفی است که در آن یک انسان هر یک را در اکسل باز و دوباره ذخیره میکند و سپس به خط لوله بازمیگرداند. این عدم تقارن ارزش آن را دارد که از ابتدا برایش طراحی کنید، چون فایلهای XLSX رمزگذاریشده همانهایی هستند که به احتمال زیاد سوابقیاند که کسی واقعاً به آنها اهمیت میدهد
بستن حلقه با راستیآزمایی
مرحله سوم همان مرحلهای است که رد میشود، و ردکردن آن همان چیزی است که یک تبدیل انبوه را به یک بدهی تبدیل میکند. هیچ مسیر ذخیرهای در HotXLS فرمولها را ارزیابی نمیکند. اکسل هنگام بازکردن فایل دوباره محاسبه میکند، پس تبدیل XLSX به XLSX درست میماند، ولی یک هدف CSV متن فرمول را عیناً دریافت میکند مگر اینکه خط لوله ابتدا Calculate را روی سلولها اجرا کند و نتایج را بازنویسد. دانستن این موضوع از پیش، تفاوت میان یک CSV پر از عدد و یک CSV پر از رشتههای =SUM(...) است که هیچکس متوجهشان نمیشود تا وقتی یک واردکردن پاییندستی روی آنها گیر کند
خود راستیآزمایی آنقدر ارزان است که هیچ بهانهای برای کنارگذاشتنش وجود ندارد. هر فایل تبدیلشده را با همان کتابخانه دوباره باز کنید، شمارندههای ممیزی را دوباره اجرا کنید، و آنها را با اعداد پیش از تبدیل که گذر فهرستبرداری از قبل ثبت کرده مقایسه کنید. تعداد برگی که کم شده، تعداد نموداری که به صفر رسیده درحالیکه منبع سه تا داشت، تعداد سلولی که ناگهان سقوط کرده: هر یک از اینها یک ازدسترفتن خاموش است که به قیمت یک بازکردن دوم گرفته میشود. علاوه بر آن، نمونهای را با چشم در اکسل یا LibreOffice بررسی کنید، و این ترکیب اکثریت قاطع آسیبهای تبدیل را پیش از تحویل میگیرد. تمام دلیل اینکه مرحله فهرستبرداری مرحله راستیآزمایی را تغذیه میکند همین است. بدون اعداد پیشین، اعداد پسین هیچچیز را ثابت نمیکنند
یک میز کار ممیزیمحور، یک تبدیل انبوه پرخطر را به فرایندی قابل اندازهگیری با یک خط قرنطینه برای فایلهایی که نمیتوانند تمیز عبور کنند تبدیل میکند. تمام فراخوانیهای کاوش، شمارش و تبدیلی که اینجا نشان داده شدند بخشی از HotXLS Delphi Component هستند، که آنها را بهصورت بومی و درونفرایندی بدون اتوماسیون اکسل اجرا میکند