Запуск внешних программ средствами Python: основные техники

Раздел: ОС -> Системное программирование

Запуск исполняемых файлов из Python: обзор подходов

В системном программировании часто требуется программно запускать внешние приложения (EXE-файлы) из скрипта Python. Рассмотрим несколько способов, от простейших до наиболее гибких и надёжных.

Основной способ: модуль subprocess

Как запустить EXE-файл с возможностью управления процессом и получения результата?

Модуль subprocess предоставляет мощные инструменты для создания новых процессов, управления их вводом/выводом и получения кода завершения. Это рекомендуемый способ для большинства задач.

import subprocess
# Простейший запуск без ожидания завершения
subprocess.Popen(['notepad.exe'])

# Запуск с ожиданием и проверкой возвращаемого кода
result = subprocess.run(['ping', '-n', '3', 'google.com'], capture_output=True, text=True)
print(result.returncode)
print(result.stdout)

System calls python (системные вызовы в python)

Пояснение:

  • Popen – запускает процесс в фоне, не блокируя программу.
  • run – запускает, дожидается завершения и возвращает объект CompletedProcess, содержащий stdout, stderr и returncode.
  • capture_output – захватывает вывод в память.
  • Аргументы передаются списком, чтобы избежать проблем с пробелами и экранированием.

Возможные проблемы и решения:

  • Не найден исполняемый файл – укажите полный путь или убедитесь, что он есть в PATH.
  • Блокировка программы – используйте Popen вместо run, если не нужно ждать завершения.
  • Пути с пробелами – передавайте аргументы списком, а не строкой.

Как запустить EXE с помощью os.system для простых команд?

os.system – самый старый способ, запускает команду через системную оболочку. Не рекомендуется для серьёзного использования, так как не даёт прямого доступа к потокам ввода/вывода и возвращает только код завершения.

import os
# Запуск блокнота (простая команда)
os.system('notepad.exe')

создание системных утилит python (создание системных утилит на python)

Случаи использования:

  • Быстрые однострочники в скриптах, где не нужно обрабатывать вывод.
  • Сценарии, где требуется запустить команду с перенаправлением оболочки (например, dir > file.txt).

Проблемы:

  • Подверженность инъекциям команд, если аргументы формируются из пользовательского ввода.
  • Невозможность получить стандартный вывод или ошибки программно.
  • Блокировка выполнения до завершения запущенного процесса.

Как открыть файл или программу как двойной щелчок (только Windows)?

os.startfile – функция Windows, имитирующая действие проводника: открывает файл ассоциированной программой или запускает EXE.

import os
# Открыть текстовый файл в Блокноте
os.startfile('readme.txt')
# Запустить приложение
os.startfile('C:\\Program Files\\MyApp\\app.exe')

Python open exe (запуск exe файла из python)

Случаи использования:

  • Имитация действий пользователя в GUI (открытие документов).
  • Быстрый запуск, когда не требуется контроль над процессом.

Проблемы:

  • Работает только в Windows.
  • Не возвращает объект процесса – нет возможности дождаться завершения или узнать PID.
  • Может завершиться ошибкой, если файл не ассоциирован ни с одним приложением.

Как запустить EXE с повышенными привилегиями или скрыть окно?

Использование subprocess с дополнительными параметрами:

import subprocess
import sys

# Запуск с правами администратора (только Windows)
if sys.platform.startswith('win'):
    # Использование shell=True небезопасно, но для примера
    subprocess.run(['runas', '/user:Administrator', 'notepad.exe'])

# Скрыть окно консоли (Windows)
STARTUPINFO = subprocess.STARTUPINFO()
STARTUPINFO.dwFlags |= subprocess.STARTF_USESHOWWINDOW
STARTUPINFO.wShowWindow = subprocess.SW_HIDE
subprocess.Popen(['some_app.exe'], startupinfo=STARTUPINFO)

библиотека команд python (библиотека для выполнения команд в python)

Случаи использования:

  • Автоматизация установки программ, требующих прав администратора.
  • Запуск фоновых агентов без видимого окна.

Проблемы:

  • Вызов runas может запросить пароль в консоли.
  • Скрытие окна работает только для консольных приложений Windows.

Как создать процесс напрямую через системные вызовы Windows (ctypes)?

ctypes позволяет вызывать WinAPI, например CreateProcess. Этот способ даёт максимальный контроль, но требует глубоких знаний Windows API.

import ctypes
from ctypes import wintypes

# Определяем структуры
class STARTUPINFO(ctypes.Structure):
    _fields_ = [
        ("cb", wintypes.DWORD),
        ("lpReserved", wintypes.LPWSTR),
        ("lpDesktop", wintypes.LPWSTR),
        ("lpTitle", wintypes.LPWSTR),
        ("dwX", wintypes.DWORD),
        ("dwY", wintypes.DWORD),
        ("dwXSize", wintypes.DWORD),
        ("dwYSize", wintypes.DWORD),
        ("dwXCountChars", wintypes.DWORD),
        ("dwYCountChars", wintypes.DWORD),
        ("dwFillAttribute", wintypes.DWORD),
        ("dwFlags", wintypes.DWORD),
        ("wShowWindow", wintypes.WORD),
        ("cbReserved2", wintypes.WORD),
        ("lpReserved2", wintypes.LPBYTE),
        ("hStdInput", wintypes.HANDLE),
        ("hStdOutput", wintypes.HANDLE),
        ("hStdError", wintypes.HANDLE),
    ]

class PROCESS_INFORMATION(ctypes.Structure):
    _fields_ = [
        ("hProcess", wintypes.HANDLE),
        ("hThread", wintypes.HANDLE),
        ("dwProcessId", wintypes.DWORD),
        ("dwThreadId", wintypes.DWORD),
    ]

# Создаём процесс
si = STARTUPINFO()
si.cb = ctypes.sizeof(STARTUPINFO)
pi = PROCESS_INFORMATION()

kernel32 = ctypes.windll.kernel32
success = kernel32.CreateProcessW(
    "C:\\Windows\\System32\\notepad.exe",  # путь к exe
    None,  # командная строка
    None,  # атрибуты безопасности процесса
    None,  # атрибуты безопасности потока
    False,  # наследование дескрипторов
    0,     # флаги создания
    None,  # окружение
    None,  # текущая директория
    ctypes.byref(si),
    ctypes.byref(pi)
)
if success:
    print(f"Процесс запущен, PID: {pi.dwProcessId}")
    kernel32.CloseHandle(pi.hProcess)
    kernel32.CloseHandle(pi.hThread)
else:
    print(f"Ошибка {ctypes.GetLastError()}")

Случаи использования:

  • Реализация своего менеджера процессов.
  • Тонкая настройка приоритетов и окружения.

Проблемы:

  • Код не кроссплатформенный, только Windows.
  • Сложно отлаживать, возможны ошибки в структурах.
  • Требует прав на создание процессов.

Расширенные примеры запуска EXE-файлов

Ниже приведены нестандартные и детализированные сценарии, которые могут потребоваться в реальных проектах.

1. Запуск с передачей аргументов и получением вывода в реальном времени

Пример
import subprocess
import sys

# Пример: запуск Python-скрипта, который выводит строки с задержкой
# Создадим временный скрипт
script_code = """
import time
for i in range(5):
    print(f'Шаг {i}')
    time.sleep(0.5)
"""
with open('test_script.py', 'w') as f:
    f.write(script_code)

# Запуск с поточным чтением stdout
proc = subprocess.Popen(
    [sys.executable, 'test_script.py'],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    bufsize=1
)
for line in proc.stdout:
    print(f'[PROCESS] {line.strip()}')
proc.wait()
print(f'Код завершения: {proc.returncode}')

# Очистка
import os
os.remove('test_script.py')
[PROCESS] Шаг 0
[PROCESS] Шаг 1
[PROCESS] Шаг 2
[PROCESS] Шаг 3
[PROCESS] Шаг 4
Код завершения: 0

Пояснение:

  • Параметр bufsize=1 включает построчную буферизацию, что позволяет читать stdout построчно без задержек.
  • Мы перенаправили stderr в stdout (stderr=subprocess.STDOUT), чтобы не потерять сообщения об ошибках.

2. Запуск EXE с ограничением времени выполнения (timeout)

Пример
import subprocess
import time

try:
    # Запускаем программу, которая может зависнуть
    proc = subprocess.Popen(['ping', '-t', 'localhost'])  # -t делает бесконечный пинг
    time.sleep(3)  # даём поработать 3 секунды
    # Принудительно завершаем
    proc.terminate()
    proc.wait(timeout=5)
    print(f'Код после завершения: {proc.returncode}')
except subprocess.TimeoutExpired:
    # Если terminate не помог, убиваем процесс
    proc.kill()
    proc.wait()
    print('Процесс был принудительно убит')
Код после завершения: 1

Пояснение:

  • terminate() посылает сигнал SIGTERM (Windows: TerminateProcess с кодом 1).
  • kill() посылает SIGKILL (Windows: TerminateProcess).
  • В реальном коде стоит обернуть в try/except для обработки таймаута ожидания.

3. Запуск с изменённой рабочей директорией и окружением

Пример
import subprocess
import os

# Создаём временную папку и файл конфигурации
os.makedirs('temp_workdir', exist_ok=True)
with open('temp_workdir\\config.ini', 'w') as f:
    f.write('[DEFAULT]\nkey=value\n')

# Запускаем Python-скрипт, который читает файл из рабочей директории
# (предположим, что у нас есть такой скрипт test_workdir.py)
with open('test_workdir.py', 'w') as f:
    f.write('''
import os
print("Текущая директория:", os.getcwd())
print("Содержимое config.ini:")
with open("config.ini", "r") as f:
    print(f.read())
''')

env = os.environ.copy()
env['MY_VAR'] = 'custom_value'

proc = subprocess.run(
    ['python', 'test_workdir.py'],
    cwd='temp_workdir',
    env=env,
    capture_output=True,
    text=True
)
print(proc.stdout)

# Очистка
import shutil
shutil.rmtree('temp_workdir')
os.remove('test_workdir.py')
Текущая директория: /home/user/temp_workdir
Содержимое config.ini:
[DEFAULT]
key=value

Пояснение:

  • Параметр cwd устанавливает рабочую директорию для дочернего процесса.
  • Параметр env заменяет или дополняет переменные окружения. Если не копировать os.environ, процесс получит пустое окружение.

4. Асинхронный запуск нескольких EXE с ожиданием завершения всех

Пример
import subprocess
import time

# Запускаем три параллельных процесса
processes = []
for i in range(3):
    time.sleep(0.2)  # небольшая задержка для наглядности
    p = subprocess.Popen(['ping', '-n', '2', '127.0.0.1'])
    processes.append(p)

# Ждём завершения всех
exit_codes = [p.wait() for p in processes]
print(f'Коды завершения: {exit_codes}')
print(f'Общее время: {time.process_time():.2f} с')
Коды завершения: [0, 0, 0]
Общее время: 4.25 с

Пояснение:

  • Все процессы запущены одновременно (асинхронно), благодаря чему общее время выполнения определяется самым медленным процессом, а не суммой.
  • Метод wait() блокирует выполнение до завершения конкретного процесса.

5. Запуск EXE с перенаправлением ввода (stdin) и получением stderr отдельно

Пример
import subprocess

# Представим, что у нас есть программа, читающая имя и выводящая приветствие
# Создадим временный скрипт
test_prog = '''
import sys
name = sys.stdin.readline().strip()
print(f"Привет, {name}!")
sys.stderr.write("Это сообщение в stderr\n")
'''
with open('greeter.py', 'w') as f:
    f.write(test_prog)

proc = subprocess.run(
    ['python', 'greeter.py'],
    input='Алексей\n',
    capture_output=True,
    text=True
)
print('stdout:', proc.stdout.strip())
print('stderr:', proc.stderr.strip())

import os
os.remove('greeter.py')
stdout: Привет, Алексей!
stderr: Это сообщение в stderr

Пояснение:

  • Параметр input передаёт данные в stdin процесса. Это удобно для автоматизации консольных диалогов.
  • capture_output=True захватывает и stdout, и stderr в соответствующие поля результата.

6. Запуск EXE с использованием shell=True (небезопасно, но иногда удобно)

Пример
import subprocess

# Пример: запуск сложной команды с перенаправлением вывода в файл
# ВАЖНО: shell=True опасно при работе с пользовательскими данными
subprocess.run('dir /B > listing.txt', shell=True)

# Чтение созданного файла
with open('listing.txt', 'r') as f:
    print(f.read()[:200])

import os
os.remove('listing.txt')
file1.exe
script.py
... (первые 200 символов)

Пояснение:

  • При shell=True команда передаётся оболочке, что позволяет использовать операторы >, |, переменные окружения и т.д.
  • Риск: если команда содержит внешние данные, злоумышленник может внедрить дополнительные команды (Shell Injection).
  • Рекомендуется избегать, если можно передать аргументы списком.

7. Получение списка запущенных процессов и завершение по имени (Windows)

Пример
import subprocess
import time

# Запускаем блокнот
notepad = subprocess.Popen(['notepad.exe'])
time.sleep(0.5)

# Получаем список процессов через tasklist
tasklist = subprocess.run(
    ['tasklist', '/FI', 'IMAGENAME eq notepad.exe', '/NH'],
    capture_output=True,
    text=True
)
print('Процессы notepad.exe:')
print(tasklist.stdout)

# Завершаем через taskkill
subprocess.run(['taskkill', '/IM', 'notepad.exe', '/F'], capture_output=True)
notepad.wait()
print('Блокнот завершён')
Процессы notepad.exe:
notepad.exe                  12345 Console            1     5 428 K

Блокнот завершён

Пояснение:

  • tasklist выводит процессы; флаг /FI фильтрует, /NH убирает заголовки.
  • taskkill /F принудительно завершает процесс.
  • Этот подход специфичен для Windows.

Запуск exe файла из Python - comments

En
Python open exe (python)