生活分享

实战:制作数据整理工具

这个实战制作 CSV 名单整理工具,保留原档并产生新结果。规则固定为修剪空白、将 email 转小写、同 email 保留第一笔有效资料、略过缺姓名或格式不合的 email,最后回报保留、重复与无效笔数。这是练习用格式检查,不保证信箱真实存在。

阅读时间约 20 分钟 · 操作 45 分钟

实作顺序示意图,非产品介面截图。
图片:Mokaair (© Mokaair)
回总目录:Codex 学习中心:完整教程目录

进阶 · Desktop / CLI / VS Code / JetBrains

本篇目录
  1. 目标与准备
  2. 步骤 1:建立可以人工验算的资料
  3. 步骤 2:请 Codex 先规划整理契约
  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 先规划整理契约

让 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 可独立执行;若你已让 Codex 生成版本,先另存备份,再比较差异。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 的 contacts-before.csv 也须是未使用档名,避免 cp 覆盖旧备份。Windows PowerShell 记下输入 SHA256;macOS/Linux 则复制自己这份虚构输入供 cmp 比较。计数写在 stderr,成功时仍会显示 JSON,不能只因这个通道有文字就判定失败。立即保存退出状态,再核对输出与原件。

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,输出两笔且顺序保留,PowerShell 比对 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 且不建立输出。把表头改成 email,name 或多加第三栏也应拒绝。

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,检查 seen 是在资料通过验证后才加入,而不是先去重再验证。原 contacts.csv 与 cleaned-01.csv 都保持原样。

cleaned-first-valid.csv 的预期内容 · csv
name,email
First valid,first@example.test

步骤 6:把验收留成可重跑测试

把下方完整测试存为 test_clean_contacts.py,放在程式与 contacts.csv 旁边。它使用暂存资料夹,会核对输入位元组未变、既有输出拒绝覆写、Unicode、引号与结构错误;不在你的练习目录累积测试产物。Windows 执行 py -3 -m unittest -v test_clean_contacts.py;macOS/Linux 执行 python3 -m unittest -v test_clean_contacts.py。参考版本应有三项测试全部通过;若生成版本的介面不同,先对齐 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、程式、测试、执行指令、计数及未修改原件的证据。可以下载完整 CSV 练习档比对;压缩档包含参考程式,不需要先完成其他专案。重新整理用新的输出名称,清理时只选自己已确认的产物。若要读真正名单、处理超大档案或汇入试算表,另设计权限、串流及公式文字的处理规则;本篇把资料当文字保存,不自动把外部内容当命令执行。下一篇练习,保留这里建立的基准与验证方法。

32. 实战:制作数据整理工具 — 实作顺序示意图,非产品介面截图。 contacts.csv → Cleanup → clean.csv
32. 实战:制作数据整理工具 — 实作顺序示意图,非产品介面截图。 contacts.csv → Cleanup → clean.csv · 图片:Mokaair (© Mokaair)
阅读完整文字说明

contacts.csv to Cleanup to clean.csv

回总目录

  • 生活分享

    Codex 学习中心:完整教程目录

    从安装、第一个任务到 MD 规则与进阶集成,规划 60 篇 Codex 教程、十个单元。按程度、平台、需求或命令搜索下一篇;尚未公开的教程会标示状态,方便安排学习路线。

  • 生活分享

    Worktree 与多任务隔离

    Worktree 让同一个 Git 程式库有不同的工作目录,各自承接不同分支。它适合让两项工作分开改档,但资料库、连接埠与外部服务仍可能共用,不能把档案隔离当成所有资源隔离。

  • 生活分享

    实战:制作小网站

    从 brief.md 规划并制作 Small Steps 待办网站,完成新增、完成、删除、筛选与本机资料保存。将 HTML、CSS、资料函式、画面事件与测试分开,以 Node 测试和浏览器操作验收,并留下可重新启动与还原的交接纪录。

  • 生活分享

    用量与效率:减少重工

    记录任务条件、模型选项、时间与成果,找出能减少无效重试和过多上下文的调整。

最新旅游情报攻略

资料来源

生活分享