Модуль 08 — Пакеты и автоматизация

Программы-инструменты: запускаются из терминала, работают с файлами и сетью, ставятся в расписание

Прогресс курса Модуль 8 из 9

Что вы освоите в этом модуле

01

Аргументы командной строки

Программа, которая спрашивает данные через input, не годится для автоматизации: её нельзя запустить из другого скрипта или по расписанию. Инструмент получает данные аргументами, как ls -l /etc. Они лежат в списке sys.argv: нулевой элемент — имя программы, дальше — аргументы строками.

import sys

print("Программа:", sys.argv[0])
print("Аргументы:", sys.argv[1:])
$ python3 args_demo.py report.csv --verbose 10
Программа: args_demo.py
Аргументы: ['report.csv', '--verbose', '10']

Разбирать sys.argv вручную можно, пока аргумент один. Как только появляются необязательные флаги, значения по умолчанию и справка, нужен argparse.

02

argparse и программа целиком

Задача: в каталоге загрузок сотни файлов вперемешку; разложить их по подкаталогам «Документы», «Картинки», «Архивы», «Прочее». Программа ниже собирает всё из модулей 05–07 и добавляет три новых модуля стандартной библиотеки:

"""Раскладывает файлы каталога по подкаталогам по типу."""
import argparse
import logging
import shutil
import sys
from pathlib import Path

TYPES = {
    "Документы": {".pdf", ".docx", ".xlsx", ".txt", ".odt"},
    "Картинки": {".jpg", ".jpeg", ".png", ".webp"},
    "Архивы": {".zip", ".7z", ".tar", ".gz"},
}

log = logging.getLogger("organize")


def category(path: Path) -> str:
    """Название подкаталога для файла; неизвестный тип — «Прочее»."""
    ext = path.suffix.lower()
    for name, extensions in TYPES.items():
        if ext in extensions:
            return name
    return "Прочее"


def organize(folder: Path, dry_run: bool) -> int:
    moved = 0
    for item in sorted(folder.iterdir()):
        if not item.is_file():
            continue
        target = folder / category(item) / item.name
        if target.exists():
            log.warning("пропускаю %s: %s уже существует", item.name, target)
            continue
        log.info("%s -> %s/", item.name, target.parent.name)
        if not dry_run:
            target.parent.mkdir(exist_ok=True)
            shutil.move(item, target)
        moved += 1
    return moved


def main() -> int:
    parser = argparse.ArgumentParser(description="Разложить файлы по подкаталогам по типу.")
    parser.add_argument("folder", type=Path, help="каталог с файлами")
    parser.add_argument("-n", "--dry-run", action="store_true",
                        help="только показать, что будет сделано")
    parser.add_argument("-q", "--quiet", action="store_true", help="выводить только ошибки")
    args = parser.parse_args()

    logging.basicConfig(level=logging.WARNING if args.quiet else logging.INFO,
                        format="%(levelname)s %(message)s")
    if not args.folder.is_dir():
        log.error("нет каталога %s", args.folder)
        return 2
    count = organize(args.folder, args.dry_run)
    print(f"{'Будет перемещено' if args.dry_run else 'Перемещено'}: {count}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

ArgumentParser описывает аргументы: folder — обязательный позиционный, type=Path сразу превращает строку в путь; -n/--dry-run — флаг: есть — True, нет — False. parse_args() разбирает sys.argv и возвращает объект, где значения лежат в атрибутах args.folder, args.dry_run. Справку и сообщения об ошибках argparse строит сам:

$ python3 organize.py --help
usage: organize.py [-h] [-n] [-q] folder

Разложить файлы по подкаталогам по типу.

positional arguments:
  folder         каталог с файлами

options:
  -h, --help     show this help message and exit
  -n, --dry-run  только показать, что будет сделано
  -q, --quiet    выводить только ошибки
$ python3 organize.py
usage: organize.py [-h] [-n] [-q] folder
organize.py: error: the following arguments are required: folder
[код 2]

Служебные слова справки (usage, options) — на английском, описания — те, что заданы в коде. При ошибке в аргументах argparse завершает программу с кодом 2 — так же поступают системные утилиты, и скрипты, вызывающие программу, отличат «неправильно вызвали» от «не получилось». sys.exit(main()) передаёт код, который вернула main, операционной системе.

03

Файлы и каталоги: пробный прогон

Программа, которая перемещает или удаляет файлы, ошибается дорого. Поэтому у неё есть пробный прогон --dry-run: показать, что будет сделано, ничего не меняя. Сначала смотрят его, потом запускают по-настоящему:

$ python3 organize.py downloads --dry-run
INFO readme -> Прочее/
INFO архив.zip -> Архивы/
INFO договор.docx -> Документы/
INFO заметки.txt -> Документы/
INFO отчёт.pdf -> Документы/
INFO смета.xlsx -> Документы/
INFO фото_001.jpg -> Картинки/
INFO фото_002.JPG -> Картинки/
Будет перемещено: 8
$ python3 organize.py downloads
INFO readme -> Прочее/
INFO архив.zip -> Архивы/
INFO договор.docx -> Документы/
INFO заметки.txt -> Документы/
INFO отчёт.pdf -> Документы/
INFO смета.xlsx -> Документы/
INFO фото_001.jpg -> Картинки/
INFO фото_002.JPG -> Картинки/
Перемещено: 8
>>> from pathlib import Path
>>> for p in sorted(Path("downloads").rglob("*")):
...     print(p)
... 
downloads/Архивы
downloads/Архивы/архив.zip
downloads/Документы
downloads/Документы/договор.docx
downloads/Документы/заметки.txt
downloads/Документы/отчёт.pdf
downloads/Документы/смета.xlsx
downloads/Картинки
downloads/Картинки/фото_001.jpg
downloads/Картинки/фото_002.JPG
downloads/Прочее
downloads/Прочее/readme

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

$ python3 organize.py downloads
Перемещено: 0

Такое свойство называют идемпотентностью: повторный запуск безопасен. Для этого программа проверяет target.exists() и не перезаписывает файл, который уже лежит на месте, а пишет предупреждение.

ДействиеФункция
Переместить или переименоватьshutil.move(откуда, куда), Path.rename
Скопировать файл с датами измененияshutil.copy2(откуда, куда)
Скопировать каталог целикомshutil.copytree(откуда, куда)
Удалить файл / пустой каталог / каталог с содержимымPath.unlink() / Path.rmdir() / shutil.rmtree()
Архив zip или tar.gz из каталогаshutil.make_archive(имя, "zip", каталог)
shutil.rmtree удаляет каталог со всем содержимым без корзины и без вопросов. Перед удалением по пути, полученному из аргументов или вычисленному в программе, проверьте путь и сделайте пробный прогон.
04

Журнал: logging

print годится для результата работы. Для сообщений о ходе работы — что сделано, что пропущено, что сломалось — используют модуль logging. У каждого сообщения есть уровень, и уровень вывода настраивается в одном месте: ключ -q в organize.py скрывает обычные сообщения, но оставляет ошибки.

УровеньКогда
DEBUGПодробности для отладки
INFOОбычный ход работы: «файл перемещён»
WARNINGЧто-то не так, но работа продолжается: «файл уже есть, пропускаю»
ERRORДействие не выполнено
CRITICALПрограмма не может продолжать
$ python3 organize.py nofolder
ERROR нет каталога nofolder
[код 2]

Журнал пишется в поток ошибок (stderr), результат print — в stdout, поэтому их можно разделить: python3 organize.py downloads 2>organize.log. Чтобы журнал шёл в файл с датой и временем, достаточно изменить настройку:

logging.basicConfig(filename="organize.log",
                    format="%(asctime)s %(levelname)s %(message)s")

В сообщениях используйте подстановку log.info("%s -> %s", a, b), а не f-строку: строка соберётся, только если сообщение действительно выводится.

05

Запуск команд: subprocess

subprocess.run запускает внешнюю программу и ждёт её завершения. Команда передаётся списком: программа и аргументы по отдельности. capture_output=True, text=True — забрать вывод строками, а не выводить в терминал.

import shutil
import subprocess

usage = shutil.disk_usage("/")
print(f"Корень: занято {usage.used / usage.total:.0%}, свободно {usage.free / 2**30:.1f} ГиБ")

result = subprocess.run(["df", "-h", "--output=target,size,pcent", "/", "/tmp"],
                        capture_output=True, text=True)
print("Код возврата df:", result.returncode)
print(result.stdout, end="")

result = subprocess.run(["ls", "/нет-такого"], capture_output=True, text=True)
print("Код возврата ls:", result.returncode)
print("Ошибка:", result.stderr.strip())
$ python3 disk.py
Корень: занято 19%, свободно 22.4 ГиБ
Код возврата df: 0
Cмонтировано в Размер Использовано%
/                 30G           21%
/tmp             2,0G            3%
Код возврата ls: 2
Ошибка: ls: невозможно получить доступ к '/нет-такого': Нет такого файла или каталога

Объект результата содержит returncode, stdout и stderr — проверяйте код возврата, а не только вывод. Если нужно, чтобы ненулевой код сразу превращался в исключение, добавьте check=True. Не все задачи требуют внешних команд: место на диске Python узнаёт сам через shutil.disk_usage, и такой код работает и в Windows.

Не собирайте команду строкой из пользовательских данных и не используйте shell=True с ними: имя файла x; rm -rf ~ выполнится как команда. Список аргументов передаётся программе как есть, оболочка в нём ничего не разбирает.
06

Пакеты: pip и виртуальные окружения

Кроме стандартной библиотеки, есть сотни тысяч сторонних пакетов в каталоге PyPI. Ставит их программа pip. В Debian 13 установить пакет в системный Python нельзя — система защищает свои программы от чужих версий библиотек:

$ pip install requests
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip.
…

Правильный путь — виртуальное окружение (venv): каталог проекта со своей копией интерпретатора и своими пакетами. У каждого проекта — своё окружение, версии библиотек разных проектов не мешают друг другу. Вывод снят в Debian 13:

$ cd ~/projects/weather
$ python3 -m venv .venv
$ ls .venv/bin
activate  activate.csh  activate.fish  Activate.ps1  pip  pip3  pip3.13  python  python3  python3.13
$ source .venv/bin/activate
(.venv) $ which python
/home/student/projects/weather/.venv/bin/python
(.venv) $ pip install requests
Collecting requests
  Downloading requests-2.34.2-py3-none-any.whl.metadata (4.8 kB)
…
Installing collected packages: urllib3, idna, charset_normalizer, certifi, requests
Successfully installed certifi-2026.7.22 charset_normalizer-3.5.2 idna-3.20 requests-2.34.2 urllib3-2.8.0
(.venv) $ pip freeze > requirements.txt
(.venv) $ cat requirements.txt
certifi==2026.7.22
charset-normalizer==3.5.2
idna==3.20
requests==2.34.2
urllib3==2.8.0
(.venv) $ deactivate
$ which python3
/usr/bin/python3

source .venv/bin/activate подменяет python и pip в текущем терминале на версии из окружения; приглашение начинается с (.venv). В Windows активация — .venv\Scripts\activate. Вместе с requests поставились его зависимости — пакеты, без которых он не работает.

Правила работы с окружением

  • Окружение создаётся в каталоге проекта и называется .venv; в git оно не попадает — добавьте .venv/ в .gitignore.
  • В git кладут requirements.txt — список пакетов с точными версиями. На другом компьютере окружение воссоздаётся командой pip install -r requirements.txt.
  • Активация нужна только для удобства в терминале. Скрипт в расписании запускают интерпретатором окружения по полному пути: ~/projects/weather/.venv/bin/python rates.py.
  • Пакет, который есть в Debian (apt install python3-requests), можно ставить и так — но тогда версию выбирает дистрибутив, а не проект.
07

HTTP-запросы: requests

Многие сервисы отдают данные по HTTP в формате JSON (что такое HTTP — в курсе «Основы компьютерных сетей»). Пакет requests делает запрос одной строкой. Курсы валют Центробанка в JSON публикует сервис cbr-xml-daily.ru:

"""Курсы валют ЦБ РФ на сегодня."""
import sys

import requests

URL = "https://www.cbr-xml-daily.ru/daily_json.js"

try:
    response = requests.get(URL, timeout=10)
    response.raise_for_status()
except requests.RequestException as e:
    sys.exit(f"Не удалось получить курсы: {e}")

data = response.json()
print("Курсы ЦБ на", data["Date"][:10])
for code in ("USD", "EUR", "CNY"):
    v = data["Valute"][code]
    rate = v["Value"] / v["Nominal"]
    print(f"{code}  {v['Name']:<20} {rate:8.4f} руб.")
$ python3 rates.py
Курсы ЦБ на 2026-10-15
USD  Доллар США            83.9497 руб.
EUR  Евро                  93.9229 руб.
CNY  Юань                  12.5358 руб.

timeout=10 обязателен: без него программа будет ждать ответа от зависшего сервера бесконечно. raise_for_status() превращает ответ с ошибкой (404, 500) в исключение; requests.RequestException — общий тип для всех сетевых ошибок: нет сети, таймаут, неверный адрес. response.json() разбирает JSON в словари и списки. Курсы в вашем запуске будут на вашу дату. Если сервис недоступен или адрес неверен, программа говорит об этом одной строкой:

import sys

import requests

try:
    response = requests.get("https://www.cbr-xml-daily.ru/no-such-file.js", timeout=10)
    response.raise_for_status()
except requests.RequestException as e:
    sys.exit(f"Не удалось получить курсы: {e}")
$ python3 rates_bad.py
Не удалось получить курсы: 404 Client Error: Not Found for url: https://www.cbr-xml-daily.ru/no-such-file.js
[код 1]

Структуру ответа (data["Valute"]["USD"]["Value"]) узнают, открыв адрес в браузере или распечатав data. Nominal нужен потому, что часть валют ЦБ котирует за 10 или 100 единиц.

08

Запуск по расписанию

Инструмент из этого модуля можно запускать без человека. В Linux — через cron (подробно — модуль 08 курса «Bash-скрипты и автоматизация»). Строка для crontab -e, которая каждый будний день в 9:00 дописывает курсы в файл:

0 9 * * 1-5  cd /home/student/projects/weather && .venv/bin/python rates.py >> rates.log 2>&1

Чем запуск по расписанию отличается от запуска руками

  • Текущий каталог другой — cron запускает из домашнего. Либо cd в каталог проекта, либо пути в программе от Path(__file__).parent.
  • Окружение не активировано — интерпретатор указывают полным путём к .venv/bin/python.
  • Никто не видит вывод — stdout и stderr направляют в файл журнала, программа сообщает об ошибках кодом возврата.
  • Нельзя ответить на вопрос — никаких input, всё через аргументы и файлы настроек.

В Windows то же делает Планировщик заданий: действие «Запуск программы», рабочая папка — каталог проекта, аргумент — rates.py, программа — интерпретатор окружения:

C:\projects\weather\.venv\Scripts\python.exe

Ключевые выводы модуля

Запомните главное

  • Инструмент получает данные аргументами: argparse даёт справку, ошибки и код возврата 2
  • Программа, которая меняет файлы, имеет --dry-run и не ломается при повторном запуске
  • Ход работы — в logging с уровнями, результат — в print
  • subprocess.run([...]) списком, с проверкой returncode; без shell=True для пользовательских данных
  • Сторонние пакеты — только в .venv проекта; версии — в requirements.txt
  • Сетевой запрос — всегда с timeout и обработкой RequestException
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 07: Файлы и исключения Все модули Итоговый проект