Перейти к содержанию
Educora
Продвинутый20 мин28 / 42

Скрипты автоматизации

Поручи рутину Python: раскладывай файлы с помощью `pathlib` и `shutil`, находи старые файлы через `datetime`, делай интерфейс командной строки на `argparse`, веди журнал через `logging` и запускай скрипт по расписанию через cron или Планировщик заданий.

Проверь себя
В этом уроке ты узнаешь
  • Безопасно создавать, перемещать и копировать файлы и папки с помощью pathlib, os и shutil
  • Форматировать и сравнивать даты и вычислять возраст файлов с помощью datetime и timedelta
  • Писать профессиональный скрипт командной строки с argparse и logging и запускать его по расписанию

В папке «Загрузки» у Лейлы вперемешку лежат сотни файлов, а каждый вечер она вручную копирует документы на резервный диск. Это несколько минут каждый день и иногда забытая резервная копия. Скрипт на Python из 25 строк за секунды раскладывает файлы по папкам, а системный планировщик сам запускает его каждый день. В этом уроке мы соберём все части такого скрипта.

Файлы и папки: pathlib, os, shutil

ЗадачаКод
содержимое папки / вместе с подпапкамиPath.iterdir(), Path.rglob('*.pdf'), os.walk()
создать папкуPath.mkdir(parents=True, exist_ok=True)
копировать файл / папкуshutil.copy2(src, dst), shutil.copytree(src, dst)
переместить или переименоватьshutil.move(src, dst), Path.rename(new)
удалить файл / всю папкуPath.unlink(), shutil.rmtree(path)

Скрипт ниже раскладывает файлы по папкам в зависимости от расширения. Чтобы не трогать настоящие файлы, создаём временную папку через tempfile.mkdtemp() и в конце удаляем её. Обрати внимание: .JPG и .jpg считаются одинаковыми, потому что расширение приводится к нижнему регистру через lower().

Python
import shutil
import tempfile
from pathlib import Path

root = Path(tempfile.mkdtemp())
for name in ['report.pdf', 'photo1.jpg', 'photo2.JPG', 'notes.txt', 'data.csv', 'song.mp3']:
    (root / name).write_text('demo')

folders = {'.pdf': 'documents', '.txt': 'documents', '.jpg': 'images', '.csv': 'data'}
for file in list(root.iterdir()):
    if file.is_file():
        target = root / folders.get(file.suffix.lower(), 'other')
        target.mkdir(exist_ok=True)
        shutil.move(file, target / file.name)

for path in sorted(root.rglob('*')):
    print(path.relative_to(root).as_posix())
shutil.rmtree(root)
print(root.exists())
▸ Ожидаемый результат
data
data/data.csv
documents
documents/notes.txt
documents/report.pdf
images
images/photo1.jpg
images/photo2.JPG
other
other/song.mp3
False
list(root.iterdir()) заранее берёт список, чтобы изменять папку во время цикла было безопасно. Неизвестные расширения попадают в папку other.

Дата и время

В модуле datetime есть date, datetime и timedelta для промежутков времени. strftime превращает дату в форматированный текст (%Y — год, %m — месяц, %d — день, %H:%M — время), а strptime — наоборот, текст в дату. Чтобы результаты были воспроизводимыми, в примерах вместо datetime.now() взята фиксированная дата.

Python
from datetime import date, datetime, timedelta

now = datetime(2026, 9, 27, 14, 30)
print(now.strftime('%Y-%m-%d_%H-%M'))
print(now.strftime('%d.%m.%Y'))
deadline = now + timedelta(days=10, hours=3)
print(deadline.isoformat(sep=' '))
print((date(2026, 12, 31) - now.date()).days, 'days left in the year')
parsed = datetime.strptime('05.10.2026 09:15', '%d.%m.%Y %H:%M')
print(parsed, parsed.strftime('%A'))
▸ Ожидаемый результат
2026-09-27_14-30
27.09.2026
2026-10-07 17:30:00
95 days left in the year
2026-10-05 09:15:00 Monday

Время последнего изменения файла даёт stat().st_mtime (секунды с 1970 года). Превращаем его в дату через datetime.fromtimestamp и сравниваем с пределом. В примере «возраст» файлов задаём искусственно через os.utime:

Python
import os
import tempfile
from datetime import datetime, timedelta
from pathlib import Path

root = Path(tempfile.mkdtemp())
now = datetime(2026, 9, 27, 12, 0)
for name, days in {'backup_old.zip': 45, 'log_week.txt': 7, 'log_today.txt': 0}.items():
    f = root / name
    f.write_text('x')
    t = (now - timedelta(days=days)).timestamp()
    os.utime(f, (t, t))

limit = now - timedelta(days=30)
for f in sorted(root.iterdir()):
    modified = datetime.fromtimestamp(f.stat().st_mtime)
    status = 'OLD' if modified < limit else 'ok'
    print(f'{f.name:15} {modified:%Y-%m-%d} {status}')
▸ Ожидаемый результат
backup_old.zip  2026-08-13 OLD
log_today.txt   2026-09-27 ok
log_week.txt    2026-09-20 ok

argparse и logging

Хороший скрипт не хранит пути и числа внутри кода — он берёт их из командной строки: python cleanup.py downloads --days 14 --dry-run. argparse разбирает позиционные аргументы (folder), опции (--days) и флаги (--dry-run), преобразует типы и сам создаёт страницу --help. В настоящем скрипте parse_args() вызывают без аргументов; здесь мы имитируем командную строку списком.

Python
import argparse

parser = argparse.ArgumentParser(prog='cleanup', description='List or delete old files.')
parser.add_argument('folder', help='folder to scan')
parser.add_argument('--days', type=int, default=30, help='age limit in days')
parser.add_argument('--dry-run', action='store_true', help='only show what would happen')

args = parser.parse_args(['downloads', '--days', '14', '--dry-run'])
print(args)
print(args.folder, args.days, args.dry_run)
print(parser.parse_args(['photos']))
▸ Ожидаемый результат
Namespace(folder='downloads', days=14, dry_run=True)
downloads 14 True
Namespace(folder='photos', days=30, dry_run=False)
--dry-run превращается в атрибут dry_run (дефис заменяется подчёркиванием). Во втором вызове неуказанные опции получили значения по умолчанию.

На экран скрипта, работающего по расписанию, никто не смотрит, поэтому вместо print используют logging: у каждого сообщения есть уровень (DEBUG, INFO, WARNING, ERROR, CRITICAL) и время, сообщения можно писать в файл и отфильтровывать неважные. Заполнители %s и %d форматируют сообщение, только когда оно действительно выводится.

Python
import logging
import sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO,
                    format='%(levelname)-8s %(name)s: %(message)s', force=True)
log = logging.getLogger('backup')
log.debug('not shown: the level is INFO')
log.info('Backup started')
log.warning('Disk is 85% full')
try:
    ratio = 10 / 0
except ZeroDivisionError:
    log.error('Could not compute the ratio')
log.info('Copied %d files in %.1f s', 128, 3.42)
▸ Ожидаемый результат
INFO     backup: Backup started
WARNING  backup: Disk is 85% full
ERROR    backup: Could not compute the ratio
INFO     backup: Copied 128 files in 3.4 s
Здесь журнал выводится на экран (sys.stdout); в настоящем скрипте указывают filename='backup.log' и добавляют в формат %(asctime)s (время). Сообщение уровня DEBUG ниже порога INFO, поэтому не видно.

Готовый скрипт и его расписание

Python
import argparse
import logging
import shutil
from datetime import datetime
from pathlib import Path

log = logging.getLogger('backup')

def backup(source, dest_root, dry_run=False):
    stamp = datetime.now().strftime('%Y-%m-%d_%H-%M')
    target = Path(dest_root) / f'{Path(source).name}_{stamp}'
    log.info('Copying %s -> %s (dry run: %s)', source, target, dry_run)
    if not dry_run:
        shutil.copytree(source, target)
    return target

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Back up a folder with a timestamp.')
    parser.add_argument('source', help='folder to copy')
    parser.add_argument('dest', help='where to put the copy')
    parser.add_argument('--dry-run', action='store_true', help='only log, do not copy')
    args = parser.parse_args()
    logging.basicConfig(filename='backup.log', level=logging.INFO,
                        format='%(asctime)s %(levelname)s %(message)s')
    backup(args.source, args.dest, args.dry_run)
backup.py. Блок if __name__ == '__main__': выполняется, только когда файл запущен напрямую, — функцию backup можно импортировать в другие скрипты и тесты. На своём компьютере: python backup.py Documents D:/Backup --dry-run, затем загляни в backup.log.

Чтобы запускать скрипт регулярно, используют планировщик операционной системы: cron в Linux и macOS и Планировщик заданий в Windows (Task Scheduler, в командной строке — schtasks). В строке cron пять полей: минута, час, день месяца, месяц, день недели; * означает «любой».

Выражение cronКогда
0 21 * * *каждый день в 21:00
*/15 * * * *каждые 15 минут
0 9 * * 1-5по будням в 09:00
0 3 1 * *1-го числа каждого месяца в 03:00
Terminal
# Linux / macOS: open the table with `crontab -e` and add a line
0 21 * * * /home/leyla/.venv/bin/python /home/leyla/scripts/backup.py /home/leyla/Documents /mnt/backup

# Windows (Command Prompt): the same task every day at 21:00
schtasks /Create /SC DAILY /ST 21:00 /TN "Daily backup" /TR "C:\scripts\.venv\Scripts\python.exe C:\scripts\backup.py C:\Users\Leyla\Documents D:\Backup"
Есть и библиотека Python schedule (pip install schedule), но с ней скрипт должен работать постоянно; системный планировщик продолжает работать и после перезагрузки компьютера.
Задание

Сгруппируй имена файлов по расширению: бери расширение в нижнем регистре, а для файлов без расширения используй '(none)'. Выведи расширения в отсортированном порядке, каждое со своим отсортированным списком файлов, на отдельных строках.

Задание · Python
from pathlib import PurePath

files = ['report.PDF', 'photo.jpg', 'notes.txt', 'scan.pdf', 'song.mp3', 'README']
# group by lowercase extension, then print: extension [sorted names]
▸ Ожидаемый результат
(none) ['README']
.jpg ['photo.jpg']
.mp3 ['song.mp3']
.pdf ['report.PDF', 'scan.pdf']
.txt ['notes.txt']
Задание

Сегодня 2026-09-27. Для каждого файла старше 30 дней выведи его имя и сколько дней назад он изменён (в порядке словаря). В конце выведи самый новый файл в виде newest: <имя>.

Задание · Python
from datetime import date

today = date(2026, 9, 27)
files = {'backup.zip': '2026-07-30', 'notes.txt': '2026-09-20',
         'photo.jpg': '2026-08-15', 'log.txt': '2026-09-27'}
# print old files with their age in days, then the newest file
▸ Ожидаемый результат
backup.zip 59
photo.jpg 43
newest: log.txt

Главное

  • pathlib — для путей и файлов, shutil — для копирования, перемещения и удаления папок; удалённые файлы не вернуть.
  • datetime, timedelta, strftime и strptime; в именах файлов пиши дату как YYYY-MM-DD.
  • argparse даёт позиционные аргументы, опции, флаги и автоматический --help.
  • В скриптах по расписанию используй logging с уровнями и временем вместо print.
  • cron или Планировщик заданий запускают скрипт регулярно; нужны абсолютные пути, полный путь к python и привычка к --dry-run.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какая функция безвозвратно удаляет папку со всем содержимым?