Урок 2 из 8 · 20 мин · обновлено

Файлы и папки: переименовать, разложить, найти дубли

Как работать с файлами в Python через pathlib и shutil: перебрать папку, разложить файлы по типам, найти одинаковые файлы по хешу и безопасно переименовать сотню файлов с пробным запуском.

Пути и pathlib

В офисе «Канцмаркета» все вложения из почты падают в одну папку «Входящие»: накладные, таблицы магазинов, фото витрин, договоры. Раз в неделю кто-то вручную раскладывает их по папкам. Поручим это скрипту.

С файлами в Python удобнее всего работать через модуль pathlib. Путь к файлу или папке там — объект Path, у которого есть полезные свойства:

Python
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

Скрипт смотрит на расширение каждого файла и переносит его в нужную папку. Какое расширение куда — задаём словарём:

Python
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:

Python
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». Ошибка в массовом переименовании может испортить сотни имён, поэтому сначала делаем пробный запуск: скрипт только печатает, что собирается сделать.

Python
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 …». Сделайте пробный запуск.

Решение
Python
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 вопроса

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

Выберите вариант, который кажется вам правильным. В конце вы увидите свой результат и объяснение к каждому вопросу.

  1. Что вернёт Path("Накладная 1406.pdf").suffix?

  2. Чем rglob("*") отличается от iterdir()?

  3. Два файла называются по-разному, но это одно и то же фото. Как скрипт это поймёт?

  4. Зачем в скрипте переименования переменная DRY_RUN = True?