Папки и пути: pathlib, os, shutil — наводим порядок в «Загрузках»

Вспомните прошлую тему: вы открывали файл через open(), читали его построчно и следили за кодировкой. Но файл всегда лежал рядом со скриптом. В жизни файлы разбросаны: «Загрузки» забиты отчётами, фото и архивами вперемешку. Сегодня научим Python ходить по папкам и раскладывать файлы по местам.

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

Путь — это объект, а не строка

Модуль pathlib даёт класс Path. Путь собирается оператором /, и Python сам поставит нужный разделитель: \ на Windows, / на macOS и Linux. Склеивать пути строками через + не нужно.

from pathlib import Path

home = Path.home()                 # домашняя папка пользователя
downloads = home / "Downloads"
report = downloads / "отчёт.pdf"
print(report)
# Windows: C:\Users\anna\Downloads\отчёт.pdf
# macOS:   /Users/anna/Downloads/отчёт.pdf

Почему в коде "Downloads", а не «Загрузки»? Это одна и та же папка. На диске она называется Downloads, а Windows и macOS только показывают её по-русски. В пути пишите настоящее имя — Downloads. «Загрузки» в примерах ниже — учебная папка рядом со скриптом.

Path.cwd() вернёт текущую рабочую папку — ту, из которой запущен скрипт. Относительный путь вроде Path("data/расходы.csv") отсчитывается как раз от неё.

Проверьте себя

Контрольный вопрос. Нужно получить путь к файлу «фото.jpg» в папке «Загрузки». Какая строка работает на любой системе?

А«Загрузки» + «фото.jpg»
БPath(«Загрузки») / «фото.jpg»
ВPath(«Загрузки») + «фото.jpg»
Г«Загрузки\\фото.jpg»

Подсказка: Путь собирают оператором /, разделитель Python подставит сам.

Онлайн-проверка ответа появится позже

Части пути: parent, name, stem, suffix

У пути есть готовые свойства. Не нужно резать строку по точкам и слэшам.

Путь Загрузки/2024/отчёт.final.pdf разрезан на части. Скобки показывают: parent — Загрузки/2024, name — отчёт.final.pdf, stem — отчёт.final, suffix — .pdf. Ниже таблица: p.parent — папка, где лежит файл; p.name — имя целиком; p.stem — имя без последнего расширения; p.suffix — расширение с точкой.
from pathlib import Path

p = Path("Загрузки/2024/отчёт.final.pdf")
print(p.parent)        # Загрузки/2024 (на Windows со знаком \)
print(p.parent.name)   # 2024
print(p.name)          # отчёт.final.pdf
print(p.stem)          # отчёт.final
print(p.suffix)        # .pdf
print(p.with_suffix(".txt").name)  # отчёт.final.txt
print(p.parts)         # ('Загрузки', '2024', 'отчёт.final.pdf')

Расширение в suffix всегда с точкой: ".pdf", а не "pdf". И регистр сохраняется: у ФОТО.JPG суффикс .JPG. Поэтому для сравнения берите p.suffix.lower().

Проверьте себя

Контрольный вопрос. Что выведет код? Введите результат ровно как на экране.

Исходный код для этого задания:

from pathlib import Path
p = Path("Загрузки/Отпуск/море.JPG")
print(p.suffix)

Подсказка: Перечитайте врезку про suffix: что он делает с точкой и меняет ли регистр букв.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет код? Введите результат.

Исходный код для этого задания:

from pathlib import Path
p = Path("Документы/договор.v2.docx")
print(p.stem)

Подсказка: Если точек в имени несколько, какое расширение отрезает stem? Сравните с примером отчёт.final.pdf в уроке.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет код? Введите одно слово.

Исходный код для этого задания:

from pathlib import Path
p = Path("Загрузки/2024/Март/счёт.pdf")
print(p.parent.name)

Подсказка: parent — папка файла, name у неё — последнее имя в пути.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет код? Введите имя файла.

Исходный код для этого задания:

from pathlib import Path
p = Path("Фото/IMG_1.heic")
print(p.with_suffix(".jpg").name)

Подсказка: with_suffix меняет расширение, name даёт имя целиком.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Что выведет код? Введите число.

Исходный код для этого задания:

from pathlib import Path
print(len(Path("Загрузки/2024/отчёт.docx").parts))

Подсказка: parts — кортеж из всех частей пути.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Программа должна считать PDF-файлы, но выводит меньше, чем нужно. Что она выведет сейчас? Введите число.

Исходный код для этого задания:

from pathlib import Path
names = ["a.PDF", "b.jpg", "c.pdf", "d.Pdf", "e.png"]
count = 0
for n in names:
    if Path(n).suffix == ".pdf":
        count += 1
print(count)

Подсказка: Сравнение учитывает регистр букв. Для «правильной» версии нужен suffix.lower().

Онлайн-проверка ответа появится позже

Проверить, что лежит по пути

p = Path("Загрузки/отчёт.pdf")
print(p.exists())   # True, если файл или папка есть
print(p.is_file())  # True, только если это файл
print(p.is_dir())   # True, только если это папка

Пока мы не прошли исключения (тема 2.1), правило простое: прежде чем открыть, переместить или удалить — проверьте exists().

Проверьте себя

Контрольный вопрос. Отметьте все методы, которые есть у объекта Path и возвращают True/False.

Выберите все верные варианты.

Аexists()
Бis_file()
Вis_dir()
Гmkdir()
Дiterdir()

Подсказка: Это проверки существования и типа. mkdir создаёт папку, а iterdir перебирает содержимое.

Онлайн-проверка ответа появится позже

Обойти папку: iterdir и glob

folder = Path.home() / "Downloads"
for item in folder.iterdir():      # всё содержимое, без вложенных папок
    if item.is_file():
        print(item.name, item.stat().st_size, "байт")

for pdf in folder.glob("*.pdf"):    # только файлы по шаблону
    print(pdf.name)

for py in folder.rglob("*.py"):     # то же, но во всех вложенных папках
    print(py)

В шаблоне * означает «любые символы». "*.pdf" — все файлы с расширением .pdf, "отчёт*" — всё, что начинается со слова «отчёт». item.stat().st_size — размер файла в байтах.

Проверьте себя

Контрольный вопрос. Нужно найти в папке все картинки с расширением .png. Какой шаблон подставить в folder.glob("___")? Выберите вариант.

А*.png
Б.png
Вpng
Г*.*

Подсказка: Звёздочка заменяет любые символы имени. Без неё шаблон ищет файл ровно с таким именем.

Онлайн-проверка ответа появится позже

Создать папку и переименовать файл

target = Path("Загрузки/Картинки")
target.mkdir(parents=True, exist_ok=True)

old = Path("Загрузки/IMG_0012.jpg")
if old.exists():
    old.rename(target / "море.jpg")   # переименовать и заодно переместить
  • parents=True — создать и промежуточные папки, если их нет;
  • exist_ok=True — не падать с ошибкой, если папка уже есть. Без него второй запуск скрипта сломается.

Проверьте себя

Контрольный вопрос. Скрипт сортировки запустили второй раз, и строка создания папки упала с ошибкой: папка уже существует. Какой параметр надо добавить в mkdir, чтобы повторный запуск не падал? Введите только имя параметра, без знака = и значения.

Исходный код для этого задания:

target = Path("Загрузки") / "Картинки"
target.mkdir(parents=True)

Подсказка: Параметр разрешает папке уже существовать.

Онлайн-проверка ответа появится позже

shutil: копировать, перемещать, удалять папки

pathlib умеет работать с отдельным путём, а для «тяжёлых» операций есть модуль shutil. Он принимает и объекты Path, и строки.

import shutil

shutil.copy("отчёт.pdf", "Архив/")          # копия: исходник остаётся на месте
shutil.move("отчёт.pdf", "Документы/")      # перенос: исходника больше нет
shutil.copytree("Проект", "Проект_копия")   # скопировать папку целиком
shutil.rmtree("Временное")                  # удалить папку со всем содержимым

shutil.rmtree удаляет безвозвратно, мимо корзины. Перед запуском выведите список того, что собираетесь удалить, через print.

Проверьте себя

Контрольный вопрос. Вы вызвали shutil.copy("отчёт.pdf", "Архив/"). Что стало с исходным файлом?

АОстался на месте, в «Архиве» появилась копия
БИсчез, файл теперь только в «Архиве»
ВУдалён безвозвратно
ГПереименован в «Архив»

Подсказка: copy — копирует, move — переносит.

Онлайн-проверка ответа появится позже

А что с модулем os?

os — старший брат pathlib. В старом коде вы встретите os.path.join, os.listdir, os.makedirs. Всё это умеет и Path, поэтому в новом коде берите его. Но у os есть то, чего нет в pathlib, например переменные окружения (os.environ). Это настройки, которые система передаёт каждой запущенной программе: имя пользователя, путь к домашней папке и другие.

import os

print(os.listdir("."))             # имена файлов в текущей папке (строки)
print(os.path.join("data", "a.csv"))  # data\a.csv или data/a.csv
print(os.environ.get("USERNAME") or os.environ.get("USER"))  # имя пользователя

Собираем: сортировщик «Загрузок»

Главный приём урока: словарь «расширение → папка», цикл по файлам, перенос. Вот как выглядят «Загрузки» до и после.

Шесть файлов из «Загрузок» и путь каждого через сортировщик. У файла берётся suffix.lower(), по нему словарь RULES даёт папку: отчёт.pdf и договор.docx — в Документы, IMG_0012.JPG (расширение .jpg после lower) и мем.png — в Картинки, архив.zip — в Архивы. У setup.exe расширение .exe, правила нет, RULES.get вернёт None, файл остаётся на месте.
Два окна проводника рядом. Слева папка «Загрузки» до сортировки: вперемешку отчёт.pdf, IMG_0012.JPG, договор.docx, setup.exe, мем.png, архив.zip. Справа та же папка после скрипта: папка Документы с отчёт.pdf и договор.docx, папка Картинки с IMG_0012.JPG и мем.png, папка Архивы с архив.zip, а setup.exe остался в корне — для него нет правила.
from pathlib import Path
import shutil

RULES = {
    ".pdf": "Документы", ".docx": "Документы",
    ".jpg": "Картинки", ".png": "Картинки",
    ".zip": "Архивы",
}

downloads = Path.home() / "Downloads"
for item in downloads.iterdir():
    if not item.is_file():
        continue
    folder_name = RULES.get(item.suffix.lower())
    if folder_name is None:
        continue                      # правила нет — не трогаем
    target = downloads / folder_name
    target.mkdir(exist_ok=True)
    shutil.move(item, target / item.name)
    print(item.name, "->", folder_name)

Осторожно с одинаковыми именами. Если в папке назначения уже лежит файл с тем же именем, shutil.move(item, target / item.name) молча заменит его новым, и старый файл пропадёт — и на Windows, и на macOS и Linux. Если передать только папку, shutil.move(item, target), при совпадении имени будет ошибка shutil.Error. Чтобы ничего не потерять, перед переносом проверьте if (target / item.name).exists(): и в этом случае пропустите файл или подберите ему свободное имя, например отчёт_1.pdf.

Прежде чем гонять скрипт по настоящим «Загрузкам», создайте тестовую папку с несколькими пустыми файлами и поставьте downloads = Path("тест_загрузки"). Замените shutil.move на print — сначала посмотрите, что скрипт собирается сделать.

Проверьте себя

Контрольный вопрос. Скрипт подбирает свободное имя, чтобы при переносе не затереть файл с тем же именем. Что он выведет? Введите имя файла.

Исходный код для этого задания:

from pathlib import Path
busy = {"отчёт.pdf", "отчёт_1.pdf"}
name = "отчёт.pdf"
new = Path(name)
i = 1
while new.name in busy:
    new = Path(f"{Path(name).stem}_{i}{Path(name).suffix}")
    i += 1
print(new.name)

Подсказка: Проверьте по шагам: какие имена уже заняты в busy.

Онлайн-проверка ответа появится позже

Задание. Миссия: сортировщик «Загрузок». Напишите функцию sort_downloads(folder), которая раскладывает файлы папки по вложенным папкам по правилам ниже (расширение сравнивать без учёта регистра, файлы без правила не трогать, повторный запуск не должен падать). Создайте тестовую папку с пустыми файлами из списка (их удобно создать циклом через touch()) и запустите функцию.
В поле ответа введите число файлов, которые попали в папку «Картинки». Приложите файл программы .py; в поле ответа — число.

Принято, если:
1. введённое число совпадает с эталоном;
2. приложен файл .py, который это число выдаёт на данных из условия;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. расширение приводится к нижнему регистру (suffix.lower());
4. файлы без правила остаются на месте;
5. повторный запуск не падает с ошибкой.

Правила: .pdf, .docx → Документы; .jpg, .jpeg, .png → Картинки; .zip, .rar → Архивы.

Файлы в тестовой папке (45 шт.):

setup_257.rar

setup_815.mp3

backup_200.jpeg

IMG_0086.txt

мем_457.jpeg

backup_812.PNG

договор_873.jpg

IMG_0289.jpeg

IMG_0636.Jpeg

договор_701.docx

фильм_836.webp

file191.rar

фильм_613.PNG

file781.zip

backup_682.docx

file58.zip

счёт_868.mp3

скрин_38.mp3

file593.heic

setup_899.jpeg

договор_833.mp3

IMG_0084.rar

IMG_0335.docx

фильм_552.Jpeg

договор_523.png

IMG_0260.jpg

setup_853.txt

скрин_393.jpeg

backup_249.jpg

фильм_549.pdf

фильм_871.zip

IMG_0514.jpeg

мем_718.Jpeg

договор_197.txt

мем_995.png

мем_335.Jpeg

мем_426.pdf

мем_958.jpeg

фильм_414.jpeg

фильм_403.webp

IMG_0765.png

мем_801.webp

setup_935.heic

договор_640.JPG

скрин_739.mp3

Подсказка: Создайте файлы циклом: for n in names: (folder / n).touch(). После запуска сортировщика посчитайте файлы в «Картинках» через len(list((folder / «Картинки»).iterdir())).

✅ Готово, если: ответ раскрывает то, что просит критерий приёмки в задании выше.

Онлайн-проверка ответа появится позже

Задание. Миссия: найти пожирателей места. Напишите скрипт, который обходит папку через rglob("*"), берёт у каждого файла размер stat().st_size и переносит в папку «Большие» все файлы крупнее 100 МБ (1 МБ = 1024 × 1024 байт). В конце скрипт печатает, сколько файлов перенесено и сколько ГБ они занимают (1 ГБ = 1024³ байт, округление до 2 знаков через round).
Гигабайтные файлы создавать не нужно: вынесите отбор в функцию pick_big(sizes), которая получает словарь «имя → размер», и прогоните её на списке из условия.
В поле ответа введите число ГБ для файлов из условия. Приложите файл программы .py; в поле ответа — число.

Принято, если:
1. введённое число совпадает с эталоном;
2. приложен файл .py, который это число выдаёт на данных из условия;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. порог считается как 100 * 1024 * 1024 байт, сравнение строго «больше»;
4. папка «Большие» создаётся, если её нет, и сама не попадает в обход;
5. скрипт печатает число файлов и сумму в ГБ.

Размеры файлов в байтах (40 шт.):

фото_77.jpg — 3158845773

архив_25.zip — 4285937585

презентация_62.pptx — 1009830

бэкап_62.7z — 99438924

дистрибутив_63.iso — 4304749768

дистрибутив_81.iso — 6920775

отчёт_40.pdf — 118896122

фото_76.jpg — 83970998

фото_36.jpg — 376750808

архив_54.zip — 3076579111

бэкап_9.7z — 1729934819

дистрибутив_86.iso — 1202003

дистрибутив_18.iso — 3857395233

бэкап_24.7z — 6156230

архив_83.zip — 8503947

архив_28.zip — 92480707

видео_92.mp4 — 1493338062

архив_20.zip — 376082

видео_59.mp4 — 146360748

презентация_74.pptx — 3685008875

презентация_40.pptx — 4080053292

дистрибутив_1.iso — 152183825

дистрибутив_2.iso — 2353867370

бэкап_8.7z — 155750652

отчёт_22.pdf — 1872532

фото_78.jpg — 1582142777

отчёт_25.pdf — 95385253

отчёт_97.pdf — 84184415

презентация_86.pptx — 1354682

отчёт_62.pdf — 152080564

фото_29.jpg — 2283434922

музыка_35.flac — 94476031

отчёт_28.pdf — 2183692

архив_77.zip — 3153198335

видео_74.mp4 — 2226346777

музыка_4.flac — 1522250

бэкап_98.7z — 140925917

архив_98.zip — 151747848

презентация_25.pptx — 94488778

архив_26.zip — 130638919

Подсказка: Сложите размеры всех файлов, у которых size > 100 * 1024 * 1024, разделите сумму на 1024 ** 3 и округлите через round(…, 2).

✅ Готово, если: ответ раскрывает то, что просит критерий приёмки в задании выше.

Онлайн-проверка ответа появится позже

Что дальше

Теперь вы умеете собирать пути через Path, разбирать их на части, искать файлы по шаблону, создавать папки и переносить файлы через shutil. В следующей теме откроем то, что лежит в папках: таблицу расходов в CSV и файл настроек в JSON.

← Назад · ↑ В начало урока · ⌂ К навигатору курса · Вперёд →

Школа Виктора Комлева