
上周同事丢给我一个不算难但特别磨人的活儿整理一份四六级词汇资料里面大量单词的英文部分被星号打成了马赛克只剩中文释义和词性还在。起初我想着用 Python 写个正则替换就完事结果一看数据傻眼了——星号数量和单词长度对不上而且同一个中文释义能对应好几个词纯规则根本没法判断该填哪个。最后我换了个思路用 Python 做文件解析和批量调度让 DeepSeek 来做语义补全中文词性和星号位置都是线索生成后自动校验回写。这篇就把整个处理过程、踩过的坑、可以直接拿去用的脚本思路都梳理一遍给同样要批量修改英语词汇的办公党省点时间。1. 这活儿的来龙去脉一堆被星号盖住的词汇怎么批量还原先说清楚原始数据长什么样不然后面讲方案都是空中楼阁。这份四六级词汇材料大概有七八百条词目每一条本来应该是三种信息齐全的英文单词、中文释义、词性标注。完整条目长这样abandon 放弃 vt. abnormal 反常的 adj. academic 学术的 adj.听起来很简单对吧但这份资料在发给学员之前做过一轮「答案隐藏处理」结果就是有一部分词目的英文被替换成了星号。有的用一个星号有的用三个星号还有的用五个完全看当初处理的人手抖到什么程度。于是数据就变成了这种鬼样子*** 放弃 vt. *** 反常的 adj. * 学术的 adj.如果只是几十条手动查一查四六级词汇表半天也能搞定。但这七八百条里大概有三百多条是带星号的还穿插在完整条目中间靠肉眼一条条去对不但慢而且眼睛很快就花了。我当时的第一反应是这不就是个「查找替换」的问题吗把***换成某个单词中文和词性都给了去网上找个四六级词汇表建个映射表Python 一行dict.get()不就解决了结果试探性地做了十分钟就发现三个绕不开的问题。第一个问题四六级词汇里同一个中文释义对应多个英文单词的情况太常见了。「放弃」既可以是 abandon也可以是 give up还可以是 surrender「建立」有 build、establish、found 三个高频选项「处理」可以是 handle、deal、cope、process。只有中文没有英文字典映射没法确定该选哪一个除非把词性也结合进去做排除但即便如此也经常剩两个以上候选人。第二个问题词性标注也不是百分之百可用。资料里有些词性写的是简写比如vt.、vi.、adj.但也夹杂着n.和v.偶尔还有漏写的、写成中文「动词」「名词」的。如果要做纯规则匹配清洗这些词性的工作量比想象中大得多。第三个问题也是最要命的资料里存在「同词不同义」的情况。同一个英文单词配套的中文释义可能是「这个单词的在四级场景下的常用义」而不是词典里排在第一位的义项。用网上扒的词汇表硬匹配很容易把语义给替换歪了。所以这个活儿必须引入能理解语义的工具来兜底。而 DeepSeek 这类大模型恰恰擅长根据「中文释义 词性限制 上下文单词列表」反推那个最合理的英文单词。于是整体方案就变成Python 负责所有确定性流程DeepSeek 负责所有需要语义判断的部分。2. 方案选型为什么我觉得纯规则跑不通非得请 AI 出手2.1 星号遮挡不是简单的等长替换正则一眼望不到头扫一眼原始数据就会发现星号的用法毫无规律。我从那堆资料里随手摘了几条真实存在的遮挡形态** 证据 n. *** 展览 vt. * 适合的 adj. **** 设备 n.注意*表示「上有政策下有对策」的产物——当初隐藏答案的人很可能是打开 Word用「查找替换」把所有英文单词批量换成了*但换完之后没有统一长度。这直接导致了一个问题你没法通过星号个数判断英文单词的字母数量。**可能是 do、go、an 这种双字母词也可能是两个星号占位但实际单词七个字母***更是没谱可以是 any、can、run也可以是 important、beautiful 这种长词。如果写正则去匹配最多只能做到「把星号序列提取出来」但提取出来之后去哪儿找候选词四六级词汇表本身就有几千条用中文反查英文再筛一次仍然绕不开「多义词选哪个」这个坎。纯代码方案在这里已经走到了死胡同。2.2 四六级词汇的语境问题靠字典解决不了再说深一层。四六级词汇处理和日常翻译有个很大区别考试词汇强调「在某个语境下最可能考的那个意思」。比如current在四级词汇里最常见的是「当前的」和「电流」两个意思词性分别是 adj. 和 n.。如果中文释义只写了「电流」词性写 n.那模型基本可以锁定 current但如果只给「当前的」和 adj.模型也能判断出是 current。可如果中文释义是「目前的」而没有词性那候选就多了current、present、recent 全都沾边。这种判断本质上是语义层面的需要一个见过大量英语语料的模型来做。它不只是在「查字典」而是在理解「中文释义 词性标签 同行词语」之后挑一个四六级语境里最自然的词。这恰恰是 DeepSeek 这种通用大模型的长处。2.3 Python DeepSeek 的组合好在哪选 Python 没什么悬念文件解析、正则匹配、批量 HTTP 请求、CSV 写回这些活 Python 的生态太成熟了几十行代码就能串起来。选 DeepSeek 则是出于三个实际考虑。第一它的接口是标准格式用常见的 OpenAI SDK 就能直接调用base_url 改一下、key 配一下就行不需要引入额外的复杂依赖。第二针对中文语义的理解能力足够稳我实测下来给足上下文之后它反推四六级词汇的准确率很高极少出现那种完全离谱的答案。第三单次请求成本低处理几百条词汇的量级整体费用可以忽略属于典型的「花小钱办大事」。数据流是这样走的Python 读原始文本 → 正则拆出「星号/英文、中文、词性」 → 拼成结构化 JSON → 分批发给 DeepSeek → 拿回补全后的 JSON → Python 校验、洗牌、写回。每一环的职责都很清楚模型只干一件事告诉你星号位置该填哪个词。3. 核心实现从零开始写这个词汇修正脚本3.1 先看原始格式把字段拆干净不管后面怎么调模型第一步永远是搞清楚原始数据长什么样。我拿到的是一个.txt文件每行一条词汇。行内字段用空格分隔三种排列都存在abandon 放弃 vt. # 完整条目可以作为参考 *** 反常的 adj. # 英文被遮挡需要补全 * 学术的 adj. # 同上还有少数几行格式不标准比如词性跑到了中文前面*** vt. 放弃所以脚本第一步不是直接调模型而是先把每行解析成三个字段英文部分可能是星号也可能是真词、中文部分、词性部分。我写了这样一个解析函数import re def parse_line(line: str): line line.strip() if not line: return None # 匹配英文/星号 中文 词性或 英文/星号 词性 中文 patterns [ r^(\S)\s(.?)\s([a-zA-Z]\.?)$, # 英文 中文 词性 r^(\S)\s([a-zA-Z]\.?)\s(.)$, # 英文 词性 中文 ] for pat in patterns: m re.match(pat, line) if m: word_part m.group(1) if set(word_part) {*}: word_part None # 表示遮挡 return { en: word_part, zh: m.group(2).strip() if not word_part else m.group(3).strip(), pos: m.group(3).strip() if not word_part else m.group(2).strip(), } return None这里有个细节容易踩坑中文和词性谁先谁后。我一开始只写了一种 pattern结果解析率只有六成后来把两种排列都考虑进去才把解析率拉到接近百分百。正则表达式里(.?)是非贪婪匹配用来抓中文或词性词性用[a-zA-Z]\.?来识别因为标准词性基本都是n.、vt.、adj.这种字母加点。解析完把带enNone的条目挑出来另外把完整条目也留着。完整条目不是摆设它们可以作为上下文喂给模型帮助模型判断这份材料的用词偏好。3.2 构造 Prompt让模型稳定输出 JSON 是关键模型调用能不能成功一半看 Prompt 写得好不好。我第一次尝试用的是特别随意的说法「请根据中文和词性补全英文单词」结果模型返回了一堆散文式回答什么「根据您的需求我认为这个单词可能是 abandon」还要我从里面手工抠答案完全没法自动化。后来我把 Prompt 改成强结构化要求强制模型只输出 JSON而且给出明确的字段名。实际跑的模板大概是这样的prompt 你是一名熟悉四六级词汇的英语教师。下面是一些四六级词汇条目每条包含中文释义和词性标注但英文部分被星号遮挡。请你根据中文释义和词性推断最可能的英文单词。 要求 1. 只输出 JSON 数组不要输出任何解释性文字。 2. JSON 数组中每个元素包含三个字段en(英文单词)、zh(中文释义)、pos(词性标注)。 3. 英文单词必须是四六级词汇范围内最常见的形式注意词性要和 pos 一致。 4. 如果某个条目无法确定en 填 null。 输入条目 [ {zh: 放弃, pos: vt.}, {zh: 反常的, pos: adj.}, {zh: 学术的, pos: adj.} ] 这个 Prompt 里我加了几个心机设计。第一是「只输出 JSON 数组」这个硬约束配合后面的解析兜底基本杜绝了解析失败。第二是「四六级词汇范围内最常见的形式」这能抑制模型给出太偏的词。第三是「无法确定时填 null」给了模型一个合法的「我不知道」出口避免它硬编一个错误词出来。3.3 调用 DeepSeek 接口分批处理加容错调用部分用的是标准的会话补全接口。因为 DeepSeek 的接口和主流 SDK 兼容所以代码很简洁import json import time from openai import OpenAI client OpenAI( api_key你的API_KEY, base_urlhttps://api.deepseek.com ) def complete_batch(items): prompt build_prompt(items) # 把条目拼成上面的格式 for attempt in range(3): # 最多重试三次 try: resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是一个严谨的英语词汇助手。}, {role: user, content: prompt}, ], temperature0.1, max_tokens1024, response_format{type: json_object}, ) content resp.choices[0].message.content return parse_json_response(content, len(items)) except Exception as e: print(f调用失败重试中{e}) time.sleep(2) return [None] * len(items)几个参数值得说道说道。temperature0.1是为了让输出尽量稳定补全单词这类任务不需要创造性温度越低越好。response_format{type: json_object}是接口本身就支持的 JSON 模式能让模型更规矩地返回结构化内容。max_tokens1024对于一次处理十五条左右的条目完全够用给太少容易截断给太多又浪费。批大小我最后定在 10 到 15 条。一开始贪心一条请求塞了五十条结果模型输出经常超出max_tokens而且单次请求耗时长稍微遇到网络抖动就整个失败。拆小之后单次请求五秒内基本能返回即使失败重试也就多花几秒。3.4 解析返回结果宽容一点别一报错就崩模型是概率输出即使开了 JSON 模式偶尔也会在 JSON 外面包一层说明或者在里面多加一个字段。所以解析函数必须做容错处理不能直接json.loads一把梭。我的解析策略分三层def parse_json_response(content, expected_count): # 第一层尝试直接解析 try: data json.loads(content) except json.JSONDecodeError: # 第二层截取方括号或花括号内的部分 start content.find([) end content.rfind(]) 1 if start -1 or end 0: start content.find({) end content.rfind(}) 1 try: data json.loads(content[start:end]) except Exception: return [None] * expected_count if isinstance(data, dict): data data.get(data) or data.get(result) or data.get(items) or [] if not isinstance(data, list): return [None] * expected_count return data第二层的「截取括号」技巧很实用。模型偶尔会输出「好的以下是结果[...]」这种带前缀的内容直接json.loads会失败但只要把[ ]或{ }之间的内容截出来绝大多数情况都能正常解析。第三层的dict兼容处理是因为模型偶尔不按数组输出而是包了一层{result: [...]}。这些情况都得兜住。3.5 校验与回写别让 AI 的答案污染原文件模型返回的结果不能直接写回文件。我加了一道校验关卡规则很简单英文必须是纯字母允许少数带连字符的词中文不能为空词性必须符合常见枚举值。校验失败的条目单独存到failed.txt方便后续人工处理。VALID_POS {n., v., vt., vi., adj., adv., prep., conj., pron., num., art.} def validate(item): if not item or not isinstance(item, dict): return False if not item.get(en) or not re.fullmatch(r[a-zA-Z\-], item[en]): return False if not item.get(zh): return False pos item.get(pos, ).strip().lower() if pos not in VALID_POS: return False return True这个校验看似简单但帮我拦住了不少问题。有一次模型把abandon的zh写成了一整句话「放弃抛弃离弃」虽然意思没毛病但作为四六级词汇表里的中文释义太啰嗦了。后来我在 Prompt 里加了「中文释义用 2 到 6 个字的短语不要用句子」才把这个问题解决。写回文件时我选择了和原文件一致的格式每行一个条目with open(output.txt, w, encodingutf-8) as f: for item in all_results: f.write(f{item[en]} {item[zh]} {item[pos]}\n)同时保留一份backup_原始文件.txt这是我在处理任何数据前都会做的固定动作——改数据前先备份永远是性价比最高的保险。4. 实操中踩过的一系列坑逐个说清楚排查思路4.1 星号的坑不是所有*都代表遮挡第一次写解析正则的时候我以为只要遇到*就当作遮挡标记。结果有一行数据是multi*意思是一个词根示例星号不是遮挡而是表示「multi 是词根后面可以接各种后缀」。这种特殊情况直接把我的解析逻辑打穿了。排查思路很简单把解析失败的样本全部打印出来肉眼扫一遍找出模式上的遗漏。我是这么修正的当星号序列里夹杂着字母时不把它当遮挡而是保留原词。用set(word_part) {*}判断「整段全是星号」才算遮挡这个细节在前面parse_line里已经写进去了。这一个条件直接帮我过滤掉了三分之一的解析异常。4.2 词性混乱的坑vt.和动词并存原始材料里词性标注并不统一。大部分是标准缩写n.、vt.、adj.但偶尔混进动词、名词这种中文写法甚至还有n这种漏了点的格式。我最初把词性枚举写得太严导致不少结果过不了校验。后来我调整了策略先用规则把中文词性统一成缩写再进 Prompt。统一函数就几行def normalize_pos(pos_raw): mapping { 动词: v., 及物动词: vt., 不及物动词: vi., 名词: n., 形容词: adj., 副词: adv., 介词: prep., 连词: conj., 代词: pron., } return mapping.get(pos_raw.strip(), pos_raw.strip())这么做的好处是模型看到的输入更干净输出也更稳定。如果直接用「动词」这种模糊词性让模型推它可能纠结该输出v.还是vt.反而增加出错概率。4.3 JSON 解析失败的坑模型喜欢在 JSON 外面套话前面讲解析函数时提过截取括号的技巧这里展开说。我实际跑的时候发现即使开了 JSON 模式模型仍然有约 5% 的概率在返回内容前面加一句「好的我已经根据你的要求完成如下」。看起来没什么危害但足以让json.loads当场崩溃。我一开始的处理方式是让程序停下来人工处理后来改成自动截取。实测下来只要 JSON 内容本身是合法的截取括号再解析的成功率接近 100%。如果截取后还解析失败那就可能真的是模型输出半截 JSON 了这种只能重试或丢弃。宁可丢掉让这一条保持待处理也不能让程序带着错误数据往下跑。还有一个坑是 JSON 里的中文字符编码。默认读取和写入都指定encodingutf-8能避免但如果你在 Windows 上跑脚本控制台输出可能显示乱码这不影响文件内容别被吓到。4.4 批量和限流的坑一开始太贪心了第一次跑全量数据时我把三百多条遮挡词全部塞进一个循环每调一次接口就串行等结果跑完一批要七八分钟中间还有几次超时。后来做了三个改进每条请求只放 10~15 个条目。加time.sleep(1)控制请求频率。超时重试加指数退避第一次失败等 2 秒第二次等 4 秒最多三次。改进之后三百条数据大约三分钟跑完期间没有一次连续失败。如果你处理的量更大还可以用ThreadPoolExecutor做并发但并发数建议控制在 5 以下否则容易被服务端限流。4.5 语义错误的坑模型补全的词确实存在但不一定是想要的那个这一类错误最隐蔽。比如有一行的中文是「机会」词性是 n.模型填了opportunity这个词没错但之前资料里已经有十几条chance了而且这份词汇表的风格更偏向基础词。后来我发现问题出在 Prompt 里缺少「参考同一批词的难度」这个约束。解决方法是把完整条目也塞进 Prompt 里让模型观察整份材料的词汇风格context_examples [ {en: abandon, zh: 放弃, pos: vt.}, {en: academic, zh: 学术的, pos: adj.}, ]模型看到这些参照后再遇到「机会 n.」时就会倾向选择和abandon、academic同一档次的词。这个改进很有效错误率明显下降。4.6 人工抽检的坑别因为准确率高就跳过最后一道检查全部跑完后我没有直接交付而是随机抽了 50 条让同事看了一遍。结果发现了两类需要手动修正的小毛病一是少数词被补成同义表达而不是考试词汇比如把「开始」补成了commence而原本应该是begin二是个别词性跟实际用法不匹配补成n.但原文想表达的是vt.。比例不高但足以说明全自动流程不能完全省掉人工质检。我建议的抽检比例是 20%。对于四六级词汇这种标准内容抽检 20% 基本能覆盖大部分系统性错误成本也不会太高。5. 把这套流程复制到其他资料上思路比脚本本身值钱这个项目做完之后我最大的感受是脚本本身只有一百多行真正值钱的是「先拆字段、再让模型补语义、最后规则校验」这套组合打法。类似的需求换个皮就能套用。5.1 不只是星号隐藏答案、乱码、缺失字段都能用如果你手上有类似的半损坏资料比如试卷里的答案被替换成下划线___、PDF 转换出来后英文字符变成乱码、表格里某列字段大面积缺失只要还剩一些语义线索中文、词性、前后文就可以用同一套流程先把带问题的条目解析出来再构造 Prompt 让模型补全最后用校验函数把关。核心没有变Python 负责确定性的活模型负责不确定性的活。5.2 Prompt 的「角色 任务 输出约束 示例」四件套是通用模板我这次用的 Prompt 结构可以抽象成一个通用的模式角色告诉模型它是什么领域的专家四六级英语教师、数据分析师、技术编辑。任务说清楚输入是什么、要做什么修改。输出约束强制 JSON 结构、字段名、枚举值。示例给一两个标准输入输出对让模型明白期望的映射关系。这个四件套我用在好几个场景上都管用包括整理会议纪里的错别字、把零散的图书目录格式统一化。写 Prompt 时多花十分钟设计示例往往比调半天参数更有效。5.3 一定要给脚本留退路备份、分批、可重试最后再分享一条经验处理办公数据时「改坏了能还原」比「一次改对」更重要。我这次在写回前留了原始文件备份每条结果单独落一个 JSON 日志文件记录哪些条目是补全的、哪些是校验失败的、哪些是人工手动修正的。这些日志不仅方便追溯也为下次处理同类型资料积累了样本数据。如果你也想复刻这个流程我的建议是从一个小文件开始先跑三十条验证效果确认输出格式没问题再上全量。不要一开始就追求「全自动一把梭」半自动、可监督、随时能停下整改才是处理这类批量修改任务更稳的姿势。