CSV 글자 수와 바이트 제한을 구분해 업로드 전에 점검하기
CSV를 받는 양식이 ‘9바이트 이하’라고 안내했다면 글자 수만 세어서는 부족합니다. ABC와 가나다는 Python에서 길이가 모두 3이지만, UTF-8로 표현한 크기는 각각 3바이트와 9바이트입니다. 확인한 수신 인코딩으로 필드 값을 인코딩한 뒤 바이트 수를 세고, 넘는 행은 원문을 남겨 검토하세요.
가상 이름 세 개가 든 작은 CSV로 연습합니다. 실제 명단은 사용하지 않습니다. 결과는 로컬 점검 보고서이며 파일이나 값을 외부로 보내지 않습니다.

1 먼저 수신 양식의 제한을 확인합니다
실습의 계약은 name 필드를 UTF-8로 표현했을 때 9바이트 이내이며, 빈 문자열은 필수값 누락으로 보류한다는 것입니다. 9바이트는 연습용 기준입니다. 실제 양식의 인코딩과 제한을 확인하기 전에는 이 결과를 제출 승인으로 쓰지 마세요.
수신 안내에서 필드 한 개의 제한인지 파일 전체의 제한인지, 글자 수인지 바이트 수인지, 인코딩이 무엇인지 먼저 확인합니다. 공백과 줄바꿈을 포함하는지, 서버가 값을 정규화하는지도 확인할 항목입니다. 이 코드는 원래 필드 값의 바이트만 측정합니다. CSV의 쉼표·따옴표, 파일 선두의 BOM, HTTP 전송 형식의 크기는 더하지 않습니다.
Python 문자열은 Unicode 코드 포인트의 시퀀스입니다. len(name)은 코드 포인트 개수이고 len(name.encode("utf-8", errors="strict"))은 인코딩한 바이트의 개수입니다. 코드 포인트 수가 화면에서 한 글자로 보이는 묶음의 수와 항상 같은 것은 아닙니다. 이 구분은 Python 문자열 문서와 len 공식 문서에서 확인할 수 있습니다.

2 가상 입력 세 행의 기대값을 정합니다
실습 ZIP 다운로드 (13,799바이트): 전체 코드, 가상 입력, 기대값, 자동 시험과 실행 기록을 함께 받습니다.
동봉한 P23_Practice_Field_Byte_Limit.zip을 새 연습 폴더에 풀고 fixtures/names.csv를 텍스트 편집기로 확인합니다. 파일은 UTF-8이며 헤더는 정확히 id,name입니다.
id,name
R1,ABC
R2,가나다
R3,가나다라
| 원래 값 | 코드 포인트 수 | UTF-8 바이트 수 | 9바이트 기준 |
|---|---|---|---|
| ABC | 3 | 3 | pass |
| 가나다 | 3 | 9 | pass |
| 가나다라 | 4 | 12 | hold |
두 번째 값은 제한과 정확히 같으므로 통과합니다. 세 번째 값은 3바이트를 넘으므로 보류합니다. 프로그램은 가나다라를 가나다로 잘라 맞추지 않습니다. 이름·상품명·코드의 끝부분을 자르면 다른 값을 만들 수 있기 때문입니다.
ZIP에는 전체 코드, 정상·경계·실패 입력, 수동으로 정한 expected 보고서, 자동 시험, 실제 실행 기록과 파일별 SHA-256 목록이 있습니다. fixtures와 expected는 보관하고 새 결과는 별도 results 폴더에 만듭니다.
3 원래 값을 남기는 점검 코드를 실행합니다
ZIP의 code/audit_field_bytes.py는 다음 코드와 같습니다. 입력 파일은 UTF-8로 읽으며 선두 UTF-8 BOM 한 개도 처리합니다. --field-encoding은 수신 필드의 인코딩을 명시하는 값입니다. 선택지는 utf-8, cp949, ascii이고 기본값은 없습니다. 인코딩을 추측해서 고르지 마세요.
import argparse
import csv
import io
import sys
from pathlib import Path
ENCODINGS = ('utf-8', 'cp949', 'ascii')
FIELDS = ('record_no', 'id', 'raw_name', 'code_points', 'byte_count',
'field_encoding', 'max_bytes', 'status', 'reason')
def audit(source, output, field_encoding, max_bytes):
if field_encoding not in ENCODINGS or not 1 <= max_bytes <= 1_000_000:
raise ValueError('unsupported encoding or byte limit')
if source.resolve() == output.resolve():
raise ValueError('input and output must be different files')
with source.open('rb') as f:
raw = f.read(1_000_001)
if len(raw) > 1_000_000:
raise ValueError('input exceeds 1000000 bytes')
text = raw.decode('utf-8-sig', errors='strict')
csv.field_size_limit(100_000)
reader = csv.reader(io.StringIO(text, newline=''), strict=True)
if next(reader, None) != ['id', 'name']:
raise ValueError('header must be exactly id,name')
rows, seen = [], set()
for number, values in enumerate(reader, 1):
if number > 10_000:
raise ValueError('input exceeds 10000 data records')
if len(values) != 2:
raise ValueError(f'record {number}: expected two fields')
key, name = values
if not key.strip() or key in seen:
raise ValueError(f'record {number}: empty or duplicate id')
seen.add(key)
count, status, reason = '', 'hold', 'unencodable'
try:
count = len(name.encode(field_encoding, errors='strict'))
if name == '':
reason = 'missing'
elif count > max_bytes:
reason = 'over_limit'
else:
status, reason = 'pass', 'within_limit'
except UnicodeEncodeError:
pass
rows.append((number, key, name, len(name), count,
field_encoding, max_bytes, status, reason))
if not rows:
raise ValueError('input has no data records')
buffer = io.StringIO(newline='')
writer = csv.writer(buffer, lineterminator='\n')
writer.writerow(FIELDS)
writer.writerows(rows)
report = buffer.getvalue()
created = False
try:
with output.open('x', encoding='utf-8', newline='') as f:
created = True
f.write(report)
except OSError:
if created:
output.unlink()
raise
passed = sum(row[-2] == 'pass' for row in rows)
print(f'rows={len(rows)} pass={passed} hold={len(rows) - passed}')
return 0 if passed == len(rows) else 1
def main():
parser = argparse.ArgumentParser(description='Audit a declared field byte limit locally')
parser.add_argument('input', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('--field-encoding', required=True, choices=ENCODINGS)
parser.add_argument('--max-bytes', required=True, type=int)
args = parser.parse_args()
try:
return audit(args.input, args.output, args.field_encoding, args.max_bytes)
except (OSError, ValueError, csv.Error) as exc:
print(f'ERROR: {exc}', file=sys.stderr)
return 2
if __name__ == '__main__':
sys.exit(main())
명령을 실행할 터미널에서 ZIP을 푼 폴더로 이동한 뒤 Python 실행 명령이 잡혀 있는지 확인합니다. 아래 python은 이 실습에 사용한 실행 명령입니다. 설치 환경에서 python3 또는 다른 명령을 쓴다면 같은 Python을 가리키는 명령으로 바꾸세요.
python --version
mkdir results
python code/audit_field_bytes.py fixtures/names.csv results/names_utf8.csv --field-encoding utf-8 --max-bytes 9
첫 점검의 요약은 rows=3 pass=2 hold=1입니다. 종료 코드 1은 보고서를 만들었고 검토할 행이 있다는 뜻입니다. 보고서 작성 실패를 뜻하지 않습니다. 모든 행이 통과하면 종료 코드 0이고, 입력 구조·옵션·파일 접근이 잘못되어 점검을 완료하지 못하면 2입니다.
results/names_utf8.csv에는 원래 ID와 name 값, 코드 포인트 수, 바이트 수, 적용 인코딩과 제한, 상태·이유가 함께 남습니다. raw_name 열을 원본과 비교하고 expected/names_utf8.csv와 전체 내용을 대조합니다. 보고서 자체의 저장 인코딩은 항상 UTF-8입니다.
CSV는 쉼표가 있는 값이나 인용부호 안의 줄바꿈을 csv.reader와 csv.writer로 처리합니다. newline을 빈 문자열로 지정하는 이유와 CSV 형식·필드 크기 옵션은 Python csv 공식 문서에 나와 있습니다. 외부 값을 담은 보고서는 먼저 텍스트 편집기로 확인하세요. 수식처럼 시작하는 값을 스프레드시트에서 열 때의 실행 위험까지 이 바이트 검사가 막아 주지는 않습니다.
4 경계값과 인코딩 차이를 따로 확인합니다
fixtures/boundaries.csv에는 일곱 행이 있습니다. 같은 9바이트 기준에서 다음 명령의 요약은 rows=7 pass=5 hold=2입니다.
python code/audit_field_bytes.py fixtures/boundaries.csv results/boundaries_utf8.csv --field-encoding utf-8 --max-bytes 9
| ID | 입력 특징 | 코드 포인트 수 | UTF-8 바이트 수 | 결과 이유 |
|---|---|---|---|---|
| B1 | 영문 숫자 123456789 | 9 | 9 | within_limit |
| B2 | 영문 숫자 1234567890 | 10 | 10 | over_limit |
| B3 | 이모지 😀 | 1 | 4 | within_limit |
| B4 | 완성된 é | 1 | 2 | within_limit |
| B5 | e와 결합 악센트 U+0301 | 2 | 3 | within_limit |
| B6 | 빈 문자열 | 0 | 0 | missing |
| B7 | ABC 앞뒤에 공백 한 개씩 | 5 | 5 | within_limit |
B4와 B5는 비슷하게 보이더라도 저장된 코드 포인트와 바이트 수가 다릅니다. 제공 파일을 눈으로 다시 입력하면 조합이 달라질 수 있으므로 그대로 사용하세요. 코드는 정규화·공백 제거·문자 절삭을 하지 않습니다. 공백만 있는 값의 업무상 유효성도 판정하지 않으며, 여기서 missing은 빈 문자열에 한정합니다.
다음 두 명령은 같은 값을 다른 인코딩으로 측정하는 비교 실습입니다. 실제 UTF-8 수신 양식을 통과시키려고 옵션을 바꾸는 방법으로 쓰면 안 됩니다.
python code/audit_field_bytes.py fixtures/names.csv results/names_cp949.csv --field-encoding cp949 --max-bytes 9
python code/audit_field_bytes.py fixtures/names.csv results/names_ascii.csv --field-encoding ascii --max-bytes 9
CP949에서는 세 값의 크기가 3·6·8바이트이고 세 행이 통과합니다. ASCII에서는 ABC 한 행만 통과하고 두 한글 값은 unencodable로 보류합니다. 표현할 수 없는 값의 byte_count는 빈칸으로 남습니다. 0바이트로 바꾸면 실제로 측정하지 못한 상태와 빈 문자열을 혼동하게 됩니다.
strict를 쓰면 표현할 수 없는 문자에서 예외가 발생합니다. ignore는 문자를 지나칠 수 있고 replace는 대체 문자로 바꾸므로 원래 값의 크기를 검사하는 이 목적에 맞지 않습니다. Python codecs 오류 처리 문서

5 보류 행과 실행 오류를 복구합니다
over_limit이 나오면 먼저 수신 계약을 재확인합니다. 실제 제한이 맞다면 원본 담당자가 축약 가능 범위를 정하거나 수신 양식의 기준을 조정해야 합니다. 프로그램이 자동으로 이름을 줄이지는 않습니다. unencodable은 목적 인코딩이 맞는지 확인하고, 값 변경이 필요하면 승인된 별도 사본에서 수정합니다. missing은 원본 자료를 확인해 필수값을 보완합니다.
입력 오류는 보고서를 만들기 전에 멈춥니다. 다음 명령은 중복 ID 때문에 종료 코드 2가 나오며 results/duplicate_audit.csv는 만들어지지 않습니다.
python code/audit_field_bytes.py fixtures/duplicate_id.csv results/duplicate_audit.csv --field-encoding utf-8 --max-bytes 9
중복 ID를 임의로 삭제하지 말고 각 행을 구별할 원래 키를 확인하세요. wrong_header.csv, invalid_utf8.csv, malformed.csv도 잘못된 헤더, UTF-8로 읽을 수 없는 바이트, 닫히지 않은 인용부호를 확인하는 실패 입력입니다. errors="ignore"로 읽어 통과시키지 않습니다.
결과 파일이 이미 있으면 x 모드의 배타 생성이 실패합니다. 기존 보고서를 보관하고 새 결과 이름을 쓰세요. Python open 공식 문서 입력과 출력에 같은 파일을 지정해도 거부합니다. 결과 폴더가 없으면 먼저 연습용 폴더를 만들고, 권한 오류가 나면 쓰기 가능한 본인 폴더에서 다시 실행합니다.
점검 범위는 쉼표 CSV, id/name 두 열, 입력 1,000,000바이트 이하, 데이터 1~10,000행, 필드당 100,000코드 포인트 이하입니다. 줄 단위가 아닌 CSV 논리 레코드를 셉니다. 바이트 제한 옵션은 1~1,000,000이고 ID는 빈값·공백만 있는 값·원문이 같은 중복을 거부합니다. 이 조건 밖의 파일은 원본을 그대로 보관하고 필요한 범위를 먼저 정하세요.
6 자동 시험과 원본 보존을 확인합니다
다음 명령은 실제 CLI를 격리 임시 폴더에서 실행합니다. 테스트는 19개이고, 별도 예제 검산은 정상 보고서 네 종류와 실패 입력 네 종류를 대조합니다.
python -m unittest discover -s tests -v
python tests/verify_examples.py
이 자료는 Linux의 CPython 3.12.14와 3.13.5에서 시험했습니다. 두 환경 모두 19개 시험과 예제 기대값 대조를 통과했고, 재실행 때 기존 보고서가 바뀌지 않았으며 가상 입력의 해시가 유지됐습니다. 실행 버전은 test_results/environment.json에, 시험·예제 결과는 같은 폴더의 로그와 JSON에 있습니다. 공식 자료 링크는 Python 3.12 문서 계열이며 열람 페이지의 패치 표시는 3.12.15였습니다.
이는 제공한 로컬 예제의 CPython 호환 검증입니다. Windows·Excel·실제 수신 시스템의 업로드 성공을 시험한 결과는 아닙니다. 작업 중 다른 프로그램이 파일·경로를 바꾸거나 프로세스를 강제 종료하는 상황까지 보호하는 도구로 쓰지 마세요.
인코딩 자체를 바꿔 전달해야 한다면 한글 CSV를 UTF-8로 바꾸고 왕복 검산하는 실습을 별도로 확인합니다. 헤더가 같아 보이는데 열 대응이 안 되면 CSV 열 이름의 유니코드 충돌 점검이 관련됩니다. 이번 결과에서 확인할 것은 원값 보존, 선택한 필드 인코딩, 실제 바이트 수와 보류 이유입니다.