Урок 2 из 8 · 20 мин · обновлено
Файлы и папки: переименовать, разложить, найти дубли
Как работать с файлами в Python через pathlib и shutil: перебрать папку, разложить файлы по типам, найти одинаковые файлы по хешу и безопасно переименовать сотню файлов с пробным запуском.
Пути и pathlib
В офисе «Канцмаркета» все вложения из почты падают в одну папку «Входящие»: накладные, таблицы магазинов, фото витрин, договоры. Раз в неделю кто-то вручную раскладывает их по папкам. Поручим это скрипту.
С файлами в Python удобнее всего работать через модуль pathlib. Путь к файлу или папке там — объект Path, у которого есть полезные свойства:
from pathlib import Path
file = Path("Входящие") / "Накладная 1406.pdf"
print(file.name) # Накладная 1406.pdf — полное имя
print(file.stem) # Накладная 1406 — без расширения
print(file.suffix) # .pdf — расширение
print(file.parent) # Входящие — папка, где лежит файл
Перебрать файлы в папке помогают три метода:
iterdir()— всё, что лежит в папке напрямую;glob("*.pdf")— файлы по шаблону, звёздочка означает «что угодно»;rglob("*.pdf")— то же, но ещё и во всех вложенных папках.
Разложить файлы по папкам
Вот что лежит во «Входящих»:
Накладная 1406.pdf
накладная_1407.PDF
Продажи магазин 1.xlsx
цены поставщика.csv
фото витрины.jpg
фото витрины (1).jpg
договор аренды.docx
readme.txt
Скрипт смотрит на расширение каждого файла и переносит его в нужную папку. Какое расширение куда — задаём словарём:
from pathlib import Path
import shutil
inbox = Path("Входящие")
folders = {
".pdf": "Документы", ".docx": "Документы",
".xlsx": "Таблицы", ".csv": "Таблицы",
".jpg": "Фото", ".png": "Фото",
}
for file in sorted(inbox.iterdir()):
if not file.is_file():
continue
target = inbox / folders.get(file.suffix.lower(), "Разное")
target.mkdir(exist_ok=True)
shutil.move(file, target / file.name)
print(f"{file.name} → {target.name}")
readme.txt → Разное
договор аренды.docx → Документы
Накладная 1406.pdf → Документы
накладная_1407.PDF → Документы
Продажи магазин 1.xlsx → Таблицы
фото витрины (1).jpg → Фото
фото витрины.jpg → Фото
цены поставщика.csv → Таблицы
Разберём важные места:
is_file()пропускает папки — их переносить не нужно;suffix.lower()приводит расширение к нижнему регистру, поэтому.PDFтоже попадёт в «Документы»;folders.get(..., "Разное")— если расширения нет в словаре, файл уйдёт в «Разное»;mkdir(exist_ok=True)создаёт папку и не ругается, если она уже есть.
Найти дубли
Одно и то же фото витрины прислали дважды: «фото витрины.jpg» и «фото витрины (1).jpg». По имени дубль не найти — имена разные. Зато у одинаковых файлов совпадает хеш — короткий «отпечаток» содержимого. Его считает модуль hashlib:
import hashlib
from pathlib import Path
seen = {}
for file in sorted(Path("Входящие").rglob("*")):
if not file.is_file():
continue
digest = hashlib.sha256(file.read_bytes()).hexdigest()
if digest in seen:
print(f"Одинаковые файлы: {seen[digest].name} и {file.name}")
else:
seen[digest] = file
Одинаковые файлы: фото витрины (1).jpg и фото витрины.jpg
Словарь seen хранит пары «хеш — первый файл с таким хешем». Если хеш уже встречался, значит, нашли копию. Скрипт ничего не удаляет, а только сообщает: решать, какой файл оставить, лучше человеку.
Переименовать без риска
Накладные называются как придётся: «Накладная 1406.pdf», «накладная_1407.PDF». Хочется единообразно: «Накладная №1406.pdf». Ошибка в массовом переименовании может испортить сотни имён, поэтому сначала делаем пробный запуск: скрипт только печатает, что собирается сделать.
from pathlib import Path
DRY_RUN = True # сначала только показываем, что сделаем
docs = Path("Входящие") / "Документы"
for file in sorted(docs.glob("*")):
if "накладная" not in file.name.lower():
continue
number = "".join(ch for ch in file.stem if ch.isdigit())
new_name = f"Накладная №{number}{file.suffix.lower()}"
print(f"{file.name} → {new_name}")
if not DRY_RUN:
file.rename(file.with_name(new_name))
Накладная 1406.pdf → Накладная №1406.pdf
накладная_1407.PDF → Накладная №1407.pdf
Всё верно — меняем DRY_RUN на False и запускаем ещё раз. with_name() возвращает путь с тем же расположением, но новым именем, а rename() переименовывает файл на диске.
Такой приём стоит использовать в любом скрипте, который что-то меняет или удаляет: сначала посмотреть, потом сделать.
Попробуйте сами. Напишите скрипт, который проходит по папке «Фото» и добавляет к имени каждой картинки номер по порядку: «001 фото витрины.jpg», «002 …». Сделайте пробный запуск.
Решение
from pathlib import Path
DRY_RUN = True
photos = Path("Входящие") / "Фото"
for i, file in enumerate(sorted(photos.glob("*.jpg")), start=1):
new_name = f"{i:03} {file.name}"
print(f"{file.name} → {new_name}")
if not DRY_RUN:
file.rename(file.with_name(new_name))
enumerate(..., start=1) выдаёт номер вместе с файлом, а {i:03} дополняет номер нулями до трёх цифр: 1 → 001. Так файлы и в проводнике отсортируются правильно.
Подумайте, что будет, если запустить скрипт дважды: имена станут «001 001 фото…». Можно добавить проверку — пропускать файлы, имя которых уже начинается с цифр.
Мини-тест · 4 вопроса
Проверьте себя
Выберите вариант, который кажется вам правильным. В конце вы увидите свой результат и объяснение к каждому вопросу.
-
Что вернёт Path("Накладная 1406.pdf").suffix?
suffix — расширение вместе с точкой. Имя без расширения хранится в stem, полное имя — в name.
-
Чем rglob("*") отличается от iterdir()?
iterdir() перебирает только то, что лежит в папке напрямую. rglob() проходит и по всем вложенным папкам.
-
Два файла называются по-разному, но это одно и то же фото. Как скрипт это поймёт?
Хеш считается по содержимому файла. У одинаковых файлов он совпадает, как бы их ни назвали.
-
Зачем в скрипте переименования переменная DRY_RUN = True?
Пробный запуск печатает план изменений. Если всё верно, меняете DRY_RUN на False и запускаете уже по-настоящему.