简介:《04741计算机网络原理填空题及答案》是一份面向计算机专业自考生、网络课程复习者及备考人员的专项练习文档,聚焦计算机网络基础概念与核心协议填空训练,帮助读者用短题快速检验对知识点的掌握程度。压缩包内含1个docx文件,大小约36KB,排版紧凑,适合打印成册或导入平板反复刷题。题目覆盖网络拓扑、OSI七层模型、物理层与数据链路层、网络层路由、传输层TCP/UDP、局域网介质访问控制、TCP/IP协议族、DNS/ARP、网络安全与网络管理等高频考点,并附参考答案,便于对照纠错和查漏补缺。目前已有221人学习,适合在考前进行概念自测、课堂随测或作为章节复习后的巩固材料。
1. 一份 04741 计算机网络原理填空题 docx,为什么值得当成数据工程来做
考前两周,把一份 04741 计算机网络原理填空题及答案 docx 打印出来反复背,是最省事也最没效率的做法——纸面上的题不会告诉你,哪个空你已经错过了三遍。填空题在这门课里分值零碎、覆盖面极广,从分层模型、三次握手到子网划分、CRC 校验几乎每章都能出,也是唯一一种「术语拼写、单位符号、答案长度」全都不容错的题型。
这份 docx 真正的价值不在「收了多少道题」,而在于它能不能被拆成结构化数据:题干、空位数量、标准答案、所属章节各占一个字段,才能被检索、被统计、被重新挖空。
适合动手的人有三类:临考想把错题压到最小的人、需要按知识点组卷的人、想练一次文档解析到自测闭环的开发者。路径是同一条:先读成 JSON,再做一致性校验,最后接一个抽题循环。
2. 把 docx 读成骨架:题干、答案区、表格排版各在哪
动正则之前必须先看结构。很多人拿到文档就unzip出来对着word/document.xml硬怼,结果发现题干顺序和文件里标签的顺序对不上,题干和答案错位,这就是典型的黑匣子式解析。我一般会先做一遍「骨架打印」,把段落和表格按原始顺序输出,人眼扫一遍再决定用哪套切分规则。
2.1 用 python-docx 按原始顺序打印段落与表格
doc.paragraphs和doc.tables是两个独立列表,分别读取会丢掉两者的相对顺序——而填空题文档里,题号段落和答案表格往往是交替出现的。正确做法是遍历 body 的子元素。
# dump_skeleton.py from docx import Document from docx.table import Table from docx.text.paragraph import Paragraph from docx.oxml.ns import qn def iter_block_items(doc): """按 XML 原始顺序同时产出段落和表格,避免顺序错位""" body = doc.element.body for child in body.iterchildren(): if child.tag == qn('w:p'): yield Paragraph(child, doc) elif child.tag == qn('w:tbl'): yield Table(child, doc) doc = Document('04741计算机网络原理填空题及答案.docx') for i, block in enumerate(iter_block_items(doc)): if isinstance(block, Paragraph): text = block.text.strip() if not text: continue # 顺带打印下划线 run 数量,用来判断空位是不是纯格式 ul = sum(1 for r in block.runs if r.underline) print(f'[P{i:04d}] style={block.style.name!r} ul={ul} :: {text[:70]}') else: print(f'[T{i:04d}] rows={len(block.rows)} cols={len(block.columns)}') for row in block.rows[:3]: cells = [c.text.strip().replace('\n', ' ')[:24] for c in row.cells] print(' |', ' | '.join(cells))逻辑说明:iter_block_items不依赖 python-docx 的高层 API,而是直接读w:body下的w:p(段落)和w:tbl(表格)节点,再用对应的包装类还原成对象,这样输出的顺序就是 Word 里的视觉顺序。
参数说明:block.style.name用来区分标题样式和正文样式,答案区常常用「标题 2」之类的样式开头,是最省事的切分锚点;ul这个计数很关键,如果某道题的题干里有空位但block.text里看不到下划线字符,说明空位是靠 run 的下划线格式画出来的,字符层面什么都不剩。
2.2 三种常见排版形态与对应的切分策略
扫完骨架,绝大多数文档会落进下面三种形态之一,或者三种混排。
| 形态 | 典型特征 | 切分策略 | 主要风险 |
|---|---|---|---|
| 题号连排型 | 每题一个段落,以1.2、开头 | 按题号正则切段 | 题干跨行被拆成两段 |
| 表格两列型 | 左列题干、右列答案,或奇数行题干偶数行答案 | 按行读取,合并单元格会重复 | 合并单元格文本重复 |
| 行内括号型 | 题干写成「TCP 的可靠传输依靠( )机制」 | 提取括号内非空内容作为答案 | 括号里是术语注释而非答案 |
题号连排型最省事,一段一题,直接用第 3 章的正则。表格两列型要注意 python-docx 的一个行为:横向合并的单元格,row.cells里会出现多个指向同一个w:tc的对象,文本被重复返回。判断方法是比较底层节点身份,cell._tc is prev._tc为真就是同一个格子,跳过即可。
行内括号型最容易翻车。文档里既有「( )」这种真空位,也有「(OSI 参考模型)」这种解释性括号。我的做法是先统计全文括号内非空内容的长度分布,如果绝大多数落在 2~12 个字符、且能对上答案区的编号,就按答案处理;否则一律当注释,保留在题干里。这个判断不要写死在正则里,留成阈值参数,换一份文档还能复用。
还有一种混排:题干在上半部分,答案集中在下半部分,中间用「参考答案」之类的段落隔开。这种结构不要试图一次性切完,先按锚点段落把文档一分为二,前半段只抽题干,后半段只抽答案,两边靠题号对齐。这样即使答案区顺序被打乱,也能靠编号重新配对,而不是靠位置硬猜。
3. 抽成结构化 JSON:题号、空位、答案的三段式切分
骨架清楚了,接下来是这套流程里最需要耐心的部分:把一行行文本变成带字段的记录。核心是把「切分」拆成三个互相独立的动作——切题号、数空位、配答案。三个动作各自可测,出问题时能立刻定位是哪一步错了,比写一个大正则一把梭要好排查得多。
3.1 题号与题干的切分骨架
题号的写法在同一份文档里都可能不统一:1.、1、、1.、(1)、1)都见过。正则需要覆盖全角半角,同时要防止把「1.5 Mbps」这种数字误判成题号。
import re # 题号:行首 1~3 位数字 + 分隔符;分隔符后不能紧跟数字,避免误伤 1.5 QNO = re.compile(r'^\s*(\d{1,3})\s*(?:[、..]\)?|[))])\s*(?!\d)') # 括号题号:(1) (1) QNO_PAREN = re.compile(r'^\s*[((]\s*(\d{1,3})\s*[))]\s*') def cut_question_no(line: str): """返回 (题号 int 或 None, 剩余正文)""" for pat in (QNO_PAREN, QNO): m = pat.match(line) if m: return int(m.group(1)), line[m.end():].strip() return None, line.strip()逻辑说明:QNO_PAREN放在前面优先匹配,因为(1)也符合QNO的分隔符规则,先匹配括号形态能避免题号被吞成1而剩下) 题干。
参数说明:(?!\d)是否定前瞻,专门挡「1.5 Mbps」。\d{1,3}限制题号上限 999,超过的几乎肯定是年份或数字列表。如果文档题号是「一、二、」这种中文序号,再加一个[一二三四五六七八九十]+的分支即可,但那通常是章节号而不是题号,要用后文说的标题样式来区分。
跨行题干是另一个高频问题。判定规则我一般设成:当前行没有匹配到题号,且长度大于 12 个字符,且上一行以非句号结尾,就拼接到上一题。拼接时用一个空格连接,不要直接+,否则中英文交界处会粘成一个词,后面做去重哈希时对不上。
3.2 答案定位:括号、下划线、文末答案区的三种写法
答案的三种来源要分开处理,优先级是:行内括号非空 > 文末答案区 > 下划线 run。下划线优先级最低,因为它只能告诉你有几个空,不能告诉你答案是什么。
import unicodedata # 空位字符形态:全角空括号、半角空括号、连续下划线、方括号 BLANK = re.compile(r'[((]\s*[))]|_{2,}|_{2,}|【\s*】|\[\s*\]') # 行内答案:括号内 1~30 个非括号字符 INLINE = re.compile(r'[((]\s*([^()()]{1,30}?)\s*[))]') # 答案区行:编号 + 内容 ANS = re.compile(r'^\s*(\d{1,3})\s*[、..::]?\s*(.+?)\s*$') def norm(s: str) -> str: """NFKC 归一:全角括号/字母/数字统一成半角,压缩空白""" s = unicodedata.normalize('NFKC', s) return re.sub(r'\s+', '', s) def parse_inline(stem: str, min_len=1, max_len=12): """行内答案型:返回 (规范化题干, 答案列表)""" answers = [m.group(1).strip() for m in INLINE.finditer(stem) if min_len <= len(m.group(1).strip()) <= max_len] if not answers: return stem, [] # 只把判定为答案的括号替换成标准空位,其余括号原样保留 rebuilt = INLINE.sub(lambda m: '( )' if min_len <= len(m.group(1).strip()) <= max_len else m.group(0), stem) return rebuilt, answers逻辑说明:norm先做 NFKC 归一,这一步能一次性解决全角括号、全角数字、全角字母的匹配问题,比在正则里堆[((]可靠。parse_inline里的rebuilt用函数式替换,只把判定为答案的那部分括号改成标准空位,注释性括号原样留着,避免题干语义被破坏。
参数说明:min_len/max_len就是 2.2 里说的阈值,默认 1~12,中文术语一般不超过 8 个字;如果发现答案被漏判,先调max_len,不要急着改正则。答案区拆分多条答案时,建议只用;、;、|作分隔符,不要用顿号——「传输层、网络层」这种答案本身带顿号,拆了就废。
3.3 落库:字段设计、哈希去重与幂等重跑
解析结果直接进 SQLite,好处是随时能重跑、能对比两次解析的差异、能用 SQL 做统计。字段设计要保证「同一道题重跑一次不会变成两条」。
import sqlite3, hashlib, json DDL = """ CREATE TABLE IF NOT EXISTS questions ( id INTEGER PRIMARY KEY, source TEXT NOT NULL, q_no INTEGER NOT NULL, chapter TEXT, -- 章节标签,来自标题样式或人工映射 stem TEXT NOT NULL, -- 已归一化的题干,空位统一成( ) blank_cnt INTEGER NOT NULL, answers TEXT NOT NULL, -- JSON 数组,按空位顺序 fp TEXT NOT NULL UNIQUE -- 指纹,用于幂等 ); CREATE INDEX IF NOT EXISTS idx_chapter ON questions(chapter); """ def fingerprint(stem: str) -> str: # 题干折叠空白 + 去标点后再哈希,避免排版差异导致重复 base = re.sub(r'[\s,。、;:?!,.;:?!]', '', norm(stem)) return hashlib.sha1(base.encode('utf-8')).hexdigest() def upsert(conn, row): conn.execute( "INSERT OR REPLACE INTO questions" "(source,q_no,chapter,stem,blank_cnt,answers,fp) VALUES(?,?,?,?,?,?,?)", (row['source'], row['q_no'], row.get('chapter'), row['stem'], row['blank_cnt'], json.dumps(row['answers'], ensure_ascii=False), fingerprint(row['stem'])))逻辑说明:fingerprint先折叠空白和常见标点再哈希,同一道题在不同排版下(多一个空格、句末多个句号)会得到同一个指纹,INSERT OR REPLACE配合UNIQUE就实现了幂等重跑——改一版解析规则跑第二遍,不会产生重复题。
参数说明:blank_cnt单独存一份,不要每次从stem现数,因为归一化过程中空位形态会变,落库时的计数才是权威值,第 4 章的对账就靠它。chapter允许为空,别指望文档里每道题都标了章节,能靠标题样式带出来的就带,带不出来的留空,后面按关键词映射补。
4. 一致性校验与挖空生成:让这份题库真的能刷
解析成功不等于能用。真正开始刷题时才会发现:有的题两个空只有一个答案、有的答案带着编号、有的术语中英文混着写。这一章做两件事——先把脏数据挡住,再把干净的题反向挖成自测题。
4.1 空位数与答案数的对账脚本
对账是最廉价的质量门。空位数和答案数不匹配的记录,几乎百分百是解析错误,先拦下来人工看一眼,比带着错误去背要好。
def audit(conn): """返回三类问题记录,供人工复核""" problems = [] for qid, q_no, stem, cnt, ans_json in conn.execute( "SELECT id,q_no,stem,blank_cnt,answers FROM questions ORDER BY q_no"): answers = json.loads(ans_json) real_blank = len(BLANK.findall(stem)) if cnt == 0: problems.append((qid, q_no, 'no_blank', stem[:40])) elif cnt != len(answers): problems.append((qid, q_no, f'blank={cnt} ans={len(answers)}', stem[:40])) elif real_blank != cnt: problems.append((qid, q_no, f'stem_blank={real_blank} cnt={cnt}', stem[:40])) elif any(len(a) == 0 for a in answers): problems.append((qid, q_no, 'empty_answer', stem[:40])) return problems def fix_shortage(conn, qid, q_no): """答案少于空位时的兜底:把多余空位标记为 unknown,不猜""" conn.execute("UPDATE questions SET answers=? WHERE id=?", (json.dumps(['?'] * conn.execute( "SELECT blank_cnt FROM questions WHERE id=?", (qid,)).fetchone()[0], ensure_ascii=False), qid))逻辑说明:audit做三重比对——存库时的blank_cnt、stem里现数的空位、答案数组长度,三者必须一致。第三重比对能抓出一种很隐蔽的问题:归一化之后两个相邻空位被粘成一个,blank_cnt还是 2,但题干里只剩 1 个空。
参数说明:fix_shortage里用?占位而不是留空字符串,是为了让「不知道答案」和「答案是空」在数据层可区分,后面对账时?会被单独统计。千万不要用模型去猜答案填空,这门课的术语一旦猜错,背下去就是负收益。
4.2 术语归一化:全角半角、中英混排、单位符号
计算机网络的填空题对写法极其敏感。同一个答案,文档里可能写成CSMA/CD、CSMA/CD、csma/cd,三种写法在字符串层面完全不同,但对人来说是一个东西。
| 归一化项 | 处理方式 | 例子 |
|---|---|---|
| 全角字母数字 | NFKC 转半角 | TCP→TCP |
| 大小写 | 英文缩写统一大写,仅用于比对不改原值 | tcp→TCP |
| 斜杠与连字符 | 全角斜杠转半角,//统一 | CSMA/CD→CSMA/CD |
| 单位与数字间空格 | 删除空格 | 100 Mbps→100Mbps |
| 括号注释 | 保留但比对时忽略 | TCP(传输控制协议) |
def answer_key(s: str) -> str: """生成用于比对和去重的答案键,不覆盖原答案""" s = unicodedata.normalize('NFKC', s).strip() s = re.sub(r'[((][^()()]*[))]', '', s) # 去掉括号注释 s = re.sub(r'\s+', '', s) s = s.replace('/', '/').replace('-', '-') if re.fullmatch(r'[A-Za-z0-9/\-.]+', s): s = s.upper() # 纯英文/数字才统一大写 return s def build_alias(conn): """同一答案键下的多种写法合并成同义词组,供判题时宽松匹配""" groups = {} for qid, ans_json in conn.execute("SELECT id,answers FROM questions"): for a in json.loads(ans_json): if a and a != '?': groups.setdefault(answer_key(a), set()).add(a.strip()) return {k: sorted(v) for k, v in groups.items() if len(v) > 1}逻辑说明:answer_key只生成比对键,不修改原始答案,原值永远保留在answers字段里,判题时先精确比、再比 key、最后比同义词组,三层降级。
参数说明:re.fullmatch(r'[A-Za-z0-9/\-.]+', s)这个判断是关键——只有纯 ASCII 的答案才做大写归一,中文答案不动,否则「IP」和「ip」虽然等价,但「MAC 地址」被大写处理就变成噪音了。
4.3 反向挖空:从答案生成自测题与干扰项
题库只有「题干 + 答案」的时候还是被动的。加一步挖空,就能把它变成可以主动回忆的自测题:把答案从题干里抹掉,让答案变成待填项,再配几个同章节的干扰项。
import random def make_quiz(row, pool, k=3, rng=None): """row: 一条题库记录;pool: 同章节其他题的答案集合""" rng = rng or random.Random(20250101) stem, answers = row['stem'], row['answers'] # 按空位顺序填充,答案不足的用 ____ 占位 out, idx = [], 0 for m in BLANK.finditer(stem): out.append(stem[idx:m.start()]) out.append('____') idx = m.end() out.append(stem[idx:]) text = ''.join(out) distractors = [a for a in pool if answer_key(a) != answer_key(answers[0] if answers else '') and abs(len(a) - len(answers[0] if answers else '')) <= 4] rng.shuffle(distractors) options = distractors[:k] + ([answers[0]] if answers else []) rng.shuffle(options) return {'qid': row['id'], 'text': text, 'options': options, 'answer': answers[0] if answers else None}逻辑说明:make_quiz不破坏原题干,用切片重建一份带____的副本;干扰项从同章节答案池里取,靠长度差 ≤ 4 保证难度不至于太离谱——「TCP」配一个「面向连接的服务」当干扰项是无效的,一眼就能排除。
参数说明:k是干扰项数量,3 表示四选一;rng传固定种子是为了让同一批练习可复现,两个人对答案时不会因为选项顺序不同吵架。pool建议按chapter分组取,跨章节混干扰项会让题目变得过于简单。
5. 排查清单:docx 题库化最容易翻车的 5 个地方
这一章是我自己踩过的、以及帮别人看脚本时反复见到的五类问题。每条都按「现象 → 原因 → 解决」写,遇到对得上的直接照做。
一、题干末尾莫名多出一串乱码或空白
现象:解析出来的题干结尾带\u200b、\xa0或者一大段空格,去重时同一道题出现两条。原因:Word 里从网页复制的内容常带零宽字符和不间断空格,strip()去不掉。解决:在norm里显式清理,s = s.replace('\u200b', '').replace('\xa0', ' '),再走 NFKC。这类字符肉眼不可见,不打印repr()根本发现不了,我一般会在骨架打印那一步就用repr(text[:70])。
二、空位数量永远比实际少一个
现象:题干明明有两个括号,blank_cnt只数到 1。原因:两个空位之间没有其他字符,写成「( )( )」,某些正则用非贪婪匹配时会跨括号吞掉中间部分。解决:把空位正则写成不含嵌套的显式形态[((]\s*[))],并在替换时用finditer逐个处理而不是sub一次性替换,替换前先print一遍匹配区间做核对。
三、答案区编号和题号对不上,整卷错位
现象:第 12 题的答案是第 13 题的内容,往后全错一位。原因:题干区某道题跨了两段但只切出一段,或者答案区漏抄了一个编号。解决:不要按位置配对,一律按题号字典配对;配对时先检查两边编号集合是否一致,缺号的题直接标unmatched打印出来,绝不顺延。这一步的后悔药就是 3.3 里的指纹字段,重跑时不至于把已经人工修好的记录覆盖掉。
四、合并单元格导致题干重复三遍
现象:表格型文档解析后,一道题变成三条一模一样的记录,指纹还都一样,INSERT OR REPLACE之后只剩一条,看起来「丢题」了。原因:横向合并的单元格在row.cells里重复出现。解决:遍历行时用cell._tc做身份去重,seen = set(),遇到id(cell._tc)已存在的跳过;同时把「一行一题」的假设改成「一行可能包含多题」,按空位数量判断该行实际有几道题。
五、全角半角混用导致判题永远判错
现象:用户输入TCP被判错,标准答案是TCP。原因:答案区是从别的文档粘过来的,带全角字符,而输入侧是半角。解决:判题统一走 4.2 的answer_key,输入和标准答案都先过一遍 key 再比;同时把原始答案保留下来展示,不要让用户看到被归一化过的字符串,否则会怀疑标准答案是错的。
6. 让题库自己出题:空位级错题统计与 15 分钟抽题循环
题刷到第三遍,真正卡住你的往往不是整道题,而是某一个空。所以错题统计的粒度要下沉到空位,而不是题目。表结构很简单:blank_state(qid, blank_idx, wrong_cnt, right_cnt, last_seen),每次作答按qid + blank_idx更新,题目级别的正确率由空位聚合出来。
抽题用 SQL 就够,不需要额外服务。遗忘间隔按照箱式模型安排:箱号 0~5 分别对应 0、1、2、4、7、15 天。答错回箱 0,答对进一箱。
-- 今天该复习的空位,优先抽低箱号和高错误率 SELECT b.qid, b.blank_idx, q.stem, q.answers, b.wrong_cnt FROM blank_state b JOIN questions q ON q.id = b.qid WHERE julianday('now') - julianday(b.last_seen) >= CASE b.box WHEN 0 THEN 0 WHEN 1 THEN 1 WHEN 2 THEN 2 WHEN 3 THEN 4 WHEN 4 THEN 7 ELSE 15 END ORDER BY b.box ASC, b.wrong_cnt DESC, b.last_seen ASC LIMIT 20; -- 答完之后按结果更新箱号 UPDATE blank_state SET box = CASE WHEN :ok THEN MIN(box + 1, 5) ELSE 0 END, wrong_cnt = wrong_cnt + CASE WHEN :ok THEN 0 ELSE 1 END, last_seen = datetime('now') WHERE qid = :qid AND blank_idx = :idx;CASE里的天数就是间隔表本身,想改成 1/3/7/14 直接改数字,不用动代码。ORDER BY里把box放在第一位,是为了让最不熟的空位先出现;last_seen作为第三排序键,避免同一批题每次都按同样顺序刷出来形成位置记忆。
实际用的时候我把它压到一个 15 分钟的循环里:前 10 分钟抽 20 个空位,答完立刻看错的是哪一类——是术语拼写错,还是概念本身没记住。拼写错的空位单独打标签,第二天只过标签,不重新抽题;概念错的空位直接回箱 0。后 5 分钟不看新题,只把当天错过的空位再念一遍答案,念的时候遮住题干,靠答案反推题干问的是什么,这一步比重做一遍题有效得多。
有一个细节值得单独说:章节标签的覆盖率决定了这套东西的上限。如果一半以上的题chapter是空的,按章节组卷就没法用。我的做法是先按关键词映射一版粗标签,比如题干里出现「握手」「确认号」「滑动窗口」就归到传输层,出现「子网」「掩码」「路由」就归到网络层,映射不上的留unknown,再用SELECT chapter, COUNT(*) FROM questions GROUP BY chapter看分布,unknown超过 20% 就说明映射词表要补,补词表比一条条手标快得多。
最后说个血泪经验:别在临考前三天改解析规则。规则一改,指纹变、去重逻辑跟着变,很可能把之前手工修过的记录冲掉。真要改,先cp questions.db questions.db.bak再动,改完只 upsert 新增的指纹,老记录一律不动。这套流程我从第一版做到现在,最大的收获不是省了多少时间,而是终于知道自己到底错在哪个空上——希望帮到你。
本文还有配套的精品资源,点击获取