파이썬 subprocess 한글 깨짐과 UnicodeDecodeError 해결: encoding·errors 설정법

파이썬 subprocess로 외부 명령을 실행했을 때 한글이 깨지거나 UnicodeDecodeError가 나면 errors="ignore"부터 넣기보다 출력이 어떤 바이트로 만들어졌는지 확인해야 합니다. 기본 캡처 결과를 bytes로 확인한 뒤 자식 명령의 인코딩에 맞춰 text=True와 encoding을 지정하는 순서가 안전합니다.

먼저 bytes로 실제 출력을 확인합니다

capture_output=True만 사용하고 text, encoding, errors를 지정하지 않으면 stdout과 stderr는 bytes로 반환됩니다. 아래 예시는 외부 네트워크나 유료 API를 호출하지 않고 현재 Python 인터프리터가 만든 UTF 인코딩 바이트를 확인합니다.

encoding·errors·text를 지정하지 않은 subprocess 캡처 결과는 bytes이므로 먼저 원시 바이트를 확인할 수 있습니다.

import subprocess
import sys
import locale

result = subprocess.run(
    [sys.executable, "-c", "print('한글 출력')"],
    capture_output=True,
    check=True,
)

print(type(result.stdout).__name__)
print(result.stdout)
    print(result.stdout.decode(locale.getpreferredencoding(False)).strip())

실행 결과:

bytes
b'한글 출력\\n'
한글 출력

이 단계에서 bytes 자체를 보존하면 Python이 잘못된 인코딩으로 먼저 해석한 것인지, 자식 명령이 다른 인코딩으로 출력한 것인지 나눠 볼 수 있습니다. 명령마다 출력 인코딩이 다를 수 있으므로 Windows에서 보이는 콘솔 인코딩만 보고 특정 UTF 인코딩을 단정하지 않는 편이 좋습니다.

명령의 인코딩을 알면 text와 encoding을 함께 지정합니다

출력 인코딩을 알고 있다면 결과를 바로 문자열로 받을 수 있습니다.

result = subprocess.run(
    [sys.executable, "-c", "print('한글 출력')"],
    capture_output=True,
    text=True,
    encoding=locale.getpreferredencoding(False),
    check=True,
)

print(type(result.stdout).__name__)
print(result.stdout.strip())

실행 결과:

str
한글 출력

text=True만 쓰면 환경과 스트림의 기본값에 기대게 됩니다. 명령이 특정 인코딩을 보장한다면 그 값을 encoding에 명시하고, 한국어 Windows 프로그램이 다른 코드 페이지로 출력한다면 그 프로그램의 문서나 재현 결과로 확인한 인코딩을 지정해야 합니다. Windows 한국어 코드 페이지가 가능한 선택지 중 하나일 뿐 모든 명령의 정답은 아닙니다.

text=True 또는 encoding·errors를 지정하면 stdout과 stderr가 텍스트 모드로 열리므로 명령 출력에 맞는 encoding을 명시합니다.

errors는 깨짐을 숨기는 옵션이 아니라 손실 정책입니다

errors="replace"는 해석하지 못한 바이트를 대체 문자로 바꾸고 errors="ignore"는 해당 바이트를 버립니다. 따라서 로그 화면처럼 일부 손실을 감수하고 계속 진행할 때만 사용하고, 파일명·식별자·명령 결과를 저장할 때는 원문 바이트를 별도로 보존하거나 원인을 먼저 해결해야 합니다.

errors=replace는 해석하지 못한 바이트를 대체 문자로 바꾸고 errors=ignore는 버리므로 진단 단계의 기본값으로 두지 않습니다.

result = subprocess.run(
    [sys.executable, "-c", "print('한글 출력')"],
    capture_output=True,
    text=True,
    encoding=locale.getpreferredencoding(False),
    errors="replace",
    check=True,
)

이 설정은 예외를 줄일 수 있지만 출력이 원래 그대로라는 보장은 하지 않습니다. UnicodeDecodeError를 없애는 것과 정확한 문자열을 얻는 것은 다른 문제입니다.

Python UTF Mode와 표준 스트림 환경변수는 다릅니다

Python 공식 문서에 따르면 PYTHONUTF8=1은 인터프리터 시작 시 Python UTF Mode를 켭니다. PYTHONIOENCODING은 Python이 사용하는 표준 스트림의 인코딩과 오류 처리기를 지정합니다. 둘 다 외부 프로그램이 어떤 바이트를 내보내는지 자동으로 바꾸는 만능 변환기는 아니므로, subprocess 호출에서 명시한 encoding과 자식 프로그램의 출력 규칙을 먼저 확인해야 합니다.

또한 shell=True를 사용하면 셸이 한 단계 추가됩니다. 인코딩 문제와 별개로 인자 해석·보안·셸의 출력 규칙까지 확인해야 하므로, 단순히 명령과 인자를 실행할 때는 리스트 형태를 우선 검토하세요.

최종 확인 순서

  • capture_output=True로 stdout과 stderr를 bytes 상태에서 확인합니다.
  • 자식 명령의 실제 출력 인코딩을 문서·실행 옵션·재현 결과로 확인합니다.
  • 확인한 값으로 text=True, encoding="..."을 지정합니다.
  • 그래도 일부 바이트가 문제일 때만 errors의 손실 가능성을 기록하고 선택합니다.
  • 종료 코드와 stderr를 함께 저장해 디코딩 문제와 명령 실행 실패를 구분합니다.

핵심은 errors="ignore"로 오류를 감추는 것이 아니라, 바이트를 먼저 확인해 어떤 인코딩을 써야 하는지 결정하는 것입니다. 다음 단계에서는 외부 명령의 종료 코드와 stderr를 함께 확인하는 흐름을 추가하면 자동화 실패 원인을 더 빨리 분리할 수 있습니다.

깨진 문자열을 무조건 replace로 숨기지 않고 원시 바이트·명령 인코딩·손실 허용 여부를 순서대로 판단하게 합니다. bytes로 확인하면 잘못된 기본 디코딩과 자식 명령 자체의 출력 문제를 구분할 수 있습니다. errors 옵션은 작업을 계속하게 만들 수 있지만 원문 일부를 바꿀 수 있으므로 로그·식별자 처리에서는 신중해야 합니다. 다음에는 외부 명령의 종료 코드와 stderr를 함께 기록해 인코딩 문제와 실행 실패를 구분하세요.

출처

Leave a Comment