Published on

Python CSV UTF-8 깨짐·UnicodeDecodeError 5분 해결

Authors

서버 로그나 데이터 파이프라인에서 CSV를 읽다가 UnicodeDecodeError: 'utf-8' codec can't decode byte ...가 터지면, 대부분은 파일 인코딩이 UTF-8이 아니거나(BOM 포함), 줄바꿈/따옴표/제어문자 등으로 파서가 꼬인 케이스입니다. 특히 한국 환경에서는 엑셀에서 저장된 CSV가 cp949(EUC-KR 계열)이거나 UTF-8 with BOM인 경우가 많아, “로컬에서는 되는데 CI/리눅스에서만 깨짐” 같은 재현성 문제로 이어집니다.

이 글은 원인 진단 → 1~2줄 수정으로 해결 → 재발 방지(저장 규칙) 순으로 정리합니다. pandas를 주로 쓰는 경우도, 표준 csv 모듈을 쓰는 경우도 모두 커버합니다.

1) 증상별로 원인 빠르게 분류하기

1-1. UnicodeDecodeError가 나는 경우

대표적인 에러 형태는 다음과 같습니다.

  • UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb0 in position ...
  • UnicodeDecodeError: 'cp949' codec can't decode byte ...

의미는 단순합니다.

  • 지정한 인코딩으로는 해당 바이트를 유효한 문자로 해석할 수 없다
  • 즉, 파일 인코딩 추정이 틀렸거나 파일이 손상/혼합 인코딩일 가능성이 있습니다.

1-2. 에러는 없는데 글자가 깨지는 경우(�, ???)

이 경우는 보통 다음 중 하나입니다.

  • 읽을 때는 에러를 무시(errors='ignore'/replace')해서 깨진 채로 들어옴
  • 실제 인코딩이 cp949인데 utf-8로 읽어 “깨진 문자열”이 생성됨
  • UTF-8인데 BOM(\ufeff) 때문에 첫 컬럼명이 \ufeffid처럼 이상해짐

1-3. 엑셀에서 저장된 CSV의 함정

엑셀은 환경/버전에 따라 다음 형태로 CSV를 저장합니다.

  • CSV(쉼표로 분리) → 대개 ANSI(윈도우 로캘), 한국이면 cp949
  • CSV UTF-8(쉼표로 분리) → UTF-8이지만 BOM이 붙는 경우가 흔함

따라서 “UTF-8로 저장했다”는 말만 믿고 encoding='utf-8'로 읽다가 BOM/구분자/따옴표 문제까지 겹치면 디버깅 시간이 늘어납니다.

2) 5분 해결: 가장 안전한 로딩 패턴 3가지

아래 3가지는 실무에서 재현성 좋게 문제를 줄여주는 패턴입니다.

2-1. pandas: utf-8-sig 먼저 시도 (BOM 제거 포함)

UTF-8인데 BOM 때문에 컬럼명이 깨지는 문제까지 한 번에 잡습니다.

import pandas as pd

path = "data.csv"

df = pd.read_csv(path, encoding="utf-8-sig")
print(df.head())
  • utf-8-sig는 UTF-8을 읽되, 파일 시작의 BOM이 있으면 자동 제거합니다.
  • 엑셀에서 만든 “CSV UTF-8”에 특히 효과적입니다.

2-2. pandas: 한국 CSV는 cp949(또는 euc-kr)로 읽기

엑셀 기본 CSV(ANSI)로 저장된 파일이면 대부분 cp949입니다.

import pandas as pd

path = "korean.csv"

# 1) cp949 먼저
try:
    df = pd.read_csv(path, encoding="cp949")
except UnicodeDecodeError:
    # 2) 일부 환경에서는 euc-kr이 더 맞는 경우가 있음
    df = pd.read_csv(path, encoding="euc-kr")

print(df.columns)

팁:

  • cp949euc-kr의 확장(윈도우 코드페이지)이라 한국 윈도우에서 생성된 CSV에 더 잘 맞습니다.

2-3. 표준 csv 모듈: newline='' + 명시적 인코딩

표준 csv 모듈은 파일을 열 때 newline=''를 권장합니다(줄바꿈 처리 안정화).

import csv

path = "data.csv"

with open(path, "r", encoding="utf-8-sig", newline="") as f:
    reader = csv.DictReader(f)
    rows = list(reader)

print(rows[0])
  • BOM 이슈까지 같이 잡으려면 역시 utf-8-sig가 편합니다.

3) 인코딩을 모를 때: 30초 진단 루틴

3-1. 앞부분 바이트를 찍어 BOM/특이값 확인

path = "data.csv"

with open(path, "rb") as f:
    head = f.read(16)

print(head)

대표적인 BOM:

  • UTF-8 BOM: b'\xef\xbb\xbf'

이게 보이면 utf-8-sig로 해결될 확률이 높습니다.

3-2. (선택) chardet로 추정하되, 100% 신뢰 금지

추정은 빠른 힌트로만 쓰고, 최종은 실제 로딩으로 검증하세요.

pip install chardet
import chardet

path = "data.csv"

with open(path, "rb") as f:
    raw = f.read()

print(chardet.detect(raw))
  • {'encoding': 'EUC-KR', 'confidence': 0.99, ...} 같은 결과가 나오면 cp949/euc-kr 계열을 우선 시도합니다.

4) UnicodeDecodeError를 “무시”하면 생기는 2차 사고

종종 아래처럼 임시로 넘기려는 시도가 있습니다.

# 비추천: 데이터 유실/오염 가능
open("data.csv", "r", encoding="utf-8", errors="ignore")

이 방식은 에러는 사라지지만 다음 문제가 생깁니다.

  • 디코딩 불가 문자가 조용히 삭제되어 키/코드/주소가 변형됨
  • 나중에 DB 적재나 조인에서 미스매치가 발생

데이터 파이프라인에서는 “에러를 없애는 것”보다 “정확히 읽는 것”이 우선입니다. pandas에서 경고를 무시했다가 데이터가 조용히 틀어지는 패턴은 다른 주제에서도 반복되는데, 비슷한 맥락으로 pandas SettingWithCopyWarning 완전 정복도 함께 참고하면 좋습니다.

5) 구분자/따옴표/줄바꿈 때문에 깨져 보이는 경우

인코딩이 맞는데도 “열이 밀리거나” “한 줄이 두 줄로 쪼개지는” 현상이 있으면 파싱 옵션 문제일 수 있습니다.

5-1. 구분자가 콤마가 아니라 세미콜론/탭인 경우

import pandas as pd

# 세미콜론 구분 CSV
pd.read_csv("data.csv", encoding="utf-8-sig", sep=";")

# TSV(탭)
pd.read_csv("data.tsv", encoding="utf-8-sig", sep="\t")

5-2. 따옴표/이스케이프가 섞여 파서가 꼬이는 경우

현장에서 종종 만나는 케이스:

  • 텍스트 컬럼에 따옴표가 포함됐는데 제대로 이스케이프되지 않음
  • 멀티라인 텍스트(개행 포함)가 CSV 셀에 들어있음

pandas에서는 엔진 변경이 도움이 될 때가 있습니다.

import pandas as pd

# 기본 C 엔진에서 문제면 python 엔진으로 시도
pd.read_csv("data.csv", encoding="utf-8-sig", engine="python")

또는 깨진 행을 확인하기 위해 일부만 읽어 디버깅합니다.

import pandas as pd

for i, chunk in enumerate(pd.read_csv("data.csv", encoding="utf-8-sig", chunksize=1000)):
    print(i, chunk.shape)
    break

6) 재발 방지: “읽기”보다 “저장 규칙”을 통일하라

팀/프로젝트에서 CSV 인코딩이 흔들리는 이유는 생성 주체가 다양하기 때문입니다(엑셀, 파이썬, 외부 솔루션, 운영 DB 덤프 등). 가장 좋은 해결은 출력 규칙을 하나로 고정하는 것입니다.

6-1. Python에서 CSV 저장은 utf-8-sig 또는 utf-8을 명시

엑셀 호환이 중요하면 utf-8-sig가 현실적으로 덜 싸웁니다.

import pandas as pd

# df는 이미 존재한다고 가정

df.to_csv("export.csv", index=False, encoding="utf-8-sig")

리눅스/서버/데이터레이크 중심이고 엑셀 의존이 낮다면 BOM 없는 utf-8도 좋습니다.

df.to_csv("export.csv", index=False, encoding="utf-8")

6-2. DB 적재 전 단계에서 인코딩 정규화(권장)

CSV를 바로 DB에 넣기 전에, 파이프라인 초입에서 UTF-8로 정규화하면 이후 단계가 단순해집니다.

from pathlib import Path

src = Path("source.csv")
dst = Path("normalized.csv")

# cp949로 읽어서 utf-8-sig로 다시 저장(엑셀 호환)
text = src.read_text(encoding="cp949")
dst.write_text(text, encoding="utf-8-sig")

이렇게 “정규화 파일”을 표준 입력으로 삼으면, 후속 작업(검증/적재/분석)에서 인코딩 이슈가 급감합니다.

7) 실전 체크리스트(이 순서대로 하면 대부분 끝)

  1. BOM 여부 확인 → 보이면 utf-8-sig로 읽기
  2. 엑셀 기본 CSV 의심 → cp949로 읽기
  3. 그래도 실패 → euc-kr 시도 + 구분자(sep) 확인
  4. 깨진 행/열 밀림이면 → engine='python' 또는 newline='' 확인
  5. 임시방편 errors='ignore'는 금지(데이터 유실)
  6. 최종적으로는 출력 인코딩 규칙을 통일(UTF-8 or UTF-8-SIG)

운영 환경에서 데이터 적재/배치가 엮이면 “한 번의 깨짐”이 재처리 비용으로 커집니다. 파이프라인 안정화 관점에서는 장애 원인을 로그로 남기고 재발을 막는 접근이 중요한데, 원인 추적과 재현/개선의 사고방식은 DB 데드락 같은 이슈에서도 동일합니다. 필요하면 MySQL·PostgreSQL 데드락 분석과 트랜잭션·인덱스 튜닝도 함께 읽어보면 트러블슈팅 흐름을 잡는 데 도움이 됩니다.

8) 바로 가져다 쓰는 “자동 인코딩 폴백” 함수

마지막으로, 실무에서 가장 많이 쓰는 형태의 유틸 함수를 제공합니다. 인코딩 후보를 순서대로 시도하고, 성공한 인코딩을 함께 반환합니다.

import pandas as pd


def read_csv_with_fallback(path, encodings=("utf-8-sig", "utf-8", "cp949", "euc-kr"), **kwargs):
    last_err = None
    for enc in encodings:
        try:
            df = pd.read_csv(path, encoding=enc, **kwargs)
            return df, enc
        except UnicodeDecodeError as e:
            last_err = e
    raise last_err


df, used = read_csv_with_fallback("data.csv")
print("used encoding:", used)
print(df.head())
  • 기본 후보 순서는 UTF-8(BOM 포함) → UTF-8 → cp949 → euc-kr
  • 조직 내 파일 특성에 따라 후보 순서를 바꾸면 더 빠르게 맞출 수 있습니다.

정리하면, Python CSV 인코딩 문제는 대부분 utf-8-sigcp949 두 갈래에서 끝납니다. “읽기 옵션”을 땜질하기보다, 파일 생성/저장 규칙을 UTF-8 계열로 표준화하면 UnicodeDecodeError는 거의 사라집니다.