Так случилось, что встала задача считывать данные из книг Excel.
Казалось бы, задача простая, но данных этих может быть очень много (десятки тысяч строк на каждом листе), а скорость считывания весьма критична. Да и набор средств весьма ограничен: либо использовать сам Excel через COM, либо его драйвер через OleDb, либо сторонние компоненты (типа FlexCel, ExcelLite и т.д.)
Первый способ был отметен сразу, т.к. работа через COM весьма медлительна. А мерять время считывания минутами не хотелось.
Второй способ был рассмотрен. OleDbDataReader проходил на "холостом ходу" по листу довольно быстро, но вот при получении значений ячеек в таблицу он тоже терял "форму".
Самым перспективным казался третий способ (через сторонние компоненты). Большинство их являются платными, но это только половина проблемы. Вторая половина ее в том, что их скорость доступа к данным тоже не очень порадовала. Так, например, FlexCel, загрузив 18-мегабайтную xls-ку (за 4-5 секунд(ы)), обращался к ее ячейкам весьма медленно (на виртуальном DataGridView-е это было заметно визуально). ExcelLite же в бесплатном варианте ограничен 150 строками, поэтому его даже и потестировать не получилось.
Итак, что мы имеем? Нужно средство, позволяющее работать с табличными данными (и в этом случае DataSet видится неплохим решением для их представления), не отягощенное форматированием, объектами, картинками и прочими украшательствами.
Что ж, напишем такое средство!
Бинарный Excel-файл, начиная с версии Excel 5.0, представляет собой OLE2-контейнер, в одном из потоков которого и заключены данные книги. Более старые версии Екселя не использовали контейнера, но они давно не используются и нам не интересны.
Структура контейнера такова:
В первых 512 байтах расположен заголовок файла и номера первых 109 секторов FAT. Если размер FAT больше 109 секторов, то в заголовке есть ссылка на продолжение списка секторов.
Далее, начиная со смещения 0x0200 идут сектора (размер их указан в заголовке), которые нумеруются с нуля. Каждый сектор файла описан в FAT (включая сектора самой FAT).
Собственно, сама FAT представляет собой упорядоченный массив двойных слов (uint), индекс в этом массиве определяет номер описываемого сектора, а значение - номер следующего сектора в цепочке данных (Обычно они идут подряд, но бывают и исключения). Также зарезервированы 4 специальных значения для FAT (0xFFFFFFFF - сектор пустой, 0xFFFFFFFE - сектор является последним в данной цепочке, 0xFFFFFFFD - в этом секторе находится сама FAT (один из первых 109 секторов), 0xFFFFFFFC - в этом секторе находится продолжение FAT (т.н. DIF-сектора)).
Соответственно, смещение сектора определяется как SectorIndex * SizeOfSector + 512.
Также из заголовка легко узнать, в каком секторе находится Root Directory (дерево каталогов). Вся потоки данных и объекты, содержащиеся в файле, описаны в этом дереве. Первой записью в дереве является корневая запись, от нее расходится "окрашенное" по приоритетам дерево. Тут описаны связанные (Linked) и внедренные (Embedded) объекты, информация о файле (Автор, Заголовок и прочее, что используется службой индексирования и показывается на вкладке "Сведения" в свойствах файла), прочая лабуда, которая в настоящее время нас не интересует. Тут же под именем "Workbook" ("Book" для старых версий) описаны и интересующие нас данные.
Данные храняться в виде BIFF-потока. Поток состоит из структур вида (ushort ID, ushort Size, byte[Size] Data). Книга состоит из нескольких блоков таких структур, первым из которых является глобальный блок (Workbook Globals), а последующие - блоки листов, диаграмм, макросов. Каждый блок структур начинается с BOF-записи (в которой определяется, что именно она начинает) и заканчивается EOF-записью.
Внутри блока содержатся записи-поля, характеризующие этот блок. В Workbook Globals содержится информация о книге, ее окне, шрифтах, стилях, кодировке и т.д. Из всего этого нас интересует только кодировка, SST (Shared String Table) (если есть) и список связанных листов.
Запись листа содержит индекс, информацию об окне листа, его разметке, размеры листа, информацию о строках и ячейках.
План чтения будет такой:
1) Сначала читаем заголовок файла. Из него узнаем размер сектора, список секторов фат, номер сектора с Root Direcoty.
2) По списку секторов фат строим саму FAT.
3) Читаем Root Directory, ищем среди него поток с именем "Workbook" или "Book".
4) Читаем этот поток.
5) Считываем Workbook Globals из потока.
6) Из Workbook Globals получаем имена и смещения листов относительно начала потока.
7) Если тип листа не Worksheet - пропускаем его.
8) Для каждого листа считываем индекс (первая запись после BOF). Из него узнаем адреса записей DbCell.
9) Считываем запись Dimensions, узнаем из нее размеры листа. Создаем DataTable с нужным количеством столбцов и строк.
10) Для каждой записи DbCell узнаем из нее номер первой записи Row, к которой она относится.
11) Последовательно читаем список ячеек до записи DbCell.
12) Декодируем значение ячейки в зависимости от ее формата, вносим это значение в DataTable.
13) Когда чтение листа закончено, привязываем DataTable к результирующему DataSet'у.
14) Возвращаем DataSet пользователю.
В теории все просто. На практике - немного сложнее из-за закрытости формата и всяких подводных камней.
Основные данные по формату были взяты
отсюда, а также собраны на просторах интернета.
Ну, и на закуску -
моя реализация читалки.
Можете использовать ее исходник как хотите: модифицировать, добавить обработку форматирования, объектов, картинок, etc.
А я буду рад, если сошлетесь на меня в About'е :)
З.Ы. От багов никто не застрахован, поэтому о них просьба сообщать (если таковые обнаружатся).