четверг, 13 мая 2010 г.

State machine CSV parser

Понадобилось нам парсить CSV файлы.

Кто-то быстренько заимплементил свою версию. По инерции своя версия поддерживалась какое-то время, пока не стало понятно, что задача сильно осложняется различными вариациями формата CSV, различными формами экранирования, обработки пустых значений, выяснилось, что и формата есть несколько вариаций. Код оброс специальными случаями и условиями, стал нечитаем.

Задача стандартная, возник запрос, зачем делать самим. Быстрый поиск по интернету готовой версии выдал несколько левеньких вариантов со своими проблемами, а также более серьезные версии типа FileHelpers. Более серьезные версии как выяснилось не очень вписываются в нашу структуру классов. К примеру, FileHelpers как бы десериализует запись в объект класса. Звучит неплохо, однако для каждой колонки в целевом классе нужно было поле. А у нас файлы по 200-400 колонок.

Попробовали порефакторить – не впервой. Пошаговый рефакторинг не сильно улучшил ситуацию, попытки придумать новый подход тоже как-то не дали результата. В итоге все же наткнулись на идею написать парсер в виде конечного автомата – догадались не сами, но все же.

Конечный автомат – это по сути набор состояний системы и переходов из одного состояния в другое. Паттерн State эксплуатирует это математическое понятие. У каждого состояния есть свой способ обработки этого состояния и свои правила перехода в другие состояния. Конечный автомат также легко визуализируется при помощи UML State Chart диаграммы (ниже можно увидеть упрощенный пример).

Неожиданным плюсом оказалось также то, что конечный автомат в .NET делать очень просто, делегаты делают код простым и понятным.

Основная задача - распарсить одну строку файла:
private string[] SplitCsvLine(string line)
Идея в том, что мы пробегаемся по символам в строке и при встрече какого-либо специального символа мы переходим в новое состояние. В разных состояниях один и тот же символ переводит в различные состояния. Пример:


Код перехода из состояний в состояние:

CsvStateHandler currentState = HandleValueStart;

for (int i = 0, lineLength = line.Length; i < lineLength; i++)
{
       currentState = currentState(line[i], currentValue, values);
}
CurrentState – это делегат вида:

private delegate CsvStateHandler CsvStateHandler(
     char currentChar, 
     StringBuilder currentValue, 
     List<string> values);

Пример обработчика состояний:
private CsvStateHandler HandleValueStart(
        char currentChar, 
        StringBuilder currentValue, 
        List<string> values)
{
        currentValue.Clear();

        if (currentChar == quoteChar)
        {
            return HandleQuotedValue;
        }

        if (currentChar == delimiter)
        {
            values.Add(String.Empty);
            return HandleValueStart;
        }

        currentValue.Append(currentChar);

        return HandleSimpleValue;
}
Таким образом сложные многоэтажные ифы превратились в одноуровневые простые и целиком распределелись по простым компактным методам. Изменение логики и добавление очередного перехода стало простым и быстрым. Код стал понятен и поддерживаем.

2 комментария:

  1. хех, это только в начале всё так просто. с учётом заквотированых переносов строк и различных типов переносов - задача не такая простая.
    например:
    1st row,abcd\n,"e,f""g","h\n\ri"\n\r2nd row,j,k,l

    здесь \n\r - новая строка

    ОтветитьУдалить
  2. Ну, решали и такую проблему. Попозже слегка. Я выкладывал, кстати, код на github в следующей статье. Если честно, не помню, сделали мы уже там переносы строк или нет, но вот ссылка - продолжение

    ОтветитьУдалить