简介:这份中南大学数据库考试题库面向高校计算机相关专业学生及备考数据库课程考试的学习者,聚焦数据库原理与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 被误命名了。对于老式 .doc,python-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', encoding='utf-8') as f: json.dump(normalized, f, ensure_ascii=False, indent=2) print(f"总题数: {len(normalized)}") print(f"异常题数: {len(all_errors)}") for e in all_errors[:10]: print(e)ensure_ascii=False让 JSON 里直接存中文,不转成\uXXXX,可读性好很多。indent=2是格式化输出,方便人工抽查。校验函数只检查最基本的字段,如果你有更细的规则,比如答案必须在选项范围内,可以在这里加。
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, content='questions', content_rowid='id' ) ''') 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就行,花不了几分钟,但能避免把错误数据交给别人用。希望帮到你。
本文还有配套的精品资源,点击获取