Чтение неструктурированных файлов с маршрутами в Delphi и Pascal
Введение
При работе с текстовыми файлами разработчики часто сталкиваются с проблемой неструктурированных или "хаотичных" данных. В этой статье мы рассмотрим практический пример обработки файла с маршрутами, где данные организованы без четких разделителей между секциями. Мы разберем несколько подходов к решению этой задачи на языке Object Pascal (Delphi).
Постановка проблемы
Исходные данные представляют собой текстовый файл с информацией о маршрутах, где каждая секция содержит:
Название пункта назначения (без заголовка)
Трейлер (без заголовка)
Место отправления (с заголовком "Location:")
Время в пути (с заголовком "Time:")
Опциональную остановку (с заголовком "Stop:")
Подробные инструкции по маршруту (с заголовком "Directions:")
Основная сложность заключается в том, что между секциями нет явных разделителей, а первые две строки следующей секции могут быть ошибочно приняты за продолжение инструкций предыдущей.
Решение с использованием конечного автомата
Один из наиболее надежных подходов - реализация конечного автомата (state machine). Рассмотрим пример кода:
type
TDirs = record
Dest: string;
Trail: string;
Loc: string;
Time: Integer;
Stop: string;
Dirs: TStringList;
end;
var
DirArray: array of TDirs;
procedure ParseRouteFile(const FileName: string);
var
sl: TStringList;
i: Integer;
CurrentState: Integer;
CurrentDir: TDirs;
begin
sl := TStringList.Create;
try
sl.LoadFromFile(FileName);
SetLength(DirArray, 0);
CurrentState := 0; // 0 - ожидаем начало новой секции
i := 0;
while i < sl.Count do
begin
case CurrentState of
0: // Поиск начала новой секции
begin
if Trim(sl[i]) = '' then
begin
Inc(i);
Continue;
end;
// Инициализация новой записи
SetLength(DirArray, Length(DirArray) + 1);
CurrentDir := DirArray[High(DirArray)];
CurrentDir.Dirs := TStringList.Create;
// Первые две строки без заголовков
CurrentDir.Dest := Trim(sl[i]);
Inc(i);
if i >= sl.Count then Break;
CurrentDir.Trail := Trim(sl[i]);
Inc(i);
if i >= sl.Count then Break;
CurrentState := 1; // Переходим к ожиданию Location
end;
1: // Ожидаем Location
begin
if Pos('Location:', sl[i]) > 0 then
begin
CurrentDir.Loc := Trim(Copy(sl[i], Pos(':', sl[i]) + 1, MaxInt));
Inc(i);
if i >= sl.Count then Break;
CurrentState := 2; // Переходим к ожиданию Time
end
else
begin
// Ошибка формата
raise Exception.Create('Ожидался заголовок Location в строке ' + IntToStr(i+1));
end;
end;
2: // Ожидаем Time
begin
if Pos('Time:', sl[i]) > 0 then
begin
CurrentDir.Time := StrToIntDef(Trim(Copy(sl[i], Pos(':', sl[i]) + 1, MaxInt)), 0);
Inc(i);
if i >= sl.Count then Break;
CurrentState := 3; // Переходим к ожиданию Stop или Directions
end
else
begin
raise Exception.Create('Ожидался заголовок Time в строке ' + IntToStr(i+1));
end;
end;
3: // Ожидаем Stop или Directions
begin
if Pos('Stop:', sl[i]) > 0 then
begin
CurrentDir.Stop := Trim(Copy(sl[i], Pos(':', sl[i]) + 1, MaxInt));
Inc(i);
if i >= sl.Count then Break;
// После Stop должен идти Directions
if Pos('Directions:', sl[i]) = 0 then
raise Exception.Create('Ожидался заголовок Directions после Stop в строке ' + IntToStr(i+1));
end;
if Pos('Directions:', sl[i]) > 0 then
begin
Inc(i);
if i >= sl.Count then Break;
CurrentState := 4; // Переходим к чтению Directions
end
else
begin
raise Exception.Create('Ожидался заголовок Stop или Directions в строке ' + IntToStr(i+1));
end;
end;
4: // Чтение Directions
begin
// Читаем до следующего Location или конца файла
while (i < sl.Count) and (Pos('Location:', sl[i]) = 0) do
begin
CurrentDir.Dirs.Add(Trim(sl[i]));
Inc(i);
end;
// Удаляем последние две строки (они принадлежат следующей секции)
if CurrentDir.Dirs.Count > 2 then
begin
CurrentDir.Dirs.Delete(CurrentDir.Dirs.Count - 1);
CurrentDir.Dirs.Delete(CurrentDir.Dirs.Count - 1);
end;
// Сохраняем запись
DirArray[High(DirArray)] := CurrentDir;
// Возвращаемся к началу
CurrentState := 0;
// Откатываем на две строки назад
if i >= 2 then
Dec(i, 2)
else
i := 0;
end;
end;
end;
// Сохраняем последнюю запись, если она была в процессе заполнения
if CurrentState = 4 then
DirArray[High(DirArray)] := CurrentDir;
finally
sl.Free;
end;
end;
Альтернативное решение: чтение файла с конца
Интересный альтернативный подход предложил участник форума Zvoni - чтение файла с конца:
procedure ParseBackwards(const FileName: string);
var
sl: TStringList;
i: Integer;
CurrentDir: TDirs;
begin
sl := TStringList.Create;
try
sl.LoadFromFile(FileName);
SetLength(DirArray, 0);
i := sl.Count - 1;
while i >= 0 do
begin
if Trim(sl[i]) = '' then
begin
Dec(i);
Continue;
end;
// Создаем новую запись
SetLength(DirArray, Length(DirArray) + 1);
CurrentDir := DirArray[High(DirArray)];
CurrentDir.Dirs := TStringList.Create;
// Directions
while (i >= 0) and (Pos('Directions:', sl[i]) = 0) do
begin
CurrentDir.Dirs.Insert(0, Trim(sl[i]));
Dec(i);
end;
// Пропускаем строку с Directions:
if i >= 0 then Dec(i);
// Stop (если есть)
if (i >= 0) and (Pos('Stop:', sl[i]) > 0) then
begin
CurrentDir.Stop := Trim(Copy(sl[i], Pos(':', sl[i]) + 1, MaxInt));
Dec(i);
end;
// Time
if (i >= 0) and (Pos('Time:', sl[i]) > 0) then
begin
CurrentDir.Time := StrToIntDef(Trim(Copy(sl[i], Pos(':', sl[i]) + 1, MaxInt)), 0);
Dec(i);
end;
// Location
if (i >= 0) and (Pos('Location:', sl[i]) > 0) then
begin
CurrentDir.Loc := Trim(Copy(sl[i], Pos(':', sl[i]) + 1, MaxInt));
Dec(i);
end;
// Trail и Dest
if i >= 1 then
begin
CurrentDir.Trail := Trim(sl[i]);
CurrentDir.Dest := Trim(sl[i-1]);
Dec(i, 2);
end;
// Сохраняем запись
DirArray[High(DirArray)] := CurrentDir;
end;
finally
sl.Free;
end;
end;
Обработка ошибок и валидация данных
При работе с неструктурированными данными важно предусмотреть обработку ошибок:
function IsValidRoute(const Route: TDirs): Boolean;
begin
Result := (Route.Dest <> '') and
(Route.Loc <> '') and
(Route.Time > 0) and
(Route.Dirs.Count > 0);
end;
procedure ValidateRoutes;
var
i: Integer;
begin
for i := 0 to High(DirArray) do
begin
if not IsValidRoute(DirArray[i]) then
raise Exception.CreateFmt('Неверные данные в маршруте %d', [i+1]);
end;
end;
Оптимизация работы с памятью
Для больших файлов можно оптимизировать использование памяти:
Чтение файла построчно без загрузки всего содержимого в память
Использование более эффективных структур данных
Постепенная обработка данных с сохранением промежуточных результатов
Пример построчного чтения:
procedure ParseLineByLine(const FileName: string);
var
F: TextFile;
Line: string;
// ... остальные переменные как в предыдущих примерах
begin
AssignFile(F, FileName);
Reset(F);
try
while not Eof(F) do
begin
ReadLn(F, Line);
// Обработка строки аналогично предыдущим примерам
end;
finally
CloseFile(F);
end;
end;
Заключение
Обработка неструктурированных данных требует тщательного анализа формата входных данных и выбора подходящего алгоритма. В статье рассмотрены два основных подхода:
Конечный автомат - последовательная обработка данных с явным выделением состояний
Обратное чтение - оригинальный метод, особенно эффективный для данных с рекурсивной структурой
Для работы с подобными файлами в Delphi и Pascal рекомендуется:
Всегда проверять границы массивов и списков
Предусматривать обработку ошибок формата
Использовать структуры данных, соответствующие характеру обрабатываемой информации
Рассмотреть возможность предварительной очистки и нормализации данных
Приведенные примеры кода можно адаптировать для решения схожих задач обработки текстовых данных со сложной структурой.
Описание обработки неструктурированных файлов с маршрутами в Delphi и Pascal, включая методы разбора данных, обработку ошибок и оптимизацию.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.