stdlib: os, pathlib, subprocess, shutil

Стандартная библиотека Python даёт богатый набор инструментов для работы с операционной системой. В этом уроке - как работать с путями и файлами (pathlib и os), запускать внешние процессы (subprocess), копировать и удалять файлы (shutil). Базовые команды и права в файловой системе - в треке Linux.

pathlib - современная работа с путями

С Python 3.4 рекомендуется использовать pathlib.Path вместо os.path:

from pathlib import Path

# Создание пути
p = Path("/home/user/data")
p = Path("data") / "file.txt"   # join через /
p = Path.home() / "Documents"
p = Path.cwd() / "log.txt"

# Информация
p.name           # 'file.txt' - имя файла
p.stem           # 'file' - без расширения
p.suffix         # '.txt' - расширение
p.parent         # директория (Path)
p.parts          # tuple компонентов пути
p.is_absolute()  # True/False

# Существование и тип
p.exists()
p.is_file()
p.is_dir()
p.is_symlink()

/ оператор для join путей - элегантно по сравнению с os.path.join("a", "b").

Чтение и запись через pathlib

p = Path("data.txt")

# Чтение
text = p.read_text(encoding="utf-8")
data = p.read_bytes()

# Запись (перезатирает)
p.write_text("hello", encoding="utf-8")
p.write_bytes(b"binary data")

# Append - через open
with p.open("a", encoding="utf-8") as f:
    f.write("more text\n")

read_text/write_text удобны для одиночных операций. Для сложного использования - open() как контекст-менеджер.

Обход директорий

# Все файлы и поддиректории
for item in Path(".").iterdir():
    print(item)

# Только Python файлы (рекурсивно)
for py_file in Path(".").rglob("*.py"):
    print(py_file)

# Не рекурсивно
for py_file in Path(".").glob("*.py"):
    print(py_file)

# Фильтрация
for item in Path(".").iterdir():
    if item.is_file() and item.suffix == ".log":
        print(item)

glob поддерживает wildcards: * (любые символы кроме /), ** (включая subdirs), ? (один символ), [abc] (любой из). Рекурсивный обход дерева каталогов через rglob разбираем в уроке про рекурсию.

Создание директорий

p = Path("path/to/new/dir")

p.mkdir()                          # одна директория
p.mkdir(parents=True)              # вместе с родителями (как mkdir -p)
p.mkdir(parents=True, exist_ok=True)   # не падать если уже существует

exist_ok=True важен в идемпотентных скриптах - не упадёт при повторном запуске.

Удаление

p = Path("file.txt")

p.unlink()              # удалить файл
p.unlink(missing_ok=True)   # не падать если нет

p = Path("dir")
p.rmdir()               # удалить ПУСТУЮ директорию

# Удалить директорию с содержимым - через shutil
import shutil
shutil.rmtree(p)

rmtree опасна - удаляет всё внутри. Использовать с осторожностью.

Перемещение и переименование

src = Path("old.txt")
dst = Path("new.txt")
src.rename(dst)         # переименование/перемещение
src.replace(dst)        # как rename, но перезатирает если dst существует

# Перемещение между ФС (rename может не работать)
import shutil
shutil.move(str(src), str(dst))

Метаданные файла

p = Path("file.txt")

stat = p.stat()
stat.st_size            # размер в байтах
stat.st_mtime           # время последнего изменения (Unix timestamp)
stat.st_ctime           # время создания (или change на Linux)

# Удобнее через datetime
from datetime import datetime
modified = datetime.fromtimestamp(stat.st_mtime)

os модуль

os это lower-level API для системных вызовов:

import os

# Текущая директория
os.getcwd()
os.chdir("/new/path")

# Переменные окружения
os.environ["PATH"]              # значение или KeyError
os.environ.get("DEBUG", "0")    # с дефолтом
os.environ["MY_VAR"] = "value"  # установка

# Информация о системе
os.name                  # 'posix' или 'nt'
os.cpu_count()           # количество CPU
os.getpid()              # PID процесса
os.getuid()              # user ID (Unix)

Для работы с путями pathlib.Path обычно лучше. os.path используется в legacy или для специфичных операций.

os vs pathlib - что когда

Задачаpathlibos
Работа с путямиPath()os.path
Чтение/запись файлаPath.read_text()open()
Обход директорийPath.glob()os.listdir, os.walk
Переменные окружения-os.environ
Запуск процессов-subprocess
Сигналы-signal

Современная практика - pathlib для путей и файлов, os для system-level операций (env, PID, signals).

subprocess - запуск внешних процессов

import subprocess

# Простой вызов
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
print(result.stdout)
print(result.returncode)

# С проверкой ошибок
result = subprocess.run(
    ["git", "status"],
    capture_output=True,
    text=True,
    check=True,   # выбросит CalledProcessError если returncode != 0
)

subprocess.run - main API с Python 3.5, он порождает дочерний процесс ОС. Принимает:

  • args - команда как список (безопасно) или строка с shell=True (опасно для user input)
  • capture_output - захватить stdout/stderr
  • text=True - декодировать в str (иначе bytes)
  • timeout - таймаут (TimeoutExpired при превышении)
  • input - передать в stdin
  • check=True - бросить exception при ненулевом коде

Без shell=True

# БЕЗОПАСНО - args как список
subprocess.run(["echo", user_input])

# ОПАСНО - shell injection
subprocess.run(f"echo {user_input}", shell=True)
# если user_input = "; rm -rf /" - катастрофа

shell=True интерпретирует строку через shell, что позволяет shell-инъекции. Использовать только когда нужны shell-фичи (pipes, globbing) и input доверенный.

subprocess для длительных процессов

# Старт процесса, не ждём
proc = subprocess.Popen(
    ["server.py"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)

# Что-то делаем
import time
time.sleep(10)

# Терминируем
proc.terminate()
proc.wait()

Popen для гибкого управления (взаимодействие через stdin/stdout, terminate, wait). Для одноразовых вызовов run() проще.

shutil - high-level file operations

import shutil

# Копирование файлов
shutil.copy("src.txt", "dst.txt")             # копия с метаданными
shutil.copy2("src.txt", "dst.txt")            # + сохранение всех метаданных
shutil.copyfile("src.txt", "dst.txt")          # просто содержимое

# Копирование директорий
shutil.copytree("src_dir", "dst_dir")
shutil.copytree("src_dir", "dst_dir", dirs_exist_ok=True)   # перезатирает

# Удаление директорий
shutil.rmtree("dir")
shutil.rmtree("dir", ignore_errors=True)

# Перемещение
shutil.move("src", "dst")

# Архивы
shutil.make_archive("backup", "zip", "data_dir")   # data_dir → backup.zip
shutil.unpack_archive("backup.zip", "out_dir")

# Disk usage
total, used, free = shutil.disk_usage("/")
print(f"Free: {free / (1024**3):.2f} GB")

# Where команда
shutil.which("python3")    # /usr/bin/python3 или None

shutil.which полезна для проверки наличия командной утилиты:

if shutil.which("git") is None:
    raise RuntimeError("git not installed")

Временные файлы и директории

import tempfile

# Временный файл
with tempfile.NamedTemporaryFile(mode="w", delete=True, suffix=".txt") as f:
    f.write("temp data")
    temp_path = f.name
    # файл доступен здесь
# удалится при выходе

# Временная директория
with tempfile.TemporaryDirectory() as tmpdir:
    p = Path(tmpdir) / "file.txt"
    p.write_text("data")
    # вся директория удалится при выходе

Context managers гарантируют cleanup. Это идиоматичный способ работы с временными ресурсами.

glob - сопоставление путей по шаблону

import glob

# Все .py файлы
files = glob.glob("**/*.py", recursive=True)

# С pathlib - современнее
files = list(Path(".").rglob("*.py"))

Pattern syntax:

  • * - любые символы (не /)
  • ** - включая поддиректории (с recursive=True или Path.rglob)
  • ? - один символ
  • [abc] - один из символов

Безопасность - file paths

# ПЛОХО - path traversal vulnerability
def serve(filename):
    return open(f"./uploads/{filename}").read()
# atacker: filename = "../../../etc/passwd"

# ХОРОШО - резолвим и проверяем
def serve(filename):
    base = Path("./uploads").resolve()
    target = (base / filename).resolve()
    if not target.is_relative_to(base):   # Py 3.9+
        raise ValueError("invalid path")
    return target.read_text()

При работе с user-supplied path всегда проверяй что итоговый путь внутри ожидаемой директории.

Распространённые ошибки

1. Конкатенация путей через +

# Плохо - не кроссплатформенно
path = base + "/" + filename

# Хорошо
path = Path(base) / filename

2. shell=True с user input

subprocess.run(f"grep {user_query} file.txt", shell=True)   # injection

Используй list args без shell=True.

3. Забыл text=True в subprocess

result = subprocess.run(["ls"], capture_output=True)
print(result.stdout)   # bytes, не str

Добавь text=True для удобной работы со строками.

4. Не проверять existence перед удалением

os.remove("file.txt")   # FileNotFoundError если нет

Используй missing_ok=True:

Path("file.txt").unlink(missing_ok=True)

5. shutil.rmtree без подтверждения

shutil.rmtree(user_input)   # ОПАСНО для user-controlled путей

Всегда валидируй пути от пользователя перед операциями удаления.

Сравнение с Go

В Go стандартные пакеты os, path/filepath, os/exec, io:

import (
    "os"
    "path/filepath"
    "os/exec"
)

// Чтение файла
data, err := os.ReadFile("file.txt")

// Путь
path := filepath.Join("dir", "file.txt")

// Запуск процесса
out, err := exec.Command("ls", "-la").Output()

Семантически очень похоже на Python. Go строже с error handling - каждый вызов возвращает (result, error), которые нужно явно проверять. В Python через exceptions.

Мини-задание

  1. Обход файлов с фильтрацией:
from pathlib import Path

# Найди все .py файлы крупнее 1KB
for p in Path(".").rglob("*.py"):
    if p.is_file() and p.stat().st_size > 1024:
        print(f"{p} - {p.stat().st_size} bytes")
  1. Subprocess для git:
import subprocess

def get_git_branch():
    result = subprocess.run(
        ["git", "rev-parse", "--abbrev-ref", "HEAD"],
        capture_output=True,
        text=True,
        check=True,
    )
    return result.stdout.strip()

print(get_git_branch())
  1. Безопасное чтение файла:
from pathlib import Path

def safe_read(base_dir, user_path):
    base = Path(base_dir).resolve()
    target = (base / user_path).resolve()

    if not target.is_relative_to(base):
        raise ValueError(f"Path traversal attempted: {user_path}")

    if not target.is_file():
        raise FileNotFoundError(f"Not a file: {target}")

    return target.read_text()

Что дальше

Освоили работу с FS, путями и процессами. В следующем уроке - datetime, json, csv: работа со временем, сериализация данных в популярные форматы.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.