if (CurField <> '') then

Лабораторная работа №2.

Синтаксический анализ файлов с разделяющими запятыми

Часто встречающаяся задача - необходимость выполнить синтаксический ана­лиз файлов с запятыми-разделителями. Файл с запятыми-разделителями представля­ет собой текстовый файл, описывающий таблицу записей. Каждая строка в файле является отдельной записью, а сами строки делятся на поля записей, разделяемые одно от другого запятыми. (Иногда эту организацию файла называют форматом CSV (comma-separated values - значения, разделяемые запятыми).) При решении этой задачи возникает ряд затруднений (как всегда!). Поле может быть окружено кавычками (в результате значение поля может содержать запятые). Поле может от­сутствовать - в этом случае две запятые означают, что поля следуют одно за дру­гим.

Ниже приведен пример строки текста в формате CSV.

Julian,Bucknall,,43,"Author, and Columnist"

Эта строка содержит пять полей. Первые два поля содержат значения [Julian] и [Bucknall], третье поле не имеет значения, значение четвертого поля - [43], а пятого - [Author, and Columnist]. (В данном случае строковые значения заключены в квадратные скобки для показа того, что двойные кавычки в исходной строке отбрасываются.)

Будем считать, что конечной целью является создание подпрограммы, которая принимает строку и список строк, разбивает строку на отдельные поля и вставляет поля в список строк. Прежде чем приступить к созданию диаграммы конечного автомата, давайте сформулируем несколько правил в отношении допустимого формата строки CSV. Во-первых, все символы являются значащими, и единственные отбрасываемые символы - запятые (естественно, после того, как они были использованы для разбиения текста CSV) и двойные кавычки, в которые заключено значение поля. Более того, двойная кавычка имеет значение открывающей двойной кавычки, если она расположена за запятой (или является первым символом строки). В частности, например, это правило означает, что если бы в приведенном примере строки между запятой и открывающей двойной кавычкой имелся один пробел, подпрограмма разбила бы строку на шесть полей, двумя последними из которых были бы ["Author] и [and Columnist"]. Более того, если бы двойная кавычка была идентифицирована в качестве открывающей двойной кавычки, то следующая двойная кавычка закрывала бы значение поля, а следующим символом должна была бы быть запятая (или конец строки). В противном случае имеет мес­то ошибка, и строка усекается.

Теперь можно нарисовать блок-схему конечного автомата. На рис. 2 отражены пять состояний. Начальное состояние названо FieldStart. Если следующий символ - двойная кавычка, выполняется переход в состояние ScanQuoted, в котором выполняется отбор символов до тех пор, пока не встретится следующая двойная кавычка и не будет выполнен переход в состояние EndQuoted. Если следующий символ - запятая, можно снова выполнить переход в состояние FieldStart. Если это не так, выполняется переход в состояние ошибки, и выполнение программы прекращается. Пребывая в состоянии FieldStart, мы также можем получить запятую (поле считается пустым). Или, если мы получаем символ, который не является запятой или двойной кавычкой, осуществляется переход в состояние ScanField. В этом состоянии выполняется ввод и накопление символов до тех пор, пока не будет получена запятая.

 

 

 

Рисунок 2. Конечный автомат синтаксического анализа строки в формате CSV

 

Как видите, в конечном автомате условия ошибки можно указывать, создавая специальное состояние. (С другой стороны, написанное можно понимать буквально. В конечном автомате, в котором не используется переход в состояние ошибки, существует только один символ, который может привести к переходу из состояния EndQuoted, - запятая, а любой другой символ приводит к "исключению".)

Преобразование блок-схемы конечного автомата в код столь же простая задача, как и в предыдущем примере. Код реализации приведен в листинге 2.

 

Листинг 2. Синтаксический анализ строки CSV

 

procedure TDExtractFields(const S : string; aList : TStrings);

Type

TStates = (FieldStart, ScanField, ScanQuoted, EndQuoted, GotError);

Var

State : TStates;

Inx : integer;

Ch : AnsiChar;

CurField: string;

Begin

{инициализация путем очистки списка строк и начало работы в состоянии FieldStart}

Assert(aList <> nil, 'TDExtractFields: list is nil');

aList.Clear;

State := FieldStart;

CurField := '';

{считывание всех символов строки}

for Inx := 1 to length(S) do begin

{получение следующего символа}

Ch := S[Inx];

{обработать в зависимости от состояния}

Case State of

FieldStart :

Begin

case Ch of

'"' :

Begin

State := ScanQuoted;

end;

',' :

Begin

aList.Add('');

end;

Else

CurField := Ch;

State := ScanField;

end;

end;

ScanField :

Begin

if (Ch = ',') then begin

aList.Add(CurField);

CurField := '';

State := FieldStart;

End

Else

CurField := CurField + Ch;

end;

ScanQuoted :

Begin

if (Ch = '"') then

State := EndQuoted

Else

CurField := CurField + Ch;

end;

EndQuoted :

Begin

if (Ch = ',') then begin

aList.Add(CurField);

CurField := '';

State := FieldStart;

End

Else

State := GotError;

end;

GotError :

Begin

raise EtdStateException.Create(

FmtLoadStr(tdeStateBadCSV,

[UnitName, 'TDExtractFields']));

end;

end;

end;

{нахождение в состоянии ScanQuoted или GotError на момент окончания строки свидетельствует о наличии проблемыг связанной с закрывающей кавычкой}

if (State = ScanQuoted) or (State = GotError) then

raise EtdStateException.Create(

FmtLoadStr(tdeStateBadCSV,

[UnitName, 'TDExtractFields']));

{если текущее поле не пусто, добавить его в список}

if (CurField <> '') then

aList.Add(CurField);

end;

 

Задание. Используя процедуру TDExtractFields создать и отладить консольное приложение, реализующее рассмотренный алгоритм.