라이프스타일
실습: CSV 정리 도구 만들기
입력을 보존하고 새 파일로 내보내는 CSV 정리 도구를 만듭니다. 공백 제거, email 소문자화, 중복 중 첫 유효 행 유지, 빈 이름이나 잘못된 형식 제외 후 세 종류의 건수를 보고합니다. 실제 이메일 존재를 보장하지는 않습니다.
읽는 데 약 20분 · 실습 45 분

이 글의 목차
Codex 학습 목차로 돌아가기Codex 학습 센터: 전체 튜토리얼 목차설치와 첫 작업부터 MD 지침과 고급 연동까지 60개 강의, 열 개 단원을 계획합니다. 수준, 환경, 목표, 명령으로 다음 글을 찾고 미게시 항목의 상태를 확인할 수 있습니다.전체 글 읽기
목표와 준비
1단계: 손으로 검산할 데이터 만들기
csv-lab을 열어 아래를 UTF-8 contacts.csv로 저장합니다. 헤더는 순서까지 name,email과 같아야 하며 Alice와 첫 email 앞뒤 공백을 시험 조건으로 유지하세요. 스프레드시트 재저장으로 인코딩·값이 바뀌지 않게 텍스트 편집기로 확인합니다. Windows는 py -3 --version, macOS/Linux는 python3 --version으로 Python 3.9 이상을 확인하며 pip·pandas·데이터베이스는 필요 없습니다.
name,email
Alice , ALICE@EXAMPLE.TEST
Alice duplicate,alice@example.test
Bob,bob@example.test
Missing,
| 입력 행 | 판정 | 출력 |
|---|---|---|
| Alice / ALICE@EXAMPLE.TEST | 유효·공백 제거·소문자화 | Alice / alice@example.test |
| Alice duplicate / alice@example.test | 유효 email 중복 | 제외 |
| Bob / bob@example.test | 유효 | Bob / bob@example.test |
| Missing / 빈 email | 무효 | 제외 |
kept 2·duplicate 1·invalid 1을 기대합니다. 이름이 아니라 정규화한 email로 중복을 판정합니다. 같은 주소의 앞 행이 빈 이름으로 무효라면 뒤의 첫 유효 행을 유지할 수 있습니다. 생성 전에 이 규칙을 명확히 하세요.
2단계: 정리 계약 먼저 계획
Codex에서 csv-lab을 열고 Plan 모드Plan 모드로 구현 전 계획하기Plan 모드는 범위와 접근법을 결정할 때 유용합니다. 결과물은 구현 계획이며 완성된 코드는 아닙니다. 입력, 출력, 제한과 검증 방법이 포함되었는지 확인합니다.전체 글 읽기로 입출력·예외·검증을 확인한 뒤 구현합니다. 출력은 매번 새 파일이며 구조 손상은 성공처럼 보이는 일부 결과를 남기지 않고 전체 실패해야 합니다. 무효 email은 집계 가능한 자료 문제지만 필드 부족·추가·헤더 오류는 계약 위반입니다. 후속 사용 가능성에 영향을 주므로 단순히 CSV 정리라고만 요청하지 마세요.
Create clean_contacts.py using Python's standard library only.
Expose clean(source: Path, target: Path) returning kept/duplicate/invalid counts.
Read UTF-8 with optional BOM using csv.DictReader, not string splitting.
Require exactly name,email. Trim both fields and lowercase email.
Keep the first valid row per email; reject empty names and invalid email shapes.
Treat missing/extra columns or malformed CSV as whole-run failures.
Read and validate before creating output. Never change input or overwrite output.
Write UTF-8 with name,email and a newline after each row.
CLI takes source and target paths, reports counts on stderr and exits 0 on success,
1 on data/file errors. Add tests for preservation, duplicates, Unicode, quoted
commas, malformed rows and overwrite refusal. Do not use real contact data.
3단계: 참고 구현의 보호 지점 확인
아래는 clean_contacts.py로 독립 실행하는 전체 참고 코드입니다. 생성본은 별도 보관한 뒤 비교하세요. csv 모듈이 따옴표·쉼표·줄바꿈을 처리하며 split(',')로 대체할 수 없습니다. 전체 입력을 읽은 뒤 x로 새 출력을 만들고 기존 대상은 거부합니다. 입력 오류에는 출력이 없지만 쓰는 도중 디스크 오류는 일부 파일을 남길 수 있어 실패로 기록하고 조사해야 합니다. 완전한 트랜잭션 시스템이라고 부르지 않습니다.
"""Clean a practice CSV without overwriting either the input or an existing output."""
import argparse
import csv
import json
import re
import sys
from pathlib import Path
def clean(source: Path, target: Path) -> dict[str, int]:
if source.resolve() == target.resolve():
raise ValueError("Input and output must differ")
rows = []
seen = set()
counts = {"kept": 0, "duplicate": 0, "invalid": 0}
with source.open(encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream, strict=True)
if reader.fieldnames != ["name", "email"]:
raise ValueError("Expected exactly: name,email")
for row in reader:
if None in row or any(value is None for value in row.values()):
raise ValueError("Malformed CSV row")
name = row["name"].strip()
email = row["email"].strip().lower()
# An exercise-level shape check, not proof an address can receive mail.
if not name or not re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", email):
counts["invalid"] += 1
elif email in seen:
counts["duplicate"] += 1
else:
seen.add(email)
rows.append({"name": name, "email": email})
counts["kept"] += 1
# Exclusive creation: a rerun never silently replaces an existing result.
with target.open("x", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=["name", "email"], lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
return counts
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path)
parser.add_argument("target", type=Path)
args = parser.parse_args()
try:
counts = clean(args.source, args.target)
except (OSError, ValueError, csv.Error) as error:
print(str(error), file=sys.stderr)
return 1
print(json.dumps(counts), file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())
4단계: 실행·상태 저장·출력 확인
cleaned-01.csv가 없는지 확인합니다. macOS/Linux의 contacts-before.csv도 사용하지 않은 이름이어야 cp가 이전 백업을 덮어쓰지 않습니다. PowerShell은 입력 SHA256, macOS/Linux는 가상 입력 사본과 cmp를 사용합니다. 성공 시에도 개수 JSON이 stderr에 나오므로 해당 채널의 텍스트만으로 실패라 판단하지 마세요. 종료 상태를 즉시 저장하고 입출력을 비교합니다.
$csvBefore = (Get-FileHash -LiteralPath contacts.csv -Algorithm SHA256).Hash
py -3 clean_contacts.py contacts.csv cleaned-01.csv
$csvExit = $LASTEXITCODE
$csvExit
Get-Content -LiteralPath cleaned-01.csv
$csvBefore -eq (Get-FileHash -LiteralPath contacts.csv -Algorithm SHA256).Hash
cp contacts.csv contacts-before.csv
python3 clean_contacts.py contacts.csv cleaned-01.csv
csv_exit=$?
echo "$csv_exit"
cat cleaned-01.csv
cmp contacts-before.csv contacts.csv
name,email
Alice,alice@example.test
Bob,bob@example.test
종료 0, kept 2·duplicate 1·invalid 1, 원래 순서의 두 행, True 또는 cmp 차이 없음을 기대합니다. 같은 명령을 반복하면 기존 cleaned-01.csv 때문에 종료 1이며 내용은 그대로여야 합니다. 먼저 삭제하고 덮어쓰기 보호를 통과했다고 하지 마세요. 입출력을 모두 contacts.csv로 지정해도 원본 불변으로 거부해야 합니다. 예상 실패도 검증 통과의 일부입니다.
5단계: Unicode·따옴표·잘못된 행
아래 contacts-unicode.csv를 만들어 cleaned-unicode.csv로 출력합니다. kept 1·duplicate 0·invalid 0이며 이름의 쉼표는 한 필드 안에 있고 Unicode가 유지되어야 합니다. 다시 열 때 깨지면 입력을 고치기 전에 UTF-8 해석을 확인하세요. bad.csv는 name,email 다음 줄에 Alice만 써 email 필드를 누락시킵니다. 새 cleaned-bad.csv가 생성되지 않고 종료 1이어야 합니다. 뒤집은 헤더·세 번째 필드 추가도 거부해야 합니다.
name,email
"林, Alex",alex@example.test
확장: 첫 번째 유효 행 보존
name,email
,first@example.test
First valid,FIRST@example.test
Later duplicate,first@example.test
같은 프로그램에 새 입력과 아직 없는 cleaned-first-valid.csv를 지정합니다. kept 1, duplicate 1, invalid 1이 기대값입니다. 빈 이름의 무효 행이 email을 선점하면 안 되며 First valid가 첫 유효 행이고 마지막 행이 중복입니다. First valid가 없으면 검증 후 seen에 추가하는지 확인하세요. contacts.csv와 cleaned-01.csv는 그대로 유지합니다.
name,email
First valid,first@example.test
6단계: 재실행 가능한 검증 보존
아래를 프로그램·contacts.csv 옆의 test_clean_contacts.py로 저장합니다. 임시 폴더에서 입력 바이트 불변·덮어쓰기 거부·Unicode·따옴표·구조 오류를 검증해 연습 폴더에 결과를 쌓지 않습니다. Windows는 py -3 -m unittest -v test_clean_contacts.py, macOS/Linux는 python3 -m unittest -v test_clean_contacts.py로 참고 시험 세 개 통과를 확인하세요. 생성본 인터페이스가 다르면 clean 계약을 맞추고 실패 사례를 삭제하지 않습니다.
import csv
import tempfile
import unittest
from pathlib import Path
from clean_contacts import clean
class CleanupTests(unittest.TestCase):
def test_contract_and_input_preservation(self):
original = (Path(__file__).parent / "contacts.csv").read_bytes()
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "input.csv"
target = Path(directory) / "output.csv"
source.write_bytes(original)
self.assertEqual(clean(source, target), {"kept": 2, "duplicate": 1, "invalid": 1})
self.assertEqual(source.read_bytes(), original)
with target.open(encoding="utf-8", newline="") as stream:
self.assertEqual(list(csv.DictReader(stream)), [{"name": "Alice", "email": "alice@example.test"}, {"name": "Bob", "email": "bob@example.test"}])
with self.assertRaises(FileExistsError):
clean(source, target)
with self.assertRaises(ValueError):
clean(source, source)
def test_unicode_and_quoted_comma(self):
with tempfile.TemporaryDirectory() as directory:
source, target = Path(directory) / "in.csv", Path(directory) / "out.csv"
source.write_text('name,email\n"林, Alex",alex@example.test\n', encoding="utf-8")
self.assertEqual(clean(source, target)["kept"], 1)
self.assertIn('"林, Alex"', target.read_text(encoding="utf-8"))
def test_bad_header_and_malformed_rows_create_no_output(self):
with tempfile.TemporaryDirectory() as directory:
source, target = Path(directory) / "in.csv", Path(directory) / "out.csv"
for value in ["email,name\na@b.test,A\n", "name,email\nAlice\n", "name,email\nA,a@b.test,extra\n"]:
source.write_text(value, encoding="utf-8")
with self.assertRaises(ValueError):
clean(source, target)
self.assertFalse(target.exists())
if __name__ == "__main__":
unittest.main()
전달·복원·제한
가상 입력·프로그램·시험·명령·집계·원본 보존 증거를 전달합니다. CSV 전체 자료는 참고 코드가 있어 이전 프로젝트 없이 비교할 수 있습니다. 재정리는 새 출력 이름, 정리는 확인한 결과만 선택합니다. 실제 연락처·대용량·스프레드시트 가져오기는 권한·스트리밍·수식 문자열 규칙을 별도로 설계하세요. 이 편은 텍스트로 저장하며 외부 내용을 명령으로 실행하지 않습니다. 같은 기준·검증법으로 기존 프로젝트 유지보수실전: 기존 프로젝트 유지보수현황 기준을 만들고 한 가지 변경을 수행해 회귀 검사와 인계 기록을 남깁니다.전체 글 읽기로 진행합니다.
Codex 학습 목차로 돌아가기Codex 학습 센터: 전체 튜토리얼 목차설치와 첫 작업부터 MD 지침과 고급 연동까지 60개 강의, 열 개 단원을 계획합니다. 수준, 환경, 목표, 명령으로 다음 글을 찾고 미게시 항목의 상태를 확인할 수 있습니다.전체 글 읽기
자세한 설명 보기
contacts.csv to Cleanup to clean.csv
같은 주제의 글
라이프스타일
Codex 학습 센터: 전체 튜토리얼 목차
설치와 첫 작업부터 MD 지침과 고급 연동까지 60개 강의, 열 개 단원을 계획합니다. 수준, 환경, 목표, 명령으로 다음 글을 찾고 미게시 항목의 상태를 확인할 수 있습니다.
라이프스타일
Worktree와 작업 격리
Worktree는 하나의 Git 저장소에 다른 브랜치의 작업 폴더를 만듭니다. 파일이 분리되어도 DB, 포트와 외부 서비스는 공유될 수 있습니다.
라이프스타일
실습: 작은 웹사이트 만들기
brief.md에서 Small Steps 할 일 사이트를 계획하고 추가·완료·삭제·필터·로컬 저장을 구현합니다. HTML·CSS·데이터 함수·화면 이벤트·시험을 분리하고 Node와 브라우저로 검증한 뒤 재시작·복원 인계 기록을 남깁니다.
라이프스타일
사용량과 효율: 재작업 줄이기
조건, 모델 선택, 시간, 결과를 기록해 불필요한 재시도와 과도한 문맥을 줄입니다.
최신 여행 소식·가이드

가이드도쿄
도쿄 어디에 묵을까? 신주쿠·우에노·도쿄역·시부야·아사쿠사·이케부쿠로·긴자 일곱 지역 비교: 공항 교통, 숙박세, 짐 배송까지
도쿄 어디에 묵을까? 신주쿠, 우에노, 도쿄역, 시부야, 아사쿠사, 이케부쿠로, 긴자 일곱 지역을 같은 기준으로 비교한다. 나리타·하네다 공항에서 오는 방법, 교통 노선, 주변의 볼거리, 동네 분위기, 적합한 여행자를 비교표와 야마노테선 안내도로 살펴보고, 2026년 9월에 확인한 도쿄도 숙박세(2027년 4월부터 3%)와 공항 택배로 짐을 보내는 규정도 정리했다.
- 예산
- 호텔

가이드도쿄
도쿄 교통패스 선택법: Suica/Welcome Suica, Tokyo Subway Ticket, JR Pass는 살 만할까?
도쿄를 처음 여행한다면 먼저 1인당 IC 카드 한 장으로 탈 때마다 결제한다(Welcome Suica는 보증금이 없고 28일간 유효). 하루에 지하철을 4번 이상 타면 2,000엔짜리 Tokyo Subway Ticket 72시간권을 추가하고, 간사이에 가지 않고 도쿄만 여행한다면 JR Pass는 반드시 손해다. TOURIST PASMO, iPhone의 Suica, 도쿄 Metro 하루권의 이용 가능·불가 범위를 결정도로 비교한다. 가격은 2026년 9월 확인.
- 교통
- 예산

가이드도쿄
도쿄 디즈니랜드·디즈니씨 가이드: 티켓 가격, 판타지 스프링스 Fantasy Springs, 디즈니 프리미어 액세스 DPA와 스탠바이 패스 이용법, 첫 방문에는 어느 파크가 좋을까
도쿄 디즈니 하루짜리 패스포트는 변동 가격제로, 2026년 9월에는 평일 대부분이 9,900엔, 주말이 10,900엔이다. 공식 홈페이지에서 매일 14:00에 두 달 뒤 같은 날짜의 티켓을 판매한다. 무료 프라이오리티 패스는 공식 서비스 목록에서 빠져 대기 시간을 줄이는 방법은 유료 디즈니 프리미어 액세스(한 사람당 한 번 1,000~3,500엔)뿐이다. 운영 시간, 25주년 행사, 스탠바이 패스, 엔트리 리퀘스트, 판타지 스프링스 이용법, 첫 방문 때 디즈니랜드와 디즈니씨 중 어디를 고를지도 담았다. 2026년 9월 공식 홈페이지에서 확인했다.
- 추천 일정
- 가족 여행
출처
- Codex prompting · 확인일:
- Python csv module · 확인일: