资讯动态

从 .doc 到 SQLite:铁路信息技术考试题结构化与组卷

发布时间:2026/9/17 16:25:01 来源:尧图企业网站定制
简介这份《铁路信息技术考试题》文档面向铁路信息化岗位人员、相关专业在校生及备考技术考核者聚焦信息系统建设规范、网络安全、设备管理与日常维护等考点可用于岗前自测、业务复习与知识点速查。压缩包内仅含1个doc文件约42KB以纯文字题目形式呈现体量轻便、便于打印或转存手机随时翻阅。文档收录了信息系统建设“五统一”原则、系统上线前的测试评审与软件修改审批、外部服务网/内部服务网/安全生产网分层防御、机房防火防雷防静电与保密要求以及路由器、交换机、光纤收发器、网桥、网关等网络设备辨析还包含现车系统确报功能、设备包保巡视制度、ping连通性测试、数据备份与销毁规范等实务内容题型兼顾填空选择与简答。目前已有572人学习适合需要梳理铁路信息化知识框架、快速定位高频考点并查漏补缺的读者。1. 铁路信息技术考试题.doc 卡在哪一份文档到一套题库的距离拿到一份「铁路信息技术考试题.doc」多数人的第一反应是打开、全选、粘进某个在线题库。真动手才发现几百道题的题干、选项、答案、解析混在同一个文档里答案常常集中堆在文末选项用手工编号和全角括号还有大量表格排版撑着版面。想按知识点检索、想随机组卷、想统计哪一章错得多靠手工整理基本走不通。这份文档真正需要的身份是数据源先转成可解析的格式再抽成结构化字段最后落库。适合往下读的是三类人——维护铁路信息系统培训题库的运维和教务人员、要给内部考试搭练习系统的数据工程师以及手上有存量 Word 题库需要做迁移的人。2. 把 .doc 题库转成可解析文本soffice 转换与 python-docx 顺序读取2.1 先分清 .doc 和 .docx别拿 python-docx 硬啃老文件python-docx 只认 .docx也就是 OOXML 那套 zip 包结构。Word 97-2003 的 .doc 是 OLE2 复合二进制格式直接用 python-docx 打开会抛PackageNotFoundError这不是编码问题是格式根本不匹配。常见的处理路线有三条选哪条取决于你要保留多少版面信息。方式输入保留表格保留顺序适用场景soffice 转 docx.doc/.docx是是首选题库里选项常用表格排版antiword 转 txt.doc否表格塌成文本部分纯文字题、无表格解压 docx 解析 XML.docx是是需要抽取批注、样式时只做纯文本提取时 antiword 够快但它会把表格单元格里的选项压成一行A、xxx B、yyy挤在一起后面按行切选项的正则全部失效。我一般先统一转成 .docx把版面结构和文字一起带过来。2.2 批量转换命令与并发坑# 批量把老式 .doc 题库转成 .docx保留段落和表格结构 mkdir -p converted for f in src/*.doc; do # -env:UserInstallation 给每次调用独立的用户配置目录 # 否则并发执行时 soffice 会因 profile 锁报 source file could not be loaded soffice --headless \ -env:UserInstallationfile:///tmp/lo_$$ \ --convert-to docx --outdir converted $f \ || echo 转换失败: $f done ls -l converted | head命令拆开看--headless不弹界面适合服务器上跑--convert-to docx指定目标格式也可以写成--convert-to txt:Text (encoded):UTF8但那样会丢表格不推荐--outdir决定输出目录文件名同源。最容易踩的是并发——soffice 默认复用同一个用户配置目录多个进程同时启动会互相抢锁表现就是随机几个文件转换失败重跑又成功。加上-env:UserInstallation指向带 PID 的临时目录这个问题就消失了。提示转换前先备份原始 .doc。soffice 对个别老版本公式、文本框的还原并不完美出问题要能回到源头。2.3 用 python-docx 按文档真实顺序读段落和表格from docx import Document from docx.oxml.ns import qn from docx.table import Table from docx.text.paragraph import Paragraph def iter_blocks(doc): 按 body 中节点的实际先后顺序交替产出段落和表格 for child in doc.element.body.iterchildren(): if child.tag qn(w:p): yield Paragraph(child, doc) elif child.tag qn(w:tbl): yield Table(child, doc) doc Document(converted/铁路信息技术考试题.docx) for blk in iter_blocks(doc): if isinstance(blk, Paragraph): text blk.text.strip() if text: print(P, text) else: # 题干下的选项常以表格承载逐行拼接成 A、xxx 形式 for row in blk.rows: cells [c.text.strip() for c in row.cells] print(T, .join(c for c in cells if c))关键在于没有用doc.paragraphs和doc.tables分别取。这两个属性各自返回一份列表互相之间没有位置关系题干和它下面的表格选项会被整体错位——第 30 题的表格可能被排到第 5 题后面。iter_blocks直接遍历body的子节点遇到w:p产出段落、遇到w:tbl产出表格顺序和 Word 里看到的一致。qn()做的是命名空间展开把w:p变成完整的 XML 标签名这是 python-docx 里判断节点类型的标准做法。表格里合并单元格会被重复读出来同一个文本可能在多行多次出现抽取选项时要按「A/B/C/D 关键字」去重而不是按文本去重。3. 用正则把铁路信息技术考试题拆成题干、选项、答案、解析3.1 题型特征先摸清再写表达式铁路信息技术的考题类型比较固定无非单选、多选、判断、填空、简答个别单位会加计算题。每类的版面特征不同判定规则也不同。题型版面特征关键正则片段必填字段单选4 个选项答案 1 个字母^[A-D][、.)]stem/options/answer多选4~5 个选项答案多字母同上答案长度 1stem/options/answer判断无选项答案为 对/错/√/×^(对错填空题干含____或 _{2,}\s*简答无选项答案为整段文字无stem/answer题干编号是最不统一的地方1、、1.、1、(1)、第1题都可能出现所以表达式要把这些分支一次覆盖而不是逐个试。3.2 一行一个状态机的解析脚本import re, json, unicodedata from dataclasses import dataclass, field, asdict QX re.compile(r^\s*(?:第)?\s*(\d{1,4})\s*[、.)题]\s*(?Pstem.)$) # 题干 OPT re.compile(r^\s*(?Pkey[A-D])\s*[、.)]\s*(?Ptext.)$) # 选项 ANS re.compile(r^\s*(?:参考|正确)?答案\s*[:]?\s*(?Pans.)$) # 答案行 EXP re.compile(r^\s*(?:试题)?(?:解析|分析)\s*[:]?\s*(?Pexp.)$) # 解析行 JUDGE_MAP {√: 对, ×: 错, T: 对, F: 错, 正确: 对, 错误: 错, 对: 对, 错: 错} def norm(s: str) - str: 全角转半角、压缩空白统一比较口径 s unicodedata.normalize(NFKC, s) return re.sub(r[\u200b\u3000\s], , s).strip() dataclass class Q: qtype: str stem: str options: list field(default_factorylist) answer: str explanation: str def parse(lines): out, cur [], None for raw in lines: line norm(raw) if not line: continue m QX.match(line) if m and not OPT.match(line): # 命中新题干先把上一题收尾 if cur and cur.stem: out.append(cur) cur Q(stemm.group(stem)) continue if cur is None: continue if (m : OPT.match(line)): cur.options.append({key: m.group(key), text: m.group(text)}) elif (m : ANS.match(line)): cur.answer m.group(ans).strip() elif (m : EXP.match(line)): cur.explanation m.group(exp).strip() else: # 续行解析或题干换行谁在最近上下文里就并给谁 if cur.explanation: cur.explanation line elif cur.options: cur.options[-1][text] line else: cur.stem line if cur and cur.stem: out.append(cur) return out def classify(q): if q.options: q.qtype multi if len(re.sub(r[^A-D], , q.answer)) 1 else single elif q.answer in JUDGE_MAP: q.answer JUDGE_MAP[q.answer] q.qtype judge elif re.search(r_{2,}|\s*, q.stem): q.qtype blank else: q.qtype short return q if __name__ __main__: text open(converted/题库.txt, encodingutf-8).read() items [classify(q) for q in parse(text.splitlines())] with open(questions.json, w, encodingutf-8) as f: json.dump([asdict(q) for q in items], f, ensure_asciiFalse, indent2) print(解析题数:, len(items))逻辑上是一个「缓冲当前题 遇到新题干才落盘」的状态机。为什么不用一次性 split题干和解析都可能跨行只有看到下一个编号才敢断定上一题结束否则最后一题的解析会被吞进下一题。norm()里NFKC负责把全角字母、全角数字、全角括号统一成半角、和A、因此走同一个分支顺手把零宽空格和全角空格压掉避免选项行首出现看不见的字符导致匹配失败。classify()的判断顺序不能调先看有没有选项再看答案是否落在判断题词表里最后才看填空和简答否则一道答案是「对」的单选题会被误判成判断题。JUDGE_MAP把各种对错写法收敛成「对/错」两个字后面做答题对比时省掉一层映射。跑完先别急着入库抽 20 道人工比对一次。常见偏差有两个答案集中在文末的文档ANS正则会一次只抓到「答案」后面跟的一长串1-5 ABCDA需要额外写一段按题号回填的逻辑表格转换出来的选项有时带表头「选项」二字会被OPT之外的行吞进题干。4. 铁路信息技术题库落库SQLite 建模、FTS5 检索与去重校验4.1 表结构选项用 JSON题干存指纹题目字段在不同题型之间差异很大简答题没有选项选择题的选项数也不固定。把选项拆成独立的 options 表会让查询变复杂而选项本身几乎不会单独被检索用 JSON 存在主表更合适。字段类型说明qtypeTEXTsingle/multi/judge/blank/shortstemTEXT归一化后的题干正文optionsTEXTJSON 数组元素形如{key:A,text:...}answerTEXT选择题为大写字母串判断题为「对/错」chapterTEXT知识点分类如「铁路通信与GSM-R」difficultyINTEGER1~5人工或按错题率回填stem_hashTEXT归一化题干指纹唯一约束做去重source_fileTEXT来源文档名便于回溯-- 主表一题一行选项以 JSON 存放避免变长选项拆表 CREATE TABLE questions ( id INTEGER PRIMARY KEY, qtype TEXT NOT NULL CHECK (qtype IN (single,multi,judge,blank,short)), stem TEXT NOT NULL, options TEXT, answer TEXT NOT NULL, explanation TEXT, chapter TEXT, difficulty INTEGER DEFAULT 3, source_file TEXT, stem_hash TEXT UNIQUE, created_at TEXT DEFAULT (datetime(now,localtime)) ); -- trigram 分词对中文短句更稳不依赖外部分词器需要 SQLite 3.34 CREATE VIRTUAL TABLE questions_fts USING fts5( stem, explanation, contentquestions, content_rowidid, tokenizetrigram );contentquestions建的是外部内容表FTS 索引不复制原文省空间但要求主表增删改时同步维护索引用触发器补上即可。tokenizetrigram是关键默认分词器按空格切词中文整句会变成一个巨大 token搜「GSM-R」能中、搜「调度」就搜不到trigram 按三字符滑窗建索引中文检索命中率明显更稳代价是索引体积大一些。4.2 三道校验 SQL把脏数据挡在入库前-- 1) 单选题答案不在选项字母里多为选项漏抓或答案行串行 SELECT id, qtype, substr(stem, 1, 30) AS stem30, answer FROM questions WHERE qtype IN (single,multi) AND NOT EXISTS ( SELECT 1 FROM json_each(questions.options) o WHERE instr(questions.answer, json_extract(o.value, $.key)) 0 ); -- 2) 指纹重复同一题在多个文档里出现过只留最早一条 SELECT stem_hash, COUNT(*) AS c FROM questions GROUP BY stem_hash HAVING c 1; -- 3) 按知识点与题型统计看看哪类题明显偏少 SELECT chapter, qtype, COUNT(*) AS n FROM questions GROUP BY chapter, qtype ORDER BY chapter, n DESC;第一条用json_each把 JSON 数组展开成行再判断答案里的每个字母是否都能在选项 key 中找到instr在这里比LIKE更直白也避免答案串里出现通配符的麻烦。第二条是硬去重stem_hash由「去标点、去空白后的题面前 60 字」做 SHA1 得到同一道题在不同文档里复制粘贴时标点差异不会造成漏判。第三条看着像统计实际是分组抽样的依据某一章判断题明显偏少时组卷要么降低该章权重要么补题不然学生练到的分布是歪的。注意stem_hash只能挡住完全一致的重复。同一道题被改写几个字的变体需要另外用相似度比对处理别指望唯一约束兜住。5. 组卷与相近题清洗把题库压到能直接练的程度5.1 按知识点配额随机组卷组卷的核心不是随机是按配额的随机。先给每个知识点定题量再在知识点内部随机抽否则整卷会被题量最大的那一章淹没。import sqlite3, json PLAN {铁路通信与GSM-R: 8, 信号与调度集中: 6, 客票与旅服系统: 5, 网络安全与等保: 6} def draw(conn, plan): paper [] for chapter, n in plan.items(): rows conn.execute( SELECT id, qtype, stem, options, answer, explanation FROM questions WHERE chapter ? AND qtype IN (single, judge) ORDER BY RANDOM() LIMIT ?, (chapter, n)).fetchall() if len(rows) n: print(f[警告] {chapter} 题量不足需要 {n}实得 {len(rows)}) paper.extend(rows) return paper conn sqlite3.connect(railway_it.db) for r in draw(conn, PLAN): print(r[0], r[2][:40], , r[4])ORDER BY RANDOM()在几千到几万行的量级上完全够用数据量再大才需要考虑随机主键或预生成抽样表。PLAN用字典而不是列表是为了让配额可以按考试大纲直接改改完重跑即可不需要动 SQL。题量不足时打警告而不是静默跳过这一点很重要——静默少抽几道等到考完才发现覆盖不全返工成本更高。5.2 相近题清洗先分桶再比相似度判断题和简答题的改写变体最多典型情况是「以下说法是否正确」后面换一个知识点题面前 20 个字完全一样。直接两两比相似度是 O(n²)一万道题就是五千万次比对跑一次要几分钟。可行的做法是先用「题面前 12 字 题型 选项数」做分桶键只在桶内比对桶的平均大小通常不到 5。import sqlite3 from difflib import SequenceMatcher def bucket_key(stem, qtype, opt_cnt): return f{qtype}|{opt_cnt}|{stem[:12]} conn sqlite3.connect(railway_it.db) rows conn.execute(SELECT id, qtype, stem, IFNULL(options,[]) FROM questions).fetchall() buckets {} for qid, qtype, stem, opts in rows: buckets.setdefault(bucket_key(stem, qtype, len(json.loads(opts))), []).append((qid, stem)) dups [] for key, group in buckets.items(): for i in range(len(group)): for j in range(i 1, len(group)): ratio SequenceMatcher(None, group[i][1], group[j][1]).ratio() if ratio 0.92: # 阈值可调0.9 偏松、0.95 偏严 dups.append((group[i][0], group[j][0], round(ratio, 3))) for a, b, r in dups: print(f疑似重复: {a} - {b} 相似度 {r})SequenceMatcher基于最长公共子序列对短中文句子的改写比编辑距离更宽容。阈值定在 0.92 是我在中文题干上试出来的经验值低于 0.9 会把同知识点的不同题目误判成重复高于 0.95 又抓不住只改了几个字的变体。分桶这一步不能省它把无效比对砍掉了绝大部分。跑出来的结果只做提示不做自动删除——合并需要对答案和解析做取舍这个判断交给题库维护人脚本负责把候选列出来就够了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价