.net. Linq для таблиц из любых файлов.
Запись от setood размещена 10.05.2019 в 00:19
Показов 1971
Комментарии 6
|
Всем привет! Эта статья про использование небольшой библиотеки, которая позволяет делать Linq запросы, для чтения таблицы из файлов. Набор поддерживаемых форматов файлов легко расширяется реализуя простой интерфейс. Раньше, в каждом проекте, приходилось импортировать табличные данные из разных файлов (как правило .txt, .csv, .xlsx). Задача всегда сводится к нескольким шагам: вытащить строки, отфильтровать, обработать. Код для этих операций довольно простой и, поэтому всегда без задней мысли, писался заново. В какой-то момент я созрел для написания библиотеки - TableReaderLib. Сейчас реализована поддержка для формата csv (с любыми разделителями, кодировками) и Excel (Необходим Excel на компьютере). Фишки которые предоставляет библиотека: - IEnumerable по строкам. Это дает возможность использовать Linq, foreach и ToArray() - Определять столбцы в виде объектов (это сильно повышает читабельность кода) - Указывать является ли первая строка заголовками и получить их содержимое - Если таблица начинается где-то в середине файла, вопрос решается строчкой StartRow= N - Можно пропускать первые M строк - Можно считывать только K строк - Реализовать специфические свойства, например, для текстовых файлов это кодировка и тип разделителя, для excel чтение блоками (что весьма ускоряет производительность) и преобразование Excel типов в .net'овские. Прочитал этой библиотекой, с удовольствием, уже не один миллион строк, и решил показать её миру. И так давайте попробуем достать табличку из простого текстового файла: В файле первые две строчки пропущены, что бы показать что делать если таблица начинается не с начала файла. Затем идёт строка заголовков столбцов. Название заголовка, для наглядности, это тип данных в нём. Далее три строчки с данными, разделителем является "|", разделитель может быть любым. Итак, в Visual Studio создаем новый консольный проект, добавляем references: - TableReaderLib.dll, содержит все общие классы для работы с таблицами, и определение интерфейса ISourceReader, который нужно реализовать для чтения конкретных типов файлов - ReadersForTableReaderLib.dll - содержит реализации ISourceReader, которые не требуют дополнительных references. (Excel, например, требует) Затем в коде нам нужно создать объект CsvSourceReader. Конструктор содержит всего три аргумента: - filePath - путь к нашему файлу - splitter - разделитель ячеек, в нашем случае "|" - encoding - кодировка файла, почти всегда можно оставлять null Объявление конструктора:
- Name - строковое имя столбца, если для столбца не создавалась переменная, можно доставать ячейки по этому имени - Type - Тип данных в ячейке. Если быть честным, не придумал как вытаскивать значения с помощью этого свойства. Используются перегруженные методы T GetCellValue<T>(TableColumn) - IndexInSource - номер столбца в исходной таблице от 0. Их объявления:
Давайте прочитаем заголовки нашей таблицы в файле через свойство TableReader.SourceHeaders и посмотрим данные из второй строки:
Для удобства, весь код примера целиком:
Весь исходный код можно скачать на GitHub и использовать как хотите: https://github.com/setood/TableReaderLib Буду рад, если кому-то библиотека поможет, обязательно сообщите. Это сильно мотивирует ![]() Так же рад замечаниям, code review и помощь в развитии либы. | ||||||||||||||||||||||||||||||
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 6
Комментарии
-
У вас метод Reset вызывается при каждом изменении свойств StartRow, SkippedRows и т.д.
Например:
Это нехорошо, потому что файл будет читаться заново при каждом изменении каждого из этих свойств.C# 1 2 3 4 5 6 7 8 9 10 11 12
int StartRow { get => _startRow; set { if (_startRow != value) { _startRow = value; Reset(); } } }
Запись от Storm23 размещена 10.05.2019 в 17:12
-
Спасибо!
Сообщение от Storm23
По задумке, изменяя любое из этих свойств, мы как бы говорим что у нас "изменилась таблица", поэтому читать её сначала вполне логично.
Однако это становится проблемой, только когда мы ещё не извлекли данные, и устанавливая эти свойства вызвали Reset 3-4 раза. В этом случае, нужно что бы метод Reset делал минимум действий, в идеале просто переместил указатель на начало таблицы в файле.
В моей же реализаций метод Reset уничтожает старый StreamReader, создает новый, и пропускает строки до нужной нам. Это не оптимально (нужно будет исправить, спасибо), но в использовании никак не ощущается, так как StreamReader не читает весь файл а построчно - при вызове Next()
Edited:
Изменил пересоздание StreamReader на сброс позиции в начало.Запись от setood размещена 11.05.2019 в 08:57
-
Извините что придалбываюсь, но я люблю чистый код.
Проблема не только в том, что создавался новый StreamReader. Проблема в том, что каждый раз при изменении свойств у вас каждый раз читается файл, вызываются методы SkipStartRows(), SkipAndReadHeaders() и SkipSkippedRows().
И это вызывается по нескольку раз.
То что они читают файл построчно - ничего не меняет. Для того, что бы читать построчно, все равно нужно читать все содержимое строк и искать их конец.
А теперь представьте, что у вас гигабайтный csv файл и необходимая часть находится где-то в середине. У вас тогда этот огромный файл будет перелопачиваться почти целиком, при каждом изменении свойств. И это не будет быстро.
Вам нужно сделать ленивую(отложенную) инициализацию. Свойства типа StartRow не должны вызывать метод Reset напрямую. Они должны лишь выставить флажок, типаneedToReset = true.
А при чтении данных из файла (во всех методах и свойствах, который возвращают информацию) нужно проверять этот флажок и если он выставлен, то вызывать Reset и сбрасывать флажок:
Тогда Reset будет вызываться только один раз. И не в момент изменения свойств (что довольно абсурдно) а в момент получения информации из файла, что вполне логично.C# 1 2 3 4 5 6
if (needToReset) { needToReset = false; Reset(); } ...
Запись от Storm23 размещена 12.05.2019 в 00:16
-
Это, просто, огонь!
Сообщение от Storm23
Edited:
Переделал, как вы сказали. Теперь флаг needToReset проверяется при вызове MoveNext().
Спасибо! захотите "придолбаться" еще - welcome!)Запись от setood размещена 12.05.2019 в 10:08
-
Да пожалуйста.
У вас интерфейс ISourceReader выглядит странно:
Кликните здесь для просмотра всего текстаC# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/// <summary> /// Реаизация интерфейся позволяет использовать объекты чтения различных источников единым образом. /// Так как объекты реализующие ISourceReader могут создаваться совершенно по разному, а вызывающемо коду необходимо создавать их копии (для повторного чтения того же источника) /// Использован создающий метод CreateReaderClone. /// IEnumerator<TableRow> - позовляет получить все строки в соответствии с параметрами метода CreateReaderClone. /// </summary> public interface ISourceReader : IEnumerator<TableRow> { /// <summary> /// Метод создает копию объекта чтения источника, и изменяет указанные в параметрах метода свойства копии. /// </summary> /// <param name="columns">Коллекция столбцов, которую необходимо считывать из источника</param> /// <param name="isFirstRowHeaders">Является ли первая строка Заголовками столбцов.</param> /// <param name="startRow">Указывает на начало таблицы. Например когда в первых строках источника идет описание таблицы, а ниже сама таблица</param> /// <param name="skippedRows">Сколько строк таблицы необходимо пропустить. Например если они были прочитаны ранее</param> /// <param name="takeRows">Сколько строк таблицы нужно прочитать. Null если необходимо считывать до конца таблицы.</param> /// <returns></returns> ISourceReader CreateReaderClone(IEnumerable<TableColumn> columns, bool isFirstRowHeaders, int startRow, int skippedRows, int? takeRows); }
Содержит всего один метод CreateReaderClone, и этот метод семантически не соотносится с названием интерфейса.
Иными словами, открывая интерфейс ISourceReader я ожидаю там увидеть методы чтения данных, но вместо этого я вижу странный метод CreateReaderClone. Это неожиданное поведение. Проще говоря WTF.
Но дальше - хуже. CreateReaderClone принимает несколько параметров. Но оказывается, что для некоторых классов, реализующих ISourceReader, эти параметры не имеют смысла.
Например, CsvSourceReader принимает и обрабатывает эти параметры, а SqlSourceReader - их все игнорирует и никак не использует. Это признак плохой архитектуры.
В данном случае, вам вместо интерфейсного метода CreateReaderClone, нужно использовать конструктор клонирования с параметрами. Конструкторы не могут быть частями интерфейса, поэтому, если у вас есть параметры специфичные для конкретного класса, то эти параметры нужно передавать через конструктор.
Метод же CreateReaderClone нужно убрать из ISourceReader.Запись от Storm23 размещена 14.05.2019 в 00:55
-
Извиняюсь, этот ридер был не доделанный. Сейчас исправил с учётом замечаний, но стало ещё хуже))
Когда я создавал библиотеку, предполагал, что заголовки таблицы могут быть в первой строке начиная от StartRow. На SqlReader стало понятно, что заголовки могут быть вообще отдельной сущностью и нужно переделывать всё от основания.
В текущей реализации SqlSourceReader не может возвращать SQL названия столбцов. С чтением и фильтрацией строк всё Ок.
--Edited
Про интерфейс еще подумаю, напишу позже.Запись от setood размещена 22.05.2019 в 10:13



