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 - что когда
| Задача | pathlib | os |
|---|---|---|
| Работа с путями | 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.
Мини-задание
- Обход файлов с фильтрацией:
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")
- 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())
- Безопасное чтение файла:
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: работа со временем, сериализация данных в популярные форматы.