Чтение и запись файлов в Python

Оглавление

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

В этом уроке вы узнаете:

  • Из чего состоит файл и почему это важно в Python
  • Основы чтения и записи файлов в Python
  • Несколько основных сценариев чтения и записи файлов

Этот учебник в основном предназначен для начинающих и средних Pythonистов, но здесь есть несколько советов, которые могут оценить и более продвинутые программисты.

Что такое файл?

Прежде чем перейти к работе с файлами в Python, важно понять, что именно представляет собой файл и как современные операционные системы обрабатывают некоторые его аспекты.

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

Файлы в большинстве современных файловых систем состоят из трех основных частей:

  1. Header: метаданные о содержимом файла (имя файла, размер, тип и так далее)
  2. Data: содержимое файла, записанное создателем или редактором
  3. Конец файла (EOF): специальный символ, указывающий на конец файла

The file format with the header on top, data contents in the middle and the footer on the bottom.

То, что представляют собой эти данные, зависит от используемой спецификации формата, которая обычно представлена расширением. Например, файл с расширением .gif, скорее всего, соответствует спецификации Graphics Interchange Format. Существуют сотни, если не тысячи, расширений файлов . В этом учебнике вы будете работать только с расширениями файлов .txt или .csv.

Пути файлов

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

  1. Путь к папке: местоположение папки файла в файловой системе, где последующие папки разделяются прямой косой чертой / (Unix) или обратной косой чертой \ (Windows)
  2. Имя файла: фактическое имя файла
  3. Расширение: конец пути к файлу, перед которым ставится точка (.), используемая для указания типа файла

Вот небольшой пример. Допустим, у вас есть файл, расположенный в файловой структуре следующего вида:

/
│
├── path/
|   │
│   ├── to/
│   │   └── cats.gif
│   │
│   └── dog_breeds.txt
|
└── animals.csv

Допустим, вы хотите получить доступ к файлу cats.gif, а ваше текущее местоположение находится в той же папке, что и path. Чтобы получить доступ к файлу, вам нужно пройти через папку path, затем через папку to и, наконец, добраться до файла cats.gif. Путь к папке - path/to/. Имя файла - cats. Расширение файла - .gif. Таким образом, полный путь будет path/to/cats.gif.

Допустим, что ваше текущее местоположение или текущий рабочий каталог (cwd) находится в папке to нашего примера структуры папок. Вместо того чтобы ссылаться на файл cats.gif по полному пути path/to/cats.gif, на него можно просто сослаться по имени файла и расширению cats.gif.

/
│
├── path/
|   │
|   ├── to/  ← Your current working directory (cwd) is here
|   │   └── cats.gif  ← Accessing this file
|   │
|   └── dog_breeds.txt
|
└── animals.csv

Но как насчет dog_breeds.txt? Как получить к нему доступ, не используя полный путь? Для перемещения на один каталог вверх можно использовать специальные символы двойной точки (..). Это означает, что ../dog_breeds.txt будет ссылаться на файл dog_breeds.txt из каталога to:

/
│
├── path/  ← Referencing this parent folder
|   │
|   ├── to/  ← Current working directory (cwd)
|   │   └── cats.gif
|   │
|   └── dog_breeds.txt  ← Accessing this file
|
└── animals.csv

Двойная точка (..) может быть использована для перехода по нескольким каталогам, расположенным выше текущего каталога. Например, чтобы получить доступ к animals.csv из папки to, вы используете ../../animals.csv.

Окончания строк

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

Позже этот стандарт был стандартизирован для телепринтеров как Международной организацией по стандартизации (ISO), так и Американской ассоциацией стандартов (ASA). Стандарт ASA гласит, что в конце строки должна использоваться последовательность символов возврата каретки (CR или \r) и перевода строки (LF или \n) CR+LF или \r\n). Однако стандарт ISO допускает использование либо символов CR+LF, либо только символа LF.

В Windows для обозначения новой строки используются символы CR+LF, а в Unix и более новых версиях Mac - только символ LF. Это может вызвать некоторые сложности при обработке файлов в операционной системе, которая отличается от исходной. Вот небольшой пример. Допустим, мы изучаем файл dog_breeds.txt, который был создан в системе Windows:

Pug\r\n
Jack Russell Terrier\r\n
English Springer Spaniel\r\n
German Shepherd\r\n
Staffordshire Bull Terrier\r\n
Cavalier King Charles Spaniel\r\n
Golden Retriever\r\n
West Highland White Terrier\r\n
Boxer\r\n
Border Terrier\r\n

Этот же вывод будет интерпретирован на устройстве Unix по-другому:

Pug\r
\n
Jack Russell Terrier\r
\n
English Springer Spaniel\r
\n
German Shepherd\r
\n
Staffordshire Bull Terrier\r
\n
Cavalier King Charles Spaniel\r
\n
Golden Retriever\r
\n
West Highland White Terrier\r
\n
Boxer\r
\n
Border Terrier\r
\n

Это может сделать итерацию по каждой строке проблематичной, и вам может понадобиться учитывать подобные ситуации.

Кодировки символов

Еще одна распространенная проблема, с которой вы можете столкнуться, - это кодировка байтовых данных. Кодировка - это перевод байтовых данных в читаемые человеком символы. Обычно это делается путем присвоения числового значения символу. Двумя наиболее распространенными кодировками являются форматы ASCII и UNICODE. ASCII может хранить только 128 символов, тогда как Unicode может содержать до 1 114 112 символов.

ASCII - это подмножество Unicode (UTF-8), что означает, что ASCII и Unicode имеют одинаковые числовые значения для символов. Важно отметить, что разбор файла с неправильной кодировкой может привести к сбоям или неправильному отображению символа. Например, если файл был создан с использованием кодировки UTF-8, а вы пытаетесь разобрать его с использованием кодировки ASCII, то при наличии символа, который находится за пределами этих 128 значений, будет выдана ошибка.

Открытие и закрытие файла в Python

Когда вы хотите работать с файлом, первое, что нужно сделать, - это открыть его. Для этого вызывается встроенная функция open() . open() имеет один обязательный аргумент - путь к файлу. open() имеет единственный возврат - объект file:

file = open('dog_breeds.txt')

После того как вы открыли файл, следующее, что нужно узнать, - это как его закрыть.

Предупреждение: Вы должны всегда убедиться, что открытый файл правильно закрыт. Чтобы узнать, почему, ознакомьтесь с учебником "Почему важно закрывать файлы в Python?"

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

Когда вы работаете с файлом, есть два способа обеспечить правильное закрытие файла, даже при возникновении ошибки. Первый способ закрыть файл - это использовать блок try-finally:

reader = open('dog_breeds.txt')
try:
    # Further file processing goes here
finally:
    reader.close()

Если вы не знакомы с тем, что такое блок try-finally, прочитайте статью Python Exceptions: An Introduction.

Второй способ закрыть файл - использовать оператор with:

with open('dog_breeds.txt') as reader:
    # Further file processing goes here

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

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

with open('dog_breeds.txt', 'r') as reader:
    # Further file processing goes here

Другие параметры режимов подробно описаны в Интернете, но наиболее часто используемыми являются следующие:

Character Meaning
'r' Открыто для чтения (по умолчанию)
'w' Открыть для записи, предварительно усекая (перезаписывая) файл
'rb' or 'wb' Открытие в двоичном режиме (чтение/запись с использованием байтовых данных)

Давайте вернемся назад и поговорим немного о файловых объектах. Файловый объект - это:

"объект, предоставляющий файлово-ориентированный API (с такими методами, как read() или write()) для базового ресурса." (Source)

Существует три различные категории файловых объектов:

  • Текстовые файлы
  • Буферизованные двоичные файлы
  • Необработанные двоичные файлы

Каждый из этих типов файлов определен в модуле io. Вот краткая информация о том, как все устроено.

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

Текстовый файл - это самый распространенный файл, с которым вы можете столкнуться. Вот несколько примеров того, как открываются эти файлы:

open('abc.txt')

open('abc.txt', 'r')

open('abc.txt', 'w')

При работе с такими типами файлов open() вернет объект TextIOWrapper file:

>>> file = open('dog_breeds.txt')
>>> type(file)
<class '_io.TextIOWrapper'>

Это файловый объект по умолчанию, возвращаемый open().

Типы буферизованных двоичных файлов

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

open('abc.txt', 'rb')

open('abc.txt', 'wb')

При работе с этими типами файлов open() вернет либо BufferedReader, либо BufferedWriter объект файла:

>>> file = open('dog_breeds.txt', 'rb')
>>> type(file)
<class '_io.BufferedReader'>
>>> file = open('dog_breeds.txt', 'wb')
>>> type(file)
<class '_io.BufferedWriter'>

Типы сырых файлов

Тип необработанного файла:

"обычно используется как низкоуровневый строительный блок для бинарных и текстовых потоков." (Источник)

Поэтому обычно его не используют.

Вот пример того, как открываются эти файлы:

open('abc.txt', 'rb', buffering=0)

При работе с такими типами файлов open() вернет объект FileIO file:

>>> file = open('dog_breeds.txt', 'rb', buffering=0)
>>> type(file)
<class '_io.FileIO'>

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

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

Method What It Does
.read(size=-1) Считывание из файла происходит по количеству байтов размера. Если аргумент не передан или передан None или -1, то считывается весь файл.
.readline(size=-1) При этом считывается максимальное количество символов из строки. Чтение продолжается до конца строки, а затем разворачивается обратно. Если аргумент не передан или передано None или -1, то читается вся строка (или остаток строки).
.readlines() Это считывает оставшиеся строки из объекта file и возвращает их в виде списка.

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

>>> with open('dog_breeds.txt', 'r') as reader:
>>>     # Read & print the entire file
>>>     print(reader.read())
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier

Вот пример того, как прочитать 5 байт строки каждый раз, используя метод Python .readline():

>>> with open('dog_breeds.txt', 'r') as reader:
>>>     # Read & print the first 5 characters of the line 5 times
>>>     print(reader.readline(5))
>>>     # Notice that line is greater than the 5 chars and continues
>>>     # down the line, reading 5 chars each time until the end of the
>>>     # line and then "wraps" around
>>>     print(reader.readline(5))
>>>     print(reader.readline(5))
>>>     print(reader.readline(5))
>>>     print(reader.readline(5))
Pug

Jack
Russe
ll Te
rrier

Вот пример того, как прочитать весь файл в виде списка, используя метод Python .readlines():

>>> f = open('dog_breeds.txt')
>>> f.readlines()  # Returns a list object
['Pug\n', 'Jack Russell Terrier\n', 'English Springer Spaniel\n', 'German Shepherd\n', 'Staffordshire Bull Terrier\n', 'Cavalier King Charles Spaniel\n', 'Golden Retriever\n', 'West Highland White Terrier\n', 'Boxer\n', 'Border Terrier\n']

Приведенный выше пример также можно выполнить, используя list() для создания списка из файлового объекта:

>>> f = open('dog_breeds.txt')
>>> list(f)
['Pug\n', 'Jack Russell Terrier\n', 'English Springer Spaniel\n', 'German Shepherd\n', 'Staffordshire Bull Terrier\n', 'Cavalier King Charles Spaniel\n', 'Golden Retriever\n', 'West Highland White Terrier\n', 'Boxer\n', 'Border Terrier\n']

Итерация над каждой строкой в файле

Обычно при чтении файла требуется итерация по каждой строке. Вот пример использования метода Python .readline() для выполнения такой итерации:

>>> with open('dog_breeds.txt', 'r') as reader:
>>>     # Read and print the entire file line by line
>>>     line = reader.readline()
>>>     while line != '':  # The EOF char is an empty string
>>>         print(line, end='')
>>>         line = reader.readline()
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier

Другой способ перебора каждой строки в файле - использовать метод Python .readlines() объекта file. Помните, что .readlines() возвращает список, каждый элемент которого представляет собой строку в файле:

>>> with open('dog_breeds.txt', 'r') as reader:
>>>     for line in reader.readlines():
>>>         print(line, end='')
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier

Однако приведенные выше примеры можно еще больше упростить, выполнив итерацию над самим файловым объектом:

>>> with open('dog_breeds.txt', 'r') as reader:
>>>     # Read and print the entire file line by line
>>>     for line in reader:
>>>         print(line, end='')
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier

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

Примечание: Некоторые из приведенных выше примеров содержат print('some text', end=''). end='' служит для того, чтобы Python не добавлял дополнительную новую строку к печатаемому тексту, а только печатал то, что считывается из файла.

Теперь перейдем к записи файлов. Как и в случае чтения файлов, у файловых объектов есть несколько методов, полезных для записи в файл:

Method What It Does
.write(string) При этом строка записывается в файл.
.writelines(seq) При этом последовательность записывается в файл. К каждому элементу последовательности не добавляются окончания строк. Вы сами должны добавить соответствующее окончание строки.

Вот быстрый пример использования .write() и .writelines():

with open('dog_breeds.txt', 'r') as reader:
    # Note: readlines doesn't trim the line endings
    dog_breeds = reader.readlines()

with open('dog_breeds_reversed.txt', 'w') as writer:
    # Alternatively you could use
    # writer.writelines(reversed(dog_breeds))

    # Write the dog breeds to the file in reversed order
    for breed in reversed(dog_breeds):
        writer.write(breed)

Работа с байтами

Иногда возникает необходимость работать с файлами, используя байтовые строки. Это делается путем добавления символа 'b' к аргументу mode. Применяются все те же методы, что и для объекта file. Однако каждый из методов ожидает и возвращает объект bytes вместо него:

>>> with open('dog_breeds.txt', 'rb') as reader:
>>>     print(reader.readline())
b'Pug\n'

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

A cute picture of a Jack Russell Terrier

Изображение: CC BY 3.0 (https://creativecommons.org/licenses/by/3.0)], с сайта Wikimedia Commons

Вы можете открыть этот файл в Python и изучить его содержимое! Поскольку формат .png файлов хорошо определен, заголовок файла состоит из 8 байт, разбитых следующим образом:

Value Interpretation
0x89 "Магическое" число, указывающее на то, что это начало PNG.
0x50 0x4E 0x47 PNG в ASCII
0x0D 0x0A DOS окончание строки стиля \r\n
0x1A Символ EOF в стиле DOS
0x0A Завершение строки в стиле Unix \n

И действительно, если открыть файл и прочитать эти байты по отдельности, то можно увидеть, что это действительно .png заголовочный файл:

>>> with open('jack_russell.png', 'rb') as byte_reader:
>>>     print(byte_reader.read(1))
>>>     print(byte_reader.read(3))
>>>     print(byte_reader.read(2))
>>>     print(byte_reader.read(1))
>>>     print(byte_reader.read(1))
b'\x89'
b'PNG'
b'\r\n'
b'\x1a'
b'\n'

Полный пример: dos2unix.py

Давайте вернем все это домой и рассмотрим полный пример чтения и записи в файл. Ниже приведен инструмент dos2unix, который преобразует файл, содержащий окончания строк \r\n в \n.

Этот инструмент разбит на три основных раздела. Первый - str2unix(), который преобразует строку с окончаниями строк \r\n в \n. Второй - dos2unix(), который преобразует строку, содержащую \r\n символов, в \n. dos2unix() внутренне вызывает str2unix(). Наконец, есть блок __main__, который вызывается только тогда, когда файл выполняется как сценарий. Думайте о нем как о функции main, встречающейся в других языках программирования.

"""
A simple script and library to convert files or strings from dos like
line endings with Unix like line endings.
"""

import argparse
import os


def str2unix(input_str: str) -> str:
    r"""
    Converts the string from \r\n line endings to \n

    Parameters
    ----------
    input_str
        The string whose line endings will be converted

    Returns
    -------
        The converted string
    """
    r_str = input_str.replace('\r\n', '\n')
    return r_str


def dos2unix(source_file: str, dest_file: str):
    """
    Converts a file that contains Dos like line endings into Unix like

    Parameters
    ----------
    source_file
        The path to the source file to be converted
    dest_file
        The path to the converted file for output
    """
    # NOTE: Could add file existence checking and file overwriting
    # protection
    with open(source_file, 'r') as reader:
        dos_content = reader.read()

    unix_content = str2unix(dos_content)

    with open(dest_file, 'w') as writer:
        writer.write(unix_content)


if __name__ == "__main__":
    # Create our Argument parser and set its description
    parser = argparse.ArgumentParser(
        description="Script that converts a DOS like file to an Unix like file",
    )

    # Add the arguments:
    #   - source_file: the source file we want to convert
    #   - dest_file: the destination where the output should go

    # Note: the use of the argument type of argparse.FileType could
    # streamline some things
    parser.add_argument(
        'source_file',
        help='The location of the source '
    )

    parser.add_argument(
        '--dest_file',
        help='Location of dest file (default: source_file appended with `_unix`',
        default=None
    )

    # Parse the args (argparse automatically grabs the values from
    # sys.argv)
    args = parser.parse_args()

    s_file = args.source_file
    d_file = args.dest_file

    # If the destination file wasn't passed, then assume we want to
    # create a new file based on the old one
    if d_file is None:
        file_path, file_extension = os.path.splitext(s_file)
        d_file = f'{file_path}_unix{file_extension}'

    dos2unix(s_file, d_file)

Советы и рекомендации

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

__file__

Атрибут __file__ - это специальный атрибут модулей, аналогичный __name__. Он представляет собой:

"имя пути к файлу, из которого был загружен модуль, если он был загружен из файла." (Source

Примечание: Повторимся, __file__ возвращает путь относительный к месту вызова начального Python-скрипта. Если вам нужен полный системный путь, вы можете использовать os.getcwd() для получения текущего рабочего каталога исполняемого кода.

Вот пример из реального мира. На одной из моих прошлых работ я проводил множество тестов для аппаратного устройства. Каждый тест был написан с помощью сценария Python, а имя файла сценария использовалось в качестве заголовка. Затем эти сценарии выполнялись и могли выводить свой статус с помощью специального атрибута __file__. Вот пример структуры папок:

project/
|
├── tests/
|   ├── test_commanding.py
|   ├── test_power.py
|   ├── test_wireHousing.py
|   └── test_leds.py
|
└── main.py

Запуск main.py приводит к следующему:

>>> python main.py
tests/test_commanding.py Started:
tests/test_commanding.py Passed!
tests/test_power.py Started:
tests/test_power.py Passed!
tests/test_wireHousing.py Started:
tests/test_wireHousing.py Failed!
tests/test_leds.py Started:
tests/test_leds.py Passed!

Я смог запустить и получить статус всех моих тестов динамически с помощью специального атрибута __file__.

Добавление в файл

Иногда требуется добавить файл или начать запись в конце уже заполненного файла. Это легко сделать, используя символ 'a' для аргумента mode:

with open('dog_breeds.txt', 'a') as a_writer:
    a_writer.write('\nBeagle')

При повторном рассмотрении dog_breeds.txt вы увидите, что начало файла осталось неизменным, а Beagle теперь добавлено в конец файла:

>>> with open('dog_breeds.txt', 'r') as reader:
>>>     print(reader.read())
Pug
Jack Russell Terrier
English Springer Spaniel
German Shepherd
Staffordshire Bull Terrier
Cavalier King Charles Spaniel
Golden Retriever
West Highland White Terrier
Boxer
Border Terrier
Beagle

Работа с двумя файлами одновременно

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

d_path = 'dog_breeds.txt'
d_r_path = 'dog_breeds_reversed.txt'
with open(d_path, 'r') as reader, open(d_r_path, 'w') as writer:
    dog_breeds = reader.readlines()
    writer.writelines(reversed(dog_breeds))

Создание собственного контекстного менеджера

Возможно, наступит момент, когда вам понадобится более тонкий контроль над файловым объектом, поместив его внутрь пользовательского класса. Когда вы это сделаете, использовать оператор with уже не получится, если только не добавить несколько волшебных методов: __enter__ и __exit__. Добавив их, вы создадите так называемый контекстный менеджер.

__enter__() вызывается при вызове оператора with. __exit__() вызывается при выходе из блока операторов with.

Вот шаблон, который вы можете использовать для создания своего собственного класса:

class my_file_reader():
    def __init__(self, file_path):
        self.__path = file_path
        self.__file_object = None

    def __enter__(self):
        self.__file_object = open(self.__path)
        return self

    def __exit__(self, type, val, tb):
        self.__file_object.close()

    # Additional methods implemented below

Теперь, когда у вас есть пользовательский класс, который теперь является менеджером контекста, вы можете использовать его аналогично встроенному open():

with my_file_reader('dog_breeds.txt') as reader:
    # Perform custom class operations
    pass

Вот хороший пример. Помните милое изображение Джека Рассела? Возможно, вы хотите открыть другие файлы .png, но не хотите каждый раз разбирать заголовочный файл. Вот пример того, как это можно сделать. В этом примере также используются пользовательские итераторы. Если вы не знакомы с ними, ознакомьтесь с Python Iterators:

class PngReader():
    # Every .png file contains this in the header.  Use it to verify
    # the file is indeed a .png.
    _expected_magic = b'\x89PNG\r\n\x1a\n'

    def __init__(self, file_path):
        # Ensure the file has the right extension
        if not file_path.endswith('.png'):
            raise NameError("File must be a '.png' extension")
        self.__path = file_path
        self.__file_object = None

    def __enter__(self):
        self.__file_object = open(self.__path, 'rb')

        magic = self.__file_object.read(8)
        if magic != self._expected_magic:
            raise TypeError("The File is not a properly formatted .png file!")

        return self

    def __exit__(self, type, val, tb):
        self.__file_object.close()

    def __iter__(self):
        # This and __next__() are used to create a custom iterator
        # See https://dbader.org/blog/python-iterators
        return self

    def __next__(self):
        # Read the file in "Chunks"
        # See https://en.wikipedia.org/wiki/Portable_Network_Graphics#%22Chunks%22_within_the_file

        initial_data = self.__file_object.read(4)

        # The file hasn't been opened or reached EOF.  This means we
        # can't go any further so stop the iteration by raising the
        # StopIteration.
        if self.__file_object is None or initial_data == b'':
            raise StopIteration
        else:
            # Each chunk has a len, type, data (based on len) and crc
            # Grab these values and return them as a tuple
            chunk_len = int.from_bytes(initial_data, byteorder='big')
            chunk_type = self.__file_object.read(4)
            chunk_data = self.__file_object.read(chunk_len)
            chunk_crc = self.__file_object.read(4)
            return chunk_len, chunk_type, chunk_data, chunk_crc

Теперь вы можете открывать файлы .png и правильно разбирать их, используя свой собственный контекстный менеджер:

>>> with PngReader('jack_russell.png') as reader:
>>>     for l, t, d, c in reader:
>>>         print(f"{l:05}, {t}, {c}")
00013, b'IHDR', b'v\x121k'
00001, b'sRGB', b'\xae\xce\x1c\xe9'
00009, b'pHYs', b'(<]\x19'
00345, b'iTXt', b"L\xc2'Y"
16384, b'IDAT', b'i\x99\x0c('
16384, b'IDAT', b'\xb3\xfa\x9a$'
16384, b'IDAT', b'\xff\xbf\xd1\n'
16384, b'IDAT', b'\xc3\x9c\xb1}'
16384, b'IDAT', b'\xe3\x02\xba\x91'
16384, b'IDAT', b'\xa0\xa99='
16384, b'IDAT', b'\xf4\x8b.\x92'
16384, b'IDAT', b'\x17i\xfc\xde'
16384, b'IDAT', b'\x8fb\x0e\xe4'
16384, b'IDAT', b')3={'
01040, b'IDAT', b'\xd6\xb8\xc1\x9f'
00000, b'IEND', b'\xaeB`\x82'

Не изобретайте змею заново

Существуют распространенные ситуации, с которыми вы можете столкнуться при работе с файлами. Большинство из них можно решить с помощью других модулей. Два распространенных типа файлов, с которыми вам может понадобиться работать, - это .csv и .json. Real Python уже собрал несколько отличных статей о том, как с ними работать:

Кроме того, существуют встроенные библиотеки, которые могут вам помочь:

  • wave: чтение и запись WAV файлов (аудио)
  • aifc: чтение и запись файлов AIFF и AIFC (аудио)
  • sunau: чтение и запись файлов Sun AU
  • tarfile: чтение и запись файлов архива tar
  • zipfile: работа с ZIP архивами
  • configparser: легко создавать и разбирать конфигурационные файлы
  • xml.etree.ElementTree: создание или чтение файлов на основе XML
  • msilib: чтение и запись файлов Microsoft Installer
  • plistlib: создание и разбор файлов Mac OS X .plist

Существует множество других инструментов. Кроме того, на PyPI доступно еще больше сторонних инструментов. Среди них можно выделить следующие:

  • PyPDF2: Набор инструментов для работы с PDF
  • .
  • xlwings: чтение и запись файлов Excel
  • Pillow: чтение и работа с изображениями

Ты - файловый волшебник Гарри!

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

В этом уроке вы узнали:

  • Что такое файл
  • Как правильно открывать и закрывать файлы
  • Как читать и записывать файлы
  • Некоторые продвинутые техники при работе с файлами
  • Некоторые библиотеки для работы с распространенными типами файлов
Вернуться на верх