Руководство по использованию pathlib в Python для работы с файловой системой

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

Примечание редакции: предлагаем почитать “Как прописать на Python путь к файлу в Windows, Mac и Linux”.

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

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

Содержание

Сравнение модулей os и pathlib

До Python 3.4 традиционным способом работы с путями к файлам было использование модуля os. Хотя этот модуль когда-то был очень эффективным, его возраст стал сказываться.

Мы можем продемонстрировать уникальную ценность pathlib, рассмотрев распространенную задачу в области науки о данных: поиск всех png-файлов внутри заданной директории и всех ее поддиректорий.

Примечание редакции: на эту тему у нас есть отдельная статья – “Как на Python найти файлы, имеющие определенное расширение”.

Если бы мы использовали модуль os, мы могли бы написать следующий код:

import os
dir_path = "/home/user/documents"
files = [
os.path.join(dir_path, f)
for f in os.listdir(dir_path)
if os.path.isfile(os.path.join(dir_path, f)) and f.endswith(".png")
]

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

Если бы вместо этого мы использовали модуль pathlib, наш код был бы намного проще. Как мы уже говорили, pathlib предоставляет объектно-ориентированный подход к работе с путями файловой системы. Взгляните:

from pathlib import Path

# Создать объект path
dir_path = Path(dir_path)

# Найти все png-файлы в директории
files = list(dir_path.glob("*.png"))

Объектно-ориентированное программирование организует код вокруг объектов и их взаимодействий, что приводит к созданию более модульного, переиспользуемого и удобного в обслуживании кода.

Работа с объектами Path

Библиотека pathlib вращается вокруг так называемых объектов Path, которые представляют пути к файловой системе в структурированном и независимом от платформы виде.

Вы уже видели, как мы перенесли класс Path из модуля pathlib в наше текущее пространство имен с помощью следующей строки кода:

from pathlib import Path

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

Давайте рассмотрим каждый из этих способов.

Создание объектов Path из строк

Мы можем создать объект Path, передав в переменную строку, представляющую путь к файловой системе. Это преобразует строковое представление пути к файлу в объект Path.

file_path_str = "data/union_data.csv"

data_path = Path(file_path_str)

Создание объектов Path из других объектов Path

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

Для этого нужно объединить базовый путь, директорию «data» и имя файла в один путь к файлу. Для расширения объектов Path не забывайте использовать прямую косую черту там, где это необходимо.

base_path = Path("/home/user")
data_dir = Path("data")

# Комбинирование нескольких путей
file_path = base_path / data_dir / "prices.csv"  
print(file_path)

# Результат:
# '/home/user/data/prices.csv'

Создание объектов Path из текущей рабочей директории

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

cwd = Path.cwd()

print(cwd)

# Результат:
# '/home/bexgboost/articles/2024/4_april/8_pathlib'

Создание объектов Path из домашней директории

Мы можем создать путь, объединив нашу домашнюю директорию с дополнительными подкаталогами. Здесь мы объединяем домашнюю директорию с подкаталогами “downloads” и “projects”.

home = Path.home()

home / "downloads" / "projects"

# Результат:
# PosixPath('/home/bexgboost/downloads/projects')

Важное замечание. Класс Path сам по себе не выполняет никаких операций с файловой системой, таких как проверка пути, создание каталогов или файлов. Он предназначен для представления и манипулирования путями. Для реального взаимодействия с файловой системой (проверка существования, чтение/запись файлов) нам придется использовать специальные методы объектов Path, а для некоторых сложных случаев – обратиться за помощью к модулю os.

Работа с компонентами пути с помощью pathlib

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

Работа с корневой директорией

Корень – это самый верхний каталог в файловой системе. В Unix-подобных системах он обозначается прямой косой чертой (/). В Windows это обычно буква диска, за которой следует двоеточие, например C:.

image_file = home / "downloads" / "midjourney.png"

image_file.root

# Результат:
# '/'

Работа с родительской директорией

Родительская директория содержит текущий файл или каталог. Она находится на один уровень выше относительно текущей директории или файла.

image_file.parent

# PosixPath('/home/bexgboost/downloads')

Работа с полным именем файла

Атрибут name возвращает полное имя файла, включая расширение, в виде строки.

image_file.name

# 'midjourney.png'

Работа с суффиксом файла

Атрибут suffix возвращает расширение файла, включая точку, в виде строки (или пустую строку, если расширение отсутствует).

image_file.suffix

# '.png'

Работа с собственно именем файла

Атрибут stem возвращает имя файла без расширения. Это может быть полезно при конвертации файлов в различные форматы.

image_file.stem

# 'midjourney'

Примечание. На Mac пути к файлам чувствительны к регистру, поэтому /Users/username/Documents и /users/username/documents – разные файлы.

Атрибут parts

Мы можем использовать атрибут parts, чтобы разделить объект Path на составляющие.

image_file.parts

# ('/', 'home', 'bexgboost', 'downloads', 'midjourney.png')

Атрибут parents

Атрибут parents, возвращающий генератор, превращает компоненты пути в объекты Path.

list(image_file.parents)

Результат:

[PosixPath('/home/bexgboost/downloads'),
PosixPath('/home/bexgboost'),
PosixPath('/home'),
PosixPath('/')]

Распространенные операции с путями

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

Вывод списков каталогов

Метод iterdir() позволяет перебирать все файлы и подкаталоги в папке. Он особенно полезен для обработки всех файлов в каталоге или выполнения операций над каждой записью.

cwd = Path.cwd()
 for entry in cwd.iterdir():
# Process the entry here
...
# print(entry)

Поскольку метод iterdir() возвращает итератор, записи извлекаются по требованию по мере выполнения цикла.

Метод is_dir()

is_dir() возвращает True, если путь указывает на директорию, и False в противном случае.

for entry in cwd.iterdir():
   if entry.is_dir():
      print(entry.name)
.ipynb_checkpoints
data
images

Метод is_file()

is_file() возвращает True, если путь указывает на файл, False – в противном случае.

for entry in cwd.iterdir():
   if entry.is_file():
      print(entry.suffix)
.ipynb
.txt

Метод exists()

Поскольку объекты Path только представляют пути, иногда необходимо проверить, существует ли путь, используя метод .exists():

image_file.exists()

# False

Метод .exists() проверяет, существует ли путь. Это полезно, так как объекты Path могут представлять файлы и каталоги, которых фактически может и не быть в файловой системе.

Создание и удаление путей

Модуль pathlib также предлагает функции для создания и удаления файлов и директорий. Давайте посмотрим, как это делается.

Метод .mkdir()

mkdir() создает новую директорию по указанному пути. По умолчанию он создает директорию в текущей рабочей директории.

from pathlib import Path

data_dir = Path("new_data_dir")

# Создать директорию 'new_data_dir' в текущей рабочей директории
data_dir.mkdir()

Когда вы хотите создать структуру директорий, в которой некоторые родительские директории могут не существовать, можно передать в метод mkdir аргумент(parents=True). Установка parents=True гарантирует, что все необходимые родительские директории будут созданы в процессе работы.

sub_dir = Path("data/nested/subdirectory")

# Создать 'data/nested/subdirectory', даже если 'data' или 'nested' не существуют
sub_dir.mkdir(parents=True)

Помните, что mkdir() вызывает исключение, если директория с таким же именем уже существует.

Path('data').mkdir()

# FileExistsError: [Errno 17] File exists: 'data'

Метод unlink()

Метод unlink() безвозвратно удаляет файл, представленный объектом Path. Чтобы не получить ошибку, рекомендуется перед запуском этого метода проверить, существует ли файл.

to_delete = Path("data/prices.csv")

if to_delete.exists():
   to_delete.unlink()
   print(f"Successfully deleted {to_delete.name}")

# Результат:
# Successfully deleted prices.csv

Метод rmdir()

Метод rmdir() удаляет пустую директорию. Помните, что rmdir() работает только для удаления пустых директорий. Самый простой способ удалить непустую директорию – использовать библиотеку shutil или терминал.

empty_dir = Path("new_data_dir")

empty_dir.rmdir()

Примечание. Будьте осторожны при использовании unlink() или rmdir(), так как результаты их работы необратимы.

Продвинутые манипуляции с путями

Давайте перейдем к некоторым продвинутым концепциям манипулирования путями и рассмотрим, как применить их на практике с помощью pathlib.

Относительные и абсолютные пути

Начнем с понимания различий между абсолютными и относительными путями.

Относительные пути

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

Например, в моей текущей рабочей директории есть папка “images”, в которой находится файл midjourney.png.

image = Path("images/midjourney.png")

image

# PosixPath('images/midjourney.png')

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

Абсолютные пути

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

image_absolute = Path("/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png")

image_absolute

# PosixPath('/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png')

Как видите, абсолютные пути могут быть довольно длинными, особенно в сложных проектах со вложенными древовидными структурами. По этой причине большинство людей предпочитают более короткие относительные пути.

Метод resolve()

Модуль pathlib позволяет преобразовывать относительные пути в абсолютные с помощью метода resolve().

relative_image = Path("images/midjourney.png")

absolute_image = relative_image.resolve()

absolute_image

# PosixPath('/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png')

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

relative_path = Path.cwd()

absolute_image.relative_to(relative_path)

# PosixPath('images/midjourney.png')

Применение шаблонов

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

files = list(dir_path.glob("*.png"))

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

Метод glob() принимает на вход строку шаблона, содержащую символы подстановки, и возвращает объект-генератор, который по запросу выдает подходящие объекты Path.

Символы подстановки и то, чему они соответствуют:

  • * – ноль или более символов
  • ? – любой одиночный символ
  • [] определяет диапазон символов (например, [a-z] – любая строчная буква).

Для примера попробуем найти все Jupyter-блокноты в моей директории «articles».

articles_dir = Path.home() / "articles"

# Найти все скрипты
notebooks = articles_dir.glob("*.ipynb")

# Вывести число найденных
print(len(list(notebooks)))

# Результат:
# 0

Метод .glob() не нашел ни одного блокнота, что на первый взгляд кажется удивительным, ведь я написал более 150 статей. Причина в том, что .glob() ищет только внутри заданной директории, но не в ее поддиректориях.

Решить эту проблему можно с помощью рекурсивного поиска, для чего необходимо использовать метод rglob(), который имеет схожий синтаксис:

notebooks = articles_dir.rglob("*.ipynb")

print(len(list(notebooks)))

# Результат:
# 357

На этот раз наш код нашел все 357 файлов.

Работа с файлами с помощью pathlib

Как мы видели, объекты Path только представляют файлы. Но у них есть и определенные методы для распространенных операций с файлами. В этом разделе мы рассмотрим, как их использовать.

Чтение файлов

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

Метод read_text() позволяет нам прочитать содержимое текстового файла и закрыть его.

file = Path("file.txt")

print(file.read_text())

# Результат:
# This is sample text.

Для бинарных файлов вместо него можно использовать метод read_bytes().

image = Path("images/midjourney.png")

image.read_bytes()[:10]

# Результат:
# b'\x89PNG\r\n\x1a\n\x00\x00'

Помните, что при использовании метода read_* важна обработка ошибок:

nonexistent_file = Path("gibberish.txt")

try:
   contents = nonexistent_file.read_text()
except FileNotFoundError:
   print("No such thing.")

# Результат:
# No such thing.

Запись файлов

Запись в файлы так же проста, как и чтение. Для записи файлов у нас есть метод write_text().

file = Path("file.txt")

file.write_text("This is new text.")

# Результат:
# 17
file.read_text()

# Результат:
# 'This is new text.'

Как видите, метод write_text() перезаписывает текст. Хотя для write_text() нет режима добавления, мы можем использовать read_text() и write_text() вместе, чтобы добавить текст в конец файла.

old_text = file.read_text() + "\n"
final_text = "This is the final text."

# Скомбинировать старый и новый текст и записать их обратно
file.write_text(old_text + final_text)

print(file.read_text())

# Результат:
# This is new text.
# This is the final text.

Функция write_bytes() работает аналогичным образом. Для примера давайте сначала продублируем изображение midjourney.png с новым именем.

original_image = Path("images/midjourney.png")

new_image = original_image.with_stem("duplicated_midjourney")

new_image

# Результат:
# PosixPath('images/duplicated_midjourney.png')

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

new_image.write_bytes(original_image.read_bytes())

# 1979612

Переименование и перемещение файлов

В дополнение к функции with_stem() для изменения имени файла pathlib предлагает метод rename() для более полного переименования.

file = Path("file.txt")

target_path = Path("new_file.txt")

file.rename(target_path)

# Результат:
# PosixPath('new_file.txt')

rename() принимает целевой путь, который может быть строкой или другим объектом Path.

Для перемещения файлов можно использовать функцию replace(), которая также принимает целевой путь:

# Define the file to be moved
source_file = Path("new_file.txt")

# Define the location to put the file
destination = Path("data/new/location")

# Create the directories if they don't exist
destination.mkdir(parents=True)

# Move the file
source_file.replace(destination / source_file.name)

# Результат:
# PosixPath('data/new/location/new_file.txt')

Создание пустых файлов

pathlib позволяет создавать пустые файлы с помощью метода touch:

# Define new file path
new_dataset = Path("data/new.csv")

new_dataset.exists()

# False
new_dataset.touch()

new_dataset.exists()

# True

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

original_image.touch()

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

Разрешения и информация о файловой системе

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

image_stats = original_image.stat()

image_stats

# os.stat_result(st_mode=33188, st_ino=1950175, st_dev=2080, st_nlink=1, st_uid=1000, st_gid=1000, st_size=1979612, st_atime=1714664562, st_mtime=1714664562, st_ctime=1714664562)

Мы также можем получить размер файла, используя точечную нотацию.

image_size = image_stats.st_size

# File size in megabytes
image_size / (1024**2)


# 1.8879051208496094

Заключение

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

Перевод статьи «A Comprehensive Guide to Using pathlib in Python For File System Manipulation».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *