news 2026/9/20 4:26:03

Python解析docx托福词表:正则切片、SQLite落库与Anki导出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python解析docx托福词表:正则切片、SQLite落库与Anki导出

简介:红宝书托福词汇是面向托福备考者与英语进阶学习者的词汇速查文档,针对词汇量大、释义分散、缺乏系统归类的痛点,按语言、学术、社会、文化等主题梳理核心词汇。压缩包内为1个docx文件,体积约71KB,以单词加词性与中文释义的条目形式编排,如abandon、abstract、accelerate、accommodate、acquire等,均附音标层级外的简明释义,便于导入笔记软件或打印背诵。内容覆盖从abundance、adolescent到aesthetic、aggressive等高频词,兼顾基础词与学术场景用词,适合按主题分批记忆或考前集中排查盲点。该资源已有206人学习使用,可用作托福阅读与听力词汇积累的辅助材料,也可配合真题练习进行查漏补缺。

1. 从 docx 到词表:先别急着 Ctrl+A

拿到「红宝书托福词汇.docx」,多数人的第一反应是 Ctrl+A 拷进记事本,结果页码、页眉和换行全糊在一起。反直觉的是:.docx 根本不是文本文件,它是个 ZIP 包,正文躺在word/document.xml里,每个词条被拆成若干 run,页面底部的「红宝书托福词汇」和行尾数字也作为普通段落混进正文流。复制粘贴拿到的是渲染结果,不是数据。这份词表从 abandon 排到 compaction,几百个词条里既有 abundant、administration 这种重复词头,也有 airborne 这种释义串行的脏行。下面按「解析文档 → 正则切片 → 结构化落库 → 导出复习 → 词族归并」的顺序走一遍。

2. 解析层:用 python-docx 把文档拆成可过滤的段落流

2.1 .docx 的物理结构与正文位置

先把文件当 ZIP 拆开看一眼,比直接上代码更省时间。一个标准 docx 里,word/document.xml存正文段落,word/header*.xmlword/footer*.xml存页眉页脚,word/styles.xml定义样式。词条文字只可能来自前两者,页眉页脚是纯噪声源。

# 列出包内文件,确认正文与页眉页脚的实际路径 unzip -l 红宝书托福词汇.docx | grep -E "document|header|footer" # 抽主文档前 3000 字节,看段落与 run 的嵌套关系 unzip -p 红宝书托福词汇.docx word/document.xml | head -c 3000

-p表示输出到标准输出不解压落盘,head -c按字节截断,避免几 MB 的 XML 直接刷屏。看这段输出重点确认两件事:词条文本是被<w:p>整段包住的,还是被<w:r>拆成好几截。如果同一个词头被拆到两个 run 里,后面走 python-docx 取paragraph.text就没事,但如果你打算自己解析 XML,就必须做 run 拼接。

2.2 按段落读取并与样式信息一起落表

python-docx 的Document.paragraphs只覆盖主文档流,不含页眉页脚,这正好省掉一轮过滤。读取时把段落序号一起留下,后面定位脏行、回溯原文都要靠它。

from docx import Document doc = Document("红宝书托福词汇.docx") rows = [] for idx, p in enumerate(doc.paragraphs): text = p.text.strip() # 空段落不参与后续判断,但保留计数用于核对总量 rows.append({"idx": idx, "text": text, "style": p.style.name}) total = len(rows) blank = sum(1 for r in rows if not r["text"]) print(f"段落总数={total} 空段落={blank} 有效段落={total - blank}")

p.text已经把该段下所有 run 的文本按顺序拼好,这是它比自己解析 XML 省事的关键。p.style.name一般是Normal,但如果原文档给词头用了自定义样式,这里会暴露出来,可以据此做二次分类。段落序号 idx 是稳定主键,导入数据库时作为src_line存下来,排查解析异常时能一键回到原文位置。

2.3 页眉、页码与粘连行的识别规则

先量化噪声,再决定过滤规则。经验上 docx 转出来的页码段是纯数字,页眉段是和文档标题一字不差的常量,最麻烦的是「上一页最后一个词条的释义」和「页码 + 页眉」粘在同一段里,比如原文里的土墙1红宝书托福词汇

import re from collections import Counter RUNNING_HEAD = "红宝书托福词汇" PAGE_NO = re.compile(r"^\d{1,3}$") heads = [r for r in rows if r["text"] == RUNNING_HEAD] pages = [r for r in rows if PAGE_NO.fullmatch(r["text"])] glued = [r for r in rows if RUNNING_HEAD in r["text"] and r["text"] != RUNNING_HEAD] print("页眉段:", len(heads), "纯数字段:", len(pages), "粘连段:", len(glued)) print("粘连样例:", [r["text"][:40] for r in glued[:5]])

三类判定分别是:全等匹配抓页眉,正则全匹配抓页码,子串匹配抓粘连。前两类直接丢,粘连段不能整段丢,因为它前半截是真词条。做法是把RUNNING_HEAD和其后的数字一起截掉,只留前面部分,再交给下一章的正则处理。

段落形态原文样例判定方式处理动作
正常词条行abandon vt. 放弃含拉丁字母且含中文保留待切片
重复页眉红宝书托福词汇与常量全等丢弃
页码12^\d{1,3}$全匹配丢弃
页眉页码粘连土墙1红宝书托福词汇含页眉且非全等尾部截断后保留
空段(空字符串)strip 后为空丢弃
中文释义换行续行意为在空中无拉丁字母并入上一行

2.4 解析失败的兜底:先统计再决定要不要人工

过滤完跑一次形态统计,输出「能匹配词条正则」和「不能匹配」两堆的数量比。比例低于 95% 就别急着往下走,先看不能匹配的那堆长什么样。这个环节最大的价值是避免你在错误的规则上继续投入。

提示:不要在第一步就写复杂的清洗规则。先把原始段落完整落一份 JSONL 备份,后面每一版清洗规则都能拿同一份输入重跑对比,否则改错规则时无法回溯。

3. 切片层:一条正则从「词头 词性 释义」里拆三件套

3.1 词条行的六种形态

红宝书这类词表看着规整,实际形态比想象中杂。动正则之前先归类,比直接写一坨表达式然后无限打补丁高效得多。

行形态原文样例切片要点
单词性单义项abandon vt. 放弃主分支一次拆净
单词性多义项acute a. 敏锐的, 严重的;剧烈的逗号、分号都可能是分隔符
一个词头多词性advocate vt. 提倡,n. 提倡者,拥护者需按词性标记二次切分
无词性标注all-star 由明星演员(或运动员)组成的词性字段留空,不能报错
释义以括号开头billion num. (英,德)万亿不能要求释义首字符是中文
释义串行的脏行airborne airbear 的过去分词,意为在空中正则命中但结果无意义,需人工

3.2 主正则的构造与参数取舍

关键是词头部分别用贪婪匹配。[A-Za-z][A-Za-z\- ]{0,28}?里的?让它非贪婪,配合后面的\s+,引擎才能正确停在词头与词性之间的空白上。

import re CJK = "\u4e00-\u9fff" # 词性标记:托福词表里出现过的全部缩写 POS_PAT = r"(?:a|ad|n|v|vt|vi|prep|num|conj|pron|int)" ENTRY = re.compile( rf"^(?P<head>[A-Za-z][A-Za-z\- ]{{0,28}}?)" rf"\s+" rf"(?:(?P<pos>{POS_PAT})\.\s*)?" rf"(?P<gloss>.+)$" ) def parse_line(line: str): m = ENTRY.match(line.strip()) if not m: return None head = m.group("head").strip() pos = (m.group("pos") or "").lower() gloss = m.group("gloss").strip(" ,,;;") # 释义里至少要有 1 个中文字符,否则判为解析残留 if sum(1 for ch in gloss if CJK[0] <= ch <= CJK[-1]) < 1: return None return {"head": head, "head_norm": head.lower(), "pos": pos, "gloss": gloss} for s in ["abandon vt. 放弃", "acute a. 敏锐的, 严重的;剧烈的", "all-star 由明星演员(或运动员)组成的"]: print(parse_line(s))

三个参数要盯住:{0,28}是词头长度上限,复合词child-careback-breaking都在 15 字符以内,28 足够,太长会让正则在大段英文噪声上白跑;\s+不能改成\s*,否则引擎会把词头切碎;POS_PAT用非捕获组并加?,让无词性的行也能落到 fallback 分支而不是整体失配。释义末尾的strip是必需的,原文不少行以分号或逗号收尾。

3.3 一个词头挂多个词性:二次切分

advocate vt. 提倡,n. 提倡者,拥护者这种行,主正则只能把整串释义吐出来,得再切一刀。用前瞻断言在词性标记前断开,且要求标记前一个字符是中文、逗号或右括号,避免误伤释义中间的正常英文缩写。

SPLIT = re.compile(rf"(?<=[{CJK},,;;、)\w])(?=(?:{POS_PAT})\.)") def split_senses(gloss: str): parts = [p.strip(" ,,;;") for p in SPLIT.split(gloss)] # 丢掉不含中文的残片,例如 "& ad. 漂浮着(的)" 切出来的 "&" return [p for p in parts if any(CJK[0] <= ch <= CJK[-1] for ch in p)] print(split_senses("提倡,n. 提倡者,拥护者"))

前瞻断言(?=...)只判断位置不消耗字符,所以n.本身会留在后半段里,方便你后面再抽一次词性。(?<=...)里的\w是为了兼容airbear这类被污染的英文尾巴。切完必过的过滤是不含中文的残片一律丢,afloat那行会切出孤零零的&,留着只会污染数据库。

3.4 重复词头与多义项的合并策略

原文里 abundant 出现两次(丰富的 / 充足的;富裕的),acquire 出现两次(获得 / 习得),administration、associate、campaign、catalog 同样如此。这些不是错误,是词表按字母序排版时的正常重复。合并原则只有一条:完全相同的释义去重,不同的释义保留为独立义项。

from collections import defaultdict buckets = defaultdict(list) for rec in records: # records 是上一节 parse 的输出 buckets[rec["head_norm"]].append(rec) merged = [] for head_norm, group in buckets.items(): seen = set() for rec in group: key = (rec["pos"], rec["gloss"]) if key in seen: continue seen.add(key) merged.append(rec) dups = {k: len(v) for k, v in buckets.items() if len(v) > 1} print("存在重复词头的数量:", len(dups)) print("重复最多的几个:", sorted(dups.items(), key=lambda x: -x[1])[:5])

key(词性, 释义)二元组而不是只用释义,是因为同一个词的不同词性下释义文字可能偶然相同,合并掉会丢信息。bucketshead_norm小写做键,处理Antarcticantarctic这类大小写不一致的分组。

3.5 必须人工兜底的脏行清单

有一批行正则能命中但结果不可信,必须列清单人工过。典型是airborne airbear 的过去分词,意为在空中,词头被切成了 airborne,但释义主体是错的,正确形态应该是形容词「空运的;在空中」。另一类是airtight vt. 捆绑;捆扎,airtight 是形容词「密封的」,而「捆绑」对应的是 bundle,属于排版串行。

注意:这类行不要试图用规则自动修。规则越复杂,误伤正常词条的概率越高,一份几百词的清单人工过一遍只要十几分钟,比调试规则便宜得多。

4. 存储与导出:SQLite 落库 + Anki TSV

4.1 中间层用 JSONL,不用 CSV

切片结果先落 JSONL。理由很实际:释义里既有中文逗号也有英文逗号,还有括号和斜杠,CSV 的引号转义在 Excel 和 Anki 之间来回倒会反复出问题,JSONL 一行一条、天然支持嵌套字段、还能 diff。

import json with open("toefl_words.jsonl", "w", encoding="utf-8") as f: for rec in merged: rec_out = dict(rec) rec_out["src"] = "红宝书托福词汇" f.write(json.dumps(rec_out, ensure_ascii=False) + "\n")

ensure_ascii=False让它直接写中文而不是\uXXXX,产物用git diff看变更时才有可读性。rec_out["src"]是为多词表合并预留的来源字段,将来再导入第二本词表时,靠它区分同一词头的出处。

4.2 建表、唯一索引与幂等写入

SQLite 单文件、零部署,几百到几十万词条完全够用。唯一索引建在(head_norm, pos, gloss)上,重复导入同一份文件不会产生脏数据。

字段类型可空说明
idINTEGER自增主键
headTEXT原始词头,保留大小写与连字符
head_normTEXT小写归一化,用于分组和关联
posTEXT词性缩写,无标注时为空串
glossTEXT单条释义,多义项已拆开
src_lineINTEGER原文档段落序号,便于回溯
CREATE TABLE IF NOT EXISTS word ( id INTEGER PRIMARY KEY, head TEXT NOT NULL, head_norm TEXT NOT NULL, pos TEXT, gloss TEXT NOT NULL, src_line INTEGER ); CREATE UNIQUE INDEX IF NOT EXISTS ux_word ON word(head_norm, pos, gloss); CREATE INDEX IF NOT EXISTS ix_head ON word(head_norm);

写入时用INSERT OR IGNORE配合唯一索引,重复运行脚本不报错也不增行,这是幂等的前提。ix_head单独建是因为按词头查是最频繁的操作,而ux_word的前缀虽然是head_norm,但索引顺序受posgloss影响,单列查询走它不如走专用索引快。

import sqlite3, json conn = sqlite3.connect("toefl.db") with open("toefl_words.jsonl", encoding="utf-8") as f: for line in f: r = json.loads(line) conn.execute( "INSERT OR IGNORE INTO word(head, head_norm, pos, gloss, src_line) " "VALUES (?,?,?,?,?)", (r["head"], r["head_norm"], r["pos"], r["gloss"], r.get("src_line")), ) conn.commit() print("总行数:", conn.execute("SELECT COUNT(*) FROM word").fetchone()[0])

4.3 四条 SQL 抽检脚本

解析质量靠抽检而不是靠感觉。下面四条覆盖了实际最常出问题的四类。

-- 1. 无词性标注的行,多为复合词或脏行 SELECT head, gloss FROM word WHERE pos IS NULL OR pos = ''; -- 2. 同一词头挂了 3 个以上释义,值得人眼过一遍 SELECT head_norm, COUNT(*) AS c FROM word GROUP BY head_norm HAVING c > 2 ORDER BY c DESC; -- 3. 释义里混进拉丁字母,基本是切分残留 SELECT head, gloss FROM word WHERE gloss GLOB '*[A-Za-z]*'; -- 4. 词头长度异常,可能把两个词粘在一起了 SELECT head, LENGTH(head) AS n FROM word WHERE LENGTH(head) > 18 ORDER BY n DESC;

第 3 条最有用。GLOB 的字符类语法比 LIKE 更适合这种单字符集合判断,能一次性捞出提倡,n. 提倡者这种没切干净的行。第 4 条抓的是词头粘连,一般源于原文某一行缺了空白分隔。

4.4 导出 Anki 可导入的 TSV

Anki 导入认 Tab 分隔的纯文本,字段顺序就是卡片正反面。关键在于关闭 csv 模块的引号包裹,因为 Anki 会把引号当字面量显示出来。

import csv, sqlite3 conn = sqlite3.connect("toefl.db") rows = conn.execute( "SELECT head, pos, gloss FROM word ORDER BY head_norm, id" ).fetchall() with open("toefl_anki.tsv", "w", encoding="utf-8", newline="") as f: w = csv.writer(f, delimiter="\t", quoting=csv.QUOTE_NONE, escapechar="\\") for head, pos, gloss in rows: back = f"{pos}. {gloss}" if pos else gloss w.writerow([head, back, "红宝书托福词汇"])

QUOTE_NONEescapechar="\\"的组合,保证字段里出现的引号被反斜杠转义而不是被包起来;newline=""防止 Windows 下写出\r\r\n。第三列直接当 Anki 的 tag,导入时在「字段 3 映射到标签」里勾一下,后面就能按词表来源筛卡。导入界面选「制表符」分隔,字段 1 正面、字段 2 背面即可。

5. 进阶:词族归并与复习排程

5.1 用前缀锚点加编辑距离把同族词归堆

词表里同族词分散在不同字母段,掐头去尾之后其实高度相关。accumulate 与 accumulation、accelerate 与 acceleration、adapt 与 adaptation/adaptable,把它们归到一族,背一个等于背一串。规则很简单:前缀锚点先命中,命中不了再用编辑距离兜。

from difflib import SequenceMatcher def same_family(a: str, b: str, anchor: int = 6, ratio: float = 0.75) -> bool: # 前 anchor 个字符完全一致,直接判同族 if len(a) >= anchor and len(b) >= anchor and a[:anchor] == b[:anchor]: return True # 兜底:整体相似度达阈值(主要覆盖前缀较短的同族词) return SequenceMatcher(None, a, b).ratio() >= ratio heads = [r[0] for r in rows] for i, h in enumerate(heads[:40]): for j in range(i + 1, min(i + 40, len(heads))): if same_family(h.lower(), heads[j].lower()): print(h, "<->", heads[j])

anchor=6是关键参数,设太小会把 acquire 和 acquaintance 这种词义无关的词黏在一起,设太大会漏掉 adapt/adaptable 这类前缀只有 5 个字符的组合。ratio=0.75是 SequenceMatcher 的经验值,低于它基本是噪声。实际跑的时候先用 anchor 扫一遍看误报率,再决定是否开兜底分支。

5.2 干扰项生成与简化版 SM-2 排程

有结构化数据之后,选择题干扰项可以自动生成:同词性优先、拼写相似度落在 0.5 到 0.75 之间的词做干扰项,难度刚好。排程用简化版 SM-2,只保留三个变量。

变量含义初始值更新规则
ef难度因子2.5答对时ef += 0.1,答错时ef -= 0.2
n连续答对次数0答对加 1,答错清零
iv下次间隔(天)1iv = 1 if n<2 else round(iv * ef)
def review(state, correct: bool): ef, n, iv = state if correct: n += 1 ef = round(ef + 0.1, 2) iv = 1 if n < 2 else max(1, round(iv * ef)) else: n = 0 ef = max(1.3, round(ef - 0.2, 2)) iv = 1 return (ef, n, iv)

ef的下限锁在 1.3,否则连续答错会让间隔被压到接近 0,卡片天天出现反而打击复习节奏。ivround而不是直接截断,避免浮点累积误差把间隔算成 0 天。把这个函数的结果写回数据库的next_review字段,每天只查next_review <= 今天的词条,一份几百词的词表不用任何第三方记忆软件也能跑自己的复习队列。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 4:25:48

基于投影寻踪-博弈论-云模型的滑坡风险评价MATLAB实现

简介&#xff1a;这份资料围绕MATLAB实现投影寻踪博弈论-云模型的滑坡风险评价展开&#xff0c;面向地质灾害研究者、城市规划与环境科学从业者及灾害应急管理人员&#xff0c;提供从数据采集与预处理、投影寻踪博弈论建模、云模型处理到风险评估输出的一体化项目范例。资源包仅…

作者头像 李华
网站建设 2026/9/20 4:24:53

5G相控阵仿真全解析:从波束成形原理到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 4:24:12

RapidOCR 文字识别入门:三步跑通你的第一个 OCR 任务

RapidOCR 文字识别入门&#xff1a;三步跑通你的第一个 OCR 任务 【免费下载链接】RapidOCR &#x1f4c4; Awesome OCR multiple programing languages toolkits based on ONNX Runtime, OpenVINO, MNN, PaddlePaddle, TensorRT and PyTorch. 项目地址: https://gitcode.com…

作者头像 李华
网站建设 2026/9/20 4:23:34

具身智能时代开启:从仿真到真机的完整学习路径与核心技术栈解析

从2027年这场发布会往回看&#xff0c;具身智能这条赛道在过去两年的升温速度&#xff0c;其实超出了很多人预期。华清远见做嵌入式与物联网教育起家&#xff0c;这次以"与智共生 具身未来"为主题发布新品&#xff0c;等于把自家产品和课程体系整体押注到了"物理…

作者头像 李华
网站建设 2026/9/20 4:23:27

Token是什么?认证、API与大模型中的三重身份解析

你有没有过这种经历&#xff1a;打开某个软件突然提示"token失效"&#xff0c;登录一个开发工具时看见"token exchange failed"&#xff0c;给大模型API充值发现按token计费……同一个词"Token"&#xff0c;在登录认证、API密钥、AI计费这几个场…

作者头像 李华