Взаимодействие с файловой системой#

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

Путь к файлу/директории#

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

Обычно путь представляет собой последовательность вложенных каталогов, разделенных специальным символом, при этом разделитель каталогов может меняться в зависимости от операционной системы: в OS Windows используется “\”, в unix-like системах — “/”. Кроме того, важно знать, что пути бывают абсолютными и относительными. Абсолютный путь всегда начинается с корневого каталога файловой системы (в OS Windows — это логический раздел (например, “C:”), в UNIX-like системах — “/”) и всегда указывает на один и тот же файл (или директорию). Относительный путь, наоборот, не начинается с корневого каталога и указывает расположение относительно текущего рабочего каталога, а значит будет указывать на совершено другой файл, если поменять рабочий каталог.

Итого, например, путь к файлу “hello.py” в домашней директории пользователя “ivan” в зависимости от операционной системы будет выглядеть приблизительно следующим образом:

OS Windows

UNIX-like

Глобальный

C:\Users\ivan\hello.py

/home/users/ivan/hello.py

Относительный

.\hello.py

./hello.py

В связи с этим требуется прикладывать дополнительные усилия, чтобы заставить работать один и тот же код на машинах с разными операционными системами. Чтобы все же абстрагироваться от того, как конкретно устроена файловая система на каждой конкретной машине, в python предусмотренны модули стандартной библиотеки os.path и pathlib.

Проблема с путями в стиле Windows#

Как было отмечено выше, в Windows в качестве разделителя используется символ обратного слеша (backslash) “\”. Это может привести к небольшой путанице у неопытных программистов. Дело в том, что во многих языка программирования (и в python, в том числе) символ “\” внутри строк зарезервирован для экранирования, т.е. если внутри строки встречается “ “, то он интерпретируется не буквально как символ обратного слеша, а изменяет смысл следующего за ним символом. Так, например, последовательность "\n" представляет собой один управляющий символ перевода строки.

new_line = "\n"

print(len(new_line))
1

Это значит, что если вы попробуете записать Windows путь не учитывая эту особенность, то высока вероятность получить не тот результат, который вы ожидали. Например, строка "C:\Users" вообще не корректна с точки зрения синтаксиса python:

users_folder = "C:\Users"
  Input In [10]
    users_folder = "C:\Users"
                             ^
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \UXXXXXXXX escape

Это объясняется тем, что последовательность "\U" используется для экранирования unicode последовательностей, а набор символов "sers" не является корректным unicode кодом. Ниже приводится пример корректного unicode кода.

snake_emoji = "\U0001F40D"
print(snake_emoji)
🐍

В python предусмотренно как минимум два подхода борьбы с этой проблемой.

Первый из них опирается на удвоение количества символов “\”. Дело в том, что в последовательности символов “\\” — первый обратный слеш экранирует второй, т.е. итоговый результат эквивалентен одному настоящему символу обратного слеша.

users_folder = "C:\\Users"
print(users_folder)

new_line = "\\n"
print(len(new_line))
C:\Users
2

Второй способ опирается на использование так называемых сырых (raw) строк: если перед началом литерала строки поставить символ “r”, то символ обратного слеша теряет свою особую роль внутри неё.

users_folder = r"C:\Users"
print(users_folder)

new_line = r"\n"
print(len(new_line))
C:\Users
2

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

Соединение элементов пути#

Рассмотрим конкретный пример. Пусть у нас имеется строка folder, представляющая путь к каталогу, и строка filename, представляющее имя некоего файла внутри этого каталога.

folder = "directory"
filename = "file.txt"

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

Конечно, можно вспомнить, что путь — строка, а значит их можно конкатенировать. Но, что если кто-то захочет запустить ваш код на машине с другой операционной системой? Гораздо целесообразнее воспользоваться для этих целей специальными средствами. Самый надежный способ — метод os.path.join, который на вход принимает произвольное количество имен файлов и соединяет их тем символом, который используется в качестве разделителя на той конкретной машине, на которой скрипт запущен сейчас.

import os

path = os.path.join(folder, filename)
print(path)
directory\file.txt

Альтернативой является модуль pathlib, который позволяет обращаться с путями файловой системы в объектно ориентированном стиле, т.е. путь больше не представляется в виде строки, а в виде специального объекта, который в любой момент может быть приведен к строке конструктором строки str.

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

from pathlib import Path

folder = Path(folder)
print(f"{folder=}, {str(folder)=}")
folder=WindowsPath('directory'), str(folder)='directory'

В ячейке выше создается объект типа Path из строки folder и вывод сообщает, что создался объект WindowsPath('directory. Обратите внимание, что автоматически создался путь OS Windows, т.к. этот скрипт запускался под управлением этой операционной системы.

Чтобы присоединить имя файла к объекту folder, можно использовать оператор “/” вне зависимости от операционной системы.

path = folder / filename
print(f"{path=}, {str(path)=}")
path=WindowsPath('directory/file.txt'), str(path)='directory\\file.txt'

Обратите внимание на то, что при приведении к строке автоматически получилась строка с разделителем в стиле OS Windows, т.к. при генерации материалов использовался компьютер под управлением OS Windows.

Автор курса рекомендует всегда использовать средства модулей os.path или pathlib, даже если вам известно, что ваш скрипт будет запускаться под управлением какой-то конкретной операционной системы, чтобы писать более надежный код и формировать полезные привычки.

Извлечение элементов из пути#

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

path = r"C:\Users\fadeev\folder\file.txt"

Метод os.path.splitdrive разбивает строку на логический раздел и остальное (актуально в основном на OS Windows).

print(f"{path=}")
drive, tail = os.path.splitdrive(path)

print(f"{drive=}, {tail=}")
path='C:\\Users\\fadeev\\folder\\file.txt'
drive='C:', tail='\\Users\\fadeev\\folder\\file.txt'

Метод os.path.dirname выделяет из пути родительский каталог.

parent_folder = os.path.dirname(path)

print(f"{parent_folder=}")
parent_folder='C:\\Users\\fadeev\\folder'

Метод os.path.basename наоборот извлекает имя файла или папки, на которую данный путь указывает без учета родительского каталога.

filename = os.path.basename(path)
print(f"{filename=}")
filename='file.txt'

Метаинформация файла/каталога#

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

Самый фундаментальный вопрос, который можно задать — существует ли вообще что-нибудь по указанному пути? Метод os.path.exists отвечает как раз на этот вопрос.

print(f"{os.path.exists(path)=}, {os.path.exists('filesystem.ipynb')=}")
os.path.exists(path)=False, os.path.exists('filesystem.ipynb')=True

Методы os.path.isdir и os.path.isfile позволяют определить располагает ли по этому пути каталог или файл соответственно. Оба метода возвращают False, если по переданному пути ничего не располагается.

print(f"{os.path.isdir(folder)=}, {os.path.isfile('filesystem.ipynb')=}")
os.path.isdir(folder)=True, os.path.isfile('filesystem.ipynb')=True

Также иногда бывает полезно узнать время создания (последнего изменения) или последнего доступа к файлу или каталогу. Для этих целей существуют методы os.path.getatime, os.path.getmtime и os.path.getctime. Размер файла можно узнать методом os.path.getsize.

Содержимое каталога#

В ряде задач может потребоваться узнать содержимое определенного каталога, например, чтобы потом в цикле обработать каждый элемент каталога. В самых простых случаях достаточно метода os.listdir, который возвращает список файлов/каталогов в указанной директории. По умолчанию — текущая директория.

for filename in os.listdir():
    print(filename, end=" ")
.ipynb_checkpoints about_12_and_so_on.ipynb about_python.ipynb argparse.ipynb custom_classes.ipynb custom_exceptions.ipynb decorators.ipynb dictionaries.ipynb dynamic_typing.ipynb exceptions.ipynb exercises1.ipynb exercises2.ipynb exercises3.ipynb files.ipynb filesystem.ipynb functions.ipynb garbage_collector.ipynb generators.ipynb if_for_range.ipynb inheritance.ipynb iterators.ipynb json.ipynb jupyter.ipynb LBYL_vs_EAFP.ipynb list_comprehensions.ipynb mutability.ipynb numbers_and_lists.ipynb operators_overloading.ipynb polymorphism.ipynb python_scripts.ipynb scripts_vs_modules.ipynb sequencies.ipynb tmp 

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

Метод glob.glob модуля стандартной библиотеки glob позволяет фильтровать содержимое каталога на основе шаблона. В ячейке ниже демонстрируется, как можно найти все файлы в каталоге, которые начинаются с символа “a”, а завершаются расширением “.ipynb”.

import glob
for filename in glob.glob("a*.ipynb"):
    print(filename)
about_12_and_so_on.ipynb
about_python.ipynb
argparse.ipynb

Создание, копирование, перемещение и удаление файлов и каталогов#

Метод os.mkdir создаёт каталог, но две особенности:

  • если такой каталог уже существует, то бросается исключение;

  • если родительского каталога не существует, то тоже бросается исключение.

Альтернативой является метод os.makedirs имеет опциональный параметр exist_ok, который позволяет игнорировать ошибку, возникающую при попытке создать уже существующий каталог. Кроме того, если для создания указанного каталога, потребуется создать несколько директорий по пути, то они тоже будут созданы.

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

Модуль стандартной библиотеки shutil содержит набор методов, имитирующих методы командной строки, что позволяет копировать файлы (методы shutil.copy, shutil.copy2 и shutil.copyfile), копировать директории с их содержимым (метод shutil.copytree), удалять директории (метод shutil.rmtree) и перемещать файлы или директории (метод shutil.move).

Удалять файлы можно методом os.remove.