- Published on
PyArrow Invalid - UTF-8 디코딩 오류 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Parquet를 읽거나, Pandas에서 Arrow 변환을 하거나, Spark 없이도 대용량 데이터를 PyArrow로 처리하다 보면 한 번쯤 마주치는 에러가 있습니다.
pyarrow.lib.ArrowInvalid: Invalid: ... UTF8 ... 혹은 Invalid: UTF8 string ... 같은 메시지로 터지는 UTF-8 디코딩 오류입니다. 이 문제는 단순히 “인코딩이 UTF-8이 아니었다”로 끝나는 경우도 있지만, 실제 현장에서는 스키마 불일치, 바이너리 컬럼을 문자열로 잘못 해석, CSV 파서 옵션 문제, Parquet 파일 일부 손상처럼 다양한 원인이 섞여 나타납니다.
이 글에서는 에러를 재현 가능한 형태로 분류하고, PyArrow 기준으로 “어디서 깨졌는지”를 좁힌 뒤, 가장 안전한 해결책부터 성능을 고려한 우회책까지 정리합니다.
이 에러가 뜨는 대표 상황 4가지
1) binary 데이터를 string으로 읽으려는 경우
Parquet/Arrow 스키마에서 binary(바이트 배열)인데, 코드나 스키마 추론이 이를 string으로 취급하면 디코딩이 발생하면서 실패합니다.
예: 해시, 암호화된 값, 압축된 페이로드, 이미지/프로토콜 버퍼 등이 문자열 컬럼으로 잘못 들어간 경우.
2) CSV에 UTF-8이 아닌 인코딩이 섞인 경우
CSV는 파일 전체가 동일 인코딩이라고 가정하기 쉽지만, 실제로는 일부 행/일부 컬럼이 cp949, latin-1 등으로 섞여 들어오는 경우가 많습니다. PyArrow CSV 파서는 기본적으로 UTF-8 텍스트를 기대하므로 특정 바이트 시퀀스에서 ArrowInvalid가 발생합니다.
3) “문자열”이지만 실제로는 잘못된 바이트 시퀀스
업스트림 시스템이 잘못된 UTF-8을 생성하거나, 바이트 단위로 자르고 붙이는 ETL 과정에서 멀티바이트 문자가 깨지면 “문자열처럼 보이지만 UTF-8로는 불가능한 바이트”가 됩니다.
4) Parquet 파일 손상(부분 업로드, truncation)
S3/오브젝트 스토리지에 업로드 중 중단되었거나, 멀티파트 업로드/병렬 처리에서 파일이 깨지면 문자열 컬럼 디코딩 시점에 에러가 터질 수 있습니다. 이 경우는 특정 row group, 특정 column chunk에서만 재현되는 패턴이 많습니다.
먼저 해야 할 1분 진단: 어디서 UTF-8 디코딩이 일어났나
에러가 “읽기 단계”에서 나는지, “변환 단계”에서 나는지를 분리해야 합니다.
pq.read_table()단계에서 터지면: Parquet 내부의 타입/데이터 자체 문제일 가능성이 큼table.to_pandas()또는pa.array(...)에서 터지면: 변환 과정에서 문자열로 강제 캐스팅하거나, Pandas object 컬럼에 비정상 바이트가 섞였을 가능성이 큼pyarrow.csv.read_csv()에서 터지면: CSV 인코딩/파서 옵션 문제일 가능성이 큼
아래처럼 최소 코드로 “어디서 깨지는지”를 고정해두면 해결이 빨라집니다.
import pyarrow.parquet as pq
import pyarrow as pa
path = "data.parquet"
# 1) Parquet -> Arrow Table 읽기
try:
t = pq.read_table(path)
print("read_table OK", t.schema)
except Exception as e:
print("read_table FAIL", repr(e))
raise
# 2) Arrow -> Pandas 변환
try:
df = t.to_pandas()
print("to_pandas OK", df.shape)
except Exception as e:
print("to_pandas FAIL", repr(e))
raise
케이스 A: Parquet에서 binary를 string으로 잘못 해석
증상
- 스키마에
binary/large_binary가 있는데, 코드에서 이를 문자열로 다루려다 디코딩 실패 - 또는 Parquet 파일이 “논리 타입”이 애매하게 저장되어 스키마 추론이 혼동
해결 1) 스키마 확인 후 문제 컬럼을 binary로 유지
import pyarrow.parquet as pq
pf = pq.ParquetFile("data.parquet")
print(pf.schema_arrow)
문제 컬럼이 binary라면, 문자열로 바꾸지 말고 그대로 처리하거나, 필요할 때만 안전하게 디코딩해야 합니다.
import pyarrow as pa
import pyarrow.compute as pc
# 예: binary 컬럼을 hex 문자열로 변환(UTF-8 디코딩이 아니라 안전한 표현)
# col: binary
hex_col = pc.binary_hex(t["payload"]) # 결과는 string
t2 = t.set_column(t.schema.get_field_index("payload"), "payload_hex", hex_col)
해결 2) “정말 문자열”이어야 한다면, 실패를 허용하는 디코딩 전략
PyArrow는 컬럼 전체를 “UTF-8로 디코딩”하려다 실패하면 전체가 터집니다. 따라서 다음 중 하나로 우회합니다.
- 업스트림에서 정상 UTF-8로 정제
- 로우 단위로 디코딩 실패를
null로 바꾸기
로우 단위 정제는 보통 Python 레벨로 내려와야 하므로 비용이 큽니다. 그래도 장애 복구/응급 처리에는 유용합니다.
import pyarrow as pa
arr = t["maybe_text"].combine_chunks() # chunked -> array
# arr 가 binary 라고 가정
def safe_decode(b: bytes):
if b is None:
return None
try:
return b.decode("utf-8")
except UnicodeDecodeError:
return None
py_list = [safe_decode(x.as_py()) for x in arr]
clean = pa.array(py_list, type=pa.string())
t = t.set_column(t.schema.get_field_index("maybe_text"), "maybe_text", clean)
성능은 떨어지지만, “일단 파이프라인을 살리는” 응급처치로는 효과적입니다.
케이스 B: CSV 읽기에서 UTF-8이 아닌 인코딩이 섞임
증상
pyarrow.csv.read_csv()에서Invalid: UTF8 string류 에러- 특정 라인/특정 필드에서만 재현
해결 1) 인코딩을 PyArrow에 맡기지 말고, 먼저 텍스트로 디코딩
PyArrow CSV는 기본적으로 UTF-8을 전제로 빠르게 파싱합니다. 인코딩이 불확실하면 “파일을 먼저 디코딩해서 UTF-8로 정규화”하는 편이 안전합니다.
from pathlib import Path
import pyarrow.csv as pacsv
raw = Path("data.csv").read_bytes()
# 예: cp949 가능성이 높을 때
text = raw.decode("cp949", errors="replace")
# UTF-8로 재인코딩한 뒤 PyArrow에 전달
utf8_bytes = text.encode("utf-8")
table = pacsv.read_csv(utf8_bytes)
errors="replace"는 데이터 품질을 희생하지만 파이프라인을 멈추지 않습니다. 품질이 중요하면 errors="strict"로 두고, 실패 위치를 로깅해 원본을 수정하는 방식이 낫습니다.
해결 2) 문제 컬럼을 일단 binary로 읽고 나중에 정제
문자열로 읽는 순간 디코딩이 터지므로, 먼저 바이트로 받아서 후처리하는 전략입니다.
import pyarrow as pa
import pyarrow.csv as pacsv
convert = pacsv.ConvertOptions(
column_types={
"name": pa.binary(),
"comment": pa.binary(),
}
)
t = pacsv.read_csv("data.csv", convert_options=convert)
이후 binary 컬럼을 안전하게 디코딩(성공 시만)하거나, latin-1로 1:1 매핑한 뒤 정제하는 식으로 진행합니다.
케이스 C: Pandas에서 Arrow 변환 시 object 컬럼에 비정상 바이트가 섞임
증상
pa.Table.from_pandas(df)또는df.to_parquet(engine="pyarrow")에서 실패- Pandas 컬럼 dtype이
object이고 값이str과bytes가 혼재
해결 1) object 컬럼의 타입 혼재를 제거
Pandas에서 먼저 “bytes는 bytes로, str은 str로” 정리해야 합니다.
import pandas as pd
def normalize_obj(x):
if x is None or (isinstance(x, float) and pd.isna(x)):
return None
if isinstance(x, bytes):
# bytes가 섞여 있으면 우선 base64/hex 같은 안전한 문자열로 바꾸는 선택도 가능
try:
return x.decode("utf-8")
except UnicodeDecodeError:
return None
return str(x)
obj_cols = [c for c in df.columns if df[c].dtype == "object"]
for c in obj_cols:
df[c] = df[c].map(normalize_obj)
정규화 후 Arrow 변환:
import pyarrow as pa
table = pa.Table.from_pandas(df, preserve_index=False)
해결 2) 스키마를 명시해 “문자열로 강제 캐스팅”을 피하기
스키마 추론이 실수하는 경우가 있으니, 특히 장애가 잦은 파이프라인이라면 스키마를 고정하는 것이 장기적으로 안정적입니다.
import pyarrow as pa
schema = pa.schema([
pa.field("id", pa.int64()),
pa.field("payload", pa.binary()),
pa.field("message", pa.string()),
])
table = pa.Table.from_pandas(df, schema=schema, preserve_index=False)
케이스 D: Parquet 파일 일부 손상(특정 row group에서만 실패)
증상
- 같은 파일을 읽을 때 환경/버전에 따라 간헐적으로 실패
- 전체가 아니라 특정 컬럼/특정 row group에서만 터짐
진단: row group 단위로 읽어 문제 구간을 특정
import pyarrow.parquet as pq
pf = pq.ParquetFile("data.parquet")
for rg in range(pf.num_row_groups):
try:
t = pf.read_row_group(rg)
except Exception as e:
print("row_group", rg, "FAIL", repr(e))
raise
print("all row groups OK")
특정 row group에서만 실패한다면 다음을 의심합니다.
- 업로드/다운로드 중 파일 손상
- 생성 라이브러리 버그 또는 비정상 종료로 footer/column chunk가 깨짐
- 스토리지 계층에서 부분 파일을 읽고 있는 상황
해결 방향
- 원본 생성 파이프라인에서 Parquet write를 원자적으로(임시 파일 후 rename) 처리
- 오브젝트 스토리지 업로드 검증(ETag, size)과 재시도
- 문제 row group만 제외하고 재생성(가능한 경우)
운영 환경에서 이런 파일 손상은 종종 “컨테이너 재시작/크래시”와 함께 나타납니다. 파이프라인이 K8s 위에서 돈다면 크래시 원인도 같이 점검해보는 게 좋습니다. 관련해서는 K8s CrashLoopBackOff 원인 10가지·즉시 진단법 글이 도움이 됩니다.
실전 체크리스트: 가장 안전한 해결 순서
- 스키마 출력:
ParquetFile(...).schema_arrow로 문제 컬럼 타입이string인지binary인지 확인 - 실패 지점 분리:
read_tablevsto_pandasvsread_csv - CSV라면 인코딩 정규화: 먼저 bytes를 올바른 인코딩으로 decode 후 UTF-8로 재encode
- 문제 컬럼만 격리: 전체를 한 번에 디코딩/캐스팅하지 말고 문제 컬럼만 별도로 처리
- row group 진단: Parquet 손상 여부 확인
- 장기 대책: 스키마 고정 + 업스트림 정제(ETL 단계에서 UTF-8 보장)
장애를 줄이는 운영 팁
로그에 “컬럼명, row group, 파일 경로”를 남겨라
UTF-8 오류는 재현이 어려운 편입니다. 최소한 아래 정보가 있어야 원인 규명이 빨라집니다.
- 파일 경로(파티션 키 포함)
- Parquet row group index
- 컬럼명
- 처리한 라이브러리 버전(
pyarrow.__version__)
CI/배포 파이프라인에서 재현 환경을 고정
PyArrow는 내부적으로 SIMD/압축 코덱/의존 라이브러리 조합에 따라 증상이 달라 보일 수 있습니다. 데이터 파이프라인을 GitHub Actions로 돌린다면 캐시/환경 차이로 디버깅이 꼬일 수 있으니, 환경 고정과 실패 재현을 쉽게 만드는 게 중요합니다. 관련해서는 GitHub Actions 캐시로 Node.js CI 2배 빠르게, 실패 디버깅 글의 “재현 가능한 CI” 관점이 참고가 됩니다.
결론
PyArrow Invalid: UTF-8 디코딩 오류는 표면적으로는 문자열 문제지만, 실제 원인은 크게 네 갈래(바이너리/문자열 타입 혼동, CSV 인코딩 혼재, 비정상 바이트 시퀀스, Parquet 손상)로 나뉩니다.
가장 중요한 포인트는 “문자열로 디코딩되는 순간”을 통제하는 것입니다. 문제 컬럼을 binary로 격리하고, 필요할 때만 안전하게 디코딩하거나(실패는 null 처리), 아예 hex/base64 같은 안전한 표현으로 변환하면 파이프라인 안정성이 크게 올라갑니다. 동시에 CSV라면 PyArrow가 읽기 전에 인코딩을 정규화하는 것이 가장 확실한 해결책입니다.