Запустіть пакетну конвертацію на десять тисяч електронних таблиць за ніч — і на ранок три з них повернуться з False. Це весь післяаналіз, який дає вам булевий результат збереження: підрахунок невдач, без жодної інформації про те, який файл, який аркуш чи яка з дюжини можливих причин була відповідальною. HotXLS, рідний компонент losLab для файлів Excel у Delphi та C++Builder, замінює цей один біт структурованими діагностиками. Інтерфейс IXLSWorkbookProgress відкриває список Diagnostics та подію OnDiagnostic, що повідомляють стабільний числовий код, рівень серйозності, операцію, яка провалилася, та аркуш, де це сталося, для кожного виклику Open, SaveAs та Recalculate
Чому булевий результат збереження провалюється на масштабі?
Проблему, яку створює булевий результат, створює не один невдалий файл; її створюють тисяча таких. Коли SaveAs повертає щось відмінне від успіху для трьох файлів із десяти тисяч, наступне питання завжди те саме: чи ці три можна повторити, чи потрібна людина? Помилка дозволу на мережевій папці — не той самий інцидент, що формула, яку рушій обчислення не може оцінити, і жодна з них не той самий інцидент, що аркуш, який мовчки перевищив обмеження формату. Маючи для роботи лише результат «пройшло/не пройшло», кожен із цих випадків стає ідентичним заявленням у службу підтримки, і комусь доводиться відкривати кожен файл вручну, в Excel, і вдивлятися, доки причина не стане очевидною. Ця ручна сортування — справжня вартість булевого API, і вона масштабується лінійно з розміром пакету, а це якраз та властивість, якої ви не хочете від обробки помилок
Всередині IXLSWorkbookProgress: що несе TXLSDiagnostic
IXLSWorkbookProgress — інтерфейс, який HotXLS використовує, щоб повідомляти і як просувається операція, і що всередині неї пішло не так, і дві половини поділяють один контракт не випадково: обидві — речі, які довготривалий виклик Open, SaveAs чи Recalculate має повідомити, не піднімаючи виняток посеред операції. Половина прогресу — це OnProgress та OnProgressEx, що спрацьовують із фазою, станом та парою поточне/загальне. Половина діагностики — та, про яку ця стаття: властивість Diagnostics, що повертає список TXLSDiagnostics, скорочення LastDiagnostic для найновішого запису та подія OnDiagnostic, що спрацьовує в момент створення кожного запису TXLSDiagnostic. Кожен запис несе числовий Code, TXLSDiagnosticSeverity, TXLSDiagnosticOperation, що його породила, зрозумілий людині Message, SheetIndex та SheetName, а також NativeCode, що зберігає будь-яке значення повернення нижчого рівня, яке спричинило запис
var
Book: TXLSXWorkbook;
Diag: TXLSDiagnostic;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.SaveAs('quarterly-report.xlsx') <> 1 then
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
Writeln(Format('[%d] severity=%d sheet="%s": %s',
[Diag.Code, Ord(Diag.Severity), Diag.SheetName, Diag.Message]));
end;
finally
Book.Free;
end;
end;
Читання Diagnostics так уже само по собі перевершує булевий результат, бо Code та SheetName перетворюють загадку на конкретний, придатний до фільтрації факт. Запис TXLSDiagnostic сягає далі, ніж виводить цей приклад: RecordId та StreamOffset існують для криміналістики на рівні байтів усередині потоку BIFF, а PartName зберігає запис zip OOXML, наприклад xl/worksheets/sheet3.xml, звідки походить проблема. Варто знати, перш ніж будувати інструментарій навколо них: у поточному випуску жодна з вбудованих точок виклику діагностики не заповнює RecordId чи StreamOffset, тож обидва залишаються на значенні за замовчуванням конструктора -1, що означає «не застосовно», а не «нуль». Ставтеся до їхньої відсутності як до норми, а не як до помилки у вашому обробнику
Два рушії, одна форма, одна тиха відмінність
HotXLS постачає два рушії за цією самою моделлю звітування: фасад BIFF8 для застарілих файлів .xls та фасад OOXML для .xlsx, і вони не відкривають IXLSWorkbookProgress ідентично. TXLSWorkbook, рушій .xls, формально реалізує IXLSWorkbookProgress, тож його можна передати будь-де, де очікується цей тип інтерфейсу. TXLSXWorkbook, рушій .xlsx, відкриває ті самі члени Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress та OnProgressEx з ідентичними іменами й типами, але як звичайний клас, а не формальну реалізацію цього інтерфейсу, тож він сам по собі не задовольнить параметр IXLSWorkbookProgress. На практиці це рідко важить, бо більшість коду працює з одним конкретним класом книги за раз, але це означає, що ви не можете написати один допоміжний метод, типізований під IXLSWorkbookProgress, і передавати йому об'єкт книги будь-якого рушія взаємозамінно. Єдина відмінність поля, що прямо випливає з поділу форматів, — PartName: лише рушій XLSX заповнює його, бо лише OOXML має частини zip, які можна назвати
Що робить код діагностики чимось, на чому безпечно розгалужуватися?
Поле Code — єдина частина діагностики, варта жорсткого порівняння; Message — ні, бо проза — саме те, що переформульовують, перекладають заново чи розширюють додатковими деталями в пізнішому випуску, не трактуючи це як зламану зміну. Вбудовані коди діагностики HotXLS уже читаються так, ніби були розроблені з цією відмінністю на увазі: коди, пов'язані зі збереженням, ідуть від 1000 до 1005, коди, пов'язані з відкриттям, сидять на 1100 і 1101, коди, пов'язані з обчисленням, на 1200 і 1201, а код непідтримуваного формату — на 1300, із прогалинами, залишеними всередині кожної смуги, а не з кодами, що йдуть послідовно через усі. Саме цей інтервал дозволяє постачальнику додати новий режим збою під час збереження, скажімо, на 1006, не перенумеровуючи коди, від яких уже залежить ваш оператор switch, і це варто перевіряти в будь-якому API діагностики, перш ніж прив'язуватися до збігу за кодом у продакшені, не лише в цьому одному. Тримайте гілку за замовчуванням у власній логіці диспетчеризації незалежно від того, наскільки стабільно виглядає нумерація, бо нові режими збою — саме те, що продовжує виявляти парсер чи записувач, що еволюціонує. NativeCode та ExceptionClass сидять на рівень нижче Code для випадків, коли потрібно ескалувати: NativeCode зберігає базове значення повернення, серед них HRESULT із виклику Structured Storage, а ExceptionClass записує тип винятку Delphi, коли такий був залучений, чого зазвичай достатньо, щоб відкрити точний запит у підтримку без прикріплення повного стека викликів
Серйозність та операція вирішують, що робить ваш код далі
Серйозність та операція — те, що перетворює діагностику з рядка журналу на рішення про маршрутизацію. TXLSDiagnosticSeverity проходить Info, Warning, Error та Fatal, а TXLSDiagnosticOperation позначає кожен запис викликом, що його породив: Open, Save, Calculate чи Export. Дві осі незалежні за задумом: xlsDiagnosticUnhandledException — один фіксований код, що спрацьовує з Operation, встановленим у той виклик, який справді його підняв, тож Code відповідає, що пішло не так, а Operation окремо відповідає, де, замість того щоб потребувати окремого коду для винятку під час відкриття проти винятку під час збереження. Ця здатність компонуватися також робить маршрутизацію механічною: залогувати попередження й продовжити — типовий приклад цього збереження, скасованого через прапорець Aborted; порахувати помилку й тримати пакет запущеним — типовий приклад цього аркуш, що не зміг серіалізуватися; зупинити пакет на серйозності fatal, бо цей рівень означає, що необроблений виняток уже розгорнув виклик, і продовження ризикує роботою з наполовину оновленим станом. Одне чесне застереження: Info існує в переліку як значення за замовчуванням, з яким починається свіжий TXLSDiagnostic, але кожна точка виклику діагностики, вбудована в сьогоднішній випуск HotXLS, піднімає лише Warning, Error чи Fatal; Info зарезервований на майбутнє використання, а не те, що рушій випускає сьогодні
// same Diagnostics loop as above, routed by severity instead of printed flat:
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
case Diag.Severity of
xlsDiagnosticWarning:
Writeln(Format('WARN [%d] %s', [Diag.Code, Diag.Message]));
xlsDiagnosticError:
begin
Writeln(Format('ERROR [%d] %s (sheet %s, native %d)',
[Diag.Code, Diag.Message, Diag.SheetName, Diag.NativeCode]));
Inc(FailedSheetCount);
end;
xlsDiagnosticFatal:
raise Exception.CreateFmt('Fatal HotXLS diagnostic %d: %s', [Diag.Code, Diag.Message]);
end;
end;
Підключення OnDiagnostic до пакетного конвеєра
Опитування Diagnostics після кожного виклику працює для одного файлу; воно перестає працювати, щойно ви повертаєтеся до того нічного пакету з десяти тисяч, бо Diagnostics очищається на початку кожного виклику Open, SaveAs та Recalculate. Прочитайте його після третього файлу в циклі — і побачите лише діагностику третього файлу; що б не повідомили перші два файли, вже зникло. OnDiagnostic вирішує це, перетворюючи колекцію на потік: підпишіться один раз перед початком циклу, і той самий обробник спрацьовує для кожного файлу по черзі, з іменем файлу, що все ще в області видимості через поле екземпляра
type
TBatchConverter = class
private
FCurrentFile: string;
FFailedFiles: TStringList;
procedure HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
end;
procedure TBatchConverter.HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
begin
if Diagnostic.Severity >= xlsDiagnosticError then
FFailedFiles.Add(Format('%s: [%d] %s (sheet %s)',
[FCurrentFile, Diagnostic.Code, Diagnostic.Message, Diagnostic.SheetName]));
end;
// inside the batch loop:
Book.OnDiagnostic := HandleDiagnostic;
for I := 0 to FileNames.Count - 1 do
begin
FCurrentFile := FileNames[I];
if Book.Open(FCurrentFile) = 1 then
Book.SaveAs(ChangeFileExt(FCurrentFile, '.xlsx'));
end;
У що насправді обходиться колбек
OnDiagnostic дешевий зі структурної причини: він спрацьовує лише тоді, коли щось уже не так, а «не так» — рідкість порівняно з кількістю клітинок, рядків чи аркушів, які містить книга. Порівняйте це з OnProgress і OnProgressEx, що повідомляють про звичайний прогрес, і їх довелося проєктувати з урахуванням частоти викликів із самого початку. HotXLS спрацьовує з прогресом на рівні аркуша один раз на аркуш під час Open та SaveAs, не один раз на клітинку чи рядок, і саме це тримає накладні витрати на виклик малими навіть на книгах із мільйонами клітинок; Recalculate йде далі й обмежує власну подію прогресу приблизно кожними чотирма відсотками графа залежностей, тож повне перерахування дає вам сердечний ритм замість того, щоб затопити ваш потік UI подіями. Діагностика не потребувала жодного з цього обмеження, бо кількість подій обмежена кількістю справжніх проблем, а не розміром файлу
Єдине місце, де продуктивність усе ще залежить від вас, — усередині самого обробника. OnDiagnostic спрацьовує синхронно, на потоці, що виконує Open, SaveAs чи Recalculate, тож обробник, що блокує, наприклад синхронний запис у віддалену службу логування, стає частиною часу настінного годинника цього виклику. Для одного файлу це непомітно. Помножене на пакет із десяти тисяч файлів це різниця між завданням, що завершується за ніч, і тим, що все ще виконується на обід, тож буферизуйте те, що потрібно зробити обробнику, і скидайте це асинхронно, а не робіть повільну частину прямо в лінії
Структуровані діагностики найцінніші саме там, де булевий результат найслабший, — у робочих процесах, що торкаються багатьох файлів замість одного. Конвеєр аудиту та конвертації книг — найясніший приклад: замість запису голого «пройшло/не пройшло» на файл, прикріпіть список Diagnostics кожного файлу до його запису аудиту, і звіт скаже вам не лише що провалилося, а й чому, а це здебільшого те, що намагається правильно зробити наша стаття про побудову верстака аудиту та конвертації книг. Те саме поєднання прогресу й діагностики також належить будь-якому робочому процесу, що вже потребує звітування про прогрес заради нього самого, а це якраз територія, покрита в нашому посібнику з продуктивності великих книг у HotXLS, де довгий виклик Open чи SaveAs достатньо поширений, щоб OnProgress уже було підключено, а OnDiagnostic — природне, майже безкоштовне доповнення поряд із ним
Ніщо з цього не вимагає встановленого Excel будь-де в конвеєрі, і ніщо з цього не вимагає перехоплення загального винятку й вгадування, що він означав. IXLSWorkbookProgress та його члени Diagnostics, LastDiagnostic та OnDiagnostic — частина стандартного компонента HotXLS для Delphi та C++Builder, поряд з повним довідником кодів діагностики та рештою поверхні Open, SaveAs та Recalculate, яку розглядала ця стаття