Programming Taskbook


E-mail:

Пароль:

Регистрация пользователя   Восстановление пароля

 

ЮФУ SMBU

Электронный задачник по программированию

©  М. Э. Абрамян (Южный федеральный университет, Университет МГУ-ППИ в Шэньчжэне), 1998–2024

 

Решения | Java | Обработка файлов

PrevNext


Выполнение заданий на обработку файлов

Данная страница содержит подробное описание процесса решения типовой задачи на обработку двоичных файлов с числовой информацией, а также примеры решения задач на обработку строковых и текстовых файлов.

Двоичные файлы с числовой информацией: File48

Особенности выполнения заданий на обработку файлов рассмотрим на примере задания File48.

File48°. Даны три файла целых чисел одинакового размера с именами SA, SB, SC и строка SD. Создать новый файл с именем SD, в котором чередовались бы элементы исходных файлов с одним и тем же номером: A1B1C1, A2B2C2, ... .

Создание программы-заготовки и знакомство с заданием

Напомним, что проект-заготовку для решения задания можно создать с помощью модуля PT4Load. В созданный проект будет входить файл с именем MyTask.java. Приведем текст функции solve из данного файла (именно в эту функцию требуется ввести решение задачи):

public static void solve() throws Exception
{
    task("File48");

}

После запуска программы на экране появится окно задачника. На рисунке приведены два варианта представления окна — в режиме с динамической и с фиксированной компоновкой:


В первой строке раздела исходных данных указаны имена трех исходных файлов (SA, SB и SC) и одного результирующего (SD). В последующих строках раздела исходных данных показано содержимое исходных файлов. Элементы файлов отображаются бирюзовым цветом, чтобы подчеркнуть их отличие от обычных исходных данных (желтого цвета) и комментариев (светло-серого цвета).

В режиме с фиксированной компоновкой для отображения содержимого каждого двоичного файла отводится по одной строке. Поскольку размер файлов, как правило, превышает количество элементов, которое может уместиться на одной экранной строке, в режиме с фиксированной компоновкой предусмотрена возможность прокрутки (листания) элементов файла с помощью мыши или клавиатуры.

В режиме с динамической компоновкой на экране отображаются все элементы двоичных файлов, даже если для этого требуется использовать более одной экранной строки; при этом в начале каждой строки указывается порядковый номер первого элемента файла, приведенного в данной строке (элементы нумеруются от 1).

Следует заметить, что при каждом запуске программы с учебным заданием исходные файлы создаются под новыми именами и заполняются новыми данными, а исходные и результирующие файлы, созданные при предыдущем запуске программы, удаляются с диска.

Вернемся к нашей программе, только что запущенной на выполнение. Так как в ней не указаны операторы ввода-вывода, запуск программы считается ознакомительным, проверка решения не производится, а на экране отображается пример верного решения (в нашем случае это числа, которые должны содержаться в результирующем файле при правильном решении задачи).

Ввод исходных данных

Все классы стандартной библиотеки Java, обеспечивающие работу с файлами, определены пакете java.io. Для того чтобы при обращении к именам этих классов не требовалось дополнительно указывать имя пакета, в начале программы надо поместить следующий оператор:

import java.io.*;

Будем использовать класс RandomAccessFile, обеспечивающий прямой доступ к файловым элементам и имеющий встроенные методы для ввода и вывода данных всех стандартных скалярных типов. В функцию solve добавим фрагмент, позволяющий ввести имена исходных файлов и связать с этими файлами экземпляры класса RandomAccessFile. Поскольку мы собираемся работать с четырьмя файлами, удобно предусмотреть массив файлов:

public static void solve() throws Exception
{
    task("File48");
    RandomAccessFile[] f = new RandomAccessFile[4];
    for (int i = 0; i < 3; i++)
        f[i] = new RandomAccessFile(getString(), "r");
}

Мы намеренно ограничились тремя итерациями цикла, оставив непрочитанным имя результирующего файла. Конструктор класса RandomAccessFIle содержит два строковых параметра: имя файла и режим его открытия; строка "r" означает, что файл будет открыт только на чтение. Введенные имена файлов не сохраняются в строковых переменных, а сразу передаются в конструкторы соответствующих файловых объектов; это объясняется тем, что после создания этих объектов все остальные действия с файлами в нашей программе будут осуществляться без обращения к именам файлов.

Запуск нового варианта программы уже не будет считаться ознакомительным, поскольку в программе выполняется ввод исходных данных. Так как имя результирующего файла осталось непрочитанным, этот вариант решения будет признан неверным и приведет к сообщению «Введены не все требуемые исходные данные. Количество прочитанных данных: 3 (из 4)».

Пример программы, приводящей к ошибке времени выполнения

Изменим программу так, чтобы в цикле выполнялось не 3, а 4 итерации:

    for (int i = 0; i < 4; i++)

Теперь запуск программы приведет к следующему сообщению об ошибке: «Error java.io.FileNotFoundException: d8kf.tst"» (имя файла, естественно, будет другим). Сообщение, начинающееся со слова «Error», означает, что при работе программы произошла ошибка времени выполнения (runtime error). В программах на языке Java любая ошибка времени выполнения приводит к возбуждению исключения (exception). В тексте сообщения вначале указывается тип возникшего исключения (в данном случае FileNotFoundException), а затем — дополнительная информация (в данном случае — имя файла, который не удалось найти).

Создание пустого результирующего файла

Чтобы избежать ошибки времени выполнения, отсутствующий файл результатов следует открыть не в режиме "r" (только для чтения), при котором предполагается, что файл уже существует, а в режиме "rw" (чтение и запись), который обеспечивает автоматическое создание файла, если он отсутствует на диске. Для этого восстановим прежнее количество итераций цикла, равное 3, а после цикла добавим оператор, открывающий последний, четвертый файловый поток в режиме "rw":

    f[3] = new RandomAccessFile(getString(), "rw");

Кроме того, после завершения работы с файлами, открытыми в программе, их необходимо закрыть методом close. Поэтому добавим в конец функции solve соответствующий фрагмент:

for (int i = 0; i < 4; i++)
    f[i].close();

В результате функция solve примет следующий вид:

public static void solve() throws Exception
{
    task("File48");
    RandomAccessFile[] f = new RandomAccessFile[4];
    for (int i = 0; i < 3; i++)
        f[i] = new RandomAccessFile(getString(), "r");
    f[3] = new RandomAccessFile(getString(), "rw");

    /* */

    for (int i = 0; i < 4; i++)
        f[i].close();
}

Комментарий /* */ расположен в том месте программы, в котором можно выполнять операции ввода-вывода для всех четырех файлов: они уже открыты в результате выполнения конструктора объекта RandomAccessFile и еще не закрыты методом close этого объекта.

Запуск нового варианта программы не приведет к ошибке времени выполнения; более того, результирующий файл будет создан. Однако созданный файл останется пустым, т. е. не содержащим ни одного элемента. Начиная с версии 4.15, в этом случае на информационной панели выводится сообщение (на светло-синем фоне): «Запуск с правильным вводом данных: все требуемые данные введены, результирующий файл является пустым» (в предыдущих версиях в этой ситуации на информационной панели выводилось сообщение «Ошибочное решение», а в строке, которая должна содержать элементы результирующего файла, отображался текст EOF — «конец файла», End Of File).

Таким образом, нам осталось реализовать фрагмент алгоритма, обеспечивающий ввод и вывод файловых данных.

Использование неправильных типов для файловых элементов

Во всех ранее рассмотренных вариантах программы мы не использовали операции ввода-вывода для файлов. Поэтому тип файловых элементов нас не интересовал.

Однако при чтении данных из файла (и при их записи в файл) крайне важно правильно указывать тип файловых элементов. Чтобы продемонстрировать это на примере нашей программы, попытаемся прочесть из исходных файлов по одному элементу вещественного типа и запишем прочитанные элементы в файл результатов. Для этого заменим комментарий /* */ на следующий фрагмент:

    for (int i = 0; i < 3; i++)
        f[3].writeDouble(f[i].readDouble());

Данный фрагмент обеспечивает считывание одного вещественного элемента для каждого из трех исходных файлов и запись этих элементов в результирующий файл (в требуемом порядке). Подчеркнем, что мы неправильно выбрали метод чтения элементов (readDouble); тем не менее, компиляция программы пройдет успешно, а после ее запуска не произойдет ошибок времени выполнения.

Результат работы программы будет неожиданным: судя по экранной строке с содержимым результирующего файла, в него будут записаны не три, а шесть элементов, по два начальных элемента из каждого исходного файла. Объясняется это тем, что считывание из файла и последующая запись в файл одного «вещественного элемента» фактически приводит к считыванию и записи блока данных размером 8 байт, содержащего два последовательных целочисленных элемента исходного файла.

Итак, мы выяснили, что ошибки, связанные с несоответствием типов файловых элементов, не выявляются при компиляции и не всегда приводят к ошибкам времени выполнения. Это следует иметь в виду, и при появлении «странных» результирующих данных начинать поиск ошибки с проверки того, соответствуют ли используемые методы ввода-вывода типу файловых элементов.

В нашем случае для исправления ошибки достаточно заменить метод readDouble на readInt (данный метод обеспечивает считывание из файла элемента целого типа). После запуска исправленной программы мы получим все еще неверный, но вполне «понятный» результат: созданный файл будет содержать три элемента, совпадающих с начальными элементами исходных файлов.

Правильное решение, его тестирование и просмотр результатов

Приведем, наконец, верное решение задачи File48:

public static void solve() throws Exception
{
    task("File48");
    RandomAccessFile[] f = new RandomAccessFile[4];
    for (int i = 0; i < 3; i++)
        f[i] = new RandomAccessFile(getString(), "r");
    f[3] = new RandomAccessFile(getString(), "rw");
    while (f[0].getFilePointer() < f[0].length())
        for (int i = 0; i < 3; i++)
            f[3].writeInt(f[i].readInt());
    for (int i = 0; i < 4; i++)
        f[i].close();
}

От предыдущего варианта данное решение отличается добавлением цикла while, условие которого будет истинным, пока в первом файле будут оставаться непрочитанные элементы (в данном условии сравниваются значение текущей позиции файлового указателя первого из исходных файлов, полученное с помощью метода getFilePointer, и длина этого файла в байтах; равенство этих двух значений означает, что файловый указатель достиг маркера конца файла). Таким образом, цикл while обеспечивает считывание всех элементов из исходных файлов (напомним, что по условию задания все исходные файлы имеют одинаковый размер) и запись их в результирующий файл в нужном порядке.

После запуска этого варианта программы и успешного прохождения 5 тестов мы получим сообщение «Задание выполнено!». Нажав клавишу [F2], мы можем вывести на экран окно результатов, в котором будут перечислены все наши попытки решения задачи:

File48     j24/03 12:15 Ознакомительный запуск.
File48     j24/03 12:16 Введены не все требуемые исходные данные.
File48     j24/03 12:17 Результирующий файл не найден.
File48     j24/03 12:18 Error java.io.FileNotFoundException.
File48     j24/03 12:20 Запуск с правильным вводом данных.
File48     j24/03 12:22 Ошибочное решение.--2
File48     j24/03 12:24 Задание выполнено!

Строковые и текстовые файлы: File67, Text21

В данном пункте описываются особенности выполнения заданий на обработку строковых файлов (т. е. двоичных файлов, содержащих строковые данные), а также текстовых файлов, содержащих строки различной длины, оканчивающиеся маркерами конца строки.

Двоичные строковые файлы

В качестве примера задания на строковые файлы рассмотрим задание File67.

File67°. Дан строковый файл, содержащий даты в формате «день/месяц/год», причем под день и месяц отводится по две позиции, а под год — четыре (например, «16/04/2001»). Создать два файла целых чисел, первый из которых содержит значения дней, а второй — значения месяцев для дат из исходного строкового файла (в том же порядке).

При выполнении заданий на языке Java предполагается, что элементы в двоичном строковом файле всегда имеют одинаковую длину, равную 80 символам (строки меньшей длины дополняются справа пробелами). Это обеспечивает возможность прямого доступа к файловой строке по ее номеру, однако требует удаления завершающих пробелов после считывания строки из файла. Соответственно, перед записью строки в двоичный файл необходимо дополнить ее справа пробелами до 80 символов. Если при выполнении задания требуется определить количество элементов в строковом файле, то достаточно разделить размер файла (в байтах) на 80.

Следует подчеркнуть, что условие на длину элементов строковых файлов накладывается задачником Programming Taskbook; в обычных программах, не связанных с выполнением учебных заданий, в строковые файлы можно записывать строки любой длины.

При выполнении заданий на обработку строковых файлов, содержащих русские буквы, необходимо устанавливать для них кодировку «Cp1251». При работе с текстовыми файлами строковый параметр "Cp1251", определяющий кодировку, следует использовать в конструкторах классов InputStreamReader, OutputStreamWriter, Scanner, PrintWriter. В случае двоичных файлов считывание и запись символьных и строковых данных следует проводить с использованием переменных типа byte и byte[] соответственно; для преобразования прочитанного набора байтов в строку необходимо использовать соответствующий конструктор класса String с дополнительным параметром "Cp1251", определяющим кодировку; для обратного преобразования необходимо использовать метод getBytes класса String с тем же дополнительным параметром.

В задании File67 строки, содержащиеся в исходном строковом файле, включают только цифры и символы «/», поэтому устанавливать формат кодирования «Cp1251» для данного файла не требуется. Кроме того, при обработке этих строк нет необходимости удалять из них завершающие пробелы. Сразу приведем правильное решение задачи File67, учитывающее отмеченные выше особенности строковых файлов (предполагается, что в программе, решающей задание, с помощью директивы import указан пакет java.io, как и при решении задачи File48):

public static void solve() throws Exception
{
    task("File67");
    RandomAccessFile f = new RandomAccessFile(getString(),"r"),
        f1 = new RandomAccessFile(getString(),"rw"),
        f2 = new RandomAccessFile(getString(),"rw");
    byte[] buf = new byte[80];
    for (int i = 0; i < f.length() / 80; i++)
    {
        f.read(buf);
        String s = new String(buf);
        f1.writeInt(Integer.parseInt(s.substring(0, 2)));
        f2.writeInt(Integer.parseInt(s.substring(3, 5)));
    }
    f.close();
    f1.close();
    f2.close();
}

В данной программе для перебора всех строк, содержащихся в исходном файле, использован не цикл while (как в примере решения задания File48), а цикл for, число итераций которого равно количеству строк, содержащихся в файле. Для нахождения этого количества функция f.length(), возвращающая размер исходного файла в байтах, делится на 80, т. е. на размер элемента строкового файла (80 символов). Для считывания данных используется байтовый буфер buf размера 80, содержимое которого преобразуется в строку. Для выделения из строки требуемых фрагментов используется метод substring, в котором указываются индексы начального и конечного символа выделяемого фрагмента. Для преобразования строкового представления целого числа в само целое число используется метод parseInt класса Integer, являющегося объектной «оболочкой» для типа int.

Примечание. Для перебора всех строк исходного файла можно было бы использовать тот факт, что метод read возвращает число фактически считанных байт, а в случае его вызова при достижении конца файла вернет значение –1. В данном варианте цикла вызов метода read выполняется в условии цикла:

    while (f.read(buf) != -1)
    {
        String s = new String(buf);
        ...
    }

Текстовые файлы

В качестве примера задания на текстовые файлы рассмотрим задание Text21.

Text21°. Дан текстовый файл, содержащий более трех строк. Удалить из него последние три строки.

Поскольку текстовые файлы, в отличие от двоичных файлов, нельзя открыть одновременно на чтение и на запись, для изменения текстового файла необходимо воспользоваться вспомогательным файлом. Во вспомогательный файл записываются необходимые результирующие данные, после чего исходный файл удаляется с диска, а имя вспомогательного файла заменяется на имя исходного. Для выполнения действий, связанных с удалением и переименованием файлов, требуется использовать соответствующие метода класса File.

Далее, поскольку строки, содержащиеся в текстовом файле, могут иметь различную длину, для определения числа строк необходимо последовательно считать из файла все его строки. Для чтения данных из текстовых файлов следует использовать класс FileInputStream, заключенный в «оболочку» класса InputStreamReader, позволяющего указать формат кодирования. В свою очередь, класс InputStreamReader следует заключить в оболочку класса BufferedReader, обеспечивающего буферизованный ввод. Для записи данных в текстовый файл удобно использовать класс PrintWriter, в конструкторе которого также можно указать дополнительный параметр — формат кодирования. При выполнении заданий на обработку текстовых файлов необходимо указывать формат кодирования «Cp1251», поскольку в большинстве заданий исходные текстовые файлы содержат текст на русском языке

Приведем решение задачи Text21, учитывающее отмеченные выше особенности текстовых файлов (предполагается, что в программе, решающей задачу, подключен пакет java.io):

public static void solve() throws Exception
{
    task("Text21");
    String name1 = getString(), name2 = "$T21$.tmp";
    BufferedReader f1 = new BufferedReader(
        new InputStreamReader(
        new FileInputStream(name1), "Cp1251"));
    PrintWriter f2 = new PrintWriter(name2, "Cp1251");
    int n = 0;
    while (f1.readLine() != null)
        n++;
    f1.close();
    f1 = new BufferedReader(
        new InputStreamReader(
        new FileInputStream(name1), "Cp1251"));
    for (int i = 0; i < n-3; i++)
        f2.println(f1.readLine());
    f1.close();
    f2.close();
    (new File(name1)).delete();
    (new File(name2)).renameTo(new File(name1));
}

Для определения того, достигнут ли конец текстового файла, можно использовать возвращаемое значение функции readLine: при достижении конца файла эта функция возвращает null. Следует также обратить внимание на повторное открытие исходного файла для вторичного считывания его данных.

Приведенный выше вариант решения является неэффективным, поскольку требует двух просмотров исходного файла f1: первый — для определения его размера, который записывается в переменную n, второй — для создания вспомогательного файла f2, содержащего все строки исходного файла, кроме трех последних.

Задание Text21 можно выполнить и за один просмотр исходного файла, если воспользоваться следующим наблюдением: строка должна быть записана во вспомогательный файл, если после нее в исходном файле находятся по крайней мере три строки. Таким образом, записывать очередную строку во вспомогательный файл следует только после считывания из исходного файла трех следующих за ней строк. Благодаря такому упреждающему считыванию необходимость в предварительном определении размера исходного файла отпадает. Для хранения строк, которые уже считаны из исходного файла, но еще не записаны во вспомогательный файл, удобно использовать массив из трех элементов строкового типа.

Приведем программу, реализующую описанный выше эффективный однопроходный алгоритм решения задания:

public static void solve() throws Exception
{
    task("Text21");
    String name1 = getString(), name2 = "$T21$.tmp";
    BufferedReader f1 = new BufferedReader(
        new InputStreamReader(
        new FileInputStream(name1), "Cp1251"));
    PrintWriter f2 = new PrintWriter(name2, "Cp1251");
    String[] s = new String[3];
    for (int i = 0; i < 3; i++)
        s[i] = f1.readLine();
    int n = 0;
    String s0;
    while ((s0 = f1.readLine()) != null)
    {
        f2.println(s[n]);
        s[n] = s0;
        n = (n + 1) % 3;
    }
    f1.close();
    f2.close();
    (new File(name1)).delete();
    (new File(name2)).renameTo(new File(name1));
}

PrevNext

 

Рейтинг@Mail.ru

Разработка сайта:
М. Э. Абрамян, В. Н. Брагилевский

Последнее обновление:
01.01.2024