Выполнение заданий на обработку файлов
Данная страница содержит подробное описание процесса решения
типовой задачи на обработку двоичных файлов с числовой информацией,
а также примеры решения задач на обработку
строковых и текстовых файлов.
Двоичные файлы с числовой информацией: File48
Особенности выполнения заданий на обработку файлов рассмотрим на примере
задания File48.
File48°. Даны три файла целых чисел одинакового размера с именами SA,
SB, SC и
строка SD. Создать новый файл с именем SD, в котором чередовались бы
элементы исходных файлов с одним и тем же номером:
A1, B1, C1,
A2, B2, C2, ... .
Создание программы-заготовки и знакомство с заданием
Напомним, что программу-заготовку для решения задания можно создать с
помощью модуля PT4Load. Эта заготовка будет иметь следующий вид
(здесь и далее мы не будем указывать начальные директивы компилятора, заголовок program и директиву uses,
поскольку эта часть программы может отличаться для разных сред языка Pascal и разных версий задачника):
begin
Task('File48');
end.
После запуска программы на экране появится окно задачника. На рисунке
приведены два варианта представления окна в режиме с динамической
и с фиксированной компоновкой:
В первой строке раздела исходных данных указаны имена трех исходных файлов
(SA, SB и SC) и одного
результирующего (SD). В последующих строках раздела
исходных данных показано содержимое исходных файлов.
Элементы файлов отображаются бирюзовым цветом, чтобы подчеркнуть их отличие от обычных
исходных данных (желтого цвета) и комментариев (светло-серого цвета).
В режиме с фиксированной компоновкой для отображения содержимого каждого двоичного файла отводится по одной строке.
Поскольку размер файлов, как правило, превышает количество элементов,
которое может уместиться на одной экранной строке, в режиме с фиксированной компоновкой предусмотрена возможность
прокрутки (листания) элементов файла с помощью мыши или клавиатуры.
В режиме с динамической компоновкой на экране отображаются все элементы двоичных файлов, даже если для
этого требуется использовать более одной экранной строки; при этом в начале каждой строки указывается
порядковый номер первого элемента файла, приведенного в данной строке (элементы нумеруются от 1).
Следует заметить, что при каждом запуске программы с учебным
заданием исходные файлы создаются под новыми именами и заполняются новыми
данными, а исходные и результирующие файлы, созданные при предыдущем запуске
программы, удаляются с диска.
Вернемся к нашей программе, только что запущенной на выполнение. Так как в
ней не указаны операторы ввода-вывода, запуск программы считается
ознакомительным, проверка решения не производится, а на экране
отображается пример верного решения
(в нашем случае это числа, которые должны содержаться в результирующем
файле при правильном решении задачи).
Ввод исходных данных
Добавим в программу фрагмент, позволяющий ввести имена файлов и связать с
этими файлами соответствующие файловые переменные. Поскольку мы собираемся
работать с четырьмя файлами одного типа, удобно предусмотреть массив для
хранения всех файловых переменных:
var
i: integer;
s: string;
f: array[1..4] of file of integer;
begin
Task('File48');
for i := 1 to 3 do
begin
GetS(s);
Assign(f[i], s);
end;
end.
Мы намеренно ограничились тремя итерациями цикла, оставив непрочитанным
имя результирующего файла. Считывание имен файлов производится в одну и ту же
переменную s, поскольку после связывания файла, имеющего имя s, с
соответствующей файловой переменной (процедурой Assign) все остальные действия
с данным файлом в нашей программе будут осуществляться с использованием
файловой переменной, без обращения к имени файла.
Запуск нового варианта программы уже не будет считаться ознакомительным,
поскольку в программе выполняется ввод исходных данных. Так как имя
результирующего файла осталось непрочитанным, этот вариант решения будет
признан неверным и приведет к сообщению «Введены не все требуемые
исходные данные. Количество прочитанных данных: 3 (из 4)».
Изменим программу, заменив в заголовке цикла число 3 на 4, и вновь запустим
программу. Теперь все данные, необходимые для выполнения задания, в программу
введены. Однако задание не выполнено, поскольку результирующий файл не создан.
Начиная с версии 4.15, в этой ситуации выводится сообщение (на светло-синем фоне):
«Запуск с правильным вводом данных:
все требуемые исходные данные введены, результирующий файл не создан»
(в предыдущих версиях сообщение имело вид «Результирующий файл не найден»).
Пример программы, приводящей к ошибке времени выполнения
Добавим в тело цикла после процедуры Assign вызов процедуры Reset,
обеспечивающей открытие существующего файла:
var
i: integer;
s: string;
f: array[1..4] of file of integer;
begin
Task('File48');
for i := 1 to 4 do
begin
GetS(s);
Assign(f[i], s);
Reset(f[i]);
end;
end.
Теперь запуск программы приведет к следующему сообщению об ошибке:
«Error EInOutError: File not found». Сообщение, начинающееся со слова
«Error», означает, что при работе программы произошла ошибка времени
выполнения (runtime error). В программах, разработанных на языке Pascal,
любая ошибка времени выполнения приводит к возбуждению исключения (exception).
В тексте сообщения вначале указывается тип возникшего исключения (в данном
случае EInOutError), а затем его краткое описание.
Создание пустого результирующего файла
Для того чтобы избежать ошибки времени выполнения, отсутствующий файл
результатов следует открыть не процедурой Reset, а процедурой Rewrite, которая и
обеспечит создание этого файла. Далее, после завершения работы с файлами,
открытыми в программе, их необходимо закрыть процедурой Close. Добавим в
программу соответствующие операторы:
var
i: integer;
s: string;
f: array[1..4] of file of integer;
begin
Task('File48');
for i := 1 to 4 do
begin
GetS(s);
Assign(f[i], s);
if i < 4 then Reset(f[i])
else Rewrite(f[i]);
end;
{ * }
for i := 1 to 4 do
Close(f[i]);
end.
Комментарий { * } расположен в том месте программы, в котором можно
выполнять операции ввода-вывода для всех четырех файлов: они уже открыты
процедурами Reset или Rewrite и еще не закрыты процедурой Close.
Запуск нового варианта программы не приведет к ошибке времени выполнения; более того,
результирующий файл будет создан. Однако созданный файл останется пустым,
т. е. не содержащим ни одного элемента.
Начиная с версии 4.15, в этом случае на информационной панели выводится сообщение (на светло-синем фоне):
«Запуск с правильным вводом данных: все требуемые данные введены, результирующий файл является
пустым» (в предыдущих версиях в этой ситуации на информационной панели выводилось сообщение
«Ошибочное решение», а в строке, которая должна содержать элементы
результирующего файла, отображался текст EOF «конец файла», End Of File).
Таким образом, нам осталось
реализовать фрагмент алгоритма, обеспечивающий ввод и вывод файловых данных.
Использование неправильных типов для файловых элементов
Во всех ранее рассмотренных вариантах программы мы не использовали
операции ввода-вывода для файлов. Поэтому тип файловых элементов не играл
никакой роли: вместо типа file of integer мы могли использовать любой другой
файловый тип, например, file of real, и результат выполнения программы был бы тем
же самым.
Тип файловых элементов становится принципиально важным, если в программе
используются операции ввода-вывода для данного файла. Чтобы продемонстрировать
это на примере нашей программы, внесем в нее следующие изменения: в описании
массива f файловых переменных тип integer заменим на real, в раздел описаний
добавим описание переменной a типа real, в раздел операторов (в позицию,
помеченную комментарием { * }) добавим следующий фрагмент:
for i := 1 to 3 do
begin
Read(f[i], a);
Write(f[4], a);
end;
Данный фрагмент обеспечивает считывание одного элемента для каждого из
трех исходных файлов и запись этих элементов в результирующий файл (в требуемом
порядке). Подчеркнем, что мы неправильно указали типы файлов; тем не менее,
компиляция программы пройдет успешно, а после ее запуска не произойдет ошибок
времени выполнения.
Результат работы программы будет неожиданным: судя по экранной строке с
содержимым результирующего файла, в него будут записаны не три, а шесть
элементов, по два начальных элемента из каждого исходного файла. Объясняется это
тем, что после связывания файлов с файловыми переменными типа file of real
элементами файлов стали считаться вещественные числа (занимающие в памяти по 8
байтов), тогда как «на самом деле», т. е. по условию задания,
элементами файлов являются целые числа (занимающие в памяти по 4 байта).
Поэтому считывание из файла и последующая запись в файл одного
«вещественного элемента» фактически приводит к считыванию и записи
блока данных размером 8 байтов, содержащего два последовательных целочисленных
элемента исходного файла.
Итак, мы выяснили, что ошибки, связанные с несоответствием типов файлов, не
выявляются при компиляции и не всегда приводят к ошибкам времени выполнения.
Это следует иметь в виду, и при появлении «странных» результирующих
данных начинать поиск ошибки с проверки типов файловых переменных.
Заменив в нашей программе все описания real на integer и повторно запустив
программу, мы получим все еще неверный, но вполне «понятный»
результат: созданный файл будет содержать три элемента, совпадающих с
начальными элементами исходных файлов.
Правильное решение, его тестирование и просмотр результатов
Приведем, наконец, верное решение задачи File48:
var
i: integer;
s: string;
f: array[1..4] of file of integer;
a: integer;
begin
Task('File48');
for i := 1 to 4 do
begin
GetS(s);
Assign(f[i], s);
if i < 4 then Reset(f[i])
else Rewrite(f[i]);
end;
while not Eof(f[1]) do
for i := 1 to 3 do
begin
Read(f[i], a);
Write(f[4], a);
end;
for i := 1 to 4 do
Close(f[i]);
end.
От предыдущего варианта данное решение отличается добавлением цикла while
not Eof(f[1]) do, который обеспечивает считывание всех элементов из исходных
файлов (напомним, что по условию задания все исходные файлы имеют одинаковый
размер) и запись их в результирующий файл в нужном порядке.
После запуска этого варианта программы и успешного прохождения 5 тестов
мы получим сообщение «Задание выполнено!».
Нажав клавишу [F2], мы можем вывести на экран окно результатов, в котором будут
перечислены все наши попытки решения задачи:
File48 p24/03 12:15 Ознакомительный запуск.
File48 p24/03 12:16 Введены не все требуемые исходные данные.
File48 p24/03 12:17 Запуск с правильным вводом данных.
File48 p24/03 12:18 Error EInOutError.
File48 p24/03 12:20 Запуск с правильным вводом данных.
File48 p24/03 12:22 Ошибочное решение.--2
File48 p24/03 12:24 Задание выполнено!
Строковые и текстовые файлы: File67, Text21
В данном пункте описываются особенности выполнения заданий на обработку
строковых файлов (т. е. двоичных типизированных файлов, элементами которых
являются строки), а также текстовых файлов, содержащих строки различной длины,
оканчивающиеся маркерами конца строки.
Двоичные строковые файлы
В качестве примера задания на строковые файлы рассмотрим задание File67.
File67°. Дан строковый файл, содержащий даты в формате
«день/месяц/год», причем под день и месяц отводится по две
позиции, а под год четыре (например, «16/04/2001»).
Создать два файла целых чисел, первый из которых содержит значения дней, а
второй значения месяцев для дат из исходного строкового файла (в том
же порядке).
Для работы со строковыми файлами необходимо использовать
файловые переменные типа file of ShortString. Переменные, которые используются
для операций ввода-вывода, связанных со строковыми файлами, должны иметь тип
ShortString. Напомним, что тип ShortString соответствует «коротким»
строкам Паскаля (имеющим длину не более 255 символов), в то время как описатель
string в языке Free Pascal по умолчанию соответствует типу AnsiString,
позволяющему хранить строки практически неограниченной длины (до 2 гигабайт).
Аналогичным свойством обладает и тип string в языка PascalABC.NET, также позволяющий
хранить очень длинные строки.
Типы ShortString и string являются совместимыми по присваиванию, все
стандартные процедуры и функции, связанные с обработкой строк, могут принимать
в качестве параметров любой из этих типов. Следует, однако, заметить, что в
процедуре ввода GetS можно использовать только переменные типа string (т. е.
AnsiString). В то же время, для вывода строк процедурой PutS можно использовать
выражения любого строкового типа.
Сразу приведем правильное решение задачи File67, учитывающее отмеченные
выше особенности строковых файлов:
var
f: file of ShortString;
f1, f2: file of integer;
s: string;
ss: ShortString;
a: integer;
begin
Task('File67');
GetS(s);
Assign(f, s);
Reset(f);
GetS(s);
Assign(f1, s);
Rewrite(f1);
GetS(s);
Assign(f2, s);
Rewrite(f2);
while not Eof(f) do
begin
Read(f, ss);
a := StrToInt(Copy(ss, 1, 2));
Write(f1, a);
a := StrToInt(Copy(ss, 4, 2));
Write(f2, a);
end;
Close(f);
Close(f1);
Close(f2);
end.
Для преобразования строки, изображающей целое число, в соответствующее
числовое значение, была использована функция StrToInt. Поскольку в среде
Lazarus данная функция
описана в стандартном модуле SysUtils, этот модуль должен быть подключен к программе
директивой uses.
Начиная с версии 4.15, модуль SysUtils автоматически подключается к любой заготовке для среды
и Lazarus.
В модуле SysUtils имеется ряд других функций,
полезных при выполнении учебных заданий на обработку строк и файлов, например:
IntToStr(n) преобразует целое число n в его строковое представление;
TrimLeft(s) , TrimRight(s) , Trim(s) удаляет в строке s соответственно
начальные пробелы, конечные пробелы, начальные и конечные пробелы и возвращает
измененную строку (параметр s не изменяется);
AnsiLowerCase(s) , AnsiUpperCase(s) преобразует в строке s все буквы
(как латинские, так и русские) к нижнему или верхнему регистру соответственно и
возвращает измененную строку (параметр s не изменяется);
FileExists(s) возвращает True, если файл с именем s существует, и False в
противном случае.
Аналогичные функции имеются и в среде PascalABC.NET (в ней роль функций AnsiLowerCase и AnsiUpperCase
выполняют функции LowerCase и UpperCase), причем для их использования не требуется подключать
к программе дополнительные стандартные модули.
Заметим, что при решении задачи в среде Lazarus
без переменной a целого типа обойтись в программе нельзя, так как
в языке Pascal при использовании процедуры Write для записи данных в двоичный
файл в качестве ее параметров можно указывать только переменные. В частности,
следующий оператор является синтаксически ошибочным:
Write(f1, StrToInt(Copy(ss, 1, 2));
В языке PascalABC.NET допускается указывать выражения в процедуре Write
при записи данных в двоичный файл, поэтому в решении для этой среды переменную a можно не использовать.
Учитывая указанные особенности языка PascalABC.NET, а также имеющуюся в нем возможность
совмещения вызовов процедур Assign и Reset/Rewrite, решение для данного языка можно представить в виде более короткой
программы:
var
f: file of ShortString;
f1, f2: file of integer;
ss: ShortString;
begin
Task('File67');
Reset(f, ReadString);
Rewrite(f1, ReadString);
Rewrite(f2, ReadString);
while not Eof(f) do
begin
Read(f, ss);
Write(f1, StrToInt(Copy(ss, 1, 2)));
Write(f2, StrToInt(Copy(ss, 4, 2)));
end;
Close(f);
Close(f1);
Close(f2);
end.
Текстовые файлы
В качестве примера задания на текстовые файлы рассмотрим задание Text21.
Text21°. Дан текстовый файл, содержащий более трех строк. Удалить из него
последние три строки.
Поскольку текстовые файлы, в отличие от двоичных, нельзя открыть
одновременно на чтение и на запись, для изменения текстового файла необходимо
воспользоваться вспомогательным файлом. Во вспомогательный файл записываются
необходимые результирующие данные, после чего исходный файл удаляется с диска,
а имя вспомогательного файла заменяется на имя исходного. Далее, поскольку
строки, содержащиеся в текстовом файле, могут иметь различную длину, для
определения числа строк необходимо последовательно считать из файла все его
строки. Заметим, что, в отличие от двоичных строковых файлов, для чтения данных
из текстовых файлов вполне допустимо использовать переменные типа string.
Приведем решение задачи Text21, учитывающее отмеченные выше
особенности текстовых файлов:
var
name, s: string;
f1, f2: text;
n, i: integer;
begin
Task('Text21');
GetS(name);
Assign(f1, name);
Reset(f1);
Assign(f2, '$T21$.tmp');
Rewrite(f2);
n := 0;
while not Eof(f1) do
begin
Readln(f1, s);
Inc(n);
end;
Reset(f1);
for i := 1 to n - 3 do
begin
Readln(f1, s);
Writeln(f2, s);
end;
Close(f1);
Close(f2);
Erase(f1);
Rename(f2, name);
end.
Этот вариант решения является неэффективным, поскольку требует двух
просмотров исходного файла f1: первый для определения его размера,
который записывается в переменную n, второй для создания
вспомогательного файла f2, содержащего все строки исходного файла, кроме трех
последних.
Задание Text21 можно выполнить и за один просмотр исходного файла, если
воспользоваться следующим наблюдением: строка должна быть записана во вспо-
могательный файл, если после нее в исходном файле находятся по крайней мере три
строки. Таким образом, записывать очередную строку во вспомогательный файл
следует только после считывания из исходного файла трех следующих за ней строк.
Благодаря такому упреждающему считыванию необходимость в предварительном
определении размера исходного файла отпадает. Для хранения строк, которые уже
считаны из исходного файла, но еще не записаны во вспомогательный файл, удобно
использовать массив из трех элементов.
Приведем программу, реализующую описанный выше эффективный
однопроходный алгоритм решения задания:
var
name: string;
f1, f2: text;
s: array[0..2] of string;
n, i: integer;
begin
Task('Text21');
GetS(name);
Assign(f1, name);
Reset(f1);
Assign(f2, '$T21$.tmp');
Rewrite(f2);
for i := 0 to 2 do
Readln(f1, s[i]);
n := 0;
while not Eof(f1) do
begin
Writeln(f2, s[n]);
Readln(f1, s[n]);
n := (n + 1) mod 3;
end;
Close(f1);
Close(f2);
Erase(f1);
Rename(f2, name);
end.
Используя дополнительные возможности языка PascalABC.NET, в частности,
работу с файловыми переменными с применением точечной нотации
и описание переменных непосредственно в разделе оперторов,
мы можем получить более краткий вариант программы для этого языка:
begin
Task('Text21');
var f1 := OpenRead(ReadString);
var f2 := OpenWrite('$T21$.tmp');
var s := new string[3];
for var i := 0 to 2 do
s[i] := f1.ReadString;
var n := 0;
while not f1.Eof do
begin
f2.Writeln(s[n]);
s[n] := f1.ReadString;
n := (n + 1) mod 3;
end;
f1.Close;
f2.Close;
f1.Erase;
f2.Rename(f1.Name);
end.
|