Выделенный путь проходит через структуру каталогов и файлов
Программирование

Файлы и пути в Python: pathlib без путаницы с каталогами

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

Содержание

Скрипт начинает работать с файлами — и сразу возникает вопрос: относительно какого каталога считается путь data/settings.json? Ответ зависит не от расположения файла с кодом, а от того, откуда запущен процесс. Из-за этой разницы программа может успешно работать в редакторе и не находить тот же файл в планировщике.

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

Учебный пример остаётся одним небольшим модулем Python. Если чтение завершится ошибкой, её смысл удобнее разбирать после урока об исключениях.

Текущий каталог задаёт начало относительного пути

Создайте локальный файл paths_demo.py. Первый фрагмент только показывает текущий каталог процесса и не меняет файловую систему:

from pathlib import Path


current_dir = Path.cwd()
report_path = current_dir / "output" / "report.txt"

print(f"Текущий каталог: {current_dir}")
print(f"Путь отчёта: {report_path}")

Запустите python paths_demo.py из каталога проекта. Path.cwd() вернёт текущий рабочий каталог, а оператор / добавит две части пути. Файл report.txt пока не создаётся.

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

Каталог скрипта не зависит от места запуска

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

script_dir = Path(__file__).resolve().parent
report_path = script_dir / "output" / "report.txt"

print(f"Каталог скрипта: {script_dir}")
print(f"Путь отчёта: {report_path}")

__file__ содержит путь загруженного файла, resolve() делает его абсолютным и разрешает символические ссылки, а parent выбирает родительский каталог. Теперь результат одинаков при запуске из разных каталогов.

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

Создайте каталог и запишите текст

Добавьте ниже предыдущего фрагмента создание выходного каталога и файла. Код работает от обычного пользователя, создаёт output, если его нет, и полностью заменяет содержимое report.txt:

output_dir = report_path.parent
output_dir.mkdir(parents=True, exist_ok=True)

written = report_path.write_text(
    "example.com: доступен\n",
    encoding="utf-8",
)

print(f"Записано символов: {written}")

parents=True разрешает создать недостающие родительские каталоги, а exist_ok=True не считает ошибкой уже существующий каталог. Метод write_text возвращает число записанных символов. После запуска откройте output/report.txt и проверьте строку.

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

Прочитайте файл с явной кодировкой

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

if not report_path.is_file():
    raise FileNotFoundError(f"Нет файла отчёта: {report_path}")

content = report_path.read_text(encoding="utf-8")
print(content, end="")

Ожидаемый вывод — та же строка без дополнительной пустой строки. is_file() отличает обычный файл от отсутствующего пути и каталога. Явная кодировка utf-8 делает результат предсказуемым между системами.

Проверка не гарантирует, что файл останется прежним до чтения: другой процесс может удалить или заменить его между двумя вызовами. Поэтому рабочая программа всё равно должна обработать FileNotFoundError, PermissionError и ошибку декодирования там, где знает, как объяснить их пользователю.

Найдите файлы по шаблону

Метод glob возвращает пути, подходящие под шаблон. Этот код только читает содержимое output и печатает имена текстовых файлов:

for path in sorted(output_dir.glob("*.txt")):
    print(path.name)

Шаблон *.txt означает любое имя с окончанием .txt только в этом каталоге. rglob("*.txt") дополнительно обходит вложенные каталоги; не запускайте рекурсивный поиск от корня диска или домашнего каталога без ограничения области.

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

Проверьте, что пользовательский путь не вышел за границу

Если программа принимает имя из аргумента, значение ../outside.txt может вывести запись из ожидаемого каталога. Функция ниже только сравнивает разрешённый базовый путь с кандидатом и ничего не удаляет:

def is_inside(base_dir, candidate):
    base = base_dir.resolve()
    target = candidate.resolve()

    try:
        target.relative_to(base)
    except ValueError:
        return False

    return True


candidate = output_dir / "daily" / "report.txt"
print(is_inside(output_dir, candidate))

Для указанного кандидата функция напечатает True. relative_to строит относительный путь только тогда, когда кандидат находится внутри базы; иначе возникает ValueError. Сравнение строк по общему началу здесь ненадёжно: каталоги output и output-old имеют похожие имена, но не находятся друг в друге.

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

Официальный справочник pathlib перечисляет методы Path и различия между путями разных систем. Следующий урок использует эти навыки для чтения структурированных данных в JSON и CSV.

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.

01Как переносимо соединить каталог и имя файла?
02Что делает write_text для существующего файла?
03Почему одной проверки exists недостаточно перед опасной операцией?

Разбираем коротко

Частые вопросы

Чем Path.cwd() отличается от Path(__file__).parent?

Path.cwd() возвращает каталог, из которого запущен процесс. Path(__file__).resolve().parent указывает на каталог самого файла со скриптом. Эти пути могут различаться.

Создаёт ли write_text родительские каталоги?

Нет. Перед записью каталог создают методом mkdir. write_text создаёт новый файл или полностью заменяет содержимое существующего.

Когда использовать glob, а когда rglob?

glob ищет по заданному шаблону в указанном каталоге, а rglob обходит ещё и вложенные каталоги. Рекурсивный поиск может быть дорогим на большом дереве.