- Безопасно создавать, перемещать и копировать файлы и папки с помощью
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().
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() взята фиксированная дата.
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:
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() вызывают без аргументов; здесь мы имитируем командную строку списком.
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 форматируют сообщение, только когда оно действительно выводится.
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, поэтому не видно.Готовый скрипт и его расписание
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 |
# 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"schedule (pip install schedule), но с ней скрипт должен работать постоянно; системный планировщик продолжает работать и после перезагрузки компьютера.Сгруппируй имена файлов по расширению: бери расширение в нижнем регистре, а для файлов без расширения используй '(none)'. Выведи расширения в отсортированном порядке, каждое со своим отсортированным списком файлов, на отдельных строках.
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: <имя>.
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.