먼저 결론부터 말씀드리면, 코드 흐름은 멀쩡하고 파일이 저장된 글자 규칙(인코딩)과 파이썬이 읽는 규칙이 어긋난 것으로 보입니다. 한 단어만 바꾸면 풀릴 가능성이 큽니다.
1. 에러 풀이
파이썬에게 "이 파일은 UTF-8로 적혀 있다"(encoding="utf-8")고 알려 줬는데, 맨 첫 바이트부터 UTF-8 규칙에 맞지 않았다는 뜻입니다. UTF-8에서 0x80~0xBF 범위의 바이트(0xB0 포함)는 한 글자의 둘째 바이트 이후에만 올 수 있어 글자의 시작이 될 수 없고, 그래서 "시작 바이트가 틀렸다(invalid start byte)"며 멈춘 것입니다. 대개는 파일이 망가진 게 아니라, 다른 인코딩으로 저장된 파일을 UTF-8로 읽으려 할 때 나는 에러입니다.
메시지에서 짚을 곳
'utf-8' codec: 지금 쓰는 읽기 규칙입니다. 코드 3번째 줄의 encoding="utf-8"에서 정해졌습니다.
byte 0xb0 in position 0: 파일의 맨 첫 바이트(0번째)가 0xB0입니다. 첫 글자부터 막혔으니 일부가 아니라 파일 전체가 UTF-8이 아닐 가능성이 큽니다.
File "read_customers.py", line 5: 붙여 주신 코드로 세면 5번째 줄은 for row in reader:입니다. 3번째 줄의 open()은 파일을 열기만 하고, 실제로 바이트를 읽어 글자로 바꾸는 일은 for가 줄을 꺼낼 때 일어나서 여기서 터집니다. 그래서 고칠 곳은 5번째 줄이 아니라 3번째 줄입니다. 파일을 못 찾았다면 3번째 줄에서 FileNotFoundError가 났을 테니, 파일을 여는 데까지는 성공했습니다.
- 함수: 붙여 주신 메시지에는 함수 이름이 없습니다(「확인 필요」). 코드에 함수가 없으니 스크립트 맨 바깥(
<module>)에서 난 것이고, 전체 트레이스백에는 그 아래에 파이썬 내부의 decode 줄이 하나 더 있을 겁니다(짐작: 메시지 끝줄만 받았습니다. 전체 트레이스백을 붙여 주시면 확실해집니다).
파일 이름을 영어로 바꿔도 같은 에러가 난 것도 이것으로 설명됩니다: 문제는 이름이 아니라 파일 안에 든 바이트라서, 이름을 바꿔도 내용은 그대로입니다.
2. 원인 후보
후보 1 (가장 유력): 엑셀이 CP949로 저장한 파일
-
왜 그렇게 보나
- 엑셀의 「CSV(쉼표로 분리)」는 UTF-8이 아니라 윈도우 기본 인코딩으로 저장하고, 한국어 윈도우에서는 그게 CP949입니다. UTF-8 형식은 「CSV UTF-8(쉼표로 분리)」로 따로 있고 그 파일은 맨 앞이
EF BB BF로 시작하는데, 이 파일은 B0으로 시작합니다. (짐작: 엑셀 버전과 윈도우 설정은 보지 못했습니다. 아래 확인 줄의 결과를 붙여 주시면 확실해집니다.)
- CP949에서
0xB0은 '가'(B0 A1), '고'(B0 ED), '공'(B0 F8) 같은 한글 글자의 첫 바이트입니다. 고객 목록이니 첫 칸 제목이 「고객…」으로 시작한다면 딱 들어맞습니다. (짐작: 파일 첫 줄은 보지 못했습니다. 이것도 아래 확인 줄로 드러납니다.)
-
어떻게 확인하나: 3번째 줄(with open(...)) 바로 위에 아래 한 줄을 넣고 실행합니다. 파일 첫 줄만 CP949로 읽어 찍어 보는 줄입니다.
print(open("고객목록.csv", "rb").readline().decode("cp949"))
엑셀 1행의 제목들이 한글로 또렷하게 나오면(예: 고객명,연락처,..., 실제 열 이름은 「확인 필요」) 후보 1로 확정입니다. 알아볼 수 없는 기호가 나오거나 'cp949' codec can't decode 에러가 나면 후보 2·3을 봅니다.
후보 2: 회사 문서 보안(DRM) 프로그램이 암호화한 파일
- 왜 그렇게 보나: 회사 엑셀에서 저장했다고 하셨습니다. 회사 PC에 문서 보안 프로그램이 깔려 있으면 엑셀로 저장한 파일이 암호화되어, 엑셀에서는 멀쩡히 열려도 파이썬에는 알 수 없는 바이트로 보입니다. (짐작: 회사 보안 환경은 보지 못했습니다. 후보 1 확인 줄의 결과와 IT 담당자의 답이 있으면 확실해집니다.)
- 어떻게 확인하나: 후보 1의 확인 줄이 한글 제목 대신 알아볼 수 없는 기호나 에러를 내는지 봅니다. 파일 아이콘에 자물쇠 같은 표시가 붙어 있는지도 보고, 회사에 문서 보안 프로그램이 있는지 IT 담당자에게 묻는 것이 가장 확실합니다.
후보 3: 윈도우 설정 때문에 CP949가 아닌 인코딩으로 저장된 파일
-
왜 그렇게 보나: 「CSV(쉼표로 분리)」가 쓰는 윈도우 기본 인코딩은 「유니코드를 지원하지 않는 프로그램용 언어」 설정을 따릅니다. 이 설정이 한국어가 아니면 다른 인코딩으로 저장됩니다. 다만 첫 바이트 0xB0이 한글과 잘 맞아서 가능성은 낮습니다. (짐작: 윈도우 설정은 보지 못했습니다. 아래 명령의 결과를 붙여 주시면 확실해집니다.)
-
어떻게 확인하나: 그 파일을 엑셀로 저장한 PC의 명령 프롬프트에서 실행합니다.
python -c "import locale; print(locale.getencoding())"
cp949가 나오면 이 후보는 빠지고 후보 1에 힘이 실립니다. 다른 값(예: cp1252)이 나오면 이 후보입니다.
3. 고치기
후보 1이면: 코드 한 곳만 바꾸기 (가장 작은 수정)
import csv
with open("고객목록.csv", encoding="cp949") as f: # ← 바꾼 줄: "utf-8" → "cp949"
reader = csv.reader(f)
for row in reader:
print(row)
- 바꾼 곳은 3번째 줄의
"utf-8" → "cp949" 하나뿐입니다. 표시용 주석은 지워도 됩니다.
- 왜 고쳐지나: 파일이 실제로 적힌 규칙(CP949)으로 읽으니
0xB0이 뒤 바이트와 묶여 한글 한 글자로 읽힙니다.
"euc-kr"보다 "cp949"를 쓰세요. euc-kr은 '똠'·'햏' 같은 일부 한글을 몰라서, 고객 이름에 그런 글자가 있으면 그 줄에서 다시 에러가 납니다.
파일을 바꿀 수 있다면: UTF-8로 다시 저장하기 (길게 보면 이쪽이 낫습니다)
-
원래 작업하던 엑셀 파일에서 「다른 이름으로 저장」 → 파일 형식 「CSV UTF-8(쉼표로 분리)(*.csv)」로 저장합니다. (짐작: 엑셀 버전에 따라 이 항목이 없을 수 있습니다. 엑셀 버전은 「확인 필요」이고, 「파일 → 계정」 화면에서 볼 수 있습니다.)
- 주의: 이미 만든 CSV를 엑셀로 다시 열어 저장하면,
01012345678처럼 숫자만으로 된 전화번호의 앞자리 0이 빠지는 식으로 값이 바뀔 수 있습니다(짐작: 파일 내용은 보지 못했습니다). 기존 CSV를 덮어쓰면 되돌리기 어려우니, 새 이름으로 저장하고 코드의 파일 이름도 같이 바꾸는 편이 안전합니다.
-
코드는 이 한 줄만 바꿉니다.
with open("고객목록.csv", encoding="utf-8-sig") as f: # ← 바꾼 줄: "utf-8" → "utf-8-sig"
- 왜
-sig인가: "utf-8-sig"는 파일 맨 앞에 BOM(EF BB BF)이라는 표식이 있으면 떼고, 없으면 그냥 읽습니다. 엑셀의 「CSV UTF-8」은 이 표식을 붙이므로(짐작), 그냥 "utf-8"로 읽으면 에러는 안 나도 첫 칸 제목 앞에 보이지 않는 가 붙습니다.
- 왜 길게 보면 나은가: 「CSV(쉼표로 분리)」로 저장하면 CP949에 없는 글자(이모지, 일부 외국어 글자 등)는 저장하는 순간
?로 바뀝니다(짐작: 엑셀 버전을 알려 주시면 확실해집니다). 고객 이름이 조용히 바뀌는 건 에러보다 나쁩니다. UTF-8에는 그런 손실이 없습니다.
후보 2·3으로 확인되면
- 후보 2(DRM): 코드로 풀 문제가 아닙니다. 보안 정책을 우회하려 하지 말고, IT 담당자에게 DRM 해제(복호화) 신청 절차를 물어보세요.
- 후보 3(다른 인코딩): 한글이 이미
?로 바뀐 채 저장됐을 수 있어 읽는 쪽에서는 되살릴 수 없습니다(짐작). 위의 「UTF-8로 다시 저장하기」로 가세요.
4. 확인
입력: 지금 그 고객목록.csv(UTF-8로 다시 저장했다면 새 파일)로 python read_customers.py를 다시 실행합니다. 확인용으로 넣었던 한 줄은 지웁니다.
기대 결과
| 볼 것 | 기대 결과 |
|---|
| 에러 | UnicodeDecodeError 없이 끝까지 실행됩니다 |
| 첫 줄 출력 | 엑셀 1행 제목이 한글 그대로 리스트로 나옵니다. 예: ['고객명', '연락처', ...] (실제 열 이름은 「확인 필요」) |
| 마지막 줄 출력 | 엑셀의 마지막 행과 같습니다. 중간에 끊기지 않고 끝까지 읽혔다는 뜻입니다 |
| UTF-8로 다시 저장했다면 | 첫 줄이 ['고객명', ...]처럼 로 시작하지 않습니다. 시작하면 "utf-8-sig"로 바꿨는지 다시 봅니다 |
한글 사이의 ? | 없어야 합니다. 있으면 저장할 때 이미 바뀐 것이라(짐작) UTF-8로 다시 저장합니다 |
UTF-8로 다시 저장했는데도 같은 에러가 그대로 나면, 방금 저장한 파일이 아닌 다른 파일을 읽고 있을 수 있습니다. 맨 위 import csv 아래에 import os를 넣고, 3번째 줄 위에 아래 줄을 넣어 지금 읽는 파일의 전체 경로를 확인하세요.
print(os.path.abspath("고객목록.csv"))
5. 다시 안 나게
① 저장 형식과 encoding을 짝으로 정해 둡니다. 직접 저장하는 파일은 「CSV UTF-8」로 통일하고, 남에게서 받은 파일은 어떤 형식으로 저장했는지 먼저 묻습니다. CSV를 같이 주고받는 사람이 있으면 이 약속을 함께 정해 두는 것이 가장 효과가 큽니다.
| 엑셀에서 고른 형식 | 코드의 encoding |
|---|
| CSV UTF-8(쉼표로 분리) | "utf-8-sig" |
| CSV(쉼표로 분리) | "cp949" (한국어 윈도우 기준, 짐작) |
② 읽기 전에 인코딩을 확인하고, 안 맞으면 덮지 말고 멈추게 합니다. 어디서 온 파일인지 모를 때는 두 규칙을 차례로 시험해 보고 어느 쪽으로 읽었는지 찍어 둡니다. errors="ignore"나 errors="replace"로 덮으면 에러는 사라지지만 한글이 빠지거나 �로 바뀐 채 다음 단계로 넘어가니 쓰지 마세요.
import csv
def find_encoding(path):
for enc in ("utf-8-sig", "cp949"): # UTF-8 쪽이 더 엄격해서 먼저 시험합니다
try:
with open(path, encoding=enc) as f:
f.read()
return enc
except UnicodeDecodeError:
pass
raise ValueError(f"{path}: utf-8도 cp949도 아닙니다. 저장 형식을 확인하세요.")
enc = find_encoding("고객목록.csv")
print("읽은 인코딩:", enc)
with open("고객목록.csv", encoding=enc) as f:
reader = csv.reader(f)
for row in reader:
print(row)
원인을 확정하려면 후보 1 확인 줄의 출력(제목 줄만, 고객 정보가 든 줄은 빼고)과 엑셀 버전이 필요한데, 붙여 주시겠어요?