ライフスタイル
実践: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 は cp が旧バックアップを上書きしないよう contacts-before.csv も未使用にします。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、元順序の2件、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 のみにして欄不足を作り、新規 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 で参考3試験全合格を確認します。生成版のインターフェースが違えば 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、ポート、外部サービスは共有される場合があります。
ライフスタイル
実践:小さな Web サイトを作る
brief.md から Small Steps のタスクサイトを計画・制作し、追加、完了、削除、絞り込み、ローカル保存を実装します。HTML、CSS、データ関数、画面イベント、テストを分け、Node とブラウザーで検証して再起動・復元の手順を残します。
ライフスタイル
使用量と効率:やり直しを減らす
条件、モデル設定、時間、成果を記録し、不要な再試行と過剰な文脈を減らします。
最新の旅の情報・ガイド

ガイド東京
東京ではどのエリアに泊まる?新宿・上野・東京駅・渋谷・浅草・池袋・銀座の七エリア比較。空港アクセス、宿泊税、荷物配送も解説
東京ではどのエリアに泊まる?新宿、上野、東京駅、渋谷、浅草、池袋、銀座の七エリアを同じ基準で比較。成田・羽田空港からのアクセス、利用できる路線、周辺にあるもの、街の雰囲気、向いている人を、比較表と山手線の概略図とともに紹介します。2026年9月に確認した東京都の宿泊税(2027年4月から3%)と、空港宅急便で荷物を送る際のルールも解説します。
- 予算
- ホテル

ガイド東京
東京の交通パスの選び方:Suica/Welcome Suica、Tokyo Subway Ticket、JR Passは買うべき?
初めての東京では、まず一人一枚ICカードで都度払い(Welcome Suicaはデポジット不要、有効期間28日)。一日に地下鉄へ4回以上乗るならTokyo Subway Ticketの72時間券2,000円を追加し、関西へ行かず東京だけならJR Passは必ず割高です。TOURIST PASMO、iPhoneのSuica、東京メトロ一日乗車券で乗れる路線・乗れない路線を決定チャートで比較。価格は2026年9月に確認。
- 交通
- 予算

ガイド東京
東京ディズニーランド・シー攻略:チケット料金、ファンタジースプリングス、ディズニー・プレミアアクセス(DPA)とスタンバイパスの使い方、初めてならどちらを選ぶ?
東京ディズニーの一日パスポートは変動価格制で、2026 年 9 月は平日の多くが 9,900 円、週末が 10,900 円。公式サイトでは毎日 14:00 に二か月後の同日分を発売します。無料のプライオリティパスは公式サイトのサービス一覧に載っておらず、待ち時間を短縮できるのは有料のディズニー・プレミアアクセス(一人一回 1,000~3,500 円)のみ。運営時間、25 周年イベント、スタンバイパス、エントリー受付、ファンタジースプリングスの利用方法、初回にランドとシーのどちらを選ぶかも解説。2026 年 9 月に公式サイトで確認しました。
- モデルコース
- 家族向け
出典
- Codex prompting · 確認日:
- Python csv module · 確認日: