生活分享
實戰:製作資料整理工具
這個實戰製作 CSV 名單整理工具,保留原檔並產生新結果。規則固定為修剪空白、將 email 轉小寫、同 email 保留第一筆有效資料、略過缺姓名或格式不合的 email,最後回報保留、重複與無效筆數。這是練習用格式檢查,不保證信箱真實存在。
閱讀時間約 20 分鐘 · 操作 45 分鐘

本篇目錄
返回 Codex 教學總目錄Codex 學習中心:完整教學目錄從安裝、第一個任務到 MD 規則與進階整合,規劃 60 篇 Codex 教學、十個單元。依程度、平台、需求或指令搜尋下一篇;尚未公開的教學會標示狀態,方便安排學習路線。閱讀全文
目標與準備
步驟 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 先規劃整理契約
讓 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 可獨立執行;若你已讓 Codex 生成版本,先另存備份,再比較差異。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 覆蓋舊備份。Windows PowerShell 記下輸入 SHA256;macOS/Linux 則複製自己這份虛構輸入供 cmp 比較。計數寫在 stderr,成功時仍會顯示 JSON,不能只因這個通道有文字就判定失敗。立即保存退出狀態,再核對輸出與原件。
$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,輸出兩筆且順序保留,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 或多加第三欄也應拒絕。
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,檢查 seen 是在資料通過驗證後才加入,而不是先去重再驗證。原 contacts.csv 與 cleaned-01.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 函式契約,不隨意刪除失敗案例。
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 練習檔比對;壓縮檔包含參考程式,不需要先完成其他專案。重新整理用新的輸出名稱,清理時只選自己已確認的產物。若要讀真正名單、處理超大檔案或匯入試算表,另設計權限、串流及公式文字的處理規則;本篇把資料當文字保存,不自動把外部內容當命令執行。下一篇練習接手既有專案實戰:維護既有專案建立現況基準,處理一項真實變更,以回歸檢查和交接紀錄交付可追溯成果。閱讀全文,保留這裡建立的基準與驗證方法。
返回 Codex 教學總目錄Codex 學習中心:完整教學目錄從安裝、第一個任務到 MD 規則與進階整合,規劃 60 篇 Codex 教學、十個單元。依程度、平台、需求或指令搜尋下一篇;尚未公開的教學會標示狀態,方便安排學習路線。閱讀全文
閱讀完整文字說明
contacts.csv to Cleanup to clean.csv
同主題延伸閱讀
生活分享
Codex 學習中心:完整教學目錄
從安裝、第一個任務到 MD 規則與進階整合,規劃 60 篇 Codex 教學、十個單元。依程度、平台、需求或指令搜尋下一篇;尚未公開的教學會標示狀態,方便安排學習路線。
生活分享
Worktree 與多任務隔離
Worktree 讓同一個 Git 程式庫有不同的工作目錄,各自承接不同分支。它適合讓兩項工作分開改檔,但資料庫、連接埠與外部服務仍可能共用,不能把檔案隔離當成所有資源隔離。
生活分享
實戰:製作小網站
從 brief.md 規劃並製作 Small Steps 待辦網站,完成新增、完成、刪除、篩選與本機資料保存。將 HTML、CSS、資料函式、畫面事件與測試分開,以 Node 測試和瀏覽器操作驗收,並留下可重新啟動與還原的交接紀錄。
生活分享
用量與效率:減少重工
記錄任務條件、模型選項、時間與成果,找出能減少無效重試和過多上下文的調整。
最新旅遊情報攻略

情報
2026 韓國楓葉預測:雪嶽山 10 月 20 日、首爾近郊 10 月底、內藏山與漢拏山 11 月上旬
韓國山林廳 2026 年 9 月 22 日公布的楓紅高峰預測:雪嶽山 10 月 20 日,春川、國立樹木園到首爾植物園落在 10 月 28 日到 11 月 2 日,內藏山 11 月 4 日、漢拏山 11 月 6 日,整體比最近 5 年晚約 0.8 天。整理各地楓樹與銀杏的預測日、首爾出發怎麼排,以及出發前去哪裡看即時楓況。2026 年 10 月查證。
- 季節活動
- 自然
- 觀景

攻略胡志明市
胡志明市到頭頓一日遊:白藤碼頭搭高速船、船票與班次,下船就是胡梅纜車與耶穌基督像
人在胡志明市挪一天去頭頓看海:市中心的白藤高速船碼頭搭船,航程 120 分鐘到頭頓的胡梅碼頭,平日成人 320,000 越南盾、週末 350,000,回程末班平日 15:00。下船就是胡梅纜車站,同一條路上有白宮,小山頂上是耶穌基督像。平日一天只有兩班船,整天要從末班船倒推著排。
- 交通
- 行程範例
- 海灘

攻略沖繩
沖繩不開車攻略:單軌只到浦添,美麗海水族館要坐兩個多小時的巴士,回那霸的最後一班直達車 17:22 就開走
不租車的沖繩怎麼移動:那霸市區靠沖繩都市單軌電車(ゆいレール),那霸機場站到終點てだこ浦西 19 站、17 公里、37 分鐘,一日券 1,000 日圓;美麗海水族館有那霸機場直達的高速巴士,單程 2,000 日圓起、官方時刻表上 2 小時上下,下車後還要走 10 分鐘;古宇利島要在今帰仁村役場轉車,當天來回光坐車就六個半小時;回程的最後一班直達車 17:22 就從記念公園前開走(2026 年 9 月查證)。
- 交通
- 行程範例
- 預算
資料來源
- Codex prompting · 查證日期:
- Python csv module · 查證日期: