从docx到刷题系统:无人机题库解析与自动判分实战
简介这份无人机理论考试题库面向无人机驾驶员、飞行教员及备考人员系统梳理了理论考试所需的核心知识体系帮助读者在飞行原理、操作规范、维护保养与法律法规等模块建立完整认知。资源包内含1个docx文档压缩包约160KB以题库形式组织涵盖空气动力学中升力、阻力、推力与重力的相互作用起飞、巡航、降落各阶段的操作要点以及航程、航时、飞行速度等性能影响因素。维护部分强调定期检查结构、动力与控制系统并说明适航证、注册要求及随机文档的作用法规部分涉及空域划分、飞行计划申请、高度速度限制与驾驶员资质认证。题库还延伸至系统组成、飞行安全与应用领域如农业植保、航拍测绘、电力巡检和物流运输。目前已有132人学习适合需要集中刷题、查漏补缺的备考者使用。1. 一份 docx 题库怎么变成能刷题、能检索、能自动判分的系统很多人拿到「无人机理论考试题库.docx」的第一反应是打开 Word 从头翻。翻到第 30 页就放弃了——几百道题混着单选、多选、判断答案有的在题尾、有的在括号里、有的干脆单独成段靠肉眼刷题效率极低。真正要解决的问题不是「有没有题」而是「怎么把这份 docx 变成结构化数据再变成一个能随机组卷、自动判分、错题重练的小系统」。这件事的门槛比想象中低一台普通电脑、Python 环境、半小时就能跑通第一版。适合三类人正在备考无人机执照、想反复刷题的考生手里有题库 docx 想做成小程序或网页的开发者以及需要把纸质/文档资料批量转成结构化题库的培训从业者。下面按「先看清 docx 里到底有什么 → 再解析成 JSON → 再落成可刷题的形态 → 最后讲踩过的坑」这条线走一遍。2. 先摸清 docx 的底题库文档的三种典型结构和解析选型2.1 为什么不能直接读文本必须先看 XML 结构docx 本质是一个 zip 包里面word/document.xml才是正文。用python-docx读出来的paragraph.text会丢掉很多信息加粗、下划线、表格、编号列表的层级。而无人机题库里正确答案经常靠加粗或下划线标记选项经常放在表格里。如果只读纯文本你会得到一堆没有边界的字符串根本分不清哪行是题干、哪行是选项。常见做法是先解压 docx直接看document.xml里题目是怎么组织的。命令很简单# 把 docx 当 zip 解开只看结构不改原文件 mkdir -p /tmp/qbank cd /tmp/qbank unzip -o 无人机理论考试题库.docx -d extracted ls extracted/word/ # 重点看 document.xml 的大小几百道题通常几百 KB 到几 MB解压后如果document.xml里大量出现w:tbl说明题目或选项用了表格排版如果大量出现w:numPr说明用了自动编号。这两种结构的解析方式完全不同先看清楚再写代码能省掉后面反复返工的时间。2.2 三种典型结构对应三种解析策略我见过的无人机题库 docx 基本逃不出这三类结构类型特征解析难点推荐策略纯段落型题干、选项、答案都是独立段落答案位置不固定正则匹配题号答案关键词表格型每题一个表格或选项在表格单元格单元格合并、跨行按表格行遍历取 cell.text混合型题干段落选项表格答案段落边界判断先按题号切块块内再分段落和表格选型理由很直接纯段落型用正则最快但容错差表格型必须用python-docx的tables接口混合型最麻烦建议先按「题号」把整个文档切成题目块再在块内判断是段落还是表格。不要一上来就追求全自动先拿 20 道题做样本把结构摸清再写通用逻辑。2.3 用 python-docx 做一次结构探查在写正式解析前先跑一段探查脚本统计段落和表格的分布from docx import Document doc Document(无人机理论考试题库.docx) # 统计段落里出现「题号」特征的频率 import re para_count 0 question_like 0 for p in doc.paragraphs: text p.text.strip() if not text: continue para_count 1 # 匹配 1. 1、 (1) 一、 等常见题号开头 if re.match(r^[\(\]?\d[\.\、\)\], text) or re.match(r^[一二三四五六七八九十][、\.], text): question_like 1 print(f非空段落数: {para_count}) print(f疑似题干段落数: {question_like}) print(f表格数量: {len(doc.tables)}) # 打印前 30 个非空段落人工确认结构 for i, p in enumerate(doc.paragraphs[:60]): t p.text.strip() if t: print(i, repr(t[:80]))这段代码的逻辑先统计段落总数和疑似题干的段落数如果两者接近说明是纯段落型如果表格数量很大而段落里题干很少说明题目在表格里。打印前 60 个非空段落是为了人工确认题号格式、选项前缀A. / A、/ A、答案标记方式。参数上正则里的\d匹配阿拉伯数字题号[一二三四五六七八九十]匹配中文题号实际题库可能只用其中一种按探查结果删掉不用的分支。提示探查阶段不要急着写解析先把题号格式、选项前缀、答案标记这三件事确认清楚后面代码的准确率取决于这三个正则写对没有。3. 把 docx 解析成结构化 JSON题号切块、选项提取、答案对齐3.1 按题号切块解析的地基不管哪种结构第一步都是把整份文档切成「一题一块」。切块的依据是题号所以题号正则必须准。常见题号格式有1.、1、、(1)、1、一、无人机题库里最常见的是1.和1、。import re from docx import Document doc Document(无人机理论考试题库.docx) # 题号正则行首可选的括号 数字/中文数字 分隔符 Q_HEAD re.compile(r^[\(\]?\s*(\d|[一二三四五六七八九十])\s*[\.\、\)\]\s*) blocks [] current None for p in doc.paragraphs: text p.text.strip() if not text: continue if Q_HEAD.match(text): if current: blocks.append(current) current {raw: [text]} else: if current: current[raw].append(text) if current: blocks.append(current) print(f切出题目块: {len(blocks)}) print(第一块内容:) for line in blocks[0][raw]: print( , line)逻辑说明遍历所有非空段落遇到匹配题号的行就开一个新块否则追加到当前块。这样每个块的raw列表里第一行是题干含题号后面是选项和答案。参数上Q_HEAD里的\s*允许题号和文字之间有空格[\.\、\)\]覆盖了点和顿号、半角和全角括号。如果题库里题号后面直接跟文字没有分隔符这个正则要相应放宽但放宽后误匹配风险上升需要人工抽查。3.2 选项和答案的提取正则 状态机切块之后块内每一行要么是选项要么是答案要么是题干的续行。选项的典型前缀是A.、A、、A答案的典型标记是答案、正确答案、参考答案。OPT re.compile(r^([A-Da-d])\s*[\.\、\\)\]\s*(.)$) ANS re.compile(r(?:答案|正确答案|参考答案)\s*[:]\s*([A-Da-d\s、,])) def parse_block(block): stem_lines [] options {} answer None for line in block[raw]: # 去掉行首题号 line_clean Q_HEAD.sub(, line, count1) if Q_HEAD.match(line) else line m_ans ANS.search(line_clean) if m_ans: answer re.sub(r[\s、,], , m_ans.group(1)).upper() continue m_opt OPT.match(line_clean) if m_opt: options[m_opt.group(1).upper()] m_opt.group(2).strip() continue stem_lines.append(line_clean) return { stem: .join(stem_lines).strip(), options: options, answer: answer } parsed [parse_block(b) for b in blocks] # 抽查前 3 题 for q in parsed[:3]: print(q)逻辑说明对块内每一行先尝试匹配答案行匹配到就记录答案并跳过再尝试匹配选项行匹配到就存入 options 字典剩下的都当作题干。参数上OPT只认 A-D如果有多选题选项到 E 或 F要把[A-Da-d]改成[A-Fa-f]。ANS里的[\s、,]用来清理「A、B、C」这种多选答案里的分隔符统一成ABC。这一步做完你会得到一批结构化对象但一定会有解析失败的题下一节讲怎么定位。3.3 解析质量校验三道检查必须过解析完不能直接用先跑校验。我一般查三件事题干为空的题、选项少于 2 个的题、答案不在选项里的题。bad [] for i, q in enumerate(parsed): if not q[stem]: bad.append((i, 题干为空)) elif len(q[options]) 2: bad.append((i, f选项过少: {list(q[options].keys())})) elif q[answer] and any(a not in q[options] for a in q[answer]): bad.append((i, f答案不在选项中: {q[answer]} vs {list(q[options].keys())})) print(f总题数: {len(parsed)}, 异常题数: {len(bad)}) for idx, reason in bad[:20]: print(idx, reason) print( raw:, parsed[idx][stem][:60])逻辑说明题干为空通常是切块时把答案行误当题干或者题号正则漏匹配导致块没切开选项过少通常是选项用了表格而段落里没读到答案不在选项里通常是答案标记格式特殊或者选项前缀不是 A-D。这三类异常各自对应不同的修复方向先分类再修不要盲目改正则。参数上len(q[options]) 2这个阈值对判断题不适用——判断题可能只有「正确/错误」两个选项但没有 A/B 前缀需要单独处理。注意如果异常题集中在某几页很可能是那几页用了不同的排版比如表格回到 2.1 的 XML 层面确认不要硬改正则。4. 从 JSON 到可刷题形态随机组卷、自动判分、错题重练4.1 存成 JSON 和 SQLite两种形态各有用解析结果先落盘最省事的是 JSON方便人工检查和版本对比要支持随机组卷和错题统计SQLite 更合适。import json import sqlite3 # 落 JSON with open(qbank.json, w, encodingutf-8) as f: json.dump(parsed, f, ensure_asciiFalse, indent2) # 落 SQLite conn sqlite3.connect(qbank.db) conn.execute( CREATE TABLE IF NOT EXISTS questions ( id INTEGER PRIMARY KEY AUTOINCREMENT, stem TEXT NOT NULL, options TEXT NOT NULL, answer TEXT, type TEXT ) ) for q in parsed: qtype 多选 if q[answer] and len(q[answer]) 1 else 单选 conn.execute( INSERT INTO questions (stem, options, answer, type) VALUES (?, ?, ?, ?), (q[stem], json.dumps(q[options], ensure_asciiFalse), q[answer], qtype) ) conn.commit() conn.close()逻辑说明JSON 保留原始结构适合 diff 和人工修SQLite 的options字段存 JSON 字符串查询时再解析。type字段按答案长度粗判单选/多选判断题需要额外规则比如选项只有「正确/错误」。参数上ensure_asciiFalse保证中文不转义方便肉眼检查AUTOINCREMENT让 id 稳定方便错题表关联。4.2 随机组卷和自动判分的最小实现有了 SQLite组卷就是一条 SQL判分就是字符串比对。import random import sqlite3 import json def make_paper(n20): conn sqlite3.connect(qbank.db) rows conn.execute( SELECT id, stem, options, answer, type FROM questions ORDER BY RANDOM() LIMIT ?, (n,) ).fetchall() conn.close() return [ {id: r[0], stem: r[1], options: json.loads(r[2]), answer: r[3], type: r[4]} for r in rows ] def grade(paper, user_answers): # user_answers: {question_id: A} 或 {A,B} wrong [] for q in paper: ua user_answers.get(q[id], ) if isinstance(ua, (list, set)): ua .join(sorted(ua)) if ua.upper() ! (q[answer] or ).upper(): wrong.append(q[id]) return wrong paper make_paper(5) for q in paper: print(q[id], q[stem][:40], list(q[options].keys()), 答案:, q[answer])逻辑说明ORDER BY RANDOM() LIMIT ?是 SQLite 里最简单的随机抽样题量上千时性能也够用。判分时把用户答案统一成大写字符串再比对多选题要求顺序无关所以先sorted再拼接。参数上n20是组卷题数按考试要求改wrong列表就是错题 id可以写回一张wrong_questions表做重练。4.3 错题重练把错题 id 存下来下次优先出错题重练的核心是记录错题 id 和错误次数组卷时给错题更高权重。conn sqlite3.connect(qbank.db) conn.execute( CREATE TABLE IF NOT EXISTS wrong_log ( qid INTEGER, wrong_count INTEGER DEFAULT 1, last_wrong_at TEXT DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (qid) ) ) def record_wrong(qids): for qid in qids: conn.execute( INSERT INTO wrong_log (qid, wrong_count) VALUES (?, 1) ON CONFLICT(qid) DO UPDATE SET wrong_count wrong_count 1, last_wrong_at CURRENT_TIMESTAMP , (qid,)) conn.commit() def make_paper_with_wrong(n20, wrong_ratio0.5): n_wrong int(n * wrong_ratio) wrong_rows conn.execute( SELECT qid FROM wrong_log ORDER BY wrong_count DESC LIMIT ?, (n_wrong,) ).fetchall() wrong_ids [r[0] for r in wrong_rows] placeholders ,.join(? * len(wrong_ids)) if wrong_ids else 0 rows conn.execute( fSELECT id, stem, options, answer, type FROM questions WHERE id IN ({placeholders}), wrong_ids ).fetchall() # 剩余题量从非错题里随机补 remain n - len(rows) if remain 0: rows conn.execute( fSELECT id, stem, options, answer, type FROM questions WHERE id NOT IN ({placeholders}) ORDER BY RANDOM() LIMIT ?, wrong_ids [remain] ).fetchall() return rows逻辑说明wrong_log用ON CONFLICT做 upsert同一题错多次就累加wrong_count。组卷时先取错误次数最多的若干题再从非错题里随机补足。参数上wrong_ratio0.5表示一半题来自错题备考后期可以调到 0.7 甚至 0.8把精力集中在薄弱题上。注意placeholders在wrong_ids为空时要给一个占位否则 SQL 语法错误。5. 避坑与排查解析无人机题库 docx 时最容易翻车的 5 个点5.1 现象解析出来题目数量对但题干和选项串行原因题库里题干和选项之间没有空行切块时把选项行也当成了题干续行或者题号正则漏匹配导致上一题的选项被并进下一题。解决在parse_block里对每一行先判断是不是选项再判断是不是题干同时打印前 20 题的stem和options人工核对边界。如果串行集中在某几题单独看那几题的 raw 行多半是题号格式特殊比如1而不是1.。5.2 现象答案全是 None原因答案标记不是「答案」而是「正确答案是」「参考答案为」「【答案】」等变体。解决把ANS正则改成(?:答案|正确答案|参考答案|【答案】)\s*[:是为]?\s*([A-Da-d\s、,])覆盖更多写法。改完先跑校验看答案不在选项里的异常是否减少。如果答案单独成段且没有标记词只能靠位置规则比如每题最后一行是答案来兜底但位置规则不稳定优先找标记词。5.3 现象选项只解析出 A 和 BC、D 丢了原因选项用了表格排版doc.paragraphs读不到表格里的文字。解决改用doc.tables遍历或者在切块时同时遍历段落和表格。python-docx里段落和表格是分开的两个集合顺序信息会丢所以混合型题库建议直接解析document.xml按 XML 节点顺序遍历才能保证题干、选项、答案的相对顺序正确。5.4 现象多选题答案被判成单选原因答案字段是「A、B、C」清理分隔符后变成ABC但type判断用了len(answer) 1如果答案只有AB会判成多选但如果答案写成「A B」中间有空格清理后是AB没问题真正的问题是有些题库多选题答案只写了一个字母但实际是多选。解决不要只靠答案长度判断题型结合题干里的「多选」「单选」「判断」关键词或者看选项数量。判断题单独用「正确/错误」选项识别。5.5 现象随机组卷每次出的题重复率高原因ORDER BY RANDOM()在题量少时重复正常但如果题量上千还重复可能是 SQLite 的随机种子问题或者组卷逻辑里错题部分固定取了同一批。解决错题部分按wrong_count排序取前 N 是故意的但非错题部分要确保NOT IN排除的是当前已选的错题 id而不是所有错题。另外如果每次组卷都从同一个连接读确认没有缓存。题量少时接受重复题量多时重复率高就检查LIMIT和NOT IN的参数拼接。6. 进阶把题库做成可检索的本地刷题页以及我自己的使用习惯解析成 JSON 和 SQLite 只是中间态真正提升刷题效率的是把它变成一个能按关键词检索、能标记、能导出错题本的本地页面。最轻量的做法是用 Python 起一个本地 HTTP 服务前端一个 HTML 页面后端两个接口/search?q和/paper?n。不需要框架http.server就够。import json import sqlite3 from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs class Handler(BaseHTTPRequestHandler): def do_GET(self): parsed urlparse(self.path) qs parse_qs(parsed.query) conn sqlite3.connect(qbank.db) if parsed.path /search: kw qs.get(q, [])[0] rows conn.execute( SELECT id, stem, options, answer FROM questions WHERE stem LIKE ? LIMIT 50, (f%{kw}%,) ).fetchall() data [{id: r[0], stem: r[1], options: json.loads(r[2]), answer: r[3]} for r in rows] elif parsed.path /paper: n int(qs.get(n, [20])[0]) rows conn.execute( SELECT id, stem, options, answer FROM questions ORDER BY RANDOM() LIMIT ?, (n,) ).fetchall() data [{id: r[0], stem: r[1], options: json.loads(r[2]), answer: r[3]} for r in rows] else: data {error: not found} conn.close() body json.dumps(data, ensure_asciiFalse).encode(utf-8) self.send_response(200) self.send_header(Content-Type, application/json; charsetutf-8) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) HTTPServer((127.0.0.1, 8765), Handler).serve_forever()逻辑说明/search用LIKE做题干模糊检索适合查某个知识点比如「空域」「气象」相关的所有题/paper返回随机组卷。参数上LIMIT 50防止检索结果过多n默认 20。前端页面用fetch调这两个接口即可不需要额外依赖。这个服务只监听127.0.0.1本机使用不对外暴露。进阶用法里我还会做两件事一是把错题导出成单独的 docx 或 Markdown考前只看错题二是给每道题打标签比如「法规」「气象」「飞行原理」组卷时按标签抽题针对薄弱模块集中练。标签可以存在questions表加一列tags或者单独一张tags表。进阶功能实现方式适用场景关键词检索SQL LIKE 或 FTS5查某个知识点所有题标签组卷questions 加 tags 列按模块集中突破错题导出查 wrong_log 生成 Markdown考前只看错题模拟考试固定题量计时考前适应节奏最后说个我自己的习惯解析完第一版后我不会马上开始刷而是先随机抽 30 题对着原始 docx 逐题核对题干、选项、答案是否一致。这一步花 20 分钟能避免后面刷了几百题才发现答案错位的血泪教训。题库这东西数据错了比没有更可怕因为它会让你把错的记成对的。希望帮到你。本文还有配套的精品资源点击获取