1. 项目背景与需求拆解
1.1 为什么选 Codex 来干这活儿
先交代一下背景。我手头有三份 CSV,分别是用户订单明细、用户基础信息、商品类目映射,需要按用户ID和商品ID合并成一份宽表,给下游的看板用。三份文件加起来大概两万行左右,不算大,但字段命名很乱,有的列叫 user_id,有的叫 userid,有的直接叫客户编号,表头还带 BOM 头和各种不可见字符。手工在 Excel 里搞也不是不行,但后续还要定期重跑,所以我决定直接用 Codex 把整个需求从描述到脚本一次性搞定。
Codex 是 OpenAI 推出的编码智能体,和普通对话式 AI 不一样,它能在你给的需求描述基础上直接生成代码、执行命令、读取文件内容,甚至帮你跑测试。它天然适配这种“需求 → 脚本 → 验收”的完整链路。而且 Codex 对被喂进去的需求质量非常敏感,需求写得越像一份正经的需求规格说明书,产出的脚本就越靠谱。这也是为什么这篇文章的标题把“需求”放在最前面——不是凑字数,是真实的工作顺序。
1.2 合并三份 CSV 的核心需求清单
在让 Codex 动手之前,我先把需求写成了下面的样子。这份需求描述我没有用任何专业模板,就是大白话,但关键点都覆盖了:
- 读取三份 CSV 文件,路径分别是 data/users.csv、data/orders.csv、data/categories.csv,编码统一按 UTF-8 处理。
- orders 表按 user_id 关联 users 表,再按 category_id 关联 categories 表,把用户昵称、注册时间、商品类目名称全部带出来。
- 输出 result.csv,只需要保留指定列:user_id、user_name、category_name、order_amount、order_time。
- 如果关联不上,比如某条订单的 user_id 在 users 表里不存在,或者 category_id 映射不到,数据不能丢,关联不到的字段填空字符串。
- 三份文件的表头可能不一致,脚本要能自动识别近义字段名,比如 user_id 和 userid 要能对上。
- 输出文件表头用 UTF-8 with BOM 编码,这样用 Excel 打开不会乱码。
- 脚本要能在命令行重复执行,第一次跑完如果 result.csv 已存在,要能覆盖而不是报错。
- 提供一条校验命令,对生成的结果做基础统计,比如总行数、空字段数量、金额合计,方便验收。
就这么几条,前前后后花了不到十分钟写完。这个需求文档里最容易被忽略的是第 6 条,大多数做数据分析的同学直接 to_csv 就完事了,结果 Windows 上 Excel 打开全乱码,然后回头怀疑脚本写错了。这里顺手说明一下,Excel 默认用 ANSI 编码打开 CSV,UTF-8 无 BOM 的文件会被当成 GBK 解析,中文必乱,所以输出带上 BOM 是刚需。
1.3 需求描述对 Codex 产出的影响
我实际测试过两种需求描述方式。第一种丢给 Codex 一句话:“合并这三个 CSV”,它确实也会写,但默认用 pandas 写了最普通的 merge,字段名全靠精确匹配,遇到 user_id 和 userid 这种差异就直接报 KeyError,而且完全没有处理关联缺失的场景。第二种就是我上面写的那八条,Codex 生成的脚本几乎不需要大改,一次跑通。
这个差异的本质是:Codex 不是读心术,它的上下文窗口里能看到你给的描述和文件列表,但它不知道你心里默认的规则。你不说“关联不上填空字符串”,它可能就按 inner join 处理了;你不说“自动识别近义字段”,它可能就只会死板匹配。所以需求这一环,表面上看是把问题描述清楚,实际上是在给 Codex 划定约束边界。约束越明确,后期返工越少。
2. Codex 环境准备与工作流设计
2.1 安装 Codex 的正确姿势
既然题目叫“让 Codex 合并三份 CSV”,那环境准备自然是绕不开的一步。Codex 的安装方式以 npm 为主,官方推荐的命令是:
npm install -g @openai/codex如果你在 Windows 上遇到 npm 无法识别的报错,通常是 Node.js 没有安装,或者安装后没有重启终端导致 PATH 没生效。先跑一下node -v,能输出版本号就说明 Node 环境没问题;如果提示 node 不是内部或外部命令,那就先去 nodejs.org 下载 LTS 版本安装包,一路默认安装,再重开一个终端窗口。
不出网环境下没有 npm 源,可以走 GitHub 的 release 页面手动下载对应平台的可执行文件,把二进制放到 PATH 能扫到的目录就行。安装完成后验证一下:
codex --version能输出版本号就算装好了。Codex 的登录方式支持 ChatGPT 账号和 API Key 两种,如果你用的是 ChatGPT 账号登录,需要保证当前账号有 Codex 的使用权限,否则运行时会提示模型不可用之类的错误。API Key 方式更直接,把 key 配到环境变量 OPENAI_API_KEY 里即可。
2.2 第一次运行 Codex 时的注意点
Codex 第一次运行会要求你确认安全设置。它会询问是否允许执行命令、是否允许读写文件,我一般会选“允许在确认后执行”这一档,既不至于让 Codex 完全放飞自我,也不会每一步都弹窗影响体验。有一点必须提醒:Codex 所有在沙箱里跑的命令,都要保证不涉及任何敏感操作,比如删除文件、修改系统配置这些,任何情况下都不要让它做。合并 CSV 这种场景,只涉及读文件和写文件,风险很低,但安全边界要始终守着。
另外,Codex 默认的工作目录就是当前终端所在目录,所以运行前先cd到项目目录,把三份 CSV 放好。目录结构我建议这样组织:
project/ ├── data/ │ ├── users.csv │ ├── orders.csv │ └── categories.csv └── output/output 目录可以留空,让脚本自己建,也可以先建好。反正需求里说清楚了“输出文件已存在要能覆盖”,脚本里要处理这一点,其实 os.makedirs(exist_ok=True) 一行就搞定了。
2.3 让 Codex 干活的标准交互流程
我的习惯是先把需求写成一份 REQUIREMENTS.md,放到项目目录下,然后启动 Codex:
codex进入交互界面后,先给一条总指令:
请先阅读项目根目录下的 REQUIREMENTS.md,然后按里面的需求编写合并三份 CSV 的脚本,脚本用 Python 实现,输出到 output/result.csv,最后运行脚本并列出结果文件的前 5 行。这种方式比直接在交互框里把需求粘贴进去要好,因为需求文档是结构化文本,Codex 读取时不会遗漏换行和列表格式,而且后续如果脚本要迭代,它随时可以重新读需求文档。
Codex 在处理任务时,会自己决定先看哪些文件、用什么工具,它会尝试读取 CSV 的表头来判断数据结构。这个过程不用干预太多,但如果发现它读文件乱码了,可以在交互框里提示一句“文件是 UTF-8 编码,请用 encoding='utf-8' 打开”,它能立刻修正。这就是 AI 写代码和传统 IDE 的差别——你可以用自然语言做实时纠偏。
3. 完整脚本实现与核心细节解析
3.1 Codex 生成的 Python 脚本全貌
下面贴的这份脚本,是 Codex 根据我那份需求文档生成的,我只做了少量格式化调整。整体逻辑清晰,关键是几个细节都处理到了:
import pandas as pd import os import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") OUTPUT_DIR = os.path.join(BASE_DIR, "output") OUTPUT_FILE = os.path.join(OUTPUT_DIR, "result.csv") FIELD_ALIASES = { "user_id": ["user_id", "userid", "客户编号", "用户ID"], "user_name": ["user_name", "username", "昵称", "用户昵称"], "category_id": ["category_id", "categoryid", "类目ID", "分类ID"], "category_name": ["category_name", "categoryname", "类目名称", "分类名称"], "order_amount": ["order_amount", "amount", "订单金额", "金额"], "order_time": ["order_time", "time", "下单时间", "时间"], } def find_column(columns, standard_name): aliases = FIELD_ALIASES.get(standard_name, []) lowered_map = {str(c).strip().lower(): c for c in columns} for alias in aliases: if alias.lower() in lowered_map: return lowered_map[alias.lower()] for c in columns: if standard_name.lower() in str(c).lower(): return c return None def read_csv(path): return pd.read_csv(path, encoding="utf-8-sig", dtype=str, keep_default_na=False) def normalize_column_name(col): return str(col).strip().lstrip("\ufeff") def main(): os.makedirs(OUTPUT_DIR, exist_ok=True) users = read_csv(os.path.join(DATA_DIR, "users.csv")) orders = read_csv(os.path.join(DATA_DIR, "orders.csv")) categories = read_csv(os.path.join(DATA_DIR, "categories.csv")) users.columns = [normalize_column_name(c) for c in users.columns] orders.columns = [normalize_column_name(c) for c in orders.columns] categories.columns = [normalize_column_name(c) for c in categories.columns] user_id_col = find_column(users.columns, "user_id") user_name_col = find_column(users.columns, "user_name") if user_id_col is None or user_name_col is None: print("users.csv 中找不到 user_id 或 user_name 字段") sys.exit(1) order_user_col = find_column(orders.columns, "user_id") order_cat_col = find_column(orders.columns, "category_id") order_amount_col = find_column(orders.columns, "order_amount") order_time_col = find_column(orders.columns, "order_time") if any(c is None for c in [order_user_col, order_cat_col, order_amount_col, order_time_col]): print("orders.csv 中找不到必要的关联字段或输出字段") sys.exit(1) cat_id_col = find_column(categories.columns, "category_id") cat_name_col = find_column(categories.columns, "category_name") if cat_id_col is None or cat_name_col is None: print("categories.csv 中找不到 category_id 或 category_name 字段") sys.exit(1) users_sub = users[[user_id_col, user_name_col]].rename( columns={user_id_col: "user_id", user_name_col: "user_name"} ) categories_sub = categories[[cat_id_col, cat_name_col]].rename( columns={cat_id_col: "category_id", cat_name_col: "category_name"} ) orders_sub = orders[[order_user_col, order_cat_col, order_amount_col, order_time_col]].rename( columns={ order_user_col: "user_id", order_cat_col: "category_id", order_amount_col: "order_amount", order_time_col: "order_time", } ) merged = orders_sub.merge(users_sub, on="user_id", how="left") merged = merged.merge(categories_sub, on="category_id", how="left") merged["user_name"] = merged["user_name"].fillna("") merged["category_name"] = merged["category_name"].fillna("") merged = merged[["user_id", "user_name", "category_name", "order_amount", "order_time"]] merged.to_csv(OUTPUT_FILE, index=False, encoding="utf-8-sig") print(f"合并完成,共 {len(merged)} 行,输出到 {OUTPUT_FILE}") if __name__ == "__main__": main()3.2 脚本里几个值得学习的细节
第一,所有的 CSV 读取都用了dtype=str和keep_default_na=False。这个处理很关键,因为 CSV 里如果有空值,pandas 默认会读成 NaN,而 NaN 在后续合并和输出时很容易变成空字符串或者 “nan” 字样。keep_default_na=False能让空单元格直接以空字符串呈现,合并时再用fillna("")统一兜底,避免输出文件里莫名多出 nan 字符串。
第二,encoding="utf-8-sig"同时负责了读取和写入。读取时它可以自动剥离 BOM 头,写入时它会自动加上 BOM 头,这样 Excel 打开 result.csv 就不会乱码。热词里有人问“csv手机打开正常电脑打开不正常”,大多就是编码问题——手机端很多 App 默认按 UTF-8 处理,电脑上的 Excel 默认按 ANSI 处理,两边解码方式不一致,表现就不一样。统一用 utf-8-sig 输出,两边都能正常显示。
第三,字段名自动匹配的逻辑用了两层:先做精确匹配,再做子串包含匹配。这个做法对真实世界的脏数据很管用。比如表头是“用户ID”这种中文列名,光靠英文精确匹配不行,只要需求里把“用户ID”加进 FIELD_ALIASES 字典,就能直接对上。这就是需求文档第四条说的“自动识别近义字段名”的落地方案。
3.3 如果不想用 Python,Shell 方案怎么搞
Python 方案是最省事的,但有些场合确实没 Python 环境,或者数据量大到 pandas 不合适(虽然两万行远不至于),那就只能用 Shell 命令组合。我给 Codex 的需求文档里也写了“如果你觉得 Shell 更合适也可以”,它给过一个 awk 版参考,这里补充说明一下思路。
Shell 方案的核心是两个命令:join和awk。join 按指定字段把两个文件的行拼接起来,但要求输入文件必须按关联字段排好序;awk 负责重排字段和填充默认值。整个过程写出来大致是这样子的骨架:
sort -t, -k1,1 users.csv > users_sorted.csv sort -t, -k2,2 orders.csv > orders_sorted.csv join -t, -1 1 -2 2 -a 2 -e "" -o 2.1,1.2,2.3,2.4 users_sorted.csv orders_sorted.csv > joined.csv这个方案的痛点在于 CSV 字段里如果出现逗号,sort 和 join 就会错位,所以它只适用于字段内容不含逗号、引号的极简场景。真实数据里用户昵称带个逗号太常见了,我建议优先用 Python。Shell 方案作为后备了解一下就够了,不值得在它上面投入太多去调试。
3.4 脚本运行的现场记录
我把三份 CSV 放进 data 目录,跑了一下:
python merge_csv.py输出:
合并完成,共 18654 行,输出到 output/result.csvorders.csv 原始行数就是 18654 行,说明 left join 之后没有丢数据,所有订单都保留下来了。然后我手动抽查了几条关联不上的记录,发现 user_name 和 category_name 字段是空的,正是需求里要求的“关联不到填空字符串”的效果。这一步其实很重要,因为如果你没有在需求里写明这个逻辑,Codex 有很大概率默认用 inner join,那样 18654 行可能就变成 18612 行,某些数据会悄无声息地消失。所以验收的时候,行数对不上不一定是你错了,也可能是你的 join 类型选错了。
4. 验收测试设计与数据正确性校验
4.1 除了“能跑”,还要怎么证明它跑对了
脚本能跑完并生成文件,只是第一步。真正的验收要回答三个问题:行数对不对、关联逻辑对不对、输出内容对不对。行数对不对主要指最终行数是否等于订单表行数(因为是 left join);关联逻辑对不对,可以看关联不到的用户名和类目名是否为空;输出内容对不对,建议用抽样交叉验证,比如随机抓 5 条订单,去原始 CSV 里人工核对一下金额和时间有没有被篡改。
这里有一个很实用的技巧:让脚本自己输出一个“验收报告”。比如在脚本最后加一小段统计代码,打印总行数、user_name 为空的行数、category_name 为空的行数、order_amount 合计,这些数就是验收的核心指标。第一次跑出这些数以后记下来,后续如果数据更新了,重跑时对比这些指标就知道新数据是否正常。Codex 在这类统计代码的生成上几乎不用怎么指导,你只要在需求里写明要哪些指标就行。
4.2 用 MD5 校验输出文件的一致性
热词里有人问“csv文件怎么进行md5校验”,这个需求在验收测试里特别有用。场景是这样的:脚本如果加入了随机性,或者经过多次修改,你怎么知道两次运行的结果是否完全一致?直接比对文件内容当然可以,但最方便的做法是给 result.csv 算一个 MD5。
Linux/macOS 下:
md5sum output/result.csvWindows PowerShell 下:
Get-FileHash output/result.csv -Algorithm MD5跑出来的哈希值可以写到验收报告里。比如第一次跑完是3f2c0a8e...,代码改动后重跑,如果哈希值变了,说明输出内容和以前不一致,你得判断是不是正常变化;如果需求没有变化、输入数据没有变化,哈希值应当完全一致,不一致就说明脚本存在非确定性问题,比如用到了集合、字典遍历顺序等不稳定因素。
实测下来,pandas 的 to_csv 输出是稳定的,同一份输入数据跑两次,文件 MD5 应该一模一样。这个特性很适合做回归验证,改代码的时候不用每次都人工比对整个文件,直接看 MD5 就行。
4.3 从行数与抽样两个视角做完整性验证
抽样验证是最像人工审计的一步,Codex 没法替你做,因为需要人的判断。我从结果里随机挑了三条订单:
- user_id=10023 的订单,去 users.csv 查这个 ID 存在,user_name 应该显示“张三”。
- user_id=99999 的订单,在 users.csv 里不存在,user_name 应为空。
- 类目映射找不到的订单,category_name 应为空。
结果全部符合预期。合并逻辑这里有一个细节值得留意:关联字段的类型必须是字符串,否则会出现 001 和 1 匹配不上的问题。我们的脚本全程用 dtype=str 读入,关联时按字符串比,就不会出这种问题。如果数据里有身份证号、订单号这类前导零的字段,这一点尤其重要。
4.4 效果对比:Codex 脚本 vs 手工处理
同样一份活儿,以前手工用 Excel VLOOKUP 做,三份表关联一遍,再清洗字段名,光是对字段就要花半小时,还得提心吊胆怕 VLOOKUP 的近似匹配搞出脏数据。用 Codex 从写需求到跑通脚本,总共二十分钟,其中写需求花了十分钟,Codex 生成代码加调试花了十分钟,后续重跑只需一条命令。
有人可能会说,你自己也会写 pandas,为什么不直接写?我的回答是:像这种一次性的数据清洗需求,每次的表结构都不一样,与其从零开始写一套新脚本,不如把需求描述清楚交给 Codex,它生成的骨架代码可以复用大半,我只需要审查关键逻辑和验收数据。这种工作模式下的 Codex,更像一个熟悉 pandas 语法的结对编程搭档,而不是简单的代码生成器。
5. 常见报错与排查技巧实录
5.1 npm 和 codex 命令无法识别的处理
热词里有一类高频问题,就是 “npm : 无法将‘npm’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。这个问题在 Windows 上特别常见,根因基本是 Node.js 没装或者装完没开新终端。排查步骤如下:
- 重新打开终端,别用旧窗口。
- 跑
node -v和npm -v,确认 Node 和 npm 都能执行。 - 如果 npm 还是不行,去“系统环境变量”里看 PATH 有没有
C:\Program Files\nodejs\这条。 - 确认 PATH 没问题后,再跑
npm install -g @openai/codex。 - 如果 codex 装完还是提示无法识别,检查 npm 全局安装目录是否在 PATH 中,Windows 上一般是
%AppData%\npm。
注意:任何情况下都不要为了绕过 PATH 问题去代码里硬编码路径,那是给自己埋坑,换个环境就废了。
5.2 Codex 运行时报模型不可用怎么办
热词里提到 “the 'gpt-5.6-sol' model is not supported when using codex with a chatgpt account” 这类信息,本质是账号权限和模型版本不匹配。Codex 的模型是否可用,取决于你用的是 ChatGPT 订阅账号还是 API Key,以及当前账号是否开通了动态模型选择功能。解决办法是检查账号配置,确认它有 Codex 的访问权限;如果是 API Key 方式,需要确认 key 的额度足够且绑定了支持 Codex 的模型服务。
这里不做具体的模型配置推荐,因为账号权限和配额随着时间变化很快,最靠谱的办法是打开 Codex 官方文档的模型列表页,对照你的账号类型看当前支持什么模型,按文档为准。
5.3 远程任务空间不足的报错
另一个常见报错是 “codex ran out of room in the model's context window”,翻译过来就是上下文窗口满了。Codex 的上下文窗口决定了它一次能“记住”多少内容,如果你的需求文档太长、输出文件太大、或者调试过程中打印日志太多,它就可能撑爆上下文。
解决办法是分步操作:不要一次把所有 CSV 都让它读完,先让它读表头和数据样例,再让它写脚本,最后单独运行。这个问题的本质是任务拆分,跟人脑处理复杂问题时一样,先把信息概要化,再动工。实测中,让 Codex 读df.head()的输出而不是整个文件,能明显减少上下文占用。
5.4 CSV 打开乱码和字段错位的终极解法
CSV 相关问题的根源高度集中。乱码是编码问题,统一用 utf-8-sig 读写即可;字段错位多半是数据内容里含有逗号或换行符,Excel 处理时没识别引号包裹规则。遇到字段错位,第一件事不是去写代码处理,而是用 VS Code 打开 CSV 看原始字节,确认有没有引号包裹、有没有异常换行。pandas 的 read_csv 默认能处理带引号的逗号,所以只要读进来没报错,字段错位通常只出现在你用文本编辑器手动改文件的时候。
我对这类问题的建议是:能用 pandas 就别手撸字符串切割;必须在 Shell 里处理时,一定要先确认数据里没逗号和换行符。
5.5 一个小技巧:让 Codex 自己排查报错
脚本报错了不用急着去搜报错信息,把报错粘贴给 Codex,加一句“请分析这个报错产生的原因,并给出修改方案”,它通常能直接指出问题在哪里并给出修改后的代码。实测中它对 pandas 的 API 报错和文件编码报错判断很准,但对环境类报错(比如刚才说的 PATH 问题)判断力有限,这时还得靠人看系统环境。所以我的建议是:代码逻辑问题找 Codex,环境配置问题按文档走。
6. 更进一步:把这种工作方式迁移到其他场景
6.1 从合并 CSV 到更通用的数据处理范式
做完这个合并任务后,我最大的感受是这套“需求文档 → 让 AI 写脚本 → 验收测试”的方法可以迁移到几乎所有数据处理场景。比如热词里提到的“neo4j导入csv文件”、“将csv导入到matlab中进行fft仿真”、“kettle两个数据表合并输出一个csv”,本质都是数据格式转换和字段映射,需求写清楚,Codex 都能出基础方案。
差别只在于具体的工具库和语法。Neo4j 导入要写 Cypher 的 LOAD CSV 语句,Matlab 导入要处理的是 xlsread 或 readtable,Kettle 则是图形化配置。Codex 的优势在于它对这些工具的基本用法都有知识储备,能给你一个可运行的最初版本,你再根据自己的数据和环境去调整。这点和以前“百度搜代码、复制粘贴改半天”完全是两个效率层级。
6.2 与需求管理结合的个人实践
热词里出现了“需求管理系统”、“产品需求文档”、“需求规格书”这些词,我猜不少人是在做正式的项目交付。我个人的实践是:和 Codex 协作时,需求文档写得越接近正式规格书的粒度,产出越可靠。但也没必要写正式的 IEEE 那种格式,只要做到“每个功能点有明确行为描述”即可。
比如“关联不上填空字符串”这种描述,如果写成“请整合用户与订单数据”,Codex 就不知道空值怎么处理。反过来,你写清楚行为,Codex 大概率会直接生成对应的 fillna 逻辑。所谓“需求管理”,在这类场景里其实就是把期望的行为逐条说清楚,并让 Codex 逐条对应着实现,验收时再逐条核对。
6.3 脚本迭代时怎么维护需求文档
脚本不会只写一次,数据字段变了、关联键换了、输出列增删了,都需要改。建议把 REQUIREMENTS.md 当作活文档,每次改需求先改文档,再让 Codex 基于新文档更新脚本。这样做的好处是:项目过一个月回来看,你还能知道脚本当初是干什么的,而不是面对一堆没有注释的代码发呆。
我在这个项目里就是这么做的,后来用户表加了一个“会员等级”字段,要求输出里加上等级列,我只需要在需求文档里加一行,Codex 就能定位到脚本里该改哪一行。这与传统的“打开代码寻找到底哪一段该改”相比,省下的时间是很可观的。
最后再分享一点个人体会。AI 写代码这件事,真正决定天花的不是模型能力,而是你提出需求的水准。一份清晰、可验收、包含边界条件的需求,比任何提示词技巧都管用。Codex 能把 CSV 合并这类脏活累活接过去,但去哪里取数据、什么算正确结果、哪些坑必须绕开,仍然需要人来定义。这个项目的全部价值,就藏在那一份不到一页纸的需求文档里。