Программы-инструменты: запускаются из терминала, работают с файлами и сетью, ставятся в расписание
argparselogging вместо printПрограмма, которая спрашивает данные через 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.
Задача: в каталоге загрузок сотни файлов вперемешку; разложить их по подкаталогам «Документы», «Картинки», «Архивы», «Прочее». Программа ниже собирает всё из модулей 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, операционной системе.
Программа, которая перемещает или удаляет файлы, ошибается дорого. Поэтому у неё есть пробный прогон --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 удаляет каталог со всем содержимым без корзины и без вопросов. Перед удалением по пути, полученному из аргументов или вычисленному в программе, проверьте путь и сделайте пробный прогон.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-строку: строка соберётся, только если сообщение действительно выводится.
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 ~ выполнится как команда. Список аргументов передаётся программе как есть, оболочка в нём ничего не разбирает.Кроме стандартной библиотеки, есть сотни тысяч сторонних пакетов в каталоге 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.requirements.txt — список пакетов с точными версиями. На другом компьютере окружение воссоздаётся командой pip install -r requirements.txt.~/projects/weather/.venv/bin/python rates.py.apt install python3-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 единиц.
Инструмент из этого модуля можно запускать без человека. В 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
cd в каталог проекта, либо пути в программе от Path(__file__).parent..venv/bin/python.input, всё через аргументы и файлы настроек.В Windows то же делает Планировщик заданий: действие «Запуск программы», рабочая папка — каталог проекта, аргумент — rates.py, программа — интерпретатор окружения:
C:\projects\weather\.venv\Scripts\python.exe
argparse даёт справку, ошибки и код возврата 2--dry-run и не ломается при повторном запускеlogging с уровнями, результат — в printsubprocess.run([...]) списком, с проверкой returncode; без shell=True для пользовательских данных.venv проекта; версии — в requirements.txttimeout и обработкой RequestException