Современные приложения - Разбор CSV-файлов в UWP-приложениях
Разбор файла Comma Separated Value (CSV) поначалу кажется достаточно простым. Однако эта задача весьма быстро становится все более сложной по мере того, как проясняются болевые точки CSV-файлов. Если вы не знакомы с этим форматом, то CSV-файлы хранят данные в виде чистого текста. Каждая строка в файле образует запись. В каждой записи есть поля, обычно отделяемые запятыми, отсюда и произошло название формата — значения, разделенные запятыми.
Сегодня разработчики используют стандартные форматы для обмена данными. Формат CSV восходит к ранним временам программной индустрии еще до появления JSON и XML. Хотя существует документ RFC (Request for Comments) для CSV-файлов (bit.ly/1NsQlvw), он не имеет официального статуса. Кроме того, он был создан в 2005 году, десятилетия спустя после того, как CSV-файлы начали появляться еще в 1970-х. В итоге существует довольно много вариаций CSV-файлов, и правила не совсем внятны. Например, поля CSV-файла могут разделяться табуляторами, точками с запятой или любым символом.
На практике стандартом де-факто стала Excel-реализация импорта и экспорта CSV, и именно она чаще всего встречается в индустрии — даже за пределами экосистемы Microsoft. Соответственно предположения, которые я допускаю в этой статье о «корректном» разборе и форматировании, будут базироваться на том, как Excel импортирует/экспортирует CSV-файлы. Большинство CSV-файлов отвечают реализации в Excel, тем не менее, таковыми являются не все файлы. В связи с этим я предложу стратегию для обработки такой неопределенности.
Возникает вопрос по существу: «Зачем писать средство разбора очень старого псевдоформата на самой новой платформе?». Ответ прост: во многих организациях имеются устаревшие системы, работающие с данными. Благодаря долгому существованию этого файлового формата почти все эти системы умеют экспортировать в CSV. Более того, экспорт данных в CSV требует минимума времени и усилий. А значит, существует множество CSV-файлов в наборах корпоративных и правительственных данных.
Проектирование средства разбора CSV универсального назначения
Несмотря на отсутствие официального стандарта CSV-файлы, как правило, имеют общие черты.
Вообще говоря, CSV-файлы являются чистым текстом, содержат по одной записи на строку, записи в каждой строке отделяются разделителем, имеют разделитель из одного символа и представляют поля в одинаковом порядке.
Эти общие черты обрисовывают универсальный алгоритм, который работал бы в три этапа.
- Разбиваем файл на строки по разделителю строк.
- Разбиваем каждую строку по разделителю полей.
- Присваиваем значение каждого поля какой-то переменной.
Это можно было бы реализовать сравнительно легко. Код на рис. 1 разбирает входную строку CSV в List<Dictionary<string, string>>.
Рис. 1. Разбор входной строки CSV в List<Dictionary<string,string>>
Этот подход отлично работает на примере следующего списка офисов, числа сотрудников и объемов продаж:
Чтобы получить значения из строки (string), вы должны были бы перебирать List и извлекать значения в Dictionary, используя индексацию полей от нуля. Например, получение поля офиса выглядело бы так:
Хотя это работает, код не столь читаем, каким мог бы быть.
Более эффективный словарь
Многие CSV-файлы включают строку заголовка (header row) для имени поля. Разработчикам было бы легче использовать средство разбора, если бы оно использовало имя поля в качестве ключа для словаря. Поскольку в конкретном CSV-файле может не оказаться строки заголовка, следует добавить свойство, сообщающее о наличии этой информации:
Например, образец CSV-файл со строкой заголовка мог бы выглядеть так:
В идеале, средство разбора CSV должно быть способно задействовать преимущества этой части метаданных. Это сделало бы код более читаемым. Получение поля Office Division выглядело бы следующим образом:
Пустые поля
Пустые поля часто встречаются в наборах данных. В CSV-файлах поле без информации представлено пустым полем в записи. При этом наличие разделителя все равно обязательно. Скажем, если для офиса East нет данных Employee, запись принимает такой вид:
Если же нет данных Unit Sales, а также данных Employee, запись выглядит так:
У каждой организации свои стандарты качества данных. Некоторые могут помещать в пустое поле значение по умолчанию, чтобы сделать CSV-файл более понятным человеку. Значения по умолчанию для чисел — это обычно 0 или NULL, а для строк — "" или NULL.
Сохранение гибкости
Учитывая всю неоднозначность файлового формата CSV, код не может делать никаких допущений. Нет никаких гарантий, что разделитель полей будет запятой, а разделитель записей — новой строкой.
Соответственно оба они будут свойствами класса CSVParser:
Чтобы упростить разработчикам использование этого компонента, нужно задать параметры по умолчанию, которые будут применяться в большинстве случаев:
Если кто-то пожелает изменить разделитель по умолчанию на табулятор, то необходимый код весьма прост:
Экранированные символы
Что будет, если само поле содержит символ-разделитель, например запятую? А если вместо объемов продаж по регионам данные содержат информацию о городе и штате? Как правило, в CSV-файлах эту проблему обходят, заключая все поле в кавычки наподобие:
Этот алгоритм превратил бы одно значение поля “New York, NY” в два отдельных поля со значениями, разбитыми по запятой: “New York” и “NY”.
В этом случае разделение названий города и штата может не оказаться не столь важным, но данные по-прежнему загрязняются дополнительными символами-кавычками. Хотя здесь их достаточно легко удалить, выполнить очистку более сложных данных может быть не так просто.
Теперь усложним задачу
Метод экранирования запятых внутри полей вводит необходимость в экранировании другого символа: кавычек. Что будет, если, например, в исходных данных были кавычки, как показано в табл. 1?
Табл. 1. Исходные данные с кавычками
Office Division Employees Unit Sales Office Motto New York, NY 73 8300 “We sell great products” Richmond, VA 42 3000 “Try it and you'll want to buy it” San Jose, CA 35 4250 “Powering Silicon Valley!” Chicago, IL 18 1200 “Great products at great value”
Исходный текст в самом CSV-файле выглядел бы так:
Один символ кавычек (“) после экранирования превращается в три символа кавычек (“””), что добавляет алгоритму интересную особенность. Конечно, сразу же возникает резонный вопрос: почему одна кавычка превращается в три? Как и в поле Office Division, содержимое этого поля заключается в кавычки. Чтобы экранировать символы кавычек, которые являются частью контента, они удваиваются. Поэтому “ становится “”.
Другой пример (табл. 2) более наглядно продемонстрирует этот процесс.
Табл. 2. Данные цитат
Quote "The only thing we have to fear is fear itself." -President Roosevelt "Logic will get you from A to B. Imagination will take you everywhere." -Albert Einstein
Данные из табл. 2 были бы представлены в CSV следующим образом:
Теперь должно быть понятнее, что поле заключается в кавычки и что индивидуальные кавычки в содержимом поля удваиваются.
Пограничные случаи
Как уже упоминалось, не все файлы соответствуют Excel-реализации CSV. Отсутствие настоящей спецификации CSV затрудняет написание одного средства разбора для обработки всех существующих CSV-файлов. Пограничные случаи будут встречаться вне всяких сомнений, а значит, код должен оставлять дверь открытой для интерпретации и адаптации.
Инверсия управления спешит на помощь
Учитывая размытость стандарта CSV, не практично писать универсальное средство разбора для всех случаев. Гораздо разумнее писать средство разбора, подходящее для конкретных потребностей какого-либо приложения. Используя инверсию управления (Inversion of Control), вы можете адаптировать механизм разбора под конкретные требования.
С этой целью я создам интерфейс, определяющий две базовые функции синтаксического разбора: для получения записей и извлечения полей. Я решил сделать интерфейс IParserEngine асинхронным. Это гарантирует, что любое приложение, использующее этот компонент, не перестанет отвечать при разборе CSV-файла даже очень большого размера:
После этого я добавляю в класс CSVParser следующее свойство:
И предлагаю разработчикам выбор: использовать средство разбора по умолчанию или встроить собственное. Чтобы упростить эту задачу, я перегрузил конструктор:
Теперь класс CSVParser предоставляет базовую инфраструктуру, а реальная логика синтаксического анализа содержится в интерфейсе IParserEngine. Для удобства разработчиков я создал DefaultParserEngine, который может обрабатывать большинство CSV-файлов.
Задача для читателя
Я принял во внимание большинство вероятных сценариев, с которыми могут столкнуться разработчики при разборе CSV-файлов. Однако неопределенность природы CSV-формата делает непрактичным создание универсального средства разбора. Учет всех вариаций и пограничных случаев добавил бы существенные издержки и сложность наряду с негативным влиянием на производительность.
Уверен, что на практике вы встретите CSV-файлы, которые DefaultParserEngine не сумеет обработать. И здесь очень хорошо подходит шаблон встраивания зависимостей. Если разработчику нужно средство разбора, которое может обрабатывать пограничные случаи, или требуется написать нечто более производительное, это определенно приветствуется. Механизмы разбора можно заменять без изменений в коде-потребителе.
Заключение
CSV-файлы — пережиток прошлого, но, несмотря на наличие более удобных и эффективных форматов XML и JSON, по-прежнему широко используются как формат для обмена данными. CSV-файлам недостает общепринятой спецификации или стандарта, и, хотя у них много общих черт, вы не можете быть уверены, что ожидаемые символы окажутся в конкретном файле. Это делает разбор CSV-файла нетривиальным упражнением.
Будь на то выбор, большинство разработчиков, по-видимому, исключило бы CSV-файлы из своих решений. Однако их широкая распространенность в устаревших корпоративных и правительственных наборах данных препятствует этому в большинстве случаев.
Если в двух словах, существует потребность в средстве разбора CSV для приложений Universal Windows Platform (UWP), и оно должно быть гибким и надежным. Заодно я продемонстрировал здесь применение на практике встраивание зависимостей, которое и обеспечивает гибкость. Хотя эта статья и приведенный в ней код ориентированы на UWP-приложения, сама концепция и код применимы к другим платформам, способным работать с C#, например к Microsoft Azure или настольным Windows-приложениям.
Исходный код можно скачать по ссылке bit.ly/1To1IVI.
Фрэнк Ла-Вине (Frank La Vigne) —идеолог технологий в группе Microsoft Technology and Civic Engagement, где он помогает пользователям применять технологии и формировать соответствующие сообщества. Регулярно ведет блог на FranksWorld.com и имеет канал на YouTube под названием «Frank’s World TV» (youtube.com/FranksWorldTV).
Выражаю благодарность за рецензирование статьи эксперту Рэчел Аппель (Rachel Appel).