
简介人教版高一英语必修二总复习单项选择题是一份面向高一学生与英语教师的复习资料针对必修二常考语法点和词汇搭配设计可帮助练习者在考前快速梳理易错考点也能为教师选题组卷或课堂小测提供现成素材。题目围绕高频短语、定语从句、主语从句、不定式结构、连词辨析、强调句型、情态动词推测等模块展开内容涵盖“think highly of”搭配、search相关短语区别、非限制性定语从句中“the larger of which”的指代、固定句式“It is known to all that...”、主动表被动的不定式、besides在语境中的补充说明用法以及强调句主谓一致等核心知识点。每道题均配有逐项答案讲解既写明正确选项依据也分析干扰项的排除逻辑并补充“be considered to have done”“cant have found”等延伸结构便于自主订正与查漏补缺。资源为1个doc文档约52KB支持打印或电子端阅读轻量实用。目前已有87人学习下载适合高一阶段期末复习、单元巩固或考前冲刺使用。1. 一份 .doc 复习题为什么值得用工程方式重做一遍拿到「人教版高一英语必修二总复习单项选择题.doc」这个文件名大部分人第一反应是双击打开、打印、做题。但如果你是 IT 岗视线会落在另外三件事上这是老式二进制 .doc 而不是 .docx说明文件在教师电脑里至少流转了十年题目是次生文本结构题号、题干、选项、答案之间存在稳定规律可以把数据抽出来重新组织复习场景天然需要反复自测而静态文档没有办法按错题自动重排、也没有办法统计正确率。三点叠在一起就是一套「文档解析 小工具开发」的标准任务。下面按「抽取文本 → 结构化解题 → 落地自测」的顺序完整走一遍新手能照着跑通熟手可以直接拿走 .doc 中文排版和答案分离上的几个边界细节省去自己趟坑的时间。2. 先把 .doc 里的题目无损抽出来工具链选型2.1 为什么 python-docx 读不了这份 .docpython-docx 是处理 Word 文本最常用的 Python 库但有一个硬边界它只支持 .docx也就是 Office 2007 之后的 OOXML 格式。遇到 .docOLE2 复合文档在构造 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 21 echo done: $f done这段脚本把当前目录下所有 .doc 转成 .docx。/dev/null 21是为了屏蔽 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-8LibreOffice 按系统 locale 做了猜测猜错了。解决办法是在转换命令前设置环境变量export LANGzh_CN.UTF-8 export LC_ALLzh_CN.UTF-8 soffice --headless --convert-to txt:Text --outdir ./txt 人教版高一英语必修二总复习单项选择题.docLANG 和 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]\)右括号手打文档常见Aseeing[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, encodingutf-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, encodingutf-8) as f: json.dump(qs, f, ensure_asciiFalse, indent2) print(fparsed {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 同样可以直接喂给前端不需要再写一层接口。自测系统用到的完整字段结构如下四个字段足够支撑随机出题和错题统计不需要额外冗余字段字段类型说明idint题号解析阶段已去重stemstring题干文本optionsarray选项列表每项含 key 与 textanswerstring正确选项的 key如A4.2 自测工具的完整代码与参数说明下面的工具支持顺序刷题、选项乱序、错题重做三个基本功能全部只依赖标准库import json import random def load(pathquestions.json): with open(path, encodingutf-8) as f: return json.load(f) def ask(q, shuffleTrue): # 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 默认开启想保持原卷顺序可以传 shuffleFalseletters 按选项数量截取兼容四选一之外的特殊题型。4.3 错题优先的出题策略前面说过复习场景需要反复自测命令行工具加上错题重做只是第一步。更进一步是给每道题维护一个权重错题权重翻倍下一轮抽题时更容易被抽中这是最简单的间隔重复雏形不引入额外依赖就能实现def pick(qs, weights, k10): ids [q[id] for q in qs] # random.choices 按权重抽样weights 与 ids 一一对应 picked random.choices(ids, weightsweights, kk) 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 和题干拼成一行题干差异清晰可见。这道命令只比较题干、不看选项和答案专门用来定位哪几道题被替换过确定范围后再回到对应行看选项差异很快就能判断两份卷子哪份覆盖的知识点更全。本文还有配套的精品资源点击获取