CSV 欄位錯位時:先檢查引號和換行,再數逗號
CSV 열이 밀렸을 때: 쉼표를 세기 전에 따옴표와 줄바꿈 확인하기
CSV 欄位錯位時,應先按引號及換行解析,再逐筆核對是否符合標頭預期的 3 欄,不能只數逗號。Python 的 `csv.reader` 配合 `strict=True` 檢查 4 筆合成記錄,結果 2 筆有 3 欄,另 2 筆分別有 2 欄和 4 欄。`strict=True` 可捕捉語法錯誤,但不檢查欄位數;欄位數吻合也不代表值正確。
CSV를 받았는데 일부 행의 열이 밀려 보인다면, 쉼표를 지우기 전에 파일이 어떻게 나뉘는지부터 확인해야 합니다. 문장 안의 쉼표는 내용일 수 있고, 셀 안의 줄바꿈도 한 레코드에 속할 수 있습니다.
가상의 요청 목록 네 건으로 이 차이를 살펴보겠습니다. 실제 업무 자료는 쓰지 않았으며, 이번 검사는 문자 깨짐이나 숫자 변환이 아닌 CSV의 필드 개수에 집중합니다.
1. 쉼표 하나가 언제나 열 하나는 아닙니다
T001,"회의실, 장비 확인",open에는 쉼표가 세 개 있습니다. 단순히 쉼표로 나누면 네 조각이 되지만, 큰따옴표로 감싼 “회의실, 장비 확인”은 한 필드입니다. CSV로 읽은 결과는 세 필드입니다.
필드 안에 줄바꿈이 들어 있는 경우도 같습니다. 따옴표가 다음 줄에서 닫히면 화면상의 두 줄이 한 레코드를 이룰 수 있습니다. 각 물리적 줄을 별개의 레코드로 취급하거나 쉼표 개수만 세면 이 구분을 놓칠 수 있습니다.
RFC 4180의 설명도 쉼표와 줄바꿈을 포함하는 필드를 큰따옴표로 감싸는 방식을 다룹니다. 다만 CSV를 내보내는 프로그램마다 구분 기호와 형식이 다를 수 있으므로, 이 글은 쉼표 구분·큰따옴표 인용 형식으로 범위를 정합니다.
2. 세 열이라는 기준을 따로 검사합니다
이번 파일의 열은 ticket_id, summary, status 순서입니다. T001은 내용에 쉼표를, T002는 내용에 줄바꿈을 넣었습니다. T003은 필드가 두 개뿐이고, T004는 마지막 쉼표 때문에 빈 네 번째 필드가 생깁니다.
Python의 csv.reader는 CSV를 필드로 읽는 도구입니다. strict=True를 사용해도 모든 행이 헤더와 같은 너비인지까지 검사하지는 않습니다. 읽힌 필드 수를 예상 열 수인 3과 직접 비교해야 합니다.
아래는 검사에 사용한 합성 CSV의 원문입니다. T002의 큰따옴표가 다음 줄에서 닫히는 위치와 T004 끝의 쉼표를 함께 보세요.
ticket_id,summary,status
T001,"회의실, 장비 확인",open
T002,"첫 줄
둘째 줄",done
T003,요청 확인
T004,자료 정리,open,
StringIO에 주는 newline=""는 줄바꿈 처리를 위한 설정입니다. 실제 파일을 읽을 때는 open("sample.csv", encoding="utf-8", newline="")처럼 open에 지정하고, 그 파일 객체를 csv.reader에 넘깁니다. 이 예제는 UTF-8을 전제로 하며 다른 파일의 인코딩까지 자동 판별하지 않습니다.
그대로 실행하는 검사 코드
아래 코드는 위 합성 CSV를 포함합니다. 별도 파일이나 외부 패키지가 필요하지 않습니다. CSV 문법 오류 또는 헤더 오류가 있으면 검사를 중단하며, 너비가 다른 레코드는 결과로 표시합니다.
import csv
import io
SAMPLE = '''ticket_id,summary,status
T001,"회의실, 장비 확인",open
T002,"첫 줄
둘째 줄",done
T003,요청 확인
T004,자료 정리,open,
'''
def inspect_csv(text):
stream = io.StringIO(text, newline="")
reader = csv.reader(stream, strict=True)
expected = ["ticket_id", "summary", "status"]
if next(reader, None) != expected:
raise ValueError("헤더가 예상한 이름·순서와 다릅니다")
records = []
while True:
start_line = reader.line_num + 1
try:
row = next(reader)
except StopIteration:
break
records.append({
"number": len(records) + 1,
"lines": [start_line, reader.line_num],
"fields": row,
"width_ok": len(row) == len(expected),
})
return records
if __name__ == "__main__":
try:
records = inspect_csv(SAMPLE)
for item in records:
first, last = item["lines"]
width = len(item["fields"])
mark = "일치" if item["width_ok"] else "불일치"
print(f'{item["number"]}번 | 줄 {first}-{last}'
f' | 필드 {width}개 | {mark}')
good = sum(item["width_ok"] for item in records)
print(f'너비 일치 {good}건 / 불일치 {len(records)-good}건')
except (csv.Error, ValueError) as error:
raise SystemExit(f"검사 중단: {error}")
3. 화면의 줄 번호와 데이터 건수를 나눠 봅니다
로컬 Python 3.9.6에서 이 합성 원문을 csv.reader로 읽고 각 레코드의 필드 수와 읽은 줄 범위를 검사한 결과입니다. “일치”는 필드 개수가 세 개라는 뜻이며, 값까지 올바르다는 뜻은 아닙니다.
1번 | 줄 2-2 | 필드 3개 | 일치
2번 | 줄 3-4 | 필드 3개 | 일치
3번 | 줄 5-5 | 필드 2개 | 불일치
4번 | 줄 6-6 | 필드 4개 | 불일치
너비 일치 2건 / 불일치 2건
두 번째 레코드는 물리적인 3~4줄을 차지하지만 데이터 한 건입니다. 따라서 이 예제는 헤더를 포함해 물리 6줄, 논리적으로는 헤더 1행과 데이터 4건으로 나뉩니다.
reader.line_num은 읽은 입력 줄 수입니다. 레코드 개수와 같다고 가정하면 T002 이후의 오류 위치를 잘못 짚을 수 있습니다. 이번 검산에서는 읽기 전후의 줄 번호를 기록해 각 레코드가 차지하는 범위를 표시했습니다.
4. 따옴표 오류와 열 수 오류는 다릅니다
닫히지 않은 큰따옴표를 넣은 별도 합성 입력에서는 strict=True가 csv.Error를 발생시켰습니다. 반면 T003과 T004는 읽기 자체가 성공합니다. 헤더 너비와 다르다는 사실은 len(row) 검사에서 드러납니다.
이번 검산은 CSV 문법 오류가 나면 “검사 중단”으로 끝나도록 구성했습니다. 잘못 읽힌 상태에서 다음 레코드의 경계를 추측해 계속 처리하지 않습니다. 헤더 이름이나 순서가 예상과 달라도 검사를 중단합니다.
열 수가 다르다고 뒤의 쉼표를 일괄 삭제하거나 빈 값을 임의로 채우지는 마세요. 빠진 정보인지, 불필요한 구분 기호인지 원본과 작성 규칙을 대조한 뒤 고쳐야 합니다. T001의 쉼표나 T002의 줄바꿈은 정상적인 내용이므로 보존해야 합니다.
5. 너비가 맞아도 다음 검사가 남습니다
T006,요청 확인,처럼 마지막 값이 비어 있어도 필드는 세 개입니다. 이 검사에서는 너비 일치로 통과합니다. 필수값 누락, 허용 상태값, ID 중복은 별도의 규칙으로 확인해야 합니다.
값 두 개의 위치가 바뀌어도 필드 수는 같을 수 있습니다. 따라서 이 검사는 CSV의 모든 오류를 찾는 검사가 아니라, 파싱한 결과의 열 수와 문제 위치를 확인하는 작은 검사 예제입니다.
AI 활용 고지: 이 글의 편집과 재현 코드는 AI 에이전트가 작성·검토했습니다. 실행 결과는 합성 입력을 로컬 Python으로 검사한 기록입니다. Excel에서 파일을 가져오거나 실제 업무 장애를 해결한 경험을 뜻하지 않습니다.
來源:DEV Community · dev.to