Форум программистов, компьютерный форум, киберфорум
Наши страницы
setood
Войти
Регистрация
Восстановить пароль
Оценить эту запись

.net. Linq для таблиц из любых файлов.

Запись от setood размещена 10.05.2019 в 00:19

Всем привет!
Эта статья про использование небольшой библиотеки, которая позволяет делать Linq запросы, для чтения таблицы из файлов.
Набор поддерживаемых форматов файлов легко расширяется реализуя простой интерфейс.


Раньше, в каждом проекте, приходилось импортировать табличные данные из разных файлов (как правило .txt, .csv, .xlsx). Задача всегда сводится к нескольким шагам: вытащить строки, отфильтровать, обработать.
Код для этих операций довольно простой и, поэтому всегда без задней мысли, писался заново.
В какой-то момент я созрел для написания библиотеки - TableReaderLib.
Сейчас реализована поддержка для формата csv (с любыми разделителями, кодировками) и Excel (Необходим Excel на компьютере).

Фишки которые предоставляет библиотека:
- IEnumerable по строкам. Это дает возможность использовать Linq, foreach и ToArray()
- Определять столбцы в виде объектов (это сильно повышает читабельность кода)
- Указывать является ли первая строка заголовками и получить их содержимое
- Если таблица начинается где-то в середине файла, вопрос решается строчкой StartRow= N
- Можно пропускать первые M строк
- Можно считывать только K строк
- Реализовать специфические свойства, например, для текстовых файлов это кодировка и тип разделителя, для excel чтение блоками (что весьма ускоряет производительность) и преобразование Excel типов в .net'овские.

Прочитал этой библиотекой, с удовольствием, уже не один миллион строк, и решил показать её миру.

И так давайте попробуем достать табличку из простого текстового файла:
Нажмите на изображение для увеличения
Название: input.png
Просмотров: 30
Размер:	5.8 Кб
ID:	5345

В файле первые две строчки пропущены, что бы показать что делать если таблица начинается не с начала файла.
Затем идёт строка заголовков столбцов. Название заголовка, для наглядности, это тип данных в нём.
Далее три строчки с данными, разделителем является "|", разделитель может быть любым.

Итак, в Visual Studio создаем новый консольный проект, добавляем references:
- TableReaderLib.dll, содержит все общие классы для работы с таблицами, и определение интерфейса ISourceReader, который нужно реализовать для чтения конкретных типов файлов
- ReadersForTableReaderLib.dll - содержит реализации ISourceReader, которые не требуют дополнительных references. (Excel, например, требует)

Затем в коде нам нужно создать объект CsvSourceReader. Конструктор содержит всего три аргумента:
- filePath - путь к нашему файлу
- splitter - разделитель ячеек, в нашем случае "|"
- encoding - кодировка файла, почти всегда можно оставлять null
Объявление конструктора:
C#
1
public CsvSourceReader(string filePath, string splitter = null, Encoding encoding = null)
Затем нужно создать набор столбцов которые нам нужны из исходной таблицы. Для простоты не будем создавать переменную для каждого столбца а сразу инициализируем их в массив:
C#
1
2
3
4
5
6
7
var columns = new TableColumn[]
            {
                new TableColumn(){IndexInSource  = 0 },
                new TableColumn(){IndexInSource  = 1 },
                new TableColumn(){IndexInSource  = 2 },
                new TableColumn(){IndexInSource  = 3 },
            };
Вообще в классе TableColumn всего три члена - свойства:
- Name - строковое имя столбца, если для столбца не создавалась переменная, можно доставать ячейки по этому имени
- Type - Тип данных в ячейке. Если быть честным, не придумал как вытаскивать значения с помощью этого свойства. Используются перегруженные методы T GetCellValue<T>(TableColumn)
- IndexInSource - номер столбца в исходной таблице от 0.
Их объявления:
C#
1
2
3
public string Name  {get; set;}
public Type Type { get; set; }
public int IndexInSource { get; set; }
Теперь создаем объект TableReader, передаем в аргументы конструктора наш CsvSourceReader и Набор столбцов. Затем указываем, с какой строки в файле начинается наша таблица, и что в таблице первая строка является заголовками:
C#
1
2
3
var table = new TableReader(reader, columns);
table.StartRow = 2;
table.IsFirstRowHeaders = true;
Настройка готова!
Давайте прочитаем заголовки нашей таблицы в файле через свойство TableReader.SourceHeaders и посмотрим данные из второй строки:
C#
1
2
3
4
5
6
7
string headers = string.Join(";\t",table.SourceHeaders);
var row = table.ToArray()[2]; 
string rowInfo =
$"column 1 value:{row.GetCellValue<int>(0)}, type:{row.GetCellValue<int>(0).GetType()}" + Environment.NewLine +
$"column 2 value:{row.GetCellValue<string>(1)}, type:{row.GetCellValue<string>(1).GetType()}" + Environment.NewLine +
$"column 3 value:{row.GetCellValue<DateTime>(2)}, type:{row.GetCellValue<DateTime>(2).GetType()}" + Environment.NewLine +
$"column 4 value:{row.GetCellValue<TimeSpan>(3)}, type:{row.GetCellValue<TimeSpan>(3).GetType()}" + Environment.NewLine;
Результат:
Нажмите на изображение для увеличения
Название: result.png
Просмотров: 31
Размер:	4.7 Кб
ID:	5346

Для удобства, весь код примера целиком:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 var reader = new CsvSourceReader(@"D:\myCsv.csv", "|");
            var columns = new TableColumn[]
            {
                new TableColumn(){IndexInSource  = 0 },
                new TableColumn(){IndexInSource  = 1 },
                new TableColumn(){IndexInSource  = 2 },
                new TableColumn(){IndexInSource  = 3 },
            };
 
            var table = new TableReader(reader, columns);
            table.StartRow = 2; //пустые строки перед началом нашей таблицы в файле
            table.IsFirstRowHeaders = true; //таблица имеет строку заголовков
            string headers = string.Join(";\t",table.SourceHeaders);
            var row = table.ToArray()[2]; 
            string rowInfo =
                $"column 1 value:{row.GetCellValue<int>(0)}, type:{row.GetCellValue<int>(0).GetType()}" + Environment.NewLine +
                $"column 2 value:{row.GetCellValue<string>(1)}, type:{row.GetCellValue<string>(1).GetType()}" + Environment.NewLine +
                $"column 3 value:{row.GetCellValue<DateTime>(2)}, type:{row.GetCellValue<DateTime>(2).GetType()}" + Environment.NewLine +
                $"column 4 value:{row.GetCellValue<TimeSpan>(3)}, type:{row.GetCellValue<TimeSpan>(3).GetType()}" + Environment.NewLine;
            Console.WriteLine(headers);
            Console.WriteLine(rowInfo);
            Console.ReadLine();



Весь исходный код можно скачать на GitHub и использовать как хотите: https://github.com/setood/TableReaderLib

Буду рад, если кому-то библиотека поможет, обязательно сообщите. Это сильно мотивирует
Так же рад замечаниям, code review и помощь в развитии либы.
Размещено в Без категории
Просмотров 262 Комментарии 6
Всего комментариев 6
Комментарии
  1. Старый комментарий
    Аватар для Storm23
    У вас метод 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 вне форума
  2. Старый комментарий
    Цитата:
    Сообщение от Storm23 Просмотреть комментарий
    У вас метод 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();
                    }
                }
            }
    Это нехорошо, потому что файл будет читаться заново при каждом изменении каждого из этих свойств.
    Спасибо!
    По задумке, изменяя любое из этих свойств, мы как бы говорим что у нас "изменилась таблица", поэтому читать её сначала вполне логично.
    Однако это становится проблемой, только когда мы ещё не извлекли данные, и устанавливая эти свойства вызвали Reset 3-4 раза. В этом случае, нужно что бы метод Reset делал минимум действий, в идеале просто переместил указатель на начало таблицы в файле.

    В моей же реализаций метод Reset уничтожает старый StreamReader, создает новый, и пропускает строки до нужной нам. Это не оптимально (нужно будет исправить, спасибо), но в использовании никак не ощущается, так как StreamReader не читает весь файл а построчно - при вызове Next()

    Edited:
    Изменил пересоздание StreamReader на сброс позиции в начало.
    Запись от setood размещена 11.05.2019 в 08:57 setood вне форума
    Обновил(-а) setood 11.05.2019 в 09:33 (Коммит на замечание Storm23)
  3. Старый комментарий
    Аватар для Storm23
    Извините что придалбываюсь, но я люблю чистый код.
    Проблема не только в том, что создавался новый StreamReader. Проблема в том, что каждый раз при изменении свойств у вас каждый раз читается файл, вызываются методы SkipStartRows(), SkipAndReadHeaders() и SkipSkippedRows().
    И это вызывается по нескольку раз.
    То что они читают файл построчно - ничего не меняет. Для того, что бы читать построчно, все равно нужно читать все содержимое строк и искать их конец.
    А теперь представьте, что у вас гигабайтный csv файл и необходимая часть находится где-то в середине. У вас тогда этот огромный файл будет перелопачиваться почти целиком, при каждом изменении свойств. И это не будет быстро.

    Вам нужно сделать ленивую(отложенную) инициализацию. Свойства типа StartRow не должны вызывать метод Reset напрямую. Они должны лишь выставить флажок, типа needToReset = true.
    А при чтении данных из файла (во всех методах и свойствах, который возвращают информацию) нужно проверять этот флажок и если он выставлен, то вызывать Reset и сбрасывать флажок:
    C#
    1
    2
    3
    4
    5
    6
    
    if (needToReset)
    {
        needToReset = false;
        Reset();
    }
    ...
    Тогда Reset будет вызываться только один раз. И не в момент изменения свойств (что довольно абсурдно) а в момент получения информации из файла, что вполне логично.
    Запись от Storm23 размещена 12.05.2019 в 00:16 Storm23 вне форума
    Обновил(-а) Storm23 12.05.2019 в 00:18
  4. Старый комментарий
    Цитата:
    Сообщение от Storm23 Просмотреть комментарий
    Извините что придалбываюсь, но я люблю чистый код.
    Проблема не только в том, что создавался новый StreamReader. Проблема в том, что каждый раз при изменении свойств у вас каждый раз читается файл, вызываются методы SkipStartRows(), SkipAndReadHeaders() и SkipSkippedRows().
    И это вызывается по нескольку раз.
    То что они читают файл построчно - ничего не меняет. Для того, что бы читать построчно, все равно нужно читать все содержимое строк и искать их конец.
    А теперь представьте, что у вас гигабайтный csv файл и необходимая часть находится где-то в середине. У вас тогда этот огромный файл будет перелопачиваться почти целиком, при каждом изменении свойств. И это не будет быстро.

    Вам нужно сделать ленивую(отложенную) инициализацию. Свойства типа StartRow не должны вызывать метод Reset напрямую. Они должны лишь выставить флажок, типа needToReset = true.
    А при чтении данных из файла (во всех методах и свойствах, который возвращают информацию) нужно проверять этот флажок и если он выставлен, то вызывать Reset и сбрасывать флажок:
    C#
    1
    2
    3
    4
    5
    6
    
    if (needToReset)
    {
        needToReset = false;
        Reset();
    }
    ...
    Тогда Reset будет вызываться только один раз. И не в момент изменения свойств (что довольно абсурдно) а в момент получения информации из файла, что вполне логично.
    Это, просто, огонь!

    Edited:
    Переделал, как вы сказали. Теперь флаг needToReset проверяется при вызове MoveNext().
    Спасибо! захотите "придолбаться" еще - welcome!)
    Запись от setood размещена 12.05.2019 в 10:08 setood вне форума
    Обновил(-а) setood 12.05.2019 в 15:37 (Коммит на замечание Storm23)
  5. Старый комментарий
    Аватар для Storm23
    Цитата:
    Спасибо! захотите "придолбаться" еще - welcome!)
    Да пожалуйста.

    У вас интерфейс 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 Storm23 вне форума
    Обновил(-а) Storm23 14.05.2019 в 00:56
  6. Старый комментарий
    Цитата:
    Но дальше - хуже. CreateReaderClone принимает несколько параметров. Но оказывается, что для некоторых классов, реализующих ISourceReader, эти параметры не имеют смысла.
    Например, CsvSourceReader принимает и обрабатывает эти параметры, а SqlSourceReader - их все игнорирует и никак не использует. Это признак плохой архитектуры.
    Извиняюсь, этот ридер был не доделанный. Сейчас исправил с учётом замечаний, но стало ещё хуже))
    Когда я создавал библиотеку, предполагал, что заголовки таблицы могут быть в первой строке начиная от StartRow. На SqlReader стало понятно, что заголовки могут быть вообще отдельной сущностью и нужно переделывать всё от основания.

    В текущей реализации SqlSourceReader не может возвращать SQL названия столбцов. С чтением и фильтрацией строк всё Ок.

    --Edited
    Цитата:
    У вас интерфейс ISourceReader выглядит странно:
    Про интерфейс еще подумаю, напишу позже.
    Запись от setood размещена Сегодня в 10:13 setood вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru