资讯详情

Python解析Word数据库题库:从.doc到结构化数据与全文检索

📅 2026/10/22 21:04:56 | 华诺云谱 👁 阅读
Python解析Word数据库题库:从.doc到结构化数据与全文检索
简介这份中南大学数据库考试题库面向高校计算机相关专业学生及备考数据库课程考试的学习者聚焦数据库原理与SQL Server基础知识点的自测与复习。题库以单选、判断等客观题型为主覆盖E-R模型、三级模式结构、关系模型、数据字典、主键与外键、Transact-SQL标识符等核心考点并附有参考答案便于对照检验掌握程度。资源包共1个doc文件大小约23KB内容紧凑、便于打印或离线查阅适合考前集中刷题与查漏补缺。目前已有263人学习下载可作为课堂同步练习与期末复习的辅助材料帮助读者快速定位概念设计、逻辑设计、物理设计等易混淆环节理清数据库设计步骤与数据模型三要素等高频考点提升应试效率。1. 一份“数据库考试题库.doc”背后藏着多少能跑通的技术活“中南大学数据库考试题库.doc”这个标题第一眼看上去像是一份复习资料但做过教学资源整理的人都知道真正麻烦的不是题目本身而是它背后的格式混乱、版本漂移和检索低效。我见过太多类似的文档有的用 Word 表格排版有的直接粘贴截图有的把选择题答案写在题干后面还有的同一道题在不同章节重复出现。如果你只是把它当普通文档打开翻两页就会放弃但如果你把它当成一个“非结构化数据源”来处理事情就变得有意思了。这篇文章面向的是需要批量处理考试题库的开发者、助教和课程维护人员目标是把一份 .doc 格式的数据库题库变成可检索、可校验、可导出的结构化数据。整个过程不依赖任何特定平台用 Python 和常见命令行工具就能复现。我会先讲清楚为什么不能直接复制粘贴再一步步拆解解析、清洗、入库和验证的完整路径最后给出几个我踩过的坑和对应的排查方法。2. 为什么直接复制粘贴会翻车.doc 格式的解析选型与预处理2.1 先搞清楚你拿到的是 .doc 还是 .docx很多人拿到文件后直接改后缀名把 .doc 改成 .docx然后用 python-docx 去读结果报错Package not found。这是最典型的翻车场景。真正的 .doc 是二进制复合文档格式而 .docx 是 ZIP 压缩的 XML 包两者底层完全不同。判断方法很简单用file命令看一眼file 数据库考试题库.doc如果输出里带Composite Document File V2那就是老式 .doc如果带Microsoft Word 2007那其实是 .docx 被误命名了。对于老式 .docpython-docx 完全无能为力常见做法是先用 LibreOffice 做一次无头转换libreoffice --headless --convert-to docx --outdir ./converted 数据库考试题库.doc这条命令会把 .doc 转成标准 .docx输出到 converted 目录。参数--headless表示不弹界面适合服务器环境--convert-to docx指定目标格式。转换完成后再用 python-docx 读取就顺畅了。注意LibreOffice 的版本会影响转换质量我一般用 7.x 以上的稳定版太老的版本对表格嵌套的处理容易丢内容。2.2 用 python-docx 提取段落和表格的骨架转换完成后下一步是把文档里的文字和表格结构抽出来。数据库题库通常有两种排版一种是纯段落题干、选项、答案各占一行另一种是表格每行一道题列分别是题干、A、B、C、D、答案。下面这段代码同时处理段落和表格from docx import Document doc Document(./converted/数据库考试题库.docx) # 提取所有段落文本去掉首尾空白 paragraphs [p.text.strip() for p in doc.paragraphs if p.text.strip()] # 提取所有表格每个表格转成二维列表 tables [] for table in doc.tables: rows [] for row in table.rows: cells [cell.text.strip() for cell in row.cells] rows.append(cells) tables.append(rows) print(f段落数: {len(paragraphs)}) print(f表格数: {len(tables)}) for i, t in enumerate(tables): print(f表格 {i} 行数: {len(t)})这段代码的逻辑很直接doc.paragraphs拿到所有段落对象p.text取文本strip()去掉空白行。表格部分用嵌套循环外层遍历行内层遍历单元格。参数上唯一需要注意的是cell.text对合并单元格会重复返回相同内容如果题库里有跨行合并的题干后面清洗时要专门去重。跑完这段你就能知道这份题库到底是以段落为主还是以表格为主决定后续用哪条解析路径。2.3 把题干、选项、答案拆成独立字段的正则策略拿到原始文本后核心任务是把一道题拆成题干、选项和答案。数据库题库的题干通常以数字编号开头比如“1.”、“2、”或“1”选项以 A/B/C/D 开头答案可能写作“答案B”或“参考答案B”。下面是一个可复用的正则拆分函数import re def parse_question_block(block): # 匹配题干以数字或括号数字开头 stem_match re.match(r^[\(]?\d[\)]?[\.、\s]*(.), block, re.S) if not stem_match: return None stem stem_match.group(1).strip() # 提取选项 A-D options {} for letter in [A, B, C, D]: pattern rf{letter}[\.、\s](.?)(?[A-D][\.、\s]|答案|参考答案|$) m re.search(pattern, stem, re.S) if m: options[letter] m.group(1).strip() # 提取答案 ans_match re.search(r(?:答案|参考答案)[:\s]*([A-D]), stem) answer ans_match.group(1) if ans_match else None return { stem: stem, options: options, answer: answer }这里的关键参数是正则里的re.S它让.能匹配换行符因为题干和选项经常跨行。选项匹配用了前瞻断言(?[A-D][\.、\s]|答案|参考答案|$)意思是匹配到下一个选项字母或答案标记就停。答案提取只认 A-D 四个字母如果题库里有判断题或填空题这个函数会返回 None需要单独处理。我一般会先跑一遍统计看看有多少题没匹配到答案再决定要不要加规则。3. 从原始文本到结构化数据清洗、去重与字段对齐3.1 处理全角半角、多余空格和换行符从 Word 里抽出来的文本经常混着全角空格、不间断空格和软换行。如果不处理后面入库时会出现“题干看起来一样但字符串不相等”的玄学问题。下面这个清洗函数覆盖了最常见的几种情况import unicodedata def clean_text(text): # 全角转半角 text unicodedata.normalize(NFKC, text) # 替换不间断空格和制表符为普通空格 text text.replace(\u00a0, ).replace(\t, ) # 合并连续空格 text re.sub(r , , text) # 去掉首尾空白 return text.strip()unicodedata.normalize(NFKC, text)会把全角字母、数字和标点统一转成半角这是处理中文文档时最省事的一步。\u00a0是不间断空格从网页或 Word 复制时经常带进来肉眼看不出来但会影响匹配。合并连续空格用re.sub(r , , text)注意这里只处理普通空格换行符保留因为后面还要按行拆分。3.2 用题干哈希做去重避免同一道题反复出现题库里重复题很常见尤其是从不同渠道拼凑来的文档。去重不能简单按题干字符串比较因为空格和标点差异会导致漏判。我的做法是先清洗再对题干做归一化然后算哈希import hashlib def stem_fingerprint(stem): # 去掉所有空白和标点只保留中文、英文和数字 normalized re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , stem) return hashlib.md5(normalized.encode(utf-8)).hexdigest() seen {} unique_questions [] for q in parsed_questions: fp stem_fingerprint(q[stem]) if fp not in seen: seen[fp] True unique_questions.append(q) else: print(f重复题: {q[stem][:30]}...)stem_fingerprint先把题干里所有非中文、非英文、非数字的字符去掉这样“数据库的三级模式是什么”和“数据库的三级模式是什么”会得到同一个指纹。用 MD5 是因为它够快碰撞概率在这个场景下可以忽略。如果你担心哈希碰撞可以换成 SHA-256代价是计算稍慢但对几千道题来说差别不大。3.3 把清洗后的题目映射到统一字段结构清洗和去重之后需要把每道题整理成统一的字典结构方便后续导出 JSON 或写入数据库。字段设计上我一般保留这些题号、题干、选项字典、答案、题型、来源章节。下面是一个映射示例def normalize_question(raw, index): return { id: index, stem: clean_text(raw[stem]), options: {k: clean_text(v) for k, v in raw[options].items()}, answer: raw[answer], type: single_choice if len(raw[options]) 4 else unknown, source: 数据库考试题库 } normalized [normalize_question(q, i) for i, q in enumerate(unique_questions)]这里type字段根据选项数量判断四个选项默认是单选题。如果题库里有多选题答案可能是“AB”或“ABC”需要在解析答案时用re.findall(r[A-D], ans_str)提取所有字母。source字段留着是为了以后合并多个题库时能追溯来源现在写死没关系后面可以改成从文件名提取。4. 避坑与排查题库解析中最容易翻车的五个地方4.1 现象转换后的 docx 打开是乱码表格全丢原因LibreOffice 转换时如果源文件里嵌入了 OLE 对象或特殊字体转换器可能直接跳过这些内容。解决先用libreoffice --headless --convert-to pdf转一份 PDF对比一下页数和表格数量确认内容没丢再转 docx。如果 PDF 里表格完整但 docx 里丢了换用antiword或catdoc做纯文本提取虽然丢格式但至少文字在。4.2 现象正则匹配选项时把题干里的“A.”也吞进去了原因题干里可能出现“A. 选项一”这样的内容但那是题干的一部分不是选项。解决在正则里加锚点要求选项字母出现在行首或换行后。把rf{letter}[\.、\s]改成rf(?:^|\n){letter}[\.、\s]这样只匹配行首的选项标记。4.3 现象答案提取为空但肉眼能看到“答案B”原因答案里的冒号可能是全角“”也可能是半角“:”甚至混着空格。解决正则里用[:\s]*同时匹配两种冒号并且允许中间有空格。如果还是不行打印出答案所在行的原始字节看看是不是有不可见字符用repr()看一眼就清楚了。4.4 现象去重后题目数量对不上少了十几道原因不同题目的题干在去掉标点后恰好相同比如“简述数据库的完整性”和“简述数据库完整性”会被判为重复。解决指纹计算时保留标点或者改用编辑距离做相似度判断。我一般先用严格指纹去重再对剩下的做一次模糊匹配相似度超过 0.95 的才合并。4.5 现象写入数据库时字段超长报错原因某些题干特别长超过了数据库字段定义的长度。解决建表时题干字段用TEXT或VARCHAR(2000)别用默认的VARCHAR(255)。如果用的是 SQLite本身没有长度限制但导出到 MySQL 时要注意字符集用utf8mb4避免中文截断。5. 把题库变成可检索资产导出 JSON、建索引与验证5.1 导出标准 JSON 并做一次完整性校验结构化之后最直接的落地方式是导出 JSON 文件方便后续导入任何系统。导出前先做一次校验确保每道题都有题干和答案import json def validate_question(q): errors [] if not q[stem]: errors.append(题干为空) if not q[answer]: errors.append(答案为空) if q[type] single_choice and len(q[options]) ! 4: errors.append(f选项数量异常: {len(q[options])}) return errors all_errors [] for q in normalized: errs validate_question(q) if errs: all_errors.append({id: q[id], errors: errs}) with open(questions.json, w, encodingutf-8) as f: json.dump(normalized, f, ensure_asciiFalse, indent2) print(f总题数: {len(normalized)}) print(f异常题数: {len(all_errors)}) for e in all_errors[:10]: print(e)ensure_asciiFalse让 JSON 里直接存中文不转成\uXXXX可读性好很多。indent2是格式化输出方便人工抽查。校验函数只检查最基本的字段如果你有更细的规则比如答案必须在选项范围内可以在这里加。5.2 用 SQLite 建一张题库表并写入数据JSON 适合交换但要做检索还是得入库。SQLite 零配置适合本地快速验证。建表和写入的代码如下import sqlite3 conn sqlite3.connect(question_bank.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS questions ( id INTEGER PRIMARY KEY, stem TEXT NOT NULL, option_a TEXT, option_b TEXT, option_c TEXT, option_d TEXT, answer TEXT, type TEXT, source TEXT ) ) for q in normalized: cursor.execute( INSERT OR REPLACE INTO questions (id, stem, option_a, option_b, option_c, option_d, answer, type, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) , ( q[id], q[stem], q[options].get(A, ), q[options].get(B, ), q[options].get(C, ), q[options].get(D, ), q[answer], q[type], q[source] )) conn.commit() conn.close()这里把选项拆成四列而不是存 JSON是为了能用 SQL 直接查“选项 A 包含某个关键词”的题。INSERT OR REPLACE保证重复执行不会报主键冲突。写入完成后用一条查询验证SELECT COUNT(*) FROM questions WHERE answer IS NOT NULL;如果这个数字和总题数接近说明答案提取基本成功。5.3 用全文检索快速定位知识点SQLite 自带 FTS5 扩展可以给题干建全文索引查起来比 LIKE 快得多conn sqlite3.connect(question_bank.db) cursor conn.cursor() cursor.execute( CREATE VIRTUAL TABLE IF NOT EXISTS questions_fts USING fts5( stem, answer, contentquestions, content_rowidid ) ) cursor.execute( INSERT INTO questions_fts (rowid, stem, answer) SELECT id, stem, answer FROM questions ) conn.commit() # 查询包含“索引”的题目 cursor.execute( SELECT q.id, q.stem FROM questions_fts f JOIN questions q ON q.id f.rowid WHERE questions_fts MATCH 索引 LIMIT 5 ) for row in cursor.fetchall(): print(row)FTS5 的中文分词默认按字切分对“索引”这种双字词效果还行但对长词可能不够精准。如果题库量大可以换用 jieba 分词后写入或者直接用 LIKE 做简单匹配。建完索引后查一道题的时间从全表扫描的几十毫秒降到几毫秒题量上千时差别很明显。5.4 一个我常用的验证习惯随机抽 20 道题人工过一遍自动化校验只能查字段缺失查不出语义错误。我每次处理完题库都会从数据库里随机抽 20 道题打印题干、选项和答案人工看一遍。这个习惯帮我抓到过不少问题选项张冠李戴、答案字母错位、题干截断。抽样查询用ORDER BY RANDOM() LIMIT 20就行花不了几分钟但能避免把错误数据交给别人用。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑