Programming Taskbook


E-mail:

Пароль:

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

 

ЮФУ

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

©  М. Э. Абрамян (Южный федеральный университет), 1998–2023

 

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

PrevNext


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

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

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

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

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

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

Напомним, что программу-заготовку для решения задания можно создать с помощью модуля PT4Load, используя ярлык PT4Load, находящийся в рабочем каталоге. Эта заготовка будет иметь следующий вид:

from pt4 import *
def solve():
    task("File48")

start(solve)

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


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

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

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

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

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

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

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

def solve():
    task("File48")
    f = []
    for i in range(3):
        f.append(open(get_str(), "rb"))
    #
    for i in f:
        i.close()

Мы намеренно ограничились тремя итерациями цикла, оставив непрочитанным имя результирующего файла. Прочитанное имя файла сразу передается функции open, которая открывает указанный файл в требуемом режиме и возвращает его дескриптор. Этот дескриптор сразу добавляется к списку f. Режим открытия файла в виде текстовой строки указывается во втором параметре функции open. В данном случае строка содержит две буквы: r — режим чтения файла (read) и b — указание на то, что файл является двоичным (бинарным, binary). В конце программы мы добавили цикл, в котором закрываются все открытые файлы.

Комментарий # расположен в том месте программы, в котором можно выполнять операции ввода-вывода для файлов: они уже открыты функцией open и еще не закрыты функцией close.

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

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

Изменим программу, заменив в заголовке цикла число 3 на 4, и вновь запустим программу. Окно задачника теперь будет содержать сообщение об ошибке вида «Error IOError: (Errno 2) No such file or directory: 'dzce.tst'» (имя файла, разумеется, будет отличаться от приведенного).

Более развернутое сообщение, содержащее оператор, который привел к ошибке, и номер его строки в файле, будет приведено в окне Python Shell:

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

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

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

def solve():
    task("File48")
    f = []
    for i in range(4):
        if i < 3:
            f.append(open(get_str(), "rb"))
        else:
            f.append(open(get_str(), "wb"))
    #
    for i in f:
        i.close()

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

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

Чтение и запись данных из двоичных файлов

Для чтения данных из файлов предусмотрено несколько функций. Поскольку мы собираемся читать двоичные данные, нам необходимо использовать функцию read с параметром — целым числом, определяющим, какое число байт следует прочесть из исходного файла (в отличие от других языков программирования, например, языка Pascal, в Python не предусмотрено непосредственного чтения числовых данных из двоичных файлов). Поскольку одно целое число в двоичном файле кодируется четырьмя байтами, нам необходимо использовать функцию read с параметром 4. Результат, возвращенный этой функцией (в Python 2.x это строка, в Python 3.x — значение типа bytes), можно сразу передавать в качестве параметра функции write для записи в создаваемый двоичный файл.

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

for i in range(3):
    f[3].write(f[i].read(4))

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

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

Для получения правильного решения нам необходимо повторить несколько раз приведенный ранее фрагмент программы, обеспечивающий считывание одного числа из каждого исходного файла и его запись в результирующий файл. К сожалению, в Python отсутствует простая стандартная функция, позволяющая определить размер файла (в байтах). Однако эту информацию можно получить с помощью вызова os.path.getsize(name), где name — имя файла (при этом дополнительно необходимо подключить к программе модуль os директивой import os. При организации цикла необходимо учесть, что количество итераций должно быть равно len // 4, где len — размер файла в байтах (поскольку каждый элемент файла занимает 4 байта). Получаем один из вариантов правильного решения:

from pt4 import *
import os
def solve():
    task("File48")
    f = []
    for i in range(4):
        if i < 3:
            f.append(open(get_str(), "rb"))
        else:
            f.append(open(get_str(), "wb"))
    len = os.path.getsize(f[0].name)
    for k in range(len // 4):
        for i in range(3):
            f[3].write(f[i].read(4))
    for i in f:
        i.close()

start(solve)

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

File48      y07/09 11:19 Ознакомительный запуск.
File48      y07/09 11:22 Введены не все требуемые исходные данные.
File48      y07/09 11:24 Error IOError.
File48      y07/09 11:26 Запуск с правильным вводом данных.
File48      y07/09 11:28 Ошибочное решение.
File48      y07/09 12:33 Задание выполнено!

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

Примечание. Задачу можно решить и не используя функцию os.path.getsize, если учесть следующую особенность функции read: при попытке ее вызова в конце файла она возвращает пустую строку. Правда, для того чтобы использовать эту особенность, придется предварительно сохранять прочитанную строку во вспомогательной переменной, после чего анализировать ее содержимое. Нам достаточно анализировать содержимое только той строки, которая считана из первого исходного файла (f[0]); обработку остальных двух файлов можно по-прежнему проводить в цикле (в котором вместо range(3) теперь надо указать range(1, 3)):

def solve():
    task("File48")
    f = []
    for i in range(4):
        if i < 3:
            f.append(open(get_str(), "rb"))
        else:
            f.append(open(get_str(), "wb"))
    while True:
        s = f[0].read(4)
        if s == "": break
        f[3].write(s)
        for i in range(1, 3):
            f[3].write(f[i].read(4))
    for i in f:
        i.close()

Вместо условия s == "" можно было использовать более короткое условие not s, так как в языке Python пустая строка (как и пустой список) интерпретируется в логическом выражении как значение False.

Получение числовых значений из двоичных файлов, преобразование файла: File25

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

Для кодирования/декодирования двоичной информации в языке Python предусмотрен стандартный модуль struct, содержащий функции pack(fmt, v1, ...) и unpack(fmt, s). Первым параметром этих функций является строка, определяющая формат двоичных данных. Для 4-байтного знакового числа целого типа предусмотрен формат "i", а для 8-байтного вещественного числа — формат "d" (от слова double — вещественное число двойной точности). Следует обратить внимание на то, что функция pack позволяет кодировать произвольное количество данных указанного формата, возвращая строку, подготовленную к записи в двоичный файл, а функция unpack всегда обрабатывает единственную двоичную строку, однако возвращает кортеж декодированных значений, поскольку допускается, чтобы двоичная строка содержала несколько закодированных данных указанного формата.

Познакомимся с использованием этих функций, выполняя задание File25 — первое из заданий группы File, связанное с преобразованием исходного файла:

File25°. Дан файл вещественных чисел. Заменить в нем все элементы на их квадраты.

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

Вначале приведем пример решения, не использующего вспомогательный файл:

def solve():
    task("File25")
    f = open(get_str(), "r+b")
    s = f.read(8)
    while s:
        x = struct.unpack("d", s)
        f.seek(-8,1)
        f.write(struct.pack("d", x[0]**2))
        f.flush()
        s = f.read(8)
    f.close()

Для возможности использования функций unpack и pack к программе необходимо подключить модуль struct; это делается оператором import struct в начале программы.

В данной программе исходный двоичный файл открывается одновременно и на чтение, и на запись; это обеспечивается указанием специального режима открытия файла "r+b". Здесь r, как обычно, означает доступ на чтение (в частности, это значит, что файл должен существовать), а символ + «позволяет» выполнять для данного файла и операции записи. Символ b по-прежнему указывает на то, что файл является двоичным.

Для перебора всех элементов файла используется цикл while, который завершится в тот момент, когда очередная операция чтения данных вернет пустую строку (таким образом, в данном варианте решения нам не требуется предварительно определять размер исходного файла). Считанная из файла непустая строка распаковывается функцией unpack (перед именем функции надо указать имя модуля, в котором она описана). Ее результатом является кортеж x, состоящий из единственного элемента x[0] (поскольку мы считываем данные порциями по 8 байт). Перед записью числа x[0]**2 (т. е. исходного значения, возведенного в квадрат), необходимо выполнить два действия. Во-первых, надо «вернуться» в файле на 8 байтов назад, чтобы квадрат числа был записан поверх его исходного значения (если этого не сделать, то квадрат будет записан поверх следующего файлового элемента). Это действие выполняется с помощью функции seek. Для того чтобы отсчет в этой функции велся от текущей позиции файлового указателя, в качестве второго параметра необходимо указать число 1 (если второй параметр равен 0 или отсутствует, то отсчет ведется от начала файла, а если второй параметр равен 2, то от его конца). Во-вторых, необходимо перевести полученное число в кодирующий его набор байтов; это действие выполняется функцией pack.

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

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

def solve():
    task("File25")
    f = open(get_str(), "rb")
    f1 = open("f25.tmp", "wb")
    s = f.read(8)
    while s:
        x = struct.unpack("d", s)
        f1.write(struct.pack("d", x[0]**2))
        s = f.read(8)
    f.close()
    f1.close()
    os.remove(f.name)
    os.rename(f1.name, f.name)

В данной программе вспомогательный файл связывается с файловой переменной f1 и открывается только на запись, тогда как исходный файл открывается только на чтение. Элементы исходного файла последовательно считываются в переменную s, после чего они декодируются, возводятся в квадрат, кодируются и записываются во вспомогательный файл. В конце программы с помощью функции remove выполняется удаление исходного файла f (в качестве параметра функции указывается имя файла, которое остается доступным для переменной f даже после закрытия файла), а затем выполняется переименование вспомогательного файла f1 с помощью функции rename. Обе эти функции реализованы в стандартном модуле os, который необходимо подключить к программе директивой import.


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

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

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

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

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

Двоичные строковые файлы отличаются от стандартных текстовых файлов тем, что в них не используются специальные маркеры конца строки, а все строки — файловые элементы — имеют одинаковый размер. Это позволяет использовать прямой доступ к любому файловому элементу, а также дает возможность изменять отдельные элементы-строки, не затрагивая их соседей. Размер строки может быть выбран произвольным образом; при выполнении заданий на языке Python с использованием задачника Programming Taskbook элементы строковых файлов имеют размер 80 символов. Таким образом, для чтения этих элементов надо использовать функцию read(80), а перед записью элементов в такой файл необходимо обеспечить их нужный размер (80 символов), дополняя их справа требуемым числом пробелов.

Для обработки строковых файлов описанного выше формата нет необходимости выполнять дополнительную перекодировку двоичных данных. Однако в задании File67 требуется сформировать два двоичных файлв с числовыми данными, для которых подобная перекодировка необходима. Поэтому в программе потребуется использовать функцию pack из модуля struct. Для выделения из строки фрагмента, изображающего целое число, надо использовать операцию среза для строки, к результату которой достаточно применить функцию int преобразования строки в целое число. Таким образом, решение задачи File67 примет следующий вид:

def solve():
    task("File67")
    f = open(get_str(), "rb")
    f1 = open(get_str(), "wb")
    f2 = open(get_str(), "wb")
    s = f.read(80)
    while s:
        f1.write(struct.pack('i', int(s[0:2])))
        f2.write(struct.pack('i', int(s[3:5])))
        s = f.read(80)
    f.close()
    f1.close()
    f2.close()

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

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

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

Строки в текстовом файле имеют разную длину, и, таким образом, нельзя изменить одну из них, не «затрагивая» соседние. Поэтому, хотя в языке Python (в отличие от большинства других языков программирования) и допускается открывать текстовые файлы одновременно на чтение и запись, обычно преобразование текстовых файлов выполняется с помощью вспомогательного текстового файла. Во вспомогательный файл записываются необходимые результирующие данные, после чего исходный файл удаляется с диска, а имя вспомогательного файла заменяется на имя исходного. Различная длина строк в текстовом файле также вынуждает организовывать их считывание последовательно, от первой до последней строки; при этом до завершения считывания файла невозможно определить количество содержащихся в нем строк. Для текстовых файлов при указании режима открытия не следует указывать символ b (признак двоичного файла). Таким образом, обычно текстовые файлы открываются в одном из трех режимов: "r" — чтение, "w" — запись, приводящая к полному обновлению содержимого файла, и "a" — режим дополнения (append), при котором новые данные добавляются в конец существующего файла. При этом для чтения отдельной текстовой строки должна использоваться функция readline(), которая возвращает не только содержимое строки, но и завершающий ее маркер в виде символа "\n". Пустая строка будет возвращена функцией readline только при попытке чтения данных за концом текстового файла.

Приведем решение задачи Text21, учитывающее отмеченные выше особенности текстовых файлов:

def solve():
    task("Text21")
    f = open(get_str(),"r")
    n = 0
    s = f.readline()
    while s:
        n += 1
        s = f.readline()
    f.close()
    f = open(f.name, "r")
    f1 = open("t21.tmp", "w")
    for i in range(n-3):
        f1.write(f.readline())
    f.close()
    f1.close()
    os.remove(f.name)
    os.rename(f1.name, f.name)

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

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

def solve():
    task("Text21")
    f = open(get_str(),"r")
    s = f.readlines()
    f.close()
    f = open(f.name,"w")
    for e in s[:-3]:
        f.write(e)
    f.close()

В этой программе используется функция readlines, считывающая все строки файла и возвращающая их в виде списка. Для получения всего списка s, за исключением последних трех элементов, проще всего воспользоваться операцией среза с отрицательным аргументом: s[:-3] (первый, отсутствующий, аргумент по умолчанию полагается равным 0, а отрицательный аргумент означает, что требуемая позиция отсчитывается от конца списка).

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

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

def solve():
    task("Text21")
    f = open(get_str(), "r")
    f1 = open("t21.tmp", "w")
    a = []
    for i in range(3):
        a.append(f.readline())
    n = 0
    s = f.readline()
    while s:
        f1.write(a[n])
        a[n] = s
        n = (n + 1) % 3
        s = f.readline()
    f.close()
    f1.close()
    os.remove(f.name)
    os.rename(f1.name, f.name)

Прокомментируем полученную программу. Вначале элементы вспомогательного списка a инициализируются первыми тремя строками из исходного файла (по условию файл содержит не менее трех строк). Для хранения индекса текущего элемента списка используется переменная n. На каждой итерации цикла while во вспомогательный файл записывается строка a[n], (в этот момент уже известно, что после данной строки в исходном файле содержатся, по крайней мере, три строки: две из этих строк уже хранятся в списке, а непустая третья строка содержится в s). Затем последняя прочитанная из исходного файла строка s записывается на место только что сохраненного во вспомогательном файле элемента списка a. Наконец, выполняется циклическое изменение номера n: 0 переходит в 1, 1 — в 2, 2 — в 0 (при этом используется операция % нахождения остатка от деления).

После завершения цикла while во вспомогательный файл будут записаны все строки исходного файла, кроме последних трех, а эти три последние строки будут содержаться в списке a.


PrevNext

 

Рейтинг@Mail.ru

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

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