简介:人教版高一英语必修二总复习单项选择题是一份面向高一学生与英语教师的复习资料,针对必修二常考语法点和词汇搭配设计,可帮助练习者在考前快速梳理易错考点,也能为教师选题组卷或课堂小测提供现成素材。题目围绕高频短语、定语从句、主语从句、不定式结构、连词辨析、强调句型、情态动词推测等模块展开,内容涵盖“think highly of”搭配、search相关短语区别、非限制性定语从句中“the larger of which”的指代、固定句式“It is known to all that...”、主动表被动的不定式、besides在语境中的补充说明用法,以及强调句主谓一致等核心知识点。每道题均配有逐项答案讲解,既写明正确选项依据,也分析干扰项的排除逻辑,并补充“be considered to have done”“can't have found”等延伸结构,便于自主订正与查漏补缺。资源为1个doc文档,约52KB,支持打印或电子端阅读,轻量实用。目前已有87人学习下载,适合高一阶段期末复习、单元巩固或考前冲刺使用。
1. 一份 .doc 复习题,为什么值得用工程方式重做一遍
拿到「人教版高一英语必修二总复习单项选择题.doc」这个文件名,大部分人第一反应是双击打开、打印、做题。但如果你是 IT 岗,视线会落在另外三件事上:这是老式二进制 .doc 而不是 .docx,说明文件在教师电脑里至少流转了十年;题目是次生文本结构,题号、题干、选项、答案之间存在稳定规律,可以把数据抽出来重新组织;复习场景天然需要反复自测,而静态文档没有办法按错题自动重排、也没有办法统计正确率。三点叠在一起,就是一套「文档解析 + 小工具开发」的标准任务。下面按「抽取文本 → 结构化解题 → 落地自测」的顺序完整走一遍,新手能照着跑通,熟手可以直接拿走 .doc 中文排版和答案分离上的几个边界细节,省去自己趟坑的时间。
2. 先把 .doc 里的题目无损抽出来:工具链选型
2.1 为什么 python-docx 读不了这份 .doc
python-docx 是处理 Word 文本最常用的 Python 库,但有一个硬边界:它只支持 .docx,也就是 Office 2007 之后的 OOXML 格式。遇到 .doc(OLE2 复合文档),在构造 Document 对象时就会直接抛异常,这不是库的缺陷,而是两种格式的底层组织方式完全不同,.doc 内部是二进制流,没有公开的纯文本读取接口。所以常见的从业方案不是硬写二进制解析,而是先把 .doc 转成 .docx 或纯文本,再交给下游处理。
转换工具有三个主流选择,区别主要落在中文支持上。antiword 对 GBK 编码的中文段落支持一般,偶尔丢字;catdoc 抽取速度快,但对混合排版和表格处理得很粗糙;LibreOffice 的无头模式兼容性最好,既能转 .docx 保留格式线索,也能直接转 txt 抽纯文本。对一份以选择题为主的复习文档,我一般直接用 LibreOffice 转 .docx,因为后续解析如果发现需要靠加粗、下划线定位知识点时还有退路;如果确认只要文字,转 txt 更快,也是后面解析脚本的直接输入。
| 工具 | 中文支持 | 输出格式 | 适用场景 |
|---|---|---|---|
| antiword | 一般,GBK 段落易丢字 | txt | 纯英文或 UTF-8 文档 |
| catdoc | 一般 | txt | 只取文字、不看排版 |
| LibreOffice headless | 好 | docx / txt / html | 中文复习卷、需保留格式 |
2.2 LibreOffice 无头模式批量转 .docx 的命令
单文件转换的最简命令是这样:
soffice --headless --convert-to docx --outdir ./converted "人教版高一英语必修二总复习单项选择题.doc"参数含义:--headless表示不启动图形界面,适合作业脚本里调用;--convert-to docx指定输出格式;--outdir指定输出目录;最后一个位置参数是输入文件。转换成功的标志是退出码为 0,并且在 ./converted 目录下出现同名的 .docx 文件。如果退出码非 0,先检查 soffice 是否在 PATH 里,再检查用户主目录是否可写,LibreOffice 首次运行会创建 .config 目录,权限不足时命令会静默失败,不报任何可见错误。
批量场景更常见。教师手里往往不止一份复习卷,同一目录下可能堆着七八个 .doc,逐个执行太慢,一条 for 循环就能解决:
mkdir -p ./converted for f in *.doc; do soffice --headless --convert-to docx --outdir ./converted "$f" >/dev/null 2>&1 echo "done: $f" done这段脚本把当前目录下所有 .doc 转成 .docx。>/dev/null 2>&1是为了屏蔽 LibreOffice 每次转换都会打印的 Profile 初始化信息,不然日志会被刷屏。注意 $f 必须加引号,中文文件名和带空格的文件名是 .doc 时代最常见的两种命名习惯,不加引号会把文件名拆成多个参数,这是实际项目里踩得最多的一处。
转换之后我习惯再多转一份纯文本,用于快速浏览和后续正则解析:
soffice --headless --convert-to txt:Text --outdir ./txt "人教版高一英语必修二总复习单项选择题.doc"这里显式写txt:Text而不是直接写--convert-to txt,是因为 txt:Text 是 Writer 的文本导出过滤器名,在部分 Linux 发行版上显式指定过滤器名更稳定,不会触发无过滤器的歧义告警。转出来的 .txt 默认按 UTF-8 编码,正好避开 Python 读取 GBK 文本的编码包袱。需要记住一个差异:如果源文件里选项 A/B/C/D 各占一行,转换后保留为独立段落;如果选项和题干挤在同一行,转换后也在同一行。这个差异直接决定下一章正则的写法,所以转换完先别急着写脚本,花一分钟看排版。
2.3 中文编码与题号乱码的定位方法
转完文本后的第一件事不是写解析脚本,而是确认三件事:中文是否正常、题号是否连续、答案栏是否在文末。用 head 快速看前 30 行:
head -30 "人教版高一英语必修二总复习单项选择题.txt"如果中文显示正常但出现成片问号,说明源 .doc 的编码不是 UTF-8,LibreOffice 按系统 locale 做了猜测,猜错了。解决办法是在转换命令前设置环境变量:
export LANG=zh_CN.UTF-8 export LC_ALL=zh_CN.UTF-8 soffice --headless --convert-to txt:Text --outdir ./txt "人教版高一英语必修二总复习单项选择题.doc"LANG 和 LC_ALL 同时设,是因为不同发行版对这两个变量的优先级处理不一致,只设一个在某些环境里不生效。还有一个隐蔽问题:.doc 里的题号可能是 Word 自动编号列表,而不是手打的数字。自动编号在转纯文本时可能保留成「数字 + 制表符」,也可能直接丢失。判断方法是用 grep 统计题号行数,和文档总题数对比:
grep -cE '^[0-9]+[\.、]' "人教版高一英语必修二总复习单项选择题.txt"如果统计结果明显小于题目总数,说明源文件用了自动编号,转换时编号被吞掉了。这种文档我放弃纯文本路线,改用转 .docx 后配合 python-docx 遍历段落读取,因为自动编号在 python-docx 里会作为文本拼进 paragraph.text,反而更完整。注意 LibreOffice 命令行一次只能处理一种输出格式,要同时拿 docx 和 txt 就得跑两次,批量几十份文件时这个开销可以接受。
3. 用正则把单选题拆成题干、选项、答案三张表
3.1 先看懂人教版必修二单选题的排版规律
人教版高一英语必修二的选择题集中在语法和词汇辨析,题干大多是单句或简短对话,选项固定四个,答案栏一般在文末,形如「1—5 ABCDA 6—10 BCDAB」。实际文档常见两种排版:第一种是每题一个自然段,题干和四个选项混排在同一行;第二种是题干一行、四个选项各占一行。两种排版的正则写法完全不同,所以解析前的「观察排版」不是走过场,而是整个流程里决定成败的一步。
观察排版最可靠的办法是用 cat -A 看真实控制字符,比肉眼判断靠谱得多:
cat -A "人教版高一英语必修二总复习单项选择题.txt" | head -40cat -A 会把行尾的 $、制表符的 ^I 以及所有不可见字符原样显示出来。重点看三处:题号后面是半角句点还是全角顿号;选项之间是空格还是换行;选项字母后面是点、顿号还是右括号。这三个细节决定正则的写法,也决定脚本要不要做预处理。从这些年接触过的复习卷来看,分隔符的写法并不统一,换过几个老师手的文档尤其混乱。
常见分隔写法汇总如下:
| 源文件写法 | 对应正则片段 | 说明 |
|---|---|---|
A. seeing | [A-D]\. | 半角点,最常见的写法 |
A、seeing | [A-D]、 | 全角顿号,早期文档多见 |
A) seeing | [A-D]\) | 右括号,手打文档常见 |
A.seeing | [A-D]. | 全角点,需要和半角点分开处理 |
3.2 Python 解析脚本:从纯文本到 JSON
下面给出一份可直接运行的解析脚本,按「题干在前、选项在后、答案在文末」的常规结构处理。脚本先做排版判断,再选择对应的选项提取正则,避免同行混排和跨行排列互相干扰:
import json import re def normalize(text): # 全角空格和全角括号统一转半角,避免正则匹配时失效 text = text.replace('\u3000', ' ') text = text.replace('(', '(').replace(')', ')') return text def one_line(s): # 把连续空白(含换行)压缩成单个空格 return re.sub(r'\s+', ' ', s).strip() def parse_answer_keys(text): """从文末提取形如 '1-5 ABCDA 6-10 BCDAB' 的答案段""" keys = {} pattern = re.compile(r'(\d+)\s*[-—]\s*(\d+)\s+([A-D]{5})') for m in pattern.finditer(text): start, end, letters = int(m.group(1)), int(m.group(2)), m.group(3) for i, ch in enumerate(letters): keys[start + i] = ch return keys def extract_options(block): # 同一行出现两个选项字母,判定为同行混排 if re.search(r'[A-D][\.、\)]\s+[A-D][\.、\)]', block): opts = re.findall(r'([A-D])[\.、\)]\s*([^\n]+)', block) else: # 跨行模式:非贪婪匹配到下一个选项字母或文本结束 opt_re = re.compile( r'([A-D])[\.、\)]\s*(.*?)' r'(?=\n\s*[A-D][\.、\)]|\Z)', re.S ) opts = opt_re.findall(block) return [(k, one_line(v)) for k, v in opts if one_line(v)] def parse_choices(text): text = normalize(text) # 按题号切分:换行后紧跟数字和点号/顿号的位置作为切分点 blocks = re.split(r'\n(?=\d+[\.、])', text.strip()) questions = [] for block in blocks: m = re.match(r'(\d+)[\.、]', block) if not m: continue qid = int(m.group(1)) opts = extract_options(block) if len(opts) < 4: continue # 题干清理:先剥掉同行选项文本,再删除独立成行的选项行 stem = block for k, v in opts: stem = stem.replace(f'{k}. {v}', '') stem = stem.replace(f'{k}、{v}', '') lines = [ln for ln in stem.split('\n') if not re.match(r'^\s*[A-D][\.、\)]', ln)] stem = one_line(' '.join(lines)) stem = re.sub(r'^(\d+)[\.、]\s*', '', stem) questions.append({ 'id': qid, 'stem': stem, 'options': [{'key': k, 'text': v} for k, v in opts], 'answer': None }) return questions if __name__ == '__main__': with open('人教版高一英语必修二总复习单项选择题.txt', encoding='utf-8') as f: content = f.read() qs = parse_choices(content) keys = parse_answer_keys(content) for q in qs: q['answer'] = keys.get(q['id']) # 只保留能匹配到答案的题目,过滤半成品文档 qs = [q for q in qs if q['answer']] with open('questions.json', 'w', encoding='utf-8') as f: json.dump(qs, f, ensure_ascii=False, indent=2) print(f'parsed {len(qs)} questions')几个关键点展开说明。re.split 用的是零宽断言(?=\d+[\.、]),只在「换行后紧跟数字与分隔符」的位置切分,题号本身不会被吞掉,切出的块天然以题号开头。extract_options 先做排版判定:同一行里出现两个选项字母,说明是混排,用[^\n]+按行抓取;否则用跨行模式,非贪婪匹配到下一个选项字母或块结束。这个分支判断是脚本能同时兼容两种排版的核心,没有这一步,混排文档会把一整行都吞进第一个选项。
题干清理分两步走:先 replace 剥掉和题干同行的选项文本,再把剩余文本里独立成行的选项行删掉,最后用 one_line 压缩空白。答案解析单独拆成函数,因为答案栏格式和题干完全不同,(\d+)\s*[-—]\s*(\d+)\s+([A-D]{5})要求每行是「起始题号 + 连字符 + 结束题号 + 五个字母」,一次解析五道题。最终保存时只保留能匹配到答案的题目,这一步天然过滤掉没附答案的半成品卷子。
3.3 三个最容易解析失败的边界
第一个边界是选项文本里出现大写字母开头的词组。比如选项是「The Great Wall」这类专有名词,跨行模式的非贪婪匹配会在「A. The Great Wall」和大写单词之间误判,提前把选项截断。缓解办法是给选项文本加长度下限,或者要求选项文本不能以句号结尾;具体取舍要看实际语料,这也是为什么解析脚本一定要先跑一遍再调正则,而不是指望一次写对。
提示:解析结果不要直接覆盖保存。第一次跑完先看 questions.json 里的总数和题干文本,确认没有把答案行当成题干、没有把选项吞进题干后再往下走。
第二个边界是题号重复。部分复习卷按单元拆开排版,第二单元重新从 1 开始编号,解析出来会出现两个 id 为 1 的题目。处理办法是解析完成后统一做一次 id 校验:
ids = [q['id'] for q in qs] dup = sorted({i for i in ids if ids.count(i) > 1}) if dup: print('duplicated ids:', dup)发现重复后,要么在上游给第二单元加题号偏移量,要么在自测阶段改用列表下标代替题号作为唯一标识。后者更省事,因为题号本身对刷题没有语义作用,只是给人看的编号。
第三个边界是全角标点混用。同一份文档里,有的题号后面是半角点,有的后面是全角顿号,甚至出现全角点「.」,normalize 函数处理了空格和括号,但没有处理全角点。在 normalize 里补一行text = text.replace('.', '.')就能兜住。这行看起来不起眼,但在真实文档里能避免一半以上的解析失败。
4. 把 JSON 题目变成命令行自测系统
4.1 为什么选 JSON 做中间格式
解析结果存成 JSON 而不是直接落 SQLite,是刻意的选择。选择题数据量撑死几百条,关系型数据库的索引和事务优势完全用不上;JSON 是纯文本,可以直接用 git 管理,后续调整解析规则时能清楚看到数据变化;命令行工具加载 JSON 只依赖标准库的 open 加 json.load,零第三方依赖。另一个好处是解耦:解析脚本和自测工具各管各的,改解析规则不需要动自测程序,反过来自测要加错题统计也改不到解析层。如果后面想做成网页版刷题,JSON 同样可以直接喂给前端,不需要再写一层接口。
自测系统用到的完整字段结构如下,四个字段足够支撑随机出题和错题统计,不需要额外冗余字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 题号,解析阶段已去重 |
| stem | string | 题干文本 |
| options | array | 选项列表,每项含 key 与 text |
| answer | string | 正确选项的 key,如A |
4.2 自测工具的完整代码与参数说明
下面的工具支持顺序刷题、选项乱序、错题重做三个基本功能,全部只依赖标准库:
import json import random def load(path='questions.json'): with open(path, encoding='utf-8') as f: return json.load(f) def ask(q, shuffle=True): # omap 保存选项 key 到文本的映射,供题目展示使用 omap = {o['key']: o['text'] for o in q['options']} keys = list(omap) if shuffle: random.shuffle(keys) letters = 'ABCDEF'[:len(keys)] display = {} # 显示字母 -> 真实 key print(f"\n第 {q['id']} 题:{q['stem']}") for i, k in enumerate(keys): display[letters[i]] = k print(f" {letters[i]}. {omap[k]}") ans = input('你的答案:').strip().upper() if ans not in display: print('无效输入,本题计为错误。') return False hit = display[ans] == q['answer'] print('正确' if hit else f"错误,正确答案是 {q['answer']}") return hit def main(): qs = load() wrong = [] for q in qs: if not ask(q): wrong.append(q['id']) print(f"\n共 {len(qs)} 题,错 {len(wrong)} 道:{wrong}") if wrong and input('重做错题?(y/n): ').strip().lower() == 'y': for q in qs: if q['id'] in wrong: ask(q) if __name__ == '__main__': main()逻辑说明:ask 函数先构造 omap,把选项 key 映射到文本;shuffle 打乱的是 keys 列表而不是 omap 本身,所以打乱后仍能通过 display 还原原始 key。用户输入显示字母后,display[ans] 还原成原始 key,再和标准答案比较,这样就不会出现选项乱序后正确答案对不上的问题。这是命令行刷题工具最容易写错的地方,初学者常见的做法是乱序后直接比较字符串,结果正确答案永远匹配不上。参数方面,shuffle 默认开启,想保持原卷顺序可以传 shuffle=False;letters 按选项数量截取,兼容四选一之外的特殊题型。
4.3 错题优先的出题策略
前面说过,复习场景需要反复自测,命令行工具加上错题重做只是第一步。更进一步是给每道题维护一个权重,错题权重翻倍,下一轮抽题时更容易被抽中,这是最简单的间隔重复雏形,不引入额外依赖就能实现:
def pick(qs, weights, k=10): ids = [q['id'] for q in qs] # random.choices 按权重抽样,weights 与 ids 一一对应 picked = random.choices(ids, weights=weights, k=k) picked_set = set(picked) return [q for q in qs if q['id'] in picked_set] # 初始权重全部为 1,错题权重 +2 weights = [1] * len(qs) wrong_ids = [q['id'] for q in qs if not ask(q)] for i, q in enumerate(qs): if q['id'] in wrong_ids: weights[i] += 2random.choices 的 weights 参数要求长度与序列一致,允许部分权重为 0。把做错的题权重加到 3、做对的保持 1,下一轮抽题时错题被抽中的概率就是正确题的三倍。这套权重可以持久化到单独的文件里,每次刷完更新一次,长期积累就是一个轻量的个人错题本。需要注意 random.choices 是有放回抽样,同一题可能在一轮里出现多次,如果不想重复,可以先把所有 id 放入池子打底,再按权重补抽到目标数量。
5. 真实试卷解析的四个收尾技巧
5.1 答案栏单独抽离的理由
答案正则和题干正则是两套完全不同的模式,建议在解析脚本里拆成两个函数而不是揉在一起。原因是答案栏的排版自由度比题干大得多:有的文档答案排在文末表格里,有的夹在试题之间,还有的用「Keys:」单独起一段。单独抽离后,答案解析失败时可以快速定位到是答案段的问题,而不是把整个脚本的错误排查拖成一次全量 debug。上面脚本里 parse_answer_keys 独立成函数、main 里最后合并,就是这个思路。
5.2 题目数量与答案数量对不上时先查这两处
对不上的时候,九成问题出在两处。第一处是答案行使用了全角连字符「—」而你的正则只写半角 -,parse_answer_keys 里已经用[-—]做了兼容,但手写正则时很容易漏掉全角字符。第二处是最后一组答案不足五个字母,比如末尾只有 3 道题,([A-D]{5})匹配失败,整个末段答案全部丢失。写解析脚本时可以保留原始答案文本备份,解析完再做一次数量校验:
grep -oE '^[0-9]+[-—][0-9]+ +[A-D]{1,5}$' *.txt这条命令把答案段的起止题号和字母组列出来,肉眼就能核对最后一段长度是否和题目数一致。比起在 Python 里打印一堆 debug 日志,命令行 grep 更适合快速扫多份文件。
5.3 两份复习卷比对:先归一化再 diff
学期末手里常有两份内容相似、题号错位的复习卷,想确认题目差异时不要直接 diff,因为行尾空格和换行差异会淹没真正的改动。先做归一化再比,命令如下:
diff -u <(sed 's/[[:space:]]*$//' old.txt) <(sed 's/[[:space:]]*$//' new.txt)sed 去掉行尾空白,diff 的 -u 输出带上下文,改动一目了然。如果改动集中在答案序列上,更快的做法是直接对解析后的 JSON 做差:
jq -r '.[] | "\(.id)\t\(.stem)"' questions_old.json > old.stem jq -r '.[] | "\(.id)\t\(.stem)"' questions_new.json > new.stem diff -u old.stem new.stemjq 把每个题目的 id 和题干拼成一行,题干差异清晰可见。这道命令只比较题干、不看选项和答案,专门用来定位哪几道题被替换过;确定范围后再回到对应行看选项差异,很快就能判断两份卷子哪份覆盖的知识点更全。
本文还有配套的精品资源,点击获取