Published on

Pandas read_csv UnicodeDecodeError 5분 해결

Authors

서버/로컬에서 CSV를 읽다가 갑자기 UnicodeDecodeError: 'utf-8' codec can't decode byte ...가 터지면, 대부분은 “파일이 UTF-8이 아니다”에서 끝나지 않습니다. 실제 현장에서는 CP949(EUC-KR 계열), UTF-8 with BOM, 부분적으로 깨진 바이트, 따옴표/구분자 문제로 인한 오탐이 섞여 원인을 헷갈리게 만듭니다.

이 글은 Pandas read_csvUnicodeDecodeError5분 안에 해결하기 위한 순서(진단 → 가설 → 조치)를 제공합니다.

Pandas 관련 경고를 함께 다루고 싶다면: pandas SettingWithCopyWarning 확실히 없애는 법

1) 에러 메시지로 1차 분류하기

에러는 보통 아래 형태 중 하나입니다.

  • UnicodeDecodeError: 'utf-8' codec can't decode byte 0x.. in position ..: invalid start byte
    • UTF-8로 디코딩하려는데 UTF-8이 아닌 바이트가 존재
  • UnicodeDecodeError: 'cp949' codec can't decode byte 0x..
    • CP949로 읽으려는데 UTF-8이거나, 깨진 바이트가 존재
  • UnicodeDecodeError: ...: unexpected end of data
    • 파일이 잘렸거나(다운로드 중단), 멀티바이트 문자가 중간에 끊김

여기서 핵심은 “어떤 코덱으로 읽으려 했는지”와 “어떤 바이트에서 실패했는지”입니다.

2) 가장 빠른 해결책: 후보 인코딩 3개로 재시도

한국어 CSV에서 가장 흔한 조합은 다음 3개입니다.

  • utf-8
  • utf-8-sig (UTF-8 BOM 포함)
  • cp949 (또는 euc-kr)

아래 코드는 5분 해결용으로 가장 먼저 돌려볼 만한 패턴입니다.

import pandas as pd

path = "data.csv"

for enc in ["utf-8", "utf-8-sig", "cp949", "euc-kr"]:
    try:
        df = pd.read_csv(path, encoding=enc)
        print("OK:", enc, df.shape)
        break
    except UnicodeDecodeError as e:
        print("FAIL:", enc, e)
  • utf-8-sig가 통과하면: 엑셀/윈도우 툴이 BOM을 붙인 케이스가 많습니다.
  • cp949가 통과하면: 윈도우 환경에서 저장된 CSV일 가능성이 큽니다.

이 단계에서 80%는 끝납니다.

3) 그래도 안 되면: 파일 앞부분 바이트로 BOM/텍스트 여부 확인

인코딩을 “찍어 맞추기” 전에, 파일이 정말 텍스트 CSV인지부터 확인해야 합니다. 간혹 확장자만 CSV이고 실제로는 다른 포맷이거나(예: XLSX), 바이너리 조각이 섞인 경우도 있습니다.

from pathlib import Path

path = Path("data.csv")
raw = path.read_bytes()

print("size:", len(raw))
print("head bytes:", raw[:32])

자주 보이는 시그니처:

  • UTF-8 BOM: b'\xef\xbb\xbf'로 시작
  • XLSX: b'PK'로 시작(ZIP 기반)

만약 b'PK'로 시작하면, 그건 CSV가 아니라 엑셀 파일일 가능성이 큽니다. 이 경우 read_csv가 아니라 read_excel을 써야 합니다.

import pandas as pd

df = pd.read_excel("data.csv")  # 파일이 실제로 xlsx인데 확장자가 csv인 경우도 있음

4) “인코딩은 맞는데도” 실패할 때: 깨진 바이트/혼합 인코딩 처리

현업에서 은근히 많이 만나는 케이스가 대부분은 CP949인데 일부 행에 UTF-8 조각이 섞였거나, 반대로 UTF-8인데 일부가 깨진 상태입니다. 이런 경우는 “정확한 복구”가 목적이냐, “일단 로딩”이 목적이냐에 따라 전략이 달라집니다.

4-1) 일단 로딩이 목적: errors 옵션으로 대체/무시

Pandas read_csv는 내부적으로 파이썬 파일 핸들을 사용할 수 있으므로, open(..., errors=...)를 직접 제어하면 됩니다.

import pandas as pd

path = "data.csv"

with open(path, "r", encoding="cp949", errors="replace") as f:
    df = pd.read_csv(f)

# errors="ignore"는 더 과감하지만 데이터 손실이 커질 수 있음
  • replace: 디코딩 불가 문자를 (replacement char)로 대체
  • ignore: 디코딩 불가 바이트를 제거(조용히 데이터 손실)

로그/리포트 목적이라면 replace가 현실적인 타협점입니다.

4-2) 정확한 복구가 목적: 문제 행을 찾아 분리

먼저 바이트 단위로 읽고, 줄 단위 디코딩을 시도해 실패한 줄을 분리합니다.

from pathlib import Path

path = Path("data.csv")
raw_lines = path.read_bytes().splitlines()

bad = []
for i, line in enumerate(raw_lines, start=1):
    try:
        line.decode("cp949")
    except UnicodeDecodeError:
        bad.append(i)

print("bad line count:", len(bad))
print("first bad lines:", bad[:10])

이후 해당 라인 주변을 별도 파일로 떼어내서 원본 생성 시스템(엑셀, DB export, 외부 벤더)에서 재추출하는 게 가장 깔끔합니다.

5) UnicodeDecodeError로 보이지만 사실은 CSV 파싱 문제인 경우

UnicodeDecodeError만 보고 인코딩만 만지다가 시간을 버리는 경우가 있습니다. 실제로는 다음 문제들이 “디코딩 에러처럼” 보이거나, 인코딩 해결 후 곧바로 다른 에러로 이어집니다.

5-1) 구분자(delimiter)가 콤마가 아니다

한국 환경에서 ; 탭, | 등을 쓰는 파일이 종종 있습니다.

import pandas as pd

# 세미콜론 구분
df = pd.read_csv("data.csv", encoding="cp949", sep=";")

# 탭 구분
df = pd.read_csv("data.csv", encoding="utf-8-sig", sep="\t")

5-2) 따옴표/이스케이프가 깨져 엔진이 흔들리는 경우

행 중간에 "가 비정상적으로 들어가면 파서가 꼬일 수 있습니다. 이때는 engine="python"으로 바꿔서 우회 진단을 해볼 수 있습니다(속도는 느리지만 관대합니다).

import pandas as pd

df = pd.read_csv(
    "data.csv",
    encoding="cp949",
    engine="python",
    on_bad_lines="skip"  # pandas 버전에 따라 동작 상이
)
  • on_bad_lines="skip"는 “깨진 행 무시”로 빠른 진행이 가능하지만, 반드시 스킵된 행 수를 기록하세요.

6) 운영 관점: 재발 방지를 위한 표준화 체크리스트

한 번 해결하고 끝내면 같은 문제가 반복됩니다. 데이터 파이프라인에서 다음을 표준으로 잡아두면 UnicodeDecodeError가 크게 줄어듭니다.

6-1) 출력 시스템에서 UTF-8로 고정하고 BOM 정책을 명확히

  • 내부 파이프라인: 가능하면 utf-8로 통일
  • 엑셀 호환이 중요: utf-8-sig로 저장(엑셀이 BOM을 선호하는 경우가 있음)

파이썬에서 저장할 때 예시:

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

6-2) 입력 단계에서 “인코딩 탐지 + 로깅”을 공통 유틸로

서비스/배치에서 CSV를 받을 때, 단순히 실패하면 끝내지 말고 다음을 로그로 남기면 트러블슈팅 시간이 줄어듭니다.

  • 파일 크기
  • 첫 32바이트(hex)
  • 시도한 인코딩 목록과 성공/실패
  • 실패한 라인 번호(가능하면)

6-3) 데이터 품질: 깨진 바이트를 허용할지 정책 결정

  • “리포트/통계용”이면 errors="replace" 허용
  • “정산/계약/법적 데이터”면 깨진 바이트 발견 시 즉시 실패하고 재수급

7) 5분 해결 플로우 요약

  1. utf-8, utf-8-sig, cp949 순서로 read_csv 재시도
  2. 실패하면 파일 헤더 바이트 확인(텍스트/엑셀/바이너리 오판 제거)
  3. 혼합/깨짐 의심 시 open(..., errors="replace")로 일단 로딩
  4. 정확한 복구가 필요하면 실패 라인 탐지 후 원본 재추출
  5. 구분자/따옴표 문제까지 점검(sep, engine)

부록) 실전용 함수: 인코딩 자동 시도 + 안전 로딩

아래는 팀에서 공용으로 두기 좋은 형태의 유틸입니다.

import pandas as pd


def read_csv_safely(path, *, encodings=None, **kwargs):
    encodings = encodings or ["utf-8", "utf-8-sig", "cp949", "euc-kr"]

    last_err = None
    for enc in encodings:
        try:
            return pd.read_csv(path, encoding=enc, **kwargs)
        except UnicodeDecodeError as e:
            last_err = e

    # 최후의 수단: cp949로 강제 + 깨진 문자 대체
    with open(path, "r", encoding="cp949", errors="replace") as f:
        return pd.read_csv(f, **kwargs)


df = read_csv_safely("data.csv")
print(df.head())

이 유틸을 쓰면 “대부분은 자동으로 해결하되, 최후에는 대체 문자로라도 로딩”하는 전략을 일관되게 적용할 수 있습니다.


CSV 인코딩 문제는 한 번 겪으면 계속 반복됩니다. 중요한 건 감으로 때려 맞추는 게 아니라, 시도 순서와 로그를 표준화해서 다음 장애 때 1~2분 만에 결론을 내릴 수 있게 만드는 것입니다.