ライフスタイル

実践:CSV 整理ツールを作る

入力を保って別ファイルに出す CSV 整理ツールを作ります。空白除去、email の小文字化、重複は最初の有効行を保持、空の名前や不正な形式を除外し、三種類の件数を報告します。実在するメールかは保証しません。

読了目安 20 分 · 操作 45 分

作業の流れを表す図です。製品画面の画像ではありません。
画像:Mokaair (© Mokaair)
総目次へ:Codex 学習ガイド:全記事の目次

上級 · Desktop / CLI / VS Code / JetBrains

この記事の目次
  1. 目標と準備
  2. 手順1:手で検算できるデータを作る
  3. 手順2:整理契約を先に計画する
  4. 手順3:参考実装の保護箇所を見る
  5. 手順4:実行・状態保存・出力確認
  6. 手順5:Unicode・引用符・壊れた行
  7. 手順6:繰返せる検証を保存する
  8. 引渡し・復元・制限

目標と準備

手順1:手で検算できるデータを作る

csv-lab を開き、下記を UTF-8 contacts.csv に保存します。ヘッダーは順序も含め name,email と完全一致させ、Alice と最初の email の前後空白を試験条件として残します。スプレッドシートの再保存でエンコードや値が変わらないようテキストエディターで確認します。Windows は py -3 --version、macOS/Linux は python3 --version で Python 3.9 以降を確認し、pip・pandas・データベースは不要です。

contacts.csv(試験用空白を保持) · csv
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 を開き、で入出力・例外・検証を確認してから実装します。出力は毎回新規で、構造損壊は成功に見える部分結果を残さず全体を失敗させます。無効 email は集計可能な資料問題ですが、欄不足・余分・ヘッダー誤りは契約違反です。下流で使えるかに関わるため、単に CSV 整理と頼むだけにしません。

実装依頼 · text
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_contacts.py(全参考実装) · python
"""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 に出るため、その文字だけで失敗としません。終了値をすぐ保存して入出力を照合します。

Windows PowerShell · powershell
$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
macOS/Linux · sh
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
cleaned-01.csv の期待内容 · 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終了することを確認します。逆順表頭や第三欄も拒否すべきです。

contacts-unicode.csv · csv
name,email
"林, Alex",alex@example.test

応用:最初の有効行を保持

contacts-first-valid.csv · csv
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 は保持します。

cleaned-first-valid.csv の期待内容 · 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 契約を揃え、失敗例を削除しません。

test_clean_contacts.py · python
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 教材一式には参考コードがあり前のプロジェクトなしで比較できます。再整理は新しい出力名、整理は確認済み産物だけです。実名簿、大ファイル、スプレッドシートインポートは権限・ストリーミング・数式として解釈される文字列の扱いの規則を別設計します。本編は文字として保存し外部内容を命令実行しません。この基準と検証法でへ進みます。

32. 実践:CSV 整理ツールを作る — 作業の流れを表す図です。製品画面の画像ではありません。 contacts.csv → Cleanup → clean.csv
32. 実践:CSV 整理ツールを作る — 作業の流れを表す図です。製品画面の画像ではありません。 contacts.csv → Cleanup → clean.csv · 画像:Mokaair (© Mokaair)
詳しい説明を読む

contacts.csv to Cleanup to clean.csv

総目次へ

  • ライフスタイル

    Codex 学習ガイド:全記事の目次

    導入と最初のタスクから MD の指示、高度な連携まで、60 レッスン・十単元を予定しています。習熟度、環境、目的、コマンドで次の記事を探せます。未公開の記事には状態を表示します。

  • ライフスタイル

    Worktree とタスクの分離

    Worktree は一つの Git リポジトリに別ブランチの作業場所を作ります。ファイルは分かれても DB、ポート、外部サービスは共有される場合があります。

  • ライフスタイル

    実践:小さな Web サイトを作る

    brief.md から Small Steps のタスクサイトを計画・制作し、追加、完了、削除、絞り込み、ローカル保存を実装します。HTML、CSS、データ関数、画面イベント、テストを分け、Node とブラウザーで検証して再起動・復元の手順を残します。

  • ライフスタイル

    使用量と効率:やり直しを減らす

    条件、モデル設定、時間、成果を記録し、不要な再試行と過剰な文脈を減らします。

最新の旅の情報・ガイド

出典

ライフスタイル