EventLog

errors in our life...

My Links

Blog Stats

Archives

Загрузка данных из книг Excel

Так случилось, что встала задача считывать данные из книг 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'е :)

З.Ы. От багов никто не застрахован, поэтому о них просьба сообщать (если таковые обнаружатся).

posted on Tuesday, May 09, 2006 6:11 PM

Feedback

No comments posted yet.
Title  
Name  
Url
Comments   
Protected by Clearscreen.SharpHIPEnter the code you see: