全国大学生英语竞赛词汇PDF解析与词频标注:构建可检索复习系统
简介这份PDF资料面向备战全国大学生英语竞赛的本科学生聚焦竞赛高频核心词汇帮助考生在有限时间内系统扩充词汇量、提升阅读理解与语言应用能力。内容按字母顺序编排覆盖A至C开头的重点词条如abbreviation、abolish、ambiguous、advocate、amplitude等每个词条标注词性、核心释义及常见搭配兼顾抽象概念与科技、商务、学术等语境下的用法便于对照记忆与查漏补缺。资源包共1个PDF文件大小约199KB轻量易存适合手机或电脑随时翻阅。目前已有3186人学习下载可作为竞赛冲刺阶段的随身词汇手册也可配合阅读、写作练习反复巩固帮助考生在词汇辨析与语境理解上稳步提分。1. 从一份词汇 PDF 到可检索词库这件事到底解决什么问题每年到了备赛季总有人问我全国大学生英语竞赛必备词汇本科PDF 拿到手了然后呢多数人的做法是打印出来从 A 开始背背到 C 就放弃了。问题不在毅力在于 PDF 是给人眼看的不是给检索和复习用的。它没有词频排序没有掌握度标记没有按题型分类你翻到 abandon 的时候永远不知道这个词在竞赛里到底考过几次、属于哪类题型。我前后帮几个同学做过词汇 PDF 的二次加工核心思路就一句话把静态 PDF 拆成结构化数据再按竞赛的考查逻辑重新组织。这件事适合两类人——一是正在备赛、想用最短时间覆盖高频词的本科生二是手里有 PDF 但不知道怎么让它“活”起来的人。下面我从 PDF 解析、词表清洗、词频标注、复习系统搭建一路讲到避坑每一步都给可复现的命令和参数。2. 把 PDF 拆成结构化词表解析工具选型与最小可用流程2.1 为什么直接复制粘贴一定会翻车很多人第一步就是把 PDF 里的文字全选复制到 Word 或记事本然后按行拆词。这个做法在简单单栏 PDF 上勉强能用但竞赛词汇 PDF 通常有几种典型排版双栏、带音标列、带词性缩写、带页码页眉。直接复制的结果是音标和释义混在一起双栏内容交错页眉页码穿插在词条中间。我见过最离谱的一份复制出来每个词后面都跟着一个数字那是页码被当成了词频。所以第一步不是复制是解析。解析的目标是拿到带坐标的文本块再按坐标重建阅读顺序。常见做法是用 Python 的 pdfplumber 或 PyMuPDF前者对表格和坐标支持更好后者速度更快。我一般先用 pdfplumber 做一页的坐标探查确认排版结构后再批量处理。2.2 用 pdfplumber 探查页面结构先装依赖然后写一个探查脚本把第一页每个字符的坐标和文本打出来看清楚词条是怎么排的。# probe_pdf.py # 探查 PDF 第一页的文本块坐标判断排版结构 import pdfplumber PDF_PATH vocab.pdf # 替换为你的 PDF 路径 with pdfplumber.open(PDF_PATH) as pdf: page pdf.pages[0] # 先看第一页 print(f页面尺寸: {page.width} x {page.height}) # 提取所有单词及其坐标 words page.extract_words( keep_blank_charsFalse, use_text_flowFalse, extra_attrs[size] # 额外拿字号用于区分词条和释义 ) for w in words[:40]: # 先看前40个 print(ftext{w[text]:20} x0{w[x0]:.1f} x1{w[x1]:.1f} top{w[top]:.1f} size{w.get(size, 0):.1f})这段代码的关键在extract_words的参数。keep_blank_charsFalse会把连续空格合并避免一个词被拆成多个块extra_attrs[size]把字号带出来因为词条通常比释义字号大这是后面区分词条和释义的依据。跑完之后你看输出如果同一行的词 x0 差距很大说明是双栏如果 top 值相近但 x0 分两簇也是双栏。字号那一列如果词条明显大于释义就可以用字号阈值做过滤。2.3 按坐标重建词条双栏和单栏的处理差异探查清楚后正式解析要按栏切分。单栏直接按 top 排序即可双栏要先按 x0 中位数把页面分成左右两半各自按 top 排序再拼接。下面是一个可复用的解析脚本输出 CSV。# parse_pdf.py # 将 PDF 解析为结构化 CSV处理单栏和双栏 import pdfplumber import csv import re PDF_PATH vocab.pdf OUT_CSV vocab_raw.csv def is_two_column(words, page_width): 通过 x0 分布判断是否双栏 if not words: return False mid page_width / 2 left sum(1 for w in words if w[x0] mid) right sum(1 for w in words if w[x0] mid) # 两侧都有足够多的词且比例接近判为双栏 return left 5 and right 5 and 0.4 left / (left right) 0.6 def parse_page(page): words page.extract_words(extra_attrs[size]) if not words: return [] two_col is_two_column(words, page.width) mid page.width / 2 if two_col: left sorted([w for w in words if w[x0] mid], keylambda w: (round(w[top]), w[x0])) right sorted([w for w in words if w[x0] mid], keylambda w: (round(w[top]), w[x0])) ordered left right else: ordered sorted(words, keylambda w: (round(w[top]), w[x0])) return ordered def merge_lines(words, y_tol3): 把 top 相近的词合并成一行 lines [] cur [] last_top None for w in words: if last_top is None or abs(w[top] - last_top) y_tol: cur.append(w) else: lines.append(cur) cur [w] last_top w[top] if cur: lines.append(cur) return [ .join(w[text] for w in line) for line in lines] rows [] with pdfplumber.open(PDF_PATH) as pdf: for i, page in enumerate(pdf.pages): words parse_page(page) lines merge_lines(words) for line in lines: line line.strip() if not line: continue # 过滤页眉页码纯数字或过短的行 if re.fullmatch(r\d, line) or len(line) 2: continue rows.append({page: i 1, raw: line}) with open(OUT_CSV, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[page, raw]) writer.writeheader() writer.writerows(rows) print(f共解析 {len(rows)} 行输出到 {OUT_CSV})逻辑说明is_two_column用 x0 分布判断栏数阈值 0.4 到 0.6 是经验值太偏就说明不是均匀双栏。merge_lines的y_tol3是纵向容差PDF 里同一行的词 top 值可能有 1 到 2 像素的抖动设 3 能合并又不至于把相邻行误合。过滤规则里re.fullmatch(r\d, line)干掉纯数字页眉页码len(line) 2干掉单字符噪声。跑完你会得到一个 raw 列里面是“单词 音标 词性 释义”混在一起的行下一步就是清洗。2.4 词条字段拆分正则的边界在哪raw 行要拆成 word、phonetic、pos、meaning 四列。不同 PDF 格式差异很大但常见模式是单词开头后面跟 /.../ 或 [...] 音标再跟 n./v./adj. 等词性最后是中文释义。正则不能写太死否则遇到变体就崩。我一般用分组捕获允许音标缺失。# clean_vocab.py # 将 raw 行拆分为结构化字段 import csv import re IN_CSV vocab_raw.csv OUT_CSV vocab_clean.csv # 词性缩写集合用于定位词性位置 POS_SET {n, v, vt, vi, adj, adv, prep, conj, pron, art, num, int, aux} def split_line(line): # 尝试匹配单词 可选音标 可选词性 释义 # 音标用 /.../ 或 [...] 包裹 m re.match(r^([A-Za-z][A-Za-z\-\ ]*?)\s*(/[^/]/|\[[^\]]\])?\s*((?:%s)\.)?\s*(.)$ % |.join(POS_SET), line) if not m: return None word m.group(1).strip() phonetic (m.group(2) or ).strip() pos (m.group(3) or ).strip().rstrip(.) meaning m.group(4).strip() # 单词里不应有空格如果有说明匹配错了 if in word: return None return {word: word, phonetic: phonetic, pos: pos, meaning: meaning} rows [] with open(IN_CSV, encodingutf-8) as f: for r in csv.DictReader(f): parsed split_line(r[raw]) if parsed: parsed[page] r[page] rows.append(parsed) with open(OUT_CSV, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[word, phonetic, pos, meaning, page]) writer.writeheader() writer.writerows(rows) print(f清洗后 {len(rows)} 条输出到 {OUT_CSV})参数说明POS_SET覆盖了常见词性缩写如果你的 PDF 用了别的缩写比如“pl.”表示复数要手动加进去。正则里([A-Za-z][A-Za-z\-\ ]*?)用非贪婪匹配单词部分允许连字符和撇号但最后用if in word兜底防止把“a lot”这种短语误拆。音标部分(/[^/]/|\[[^\]]\])?是可选的问号保证没有音标也能匹配。跑完后建议人工抽查 20 行重点看多词性和多释义的词有没有被截断。3. 词频标注与竞赛题型映射让词表有优先级3.1 为什么必须做词频标注一份竞赛词汇 PDF 通常收录几千词但真正高频的也就几百个。没有词频你就是在均匀用力这是备赛最大的浪费。词频来源有两个一是 PDF 自带的标注有些版本会标星级或考频二是用历年真题语料自己统计。前者如果有直接解析后者需要你手上有真题文本。我一般建议两者结合PDF 有标注就用标注没有就用真题语料统计再人工校准。3.2 用真题语料统计词频的脚本假设你有一份真题文本合集exam_corpus.txt每行是一篇阅读或一段完形。统计时要注意先转小写去标点再做词形还原lemmatization否则 run/running/ran 会被当成三个词。用 nltk 的 WordNetLemmatizer 做还原配合词性标注效果更好但为了脚本简洁这里用规则还原。# word_freq.py # 基于真题语料统计词频并做词形还原 import re from collections import Counter from nltk.stem import WordNetLemmatizer import nltk nltk.download(wordnet, quietTrue) nltk.download(omw-1.4, quietTrue) CORPUS exam_corpus.txt VOCAB_CSV vocab_clean.csv OUT_CSV vocab_freq.csv lemmatizer WordNetLemmatizer() def normalize(word): word word.lower() # 简单规则还原先按动词还原再按名词 w lemmatizer.lemmatize(word, posv) w lemmatizer.lemmatize(w, posn) return w # 统计语料词频 counter Counter() with open(CORPUS, encodingutf-8) as f: for line in f: tokens re.findall(r[a-zA-Z][a-zA-Z\-], line) for t in tokens: counter[normalize(t)] 1 # 给词表打词频 import csv rows [] with open(VOCAB_CSV, encodingutf-8) as f: for r in csv.DictReader(f): key normalize(r[word]) r[freq] counter.get(key, 0) rows.append(r) # 按词频降序方便优先复习 rows.sort(keylambda x: -x[freq]) with open(OUT_CSV, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[word, phonetic, pos, meaning, freq, page]) writer.writeheader() writer.writerows(rows) print(f词频标注完成最高频词{rows[0][word]} ({rows[0][freq]}))逻辑说明normalize先按动词还原再按名词还原能覆盖大部分屈折变化。re.findall(r[a-zA-Z][a-zA-Z\-], line)只抓字母词过滤数字和标点。词频为 0 的词不是没用而是没在真题语料里出现可能是 PDF 收录但竞赛不常考复习优先级可以往后放。注意如果你的真题语料是扫描版 OCR 出来的噪声会很大建议先人工校对一遍再统计。3.3 把词频映射到竞赛题型竞赛题型大致分听力、词汇语法、完形、阅读、翻译、写作。不同题型对词汇的要求不同听力要求“听到能反应”阅读要求“看到能认识”写作要求“能拼写会用”。所以词表不能只按词频排还要按题型打标签。常见做法是从真题里按题型分语料分别统计词频一个词在多个题型里高频就是核心词。题型语料来源词汇要求复习动作听力听力原文音形义快速关联听写、跟读词汇语法单选题干辨析、搭配做错题、记搭配完形完形文本上下文推断填空练习阅读阅读文本快速识别限时阅读翻译翻译题干拼写、用法默写、造句写作范文主动输出仿写、替换这张表不是让你每个题型都单独做一遍而是告诉你同一个词在不同题型下的复习方式不同。比如“account”在阅读里认识就行在写作里你得会“account for”的用法。我一般会在词表里加一列tags用逗号分隔题型标签复习时按标签筛选。4. 搭建可检索复习系统从 CSV 到 Anki 和本地查询4.1 生成 Anki 导入文件词表清洗完最直接的落地就是导入 Anki 做间隔重复。Anki 支持 CSV 导入但要注意字段分隔符和 HTML 转义。下面脚本把vocab_freq.csv转成 Anki 可导入的格式正面是单词和音标背面是词性、释义、词频和题型标签。# to_anki.py # 生成 Anki 导入用的 TSV 文件 import csv import html IN_CSV vocab_freq.csv OUT_TSV anki_import.tsv with open(IN_CSV, encodingutf-8) as f, open(OUT_TSV, w, encodingutf-8) as out: reader csv.DictReader(f) for r in reader: front f{html.escape(r[word])}brspan stylecolor:gray{html.escape(r[phonetic])}/span back (fb{html.escape(r[pos])}/b {html.escape(r[meaning])}br f词频: {r[freq]}) # Anki 用 tab 分隔字段换行用 br out.write(f{front}\t{back}\n) print(f生成 {OUT_TSV}在 Anki 中选择「文件-导入」即可)参数说明Anki 导入时字段分隔符选 Tab允许 HTML。html.escape防止释义里的被当成标签。词频放在背面复习时能看到优先级。导入后建议建一个“竞赛词汇”牌组每天新卡不超过 50否则复习量会爆炸。4.2 本地命令行查询用 sqlite 做快速检索Anki 适合复习但如果你想快速查一个词在词表里的信息开 Anki 太慢。我一般会把词表导入 sqlite写一个命令行查询脚本支持按单词、词性、词频范围查。# build_db.py # 把词表导入 sqlite方便快速查询 import csv import sqlite3 DB vocab.db CSV vocab_freq.csv conn sqlite3.connect(DB) conn.execute(DROP TABLE IF EXISTS vocab) conn.execute( CREATE TABLE vocab ( word TEXT PRIMARY KEY, phonetic TEXT, pos TEXT, meaning TEXT, freq INTEGER, page INTEGER ) ) with open(CSV, encodingutf-8) as f: rows [(r[word], r[phonetic], r[pos], r[meaning], int(r[freq]), int(r[page])) for r in csv.DictReader(f)] conn.executemany(INSERT OR REPLACE INTO vocab VALUES (?,?,?,?,?,?), rows) conn.commit() print(f导入 {len(rows)} 条到 {DB})建库后查询就一行命令的事。比如查高频词# 查询词频大于等于 5 的词按词频降序 sqlite3 -header -column vocab.db SELECT word, pos, meaning, freq FROM vocab WHERE freq 5 ORDER BY freq DESC LIMIT 20;再比如查某个词根相关的词# 查询包含 spect 的词 sqlite3 -header -column vocab.db SELECT word, meaning, freq FROM vocab WHERE word LIKE %spect% ORDER BY freq DESC;这种查询在备赛后期特别有用你想集中突破某一类词比如所有带 spect 的词或者想看看高频词里还有哪些没掌握sqlite 比翻 PDF 快得多。4.3 用标签做题型筛选如果词表里有tags列可以在 sqlite 里加一列查询时按标签过滤。比如只看写作高频词sqlite3 -header -column vocab.db SELECT word, meaning, freq FROM vocab WHERE tags LIKE %writing% ORDER BY freq DESC LIMIT 30;这一步的价值在于备赛时间有限时你可以按题型优先级分配时间。听力词汇先过一遍写作词汇重点默写阅读词汇混个眼熟。词表从“一份 PDF”变成了“一个可按需筛选的数据库”这才是它该有的样子。5. 避坑与排查PDF 解析和词表清洗的 5 个血泪教训5.1 音标里的斜杠把正则搞崩了现象清洗后大量词条的 meaning 字段为空word 字段里混进了音标。原因正则里音标部分用了/[^/]/但有些 PDF 的音标用双斜杠//或者音标内部本身有斜杠比如表示可选发音。解决先把音标模式放宽为/[^/]{1,50}/限制长度防止跨行匹配如果还不行改用“先定位词性再往前找音标”的策略因为词性位置比音标稳定。5.2 双栏 PDF 的阅读顺序错乱现象解析出来的词条顺序是乱的A 栏的词和 B 栏的词交错。原因is_two_column判断失误或者页面有跨栏标题。解决不要只靠 x0 中位数先用page.extract_words看 top 值的分布如果同一 top 值上左右都有词基本就是双栏。跨栏标题单独处理标题行的 x0 通常横跨两栏可以按宽度过滤掉。我一般会先跑一页把解析结果打印出来人工确认再批量跑。5.3 词形还原把专有名词还原错了现象统计词频时“China”被还原成“china”“US”被还原成“u”。原因normalize无条件转小写并还原没有排除专有名词。解决在统计前先做命名实体识别或者简单规则如果单词首字母大写且不在句首跳过还原。更稳妥的做法是维护一个停用词表把常见专有名词排除。竞赛语料里专有名词不多但一旦混进去会污染词频。5.4 Anki 导入后卡片显示乱码现象导入 Anki 后释义里的中文变成问号或方块。原因TSV 文件编码不是 UTF-8或者 Anki 导入时编码选错。解决生成 TSV 时显式用encodingutf-8Anki 导入界面里编码选 UTF-8。如果还有问题检查是否有 BOM用utf-8-sig写入。另外HTML 里的br要确保 Anki 允许 HTML 字段。5.5 词频统计把 PDF 页眉页码算进去了现象词频最高的词是“Page”或“Unit”。原因解析时没有过滤页眉页码这些词在每页都出现词频自然高。解决在parse_pdf.py的过滤规则里除了纯数字还要过滤掉重复出现的短行。一个实用技巧统计所有行出现的次数出现次数超过页数 80% 的行大概率是页眉页脚直接删掉。这个规则比硬编码“Page”更通用。6. 进阶用词向量找近义词和易混词词表做到这一步已经能支撑备赛了。但如果你想再进一步比如找出易混词、生成近义词替换练习可以用词向量。常见做法是下载一个预训练的 GloVe 或 fastText 向量用词表里的词去查最近邻。注意不要用需要联网的 API本地跑就行。# similar_words.py # 用预训练词向量找近义词辅助易混词辨析 import numpy as np # 假设你有一个本地词向量文件 vectors.txt格式word v1 v2 ... VEC_FILE vectors.txt VOCAB_CSV vocab_freq.csv def load_vectors(path, vocab_set): vectors {} with open(path, encodingutf-8) as f: for line in f: parts line.rstrip().split( ) if len(parts) 10: continue w parts[0] if w in vocab_set: vectors[w] np.array([float(x) for x in parts[1:]]) return vectors import csv vocab_set set() with open(VOCAB_CSV, encodingutf-8) as f: for r in csv.DictReader(f): vocab_set.add(r[word].lower()) vecs load_vectors(VEC_FILE, vocab_set) print(f加载 {len(vecs)} 个词向量) def nearest(word, topn5): if word not in vecs: return [] target vecs[word] sims [] for w, v in vecs.items(): if w word: continue # 余弦相似度 sim np.dot(target, v) / (np.linalg.norm(target) * np.linalg.norm(v) 1e-9) sims.append((w, sim)) sims.sort(keylambda x: -x[1]) return sims[:topn] # 示例找 affect 的近义词 for w, s in nearest(affect): print(f{w}: {s:.3f})逻辑说明load_vectors只加载词表里出现的词减少内存占用。nearest用余弦相似度分母加1e-9防止除零。跑出来如果 affect 的最近邻是 influence、impact说明向量质量可用如果出来一堆无关词可能是向量维度或语料不匹配。这个方法的边界是词向量只能给候选不能替代人工判断。易混词辨析最终还是要看搭配和语境向量只是帮你缩小范围。我自己的习惯是词表清洗完先跑一遍词频把前 500 词打印出来贴墙上每天过一遍然后用 sqlite 按题型筛写作词单独默写。这套流程跑下来从 PDF 到可复习系统大概两三个小时但省下的翻 PDF 时间远不止这些。希望帮到你。本文还有配套的精品资源点击获取