파이썬124번6 MIN
파일 인코딩: UTF-8부터 EUC-KR까지
Python 파일 입출력에서 발생하는 인코딩 문제를 완전히 정리합니다. UTF-8·EUC-KR·CP949·UTF-8-SIG의 차이, UnicodeDecodeError 원인과 해결, chardet 감지, 인코딩 변환 패턴까지 다룹니다.
이 글은 PALDYN Tech Blog의 글입니다.원문 보기
지난 글에서 텍스트 모드와 이진 모드의 차이를 살펴봤다. 텍스트 모드에서 가장 자주 발생하는 문제가 인코딩이다. Python으로 한국어 파일을 다루다 보면 UnicodeDecodeError를 피할 수 없다. 이 오류의 원인을 정확히 알아야 빠르게 고칠 수 있다.
인코딩이란
인코딩은 문자를 바이트로 표현하는 규칙이다. '가'라는 글자를 디스크에 저장하려면 어떤 바이트로 표현할지 정해야 한다.
# '가'의 인코딩별 바이트 표현
char = "가"
print(char.encode("utf-8")) # b'\xea\xb0\x80' — 3바이트
print(char.encode("euc-kr")) # b'\xb0\xa1' — 2바이트
print(char.encode("cp949")) # b'\xb0\xa1' — 2바이트 (EUC-KR 상위 호환)
같은 글자라도 인코딩에 따라 다른 바이트가 된다. 파일을 쓴 인코딩과 읽는 인코딩이 다르면 깨진다.
Python 환경의 기본 인코딩
import locale
import sys
# 파일 시스템 기본 인코딩
print(sys.getfilesystemencoding()) # utf-8 (대부분 Linux/Mac)
# mbcs (Windows)
# open() 기본 인코딩
print(locale.getpreferredencoding(False)) # UTF-8 (Linux/Mac)
# cp949 (한국어 Windows)
# 표준 입출력 인코딩
print(sys.stdin.encoding) # utf-8
print(sys.stdout.encoding) # utf-8
Windows 한국어 환경에서는 open()의 기본 인코딩이 cp949다. 이 때문에 Windows에서 encoding 없이 작성한 코드가 Linux/Mac에서 깨지는 경우가 많다.
주요 인코딩 비교
UTF-8
현재 인터넷과 대부분 운영체제의 표준이다. 유니코드 전체를 지원하고, ASCII 문자(영문·숫자·기호)는 1바이트로 표현해 영문 전용 텍스트와 호환된다.
text = "Hello 안녕 🎉"
b = text.encode("utf-8")
print(b)
# b'Hello \xec\x95\x88\xeb\x85\x95 \xf0\x9f\x8e\x89'
print(len(b)) # 19 (영문 5 + 공백 + 한글 6 + 공백 + 이모지 4)
EUC-KR / CP949
한국어 전용 인코딩이다. 1970~2000년대에 만들어진 한국어 문서나 정부·공공기관 데이터는 EUC-KR이나 CP949로 인코딩된 경우가 많다. CP949는 EUC-KR의 상위 호환으로, 현대 한국어 Windows의 코드 페이지다.
# CP949(EUC-KR 상위 호환) 파일 읽기
with open("legacy.txt", "r", encoding="cp949") as f:
content = f.read()
# EUC-KR로 인코딩된 바이트 → str
b = b'\xb0\xa1\xb3\xaa\xb4\xd9' # "가나다" in EUC-KR
print(b.decode("euc-kr")) # → 가나다
UTF-8-SIG (BOM 포함 UTF-8)
Microsoft Excel은 CSV를 저장할 때 UTF-8 파일 앞에 BOM(Byte Order Mark) \xef\xbb\xbf 3바이트를 붙인다. 일반 utf-8로 열면 BOM이 문자열 앞에 붙어 문제가 생긴다.
# Excel에서 저장한 CSV: BOM 포함
with open("excel_export.csv", "r", encoding="utf-8") as f:
first = f.read(3)
print(repr(first)) # '이름' — 가 BOM
# utf-8-sig: BOM 자동 제거
with open("excel_export.csv", "r", encoding="utf-8-sig") as f:
first = f.read(3)
print(repr(first)) # '이름' — 깔끔
UnicodeDecodeError 해결
# 가장 흔한 원인: 인코딩 불일치
with open("cp949_file.txt", "r", encoding="utf-8") as f:
f.read()
# → UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb0
# 해결 1: 올바른 인코딩 지정
with open("cp949_file.txt", "r", encoding="cp949") as f:
content = f.read()
# 해결 2: 오류 허용 (데이터 손실 가능)
with open("unknown.txt", "r", encoding="utf-8", errors="replace") as f:
content = f.read() # 깨진 바이트 → '?'
# 해결 3: 이진으로 읽고 직접 디코딩
with open("unknown.txt", "rb") as f:
raw = f.read()
for enc in ["utf-8", "cp949", "euc-kr", "latin-1"]:
try:
text = raw.decode(enc)
print(f"성공: {enc}")
break
except UnicodeDecodeError:
continue
chardet로 인코딩 자동 감지
# pip install chardet
import chardet
def detect_encoding(path: str) -> str:
with open(path, "rb") as f:
raw = f.read(10_000) # 앞 10KB 샘플
result = chardet.detect(raw)
enc = result["encoding"]
confidence = result["confidence"]
print(f"추정 인코딩: {enc} (신뢰도: {confidence:.0%})")
return enc
enc = detect_encoding("mystery.txt")
with open("mystery.txt", "r", encoding=enc, errors="replace") as f:
content = f.read()
chardet은 통계적 분석으로 인코딩을 추정하므로 짧은 파일은 정확도가 낮을 수 있다.
실전 패턴
인코딩 변환 전략
def safe_read(path: str) -> str:
"""여러 인코딩을 순서대로 시도해 읽기"""
encodings = ["utf-8", "utf-8-sig", "cp949", "euc-kr", "latin-1"]
for enc in encodings:
try:
with open(path, "r", encoding=enc) as f:
return f.read()
except UnicodeDecodeError:
continue
raise ValueError(f"인코딩 감지 실패: {path}")
content = safe_read("unknown_encoding.txt")
latin-1(ISO-8859-1)은 모든 바이트를 유효한 문자로 해석하므로 마지막 폴백으로 유용하다. 다만 한글이 깨진 형태로 나타난다.
요약
| 인코딩 | 사용 상황 |
|---|---|
utf-8 |
새로 작성하는 모든 파일 |
utf-8-sig |
Excel CSV 읽기/쓰기 |
cp949 |
한국어 Windows 레거시 파일 |
euc-kr |
오래된 리눅스 서버 한국어 파일 |
latin-1 |
바이너리를 텍스트로 강제 읽기 (폴백용) |
새로 만드는 파일은 항상 utf-8로 작성하고, 레거시 파일은 chardet으로 감지하거나 소스를 확인해 올바른 인코딩을 지정한다.
지난 글: 텍스트 vs 이진 모드: 개행 문자와 인코딩의 차이
다음 글: readline vs readlines: 파일 읽기 방법 비교
읽어주셔서 감사합니다. 😊

