반응형

Python Subprocess 모듈 사용법을 간단히 정리했습니다. 



subprocess 모듈을 사용하면 파이썬 코드에서 셸 명령을 실행할 수 있고 실행결과의 출력을 가져올 수도 있습니다. 또한 pipe를 사용하거나 입력/출력 리다이렉션을 할 수 있습니다.  기존에 쉘 명령을 실행하기 위해 사용했던 os.system와 os.spawn 등을 대체하여 사용할 수 있습니다.




1. subprocess.run() - 쉘 명령 실행

2. subprocess.run() - 입력 리다이렉션

3. subprocess.call() - 명령 실행

4. subprocess.check_call() - 예외 처리

5. subprocess.check_output() - 실행 결과 캡처

6. subprocess.Popen

6.1. 표준입력, 표준출력, 표준에러 리다이렉션

6.2. 프로세스 스트림과 상호 작용하기

7. 예외 다루기

7.1. CalledProcessError 예외처리

8. 타임아웃(timeout)

9. subprocess.PIPE 메커니즘

10. 환경 변수 설정

11. 보안 측면 고려사항

12. shell 파라미터의 중요성

13. 성능을 위한 Subprocess 호출 최적화



2024. 3. 4  최초작성



1. subprocess.run() - 쉘 명령 실행

화면에 'Hello, World!'를 출력하는 다음 쉘 명령을 파이썬 코드에서 실행해보겠습니다.

 

$ echo 'Hello, World!'

Hello, World!



subprocess.run()를 사용하여 echo 명령을 실행하고 결과를 가져와 화면에 출력합니다. 실행결과는 위에서 실행한 쉘 명령과 동일하게 화면에 'Hello, World!'를 출력합니다. subprocess.run에 명령을 전달시  echo 'Hello, World!' 대신에 ['echo', 'Hello, World!']를 전달하는 이유는 셸 인젝션(Shell Injection)을 방지하기 위해서입니다.  셸 인젝션에 대한 자세한 내용은 “11. 보안 측면 고려사항”에서 다룹니다.

 

import subprocess


# 쉘 명령을 리스트에 담아 전달합니다.
# capture_output=True : 실행결과를 반환하여 변수 result에 저장하게 됩니다.
# text=True : 실행결과를 텍스트로 변환해줍니다.

result = subprocess.run(['echo', 'Hello, World!'], capture_output=True, text=True)

# 반환 받은 변수의 stdout을 사용하여 실행결과를 가져올 수 있습니다.
print(result.stdout)




subprocess.run는 실행결과 외에도 여러 정보를 반환합니다.

 

import subprocess

result = subprocess.run(['ls', '-l'], capture_output=True, text=True)

print(result)



실행결과에서 다음처럼 각 항목을 접근할 수 있습니다.  result.stdout와 result.stderr는 capture_output=True 인자를 사용한 경우에만 추가됩니다.  에러가 나지 않았기 때문에 returncode는 0이고 result.stderr에는 아무 내용이 없고 result.stdout에 실행결과가 저장되어 있습니다. 

 

result.args              : 실행한 명령

result.returncode    : 종료코드

result.stdout           : 실행한 결과 - 표준 출력

result.stderr            : 실행한 결과 - 표준 에러

 

CompletedProcess(args=['ls', '-l'], returncode=0, stdout='total 107696\n-rw-r--r--      1 webnautes  staff      7058 Mar  3 14:20 keras-cnn-dog-or-cat-classification.py\n-rw-r--r--      1 webnautes  staff  54051016 Mar  3 14:12 model.h5\n-rw-r--r--      1 webnautes  staff       103 Mar  4 21:15 test.py\ndrwxr-xr-x@ 12502 webnautes  staff    400064 Sep 20  2013 test1\n-rw-r--r--@     1 webnautes  staff     39968 Sep 24 20:42 titanic-data-science-solutions.py\ndrwxr-xr-x@ 25002 webnautes  staff    800064 Sep 20  2013 train\n', stderr='')



실행한 명령이 에러난 경우도 살펴봅니다. ls 명령으로 존재하지 않는 디렉토리의 내용을 출력하도록 한 경우입니다.

 

import subprocess

result = subprocess.run(['ls', '-l', '/nothing'] , capture_output=True, text=True)
print(result)



에러가 났기 때문에 returncode는 1이고 result.stdout에는 아무 내용이 없고 result.stderr에는 에러 메시지가 저장되어 있습니다. 

 

CompletedProcess(args=['ls', '-l', '/nothing'], returncode=1, stdout='', stderr='ls: /nothing: No such file or directory\n')




2. subprocess.run() - 입력 리다이렉션

쉘 명령어에 입력을 전달할 수 있습니다. 

 

grep python 명령은 주어진 입력에 인자로 주어진 python 문자열이 있으면 해당 입력을 화면 출력하고 그렇지 않으면 아무것도 출력하지 않습니다. 아래 코드에서는 python이 포함된 문자열과 python이 포함되지 않은 문자열을 사용하여 테스트합니다. 실행해보면 python 문자열이 포함된  'I love python programming.'만 출력되는 것을 확인할 수 있습니다. 

 

import subprocess

result = subprocess.run(['grep', 'python'], input='I love python programming.', capture_output=True, text=True)
print(result.stdout) 

result = subprocess.run(['grep', 'python'], input='I love C++ programming.', capture_output=True, text=True)
print(result.stdout)  




3. subprocess.call() - 명령 실행

 

subprocess.call() 메서드는  프로세스를 생성하고 명령을 실행하는 데 사용됩니다. 이 메서드는 명령이 완료될 때까지 기다린 다음 명령의 종료  코드를 반환합니다. 종료 코드가 0이면 성공을 나타내며, 0이 아닌 숫자라면  오류를 나타냅니다.

 

다음 코드를 실행하면  ls -l 명령을 실행하여 현재 디렉토리의 내용을 출력한 다음 ls 명령의 종료 코드를 다음처럼 출력합니다.  

 

Command exited with code 0

 

import subprocess

exit_code = subprocess.call(['ls', '-l'])
print(f"Command exited with code {exit_code}")



존재하지 않는 디렉토리의 내용을 출력하도록 해봅니다.

 

import subprocess

exit_code = subprocess.call(['ls', '-l', '/nothing'])
print(f"Command exited with code {exit_code}")



ls 명령의 실행결과를 보여주고 종료코드가 1이 출력되었습니다. 1이 출력된 것은 해당 디렉토리가 존재하지 않기 때문입니다. 

 

ls: /nothing: No such file or directory

Command exited with code 1




4. subprocess.check_call() - 예외 처리

subprocess.call()은 명령 실행시 에러 났는지 여부에 관계없이 계속 진행되지만, subprocess.check_call()의 경우엔 명령 실행에 문제가 있어서  0이 아닌 종료 코드를 반환하면 CalledProcessError 예외가 발생합니다.

 

다음 예제 코드는 지정한 디렉터리가 존재하지 않기 때문에 ls 명령에 에러가 발생하고 예외 CalledProcessError가 발생합니다. 

 

import subprocess

try:
    subprocess.check_call(['ls', '/nothing'])
except subprocess.CalledProcessError:
    print("Command failed!")



ls 명령 실행결과로 에러 메시지가  출력된 후, 예외에서 출력한 Command failed! 메시지가 추가로  보입니다.

 

ls: /nothing: No such file or directory

Command failed!



앞 예제 코드에서 “ls: /nothing: No such file or directory”는 표준에러(stderr)라서 화면에 출력되는데 다음 코드를 사용하면 출력되지 않도록 할 수 있습니다. 표준 에러를 포함하여 실행 결과를 캡처하기 때문입니다. 

 

import subprocess

try:
    subprocess.run(['ls', '/nothing'], check=True, capture_output=True, text=True)
except subprocess.CalledProcessError:
    print("Command failed!")



실행하면 다음 메시지만 화면에 출력됩니다.

 

Command failed!




5. subprocess.check_output() - 실행 결과 캡처

명령의 실행결과를 캡처합니다. 

 

echo 명령의 출력인 "Hello, World!"가 캡처되어 변수 output에 저장됩니다.  앞에서 살펴본 “subprocess.run() - 쉘 명령 실행”으로 대체될 수 있습니다. 

 

import subprocess

try:
    output = subprocess.check_output(['echo', 'Hello, World!'], text=True)
    print(output)
except subprocess.CalledProcessError:
    print("Command failed!")



실행결과 echo 명령 실행결과를 캡쳐한 결과를 화면에 출력합니다.

 

Hello, World!



명령이 에러가 발생한 경우를 확인해봅니다.

 

import subprocess

try:
    output = subprocess.check_output(['ls', '/nothing'], text=True)
    print(f"Command output: {output}")
except subprocess.CalledProcessError:
    print("Command failed!")



실행결과 ls 명령의 에러 메시지가 출력된 후, CalledProcessError 예외가 발생하여 print문에 있는 메시지가 출력됩니다. 

 

ls: /nothing: No such file or directory

Command failed!



ls 명령의 에러 메시지는 표준 에러라서 화면에 출력되는데 다음 코드를 사용하면 출력되지 않도록 할 수 있습니다.

 

import subprocess

try:
    subprocess.run(['ls', '/nothing'], check=True, capture_output=True, text=True)
except subprocess.CalledProcessError:
    print("Command failed!")




6. subprocess.Popen

"Process Open"의 약자인 Popen 클래스는 프로세스를 생성하고 subprocess.call()과 유사한 방식으로 명령을 실행하지만 더 다양한 옵션을 제공합니다.

 

subprocess.call 함수와 Popen 클래스는 차이가 있습니다. 

 

subprocess.call 함수는 동기적으로 외부 명령을 실행하기 때문에 명령이 완료될 때까지 프로그램 실행이 대기 상태가 됩니다.

Popen 클래스는 비동기적으로 외부 명령을 실행하기 때문에 명령을 실행하는 프로세스가 실행되는 동안 다른 작업을 계속할 수 있습니다.

 

앞에서 살펴본  subprocess.call() 예제 코드를 Popen를 사용하여 다시 구현한 예제 코드입니다. 프로세스가 종료되길 기다리게 하기 위해서 process.wait()를 사용했습니다.

 

import subprocess

# 프로세스 초기화합니다.
process = subprocess.Popen(['ls', '-l'])

# 프로세스가 완료될 때까지 기다렸다가 반환 코드를 가져옵니다.
return_code = process.wait()

print(f"Command exited with code {return_code}")



실행 결과 현재 디렉토리 내용이 출력되고 다음 메시지가 출력됩니다.

 

Command exited with code 0




6.1. 표준입력, 표준출력, 표준에러 리다이렉션

Popen을 사용하여 프로세스의 표준 입력(`stdin`), 표준 출력(`stdout`), 표준 에러(`stderr`) 스트림을 리다이렉션할 수 있습니다. 



다음 에제 코드는 명령의 실행결과의 표준 출력을 리다이렉션하여 캡처합니다.

 

import subprocess

process = subprocess.Popen(['ls', '-l'], stdout=subprocess.PIPE)

#프로세스가 완료될 때까지 기다렸다가  프로그램 실행 결과의 표준 출력을 반환합니다.
output, _ = process.communicate()
print(f"Captured output:\n{output.decode('utf-8')}")

 

실행결과입니다.

 

Captured output:

total 107696

-rw-r--r--      1 webnautes  staff       293 Mar  4 10:28 test.py




다음 에제 코드는 명령의 실행결과의 표준 에러를 리다이렉션하여 캡처합니다.

 

import subprocess

process = subprocess.Popen(['ls', '/nothing'], stderr=subprocess.PIPE)
_, error = process.communicate()
print(f"Captured error:\n{error.decode('utf-8')}")



실행결과입니다.

 

Captured error:

ls: /nothing: No such file or directory




6.2. 프로세스 스트림과 상호 작용하기

실시간으로 프로세스 스트림과 상호 작용할 수 있습니다.

 

다음 에제 코드는 cat 명령을 호출하는 프로세스를 시작하여 stdin에서 읽고 stdout에 쓰도록 합니다. 

그런 다음 프로세스의 stdin으로 문자열을 보낸 다음, stdout에서 응답을 캡처하여 화면에 출력합니다.

 

import subprocess


# 프로세스에서 실행한 cat 명령은 표준 입력으로 받은 데이터를 표준 출력으로 전송합니다.
# stdin=subprocess.PIPE와 stdout=subprocess.PIPE는 각각 표준 입력과 표준 출력을 명령을 실행한 프로세스의 파이프로 # 설정합니다. 
# 이렇게 설정하면 Python 코드 내에서 프로세스의 표준 입력과 출력을 프로그래밍 방식으로 제어할 수 있습니다.
process = subprocess.Popen(['cat'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)

# 프로세스의 표준 입력으로 "Hello, Popen!" 문자열을 전송하고, 프로세스가 종료될 때까지 기다립니다.
# 프로세스가 종료되면 표준 출력과 표준 에러를 튜플로 리턴받는데 여기에선 표준 출력만 받기위해 output,_ 를 사용했습니다.
output, _ = process.communicate(input="Hello, Popen!")

print(f"Received: {output}")




7. 예외 다루기

프로세스에서 쉘 명령을 실행한 후,  상호 작용하다가 문제가 발생할 수 있습니다. 명령이 실패하거나 리소스를 사용할 수 없거나 예기치 않은 오류가 발생할 수 있습니다. subprocess 모듈은 이러한 문제를 예상하여 예외를 원활하게 처리하는 메커니즘을 제공합니다.



7.1. CalledProcessError 예외처리

subprocess.check_call() 또는 subprocess.check_output()과 같은 함수를 사용하여 실행한 명령에 오류가 있어  0이 아닌 종료 코드를 반환하는 경우 CalledProcessError 예외가 발생할 수 있습니다. 예외 발생시 원하는 작업을 수행하도록 할 수 있습니다. 

 

예외 발생하여 얻은 CalledProcessError 객체로부터 얻을 수 있는 정보입니다. 

  • CalledProcessError.cmd : 실행한 명령
  • CalledProcessError.returncode : 실행한 명령의 종료코드
  • CalledProcessError.output : 명령이 실패하기 전의 실행결과 출력. 실행결과를 캡처하도록 해야 얻을 수 있습니다.  



간단한 예제 코드입니다.

 

import subprocess

try:
    # subprocess.check_output를 사용하여 존재하지 않는 디렉토리의 내용을 출력하도록 하는 ls 명령을 실행합니다.
    output = subprocess.check_output(['ls', '/nothing'])

except subprocess.CalledProcessError as e:
    # 존재하지 않는 디렉토리의 내용을 출력할 수 없으므로 ls에서 에러 메시지가 발생하여 예외 CalledProcessError가 발생합니다.
    print(f"Command '{' '.join(e.cmd)}' failed with error code {e.returncode}.")

    if e.output:
                # 명령이 실패하기 전의 실행결과 출력. 실행결과를 캡처하도록 해야 얻을 수 있습니다. 
                print(f"Command output before failure:\n{e.output.decode('utf-8')}")



실행하면 명령 실행결과와 예외 발생하여 출력하도록 한 메시지가 출력됩니다.

 

ls: /nothing: No such file or directory

Command ['ls', '/nothing'] failed with error code 1.



8. 타임아웃(timeout)

명령 실행 후 계속 대기하게 되는 상황을 방지하기위해 timeout 인자를 지정할 수 있습니다. timeout 인자에 지정한 일정 시간이 지나도 실행결과가 넘어오지 않는 경우에  TimeoutExpired 예외를 발생시키도록 합니다.  timeout 인수는 run(), call(), check_call() 등 많은 Subprocess 함수에서 사용할 수 있습니다. 



다음은 timeout의 동작을 확인시켜주는 실험적인 코드입니다.  sleep 10 명령으로 10초동안 대기하도록 하면서 동시에 타임아웃 시간을 5초로 설정했습니다. 5초 이내에 실행이 종료되지 않기 때문에 TimeoutExpired 예외가 발생하게 됩니다. 



import subprocess

try:

    # sleep 10으로 10초 대기하는 명령을 실행하며 5초 타임아웃을 설정합니다.
    subprocess.run(['sleep', '10'], timeout=5)

except subprocess.TimeoutExpired:
    print("지정한 타임아웃 5초를 초과했지만 프로그램이 종료되지 않아 강제 종료했습니다. ")


실행 결과입니다. TimeoutExpired 예외가 발생하여 다음 메시지가 출력됩니다.

지정한 타임아웃 5초를 초과했지만 프로그램이 종료되지 않아 강제 종료했습니다. 





9. subprocess.PIPE 메커니즘

 

subprocess.PIPE는 Subprocess의 표준 입력/표준 출력/표준 에러에 대한 pipe를 여는 방법을 제공합니다. 

 

 표준 출력에 대한 pipe를 여는 예제입니다.

 

import subprocess

# 표준 출력을 프로세스로 가져올 수 있도록 pipe를 열어줍니다.
process = subprocess.Popen(['echo', 'Hello from subprocess!'], stdout=subprocess.PIPE)

# 실행이 종료되기를 기다리다가 실행 결과의 표준 출력을 output 변수에 반환받습니다.
output, _ = process.communicate()

# 표준 출력을 utf-8로 디코딩하여 화면에 출력합니다.
print(output.decode('utf-8'))

 

실행해보면 echo 명령의 표준 출력을 캡처하여 변수 output에 저장했다가 화면에 출력합니다. 

 

Hello from subprocess!



10. 환경 변수 설정

명령을 실행할 때 환경 변수를 수정해야 하는 경우 env 매개변수를 사용하면 됩니다.

 

import subprocess
import os

# 현재 시스템의 환경 변수를 복사합니다.
env_vars = os.environ.copy()

# 환경변수를 추가하거나 삭제할 수 있습니다. 여기에선 환경 변수에 MY_VARIABLE를 추가합니다.
env_vars["MY_VARIABLE"] = "MyValue"

# 수정한 환경변수가 저장된 env_vars를 전달하여 명령을 실행합니다. 이렇게 하면 시스템에 영향을 주지 않고 명령을 위해서만 환경변수를 변경할 수 있습니다. 
# printenv 명령을 사용하여  새로 추가된 환경변수 MY_VARIABLE의 값을 출력합니다.
subprocess.run(['printenv', 'MY_VARIABLE'], env=env_vars)



실행결과 환경 변수 MY_VARIABLE의 값이 출력됩니다. 

 

MyValue



11. 보안 측면 고려사항

사용자의 입력을 받는 프로세스를 다룰 때는 보안 취약성에 주의해야 합니다. 잘못 사용하면 악의적인 활동에 악용될 수 있습니다. 

 

셸 인젝션(Shell Injection)은 공격자가 시스템의 셸에서 임의의 명령을 실행하려고 시도하는 것입니다.  Subprocess에 전달되는 입력에 악의적인 명령을 전달하여 발생할 수 있습니다. 



다음의 코드를 사용시 공격자가 "`; rm -rf"와 같은 파일 이름을 입력하면 "cat ; rm -rf `" 명령이 실행되어 현재 디렉터리의 모든 파일이 삭제될 수 있습니다. 

 

import subprocess

user_input = input("Enter a filename to display: ")
subprocess.run(f'cat {user_input}', shell=True)



이런 악의적인 입력이 실행되는 것을 방지하려면 shell=True 파라미터를 사용하면 안됩니다.  또한 리스트 형태로 명령과 인자를 분리하여 전달해야 합니다. 

 

import subprocess

user_input = input("Enter a filename to display: ")
subprocess.run(['cat', user_input])




12. shell 파라미터의 중요성

shell 파라미터를 True로 설정하면 시스템 셸에서 명령을 실행할 수 있습니다. 복잡한 명령을 실행하거나 셸 전용 기능을 사용해야 할때 유용할 수 있지만 위험할 수 있습니다.

 

shell = True를 사용하면 신중하게 사용하지 않을 경우 앞에서 살펴본 것처럼 코드가 셸 인젝션 공격에 취약해질 수 있습니다.

shell=True를 사용하면 Python이 새로운 셸 프로세스를 시작한 다음 이 셸 내에서 명령을 실행하므로 약간의 추가 오버헤드가 발생합니다.

플랫폼 별로 셸의 동작에 차이가 있을 수 있으므로 테스트에 사용한 플랫폼에서  문제 없던 코드를 다른 플랫폼에서 실행시 문제가 발생할 수 있습니다.



문제를 미연에 방지하려면 shell=True 파라미터는 필요한 경우에만 사용해야 합니다. 또한 shell=True를 사용할 경우 검증되지 않거나 신뢰할 수 없는 입력을 사용할 수 없도록 해야 합니다. 그리고 명령은 분리하여 명령과 인자로 구성된 리스트를 사용해야 합니다. 




다음처럼 사용하는 대신

 

subprocess.run("echo " + user_input, shell=True)



다음처럼 사용하세요. 

subprocess.run(["echo", user_input])



13. 성능을 위한 Subprocess 호출 최적화

subprocesses를 실행하면 오버헤드가 발생합니다. 자주 호출하는 경우 이 오버헤드가 더 커질 수 있습니다. 최적화하는 방법은 다음과 같습니다:

 

1. 종료 코드에만 관심이 있는 경우 프로세스 간 불필요한 데이터 전송을 줄이기 위해 실행결과 출력 캡처를 피하세요.

2. 명령 실행이 중단되거나 예상보다 오래 걸릴 수 있는 경우 외부 프로세스를 호출할 때는 항상 타임아웃을 설정하세요.




참고

https://www.linkedin.com/pulse/subprocess-module-python-manish-v--5dzcf/



반응형

문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.

도움이 되셨다면 토스아이디로 후원해주세요.
https://toss.me/momo2024


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts