Запустите пакетную конвертацию десяти тысяч электронных таблиц за ночь, и к утру три из них вернут False. Это весь постмортем, который даёт вам булев результат сохранения: счётчик отказов, без единого слова о том, какой файл, какой лист или какая из десятка возможных причин была виновна. HotXLS, нативный компонент losLab для Delphi и C++Builder для файлов Excel, заменяет этот единственный бит структурированной диагностикой. Интерфейс 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 от вызова структурированного хранилища, а 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, которую разбирала эта статья