
简介这份资源是西南科技大学《信号与系统》课程5套历年期末考试试卷及参考答案的PDF合集面向本校修读该课程的本科生以及考研复习、期末冲刺或需要刷题巩固的理工科学生。试卷按年份与A/B卷编排覆盖填空题、判断题、证明题、绘图题、计算题、分析题与综合应用题等完整题型涉及卷积与冲激响应、Z变换与LT变换、因果稳定性判定、采样定理、傅里叶变换与频谱分析、滤波器频率响应等核心考点答案部分附有评分细则便于对照自查与规范答题步骤。资源包共1个PDF文件约1.76MB篇幅紧凑、便于打印与移动端查阅目录按考试场次顺序组织可快速定位薄弱章节。目前已有1526人学习下载适合用作期末前的系统自测与考点梳理材料。1. 五套《信号与系统》期末卷摆在面前第一步不是打开看而是当成数据集每到期末群里总会传一份「西南科技大学《信号与系统》5套历年期末考试含答案.pdf」。多数人打开翻两页就放下了手机上公式糊成一片想只刷卷积那一章的题做不到答案区写着「1.B 2.C」还得来回翻页对题号。做过数据清洗的人一眼能看出这不是复习问题而是一份排版混杂、半结构化、夹带数学符号的 PDF 语料工程问题。把文本层、题号层级、公式片段、答案索引拆成四层数据之后按知识点抽题、导进 Anki、做错题本才有地基。下面这套流程适合自己搭题库的开发者、做教育工具的后端以及需要长期在 LaTeX 与 PDF 之间来回倒腾的人。2. 先探文本层再谈解析期末卷 PDF 的提取选型2.1 用 PyMuPDF 判断这份卷子是文本层还是扫描件拿到 PDF 别急着写解析器先花三分钟探一下它是「真文本」还是「扫描图」。这两种情况后面的路子完全不同文本层可以直接抽字符扫描件必须先走 OCR否则你抽出来的是一堆空行加图片对象。我一般用 PyMuPDF 做一次概率性抽样只看前几页就够判断。import fitz # PyMuPDFpip install pymupdf from pathlib import Path def probe(path: str, sample_pages: int 5) - list[dict]: 抽样探测每页的文本层密度判断是文本型还是扫描型 PDF doc fitz.open(path) rows [] for i, page in enumerate(doc): if i sample_pages: break # text 模式只取文本层不含图片里的字 text page.get_text(text).strip() imgs page.get_images(fullTrue) rows.append({ page: i 1, chars: len(text), # 纯文本字符数 imgs: len(imgs), # 图片对象数量 # 密度字符数 / 图片数扫描件这一项会塌到个位数 density: round(len(text) / max(len(imgs), 1), 1), }) doc.close() return rows for r in probe(swust-signals-final.pdf): print(r)逻辑说明get_text(text)读的是 PDF 内部的字形流扫描件里这一层基本是空的get_images(fullTrue)返回页面上被引用的图片对象扫描页通常一张整页大图。经验阈值是——正文页普遍低于 200 字符、同时每页都带整页图就按扫描件处理。参数上sample_pages不用调大5 页足够因为历年卷的排版风格基本一致。提示有些卷子是「文字覆盖在扫描图之上」肉眼看着一样但文本层是有的。这种情况密度会中等偏高往下走正常解析即可别白白上一遍 OCR。2.2 提取工具怎么选pdfplumber、PyMuPDF、pdftotext 的边界《信号与系统》试卷的难点不在字数而在三样东西题号层级「一、选择题」下面挂「1.」「(1)」、上下标和积分号这类数学排版、以及选择题的 A/B/C/D 分行对齐。不同工具在这三件事上的表现差别很大。工具依赖公式/上下标保留版面坐标典型用途PyMuPDF (fitz)自带二进制 wheel好保留 glyph 顺序可拿到批量探测、全量取文pdfplumberpdfminer.six一般行内空隙偏多强给到 word 级 bbox按坐标切选项、抽答案表pdftotext -layoutpoppler一般靠空格凑命令行先看个大概pdfminer.six纯 Python一般弱深度定制解析逻辑我的常见组合是PyMuPDF 负责把每页正文整段拉出来做初筛和索引pdfplumber 只在需要精确切「A. xxx B. xxx」这种同行多列选项时上场因为它给得出每个 word 的坐标。pdftotext 留给 shell 里快速的 grep比如先确认这五套卷子里有没有出现「z 变换」章节的题。# 先把五套卷子统一命名方便后面按年份/学期建索引 mkdir -p papers/raw for f in *.pdf; do # 统一成 lower 下划线避免中文空格和全角括号带来路径问题 n$(echo $f | tr () | tr _) mv $f papers/raw/$n done ls papers/raw这一步看着琐碎但后面所有脚本都靠文件名区分年份和 A/B 卷命名乱了就得手工补元数据。2.3 最小可用的分页落盘脚本先把每页文本按页存成 JSONL解析失败能定位到具体页而不是重跑整本。import fitz, json from pathlib import Path def dump_pages(pdf: str, out: str) - int: doc fitz.open(pdf) n 0 with open(out, w, encodingutf-8) as fp: for i, page in enumerate(doc): rec { paper: Path(pdf).stem, # 卷子标识来自文件名 page: i 1, text: page.get_text(text), w: round(page.rect.width, 1), h: round(page.rect.height, 1), } fp.write(json.dumps(rec, ensure_asciiFalse) \n) n 1 doc.close() return n Path(papers/jsonl).mkdir(parentsTrue, exist_okTrue) for p in Path(papers/raw).glob(*.pdf): c dump_pages(str(p), fpapers/jsonl/{p.stem}.jsonl) print(p.name, c, pages)page.rect顺手存下来是有用的A4 与 B5 的宽度不同后面按坐标切双栏排版时要用页面宽度做归一化。ensure_asciiFalse必须加不然中文题面会被写成\uXXXX肉眼没法排查。3. 题目切分与公式还原把 5 套期末卷拆成结构化 JSON3.1 题号层级识别从「一、选择题」到「(1)」的正则《信号与系统》试卷的骨架相当稳定多数是「一、填空题 / 二、选择题 / 三、计算题 / 四、综合题」大题下面是 1、2、3小题是 (1)(2) 或 ①②。切分的关键是先定大题边界再在区间内切小题千万别一把梭全用行首数字正则——「1/2π」「0.5」这种内容行会被误伤。import re # 大题一、二、… 或 第一部分后接 2~30 字的题型描述 SEC_RE re.compile(r^\s*([一二三四五六七八九十])\s*[、.]\s*(\S{2,30}?题)) # 小题支持 (1) 1 1. 1、 1 四种写法 ITEM_RE re.compile( r^\s*(?:[(](\d{1,2})[)]|(\d{1,2})\s*[、.])\s*(.)$ ) def split_paper(text: str) - list[dict]: sections, cur_sec, cur_item [], None, None for line in text.splitlines(): if m : SEC_RE.match(line): cur_item None cur_sec {title: line.strip(), items: []} sections.append(cur_sec) continue if (m : ITEM_RE.match(line)) and cur_sec is not None: # group(3) 是题面开头前面两组分别对应两种题号写法 cur_item {no: m.group(1) or m.group(2), stem: m.group(3).strip()} cur_sec[items].append(cur_item) continue if cur_item is not None: # 续行题干换行、公式独占一行都落在这里 cur_item[stem] \n line.strip() return sections逻辑上先把「大题」当成状态机只有当cur_sec已存在时才认小题能挡掉答案区里独立的行首编号。SEC_RE里的\S{2,30}?题用非贪婪是必要的否则「二、选择题每小题 3 分共 30 分」会把整行都吞成标题。续行拼接那一段不要漏信号题的积分区间经常单独占一行丢了这行题干就不完整。3.2 公式怎么办四种还原路线的取舍公式是这份语料最麻烦的部分。傅里叶变换、拉普拉斯、Z 变换的题干里全是分式、上下标和积分号纯文本抽取后会变成X(jw) ∫ x(t)e^{-jwt} dt或者更糟的字符乱序。要不要上 OCR 取决于你的下游用途。路线成本离线适合场景纯文本抽取 手工替换规则最低是只要能搜关键词不要求排版还原整页渲染成图后走数学 OCR高视模型要生成可读题面、导出 PDF只对含公式的行做局部裁剪识别中是题量不大、追求性价比全部人工校对极高不适用要正式出版或对外发布我一般走「先规则后 OCR」先用替换表把高频写法归一化剩下识别不了的行再裁图送数学 OCR。规则表不长写十几条就能覆盖大部分卷子。import re NORMALIZE [ (r\s*∫\s*, ∫ ), # 积分号周围留空格便于后续切词 (r\bw\b, ω), # 卷子里常把 ω 打成 w统一成希腊字母 (r\be\s*\^\s*\{?(-?\s*j\s*[ωw]t)\}?, re^{jωt}), (r[(]\s*\)], ), # 空括号多为公式渲染失败 (r\s, ), # 行内多空格压成一个 ] def normalize_formula(s: str) - str: for pat, rep in NORMALIZE: s re.sub(pat, rep, s) return s.strip()参数说明替换表顺序有讲究积分号的空格要放在最前面否则后面的\s压缩会把刚加的空格吃掉。\bw\b一定要带词边界不然会把width里的 w 也换掉。替换完之后如果还有大量「」空括号说明公式是以图形对象绘制的纯文本路线到此为止走裁剪识别。注意不要为了追求公式好看把整本卷子按页整图送去 OCR。五套卷子里真正含复杂公式的页面通常不到三成全量识别既慢又会在汉字上引入新错误。3.3 落成一份能校验的 JSON Schema结构化输出最好定死字段后面写校验脚本才有的放矢。{ paper_id: swust_signals_2019_a, year: 2019, term: A, sections: [ { title: 三、计算题, items: [ { no: 1, stem: 已知 x(t) e^{-2t}u(t)求其拉普拉斯变换 X(s) 及收敛域。, answer: X(s) 1/(s2), Re(s) -2, knowledge: [laplace, roc], page: 4, review: false } ] } ] }page字段一定要留人工校对时直接翻到原页对比比在 JSON 里盲猜快得多。review是布尔位标记那些公式没还原干净、需要人眼过一遍的条目后面抽题时可以按需过滤。knowledge数组允许一题多标签计算题经常同时考变换和收敛域。4. 答案对齐与知识点检索让「含答案」真正可用4.1 把答案区的「1. B 2. C」回填到题目上带答案的卷子通常把答案集中放在末尾几页格式是选择题一长串字母计算题则是按题号给几行过程。回填的难点是题号在不同大题里会重复选择题的第 1 题和计算题的第 1 题所以键必须是「大题序号 小题序号」。import re ANS_LINE_RE re.compile(r(\d{1,2})\s*[、.:]\s*([A-D]|\S.{0,80})) def parse_answer_block(text: str) - dict[str, str]: 把答案区解析成 {题号: 答案} 的扁平字典 out {} for line in text.splitlines(): for no, ans in ANS_LINE_RE.findall(line): # 后者优先同一题号后出现的通常是更完整的解答 out[no] ans.strip() return out def attach(sections: list[dict], ans: dict[str, str]) - int: hit 0 for sec in sections: for it in sec[items]: a ans.get(it[no]) if a: it[answer] a hit 1 else: it[review] True # 没配到答案标记待人工处理 return hitANS_LINE_RE里那个\S.{0,80}分支是给计算题用的它对选择题的单个字母同样能匹配。回填后统计hit数量如果命中率明显低于题目总数八成是大题序号没对上需要回到切分阶段检查SEC_RE有没有漏掉某一节。4.2 用 SQLite FTS5 建一个按知识点检索的题库不用上向量库SQLite 的 FTS5 处理几千道题绰绰有余还能随手拷给别人。-- 建表题干、答案、知识点三个字段都参与检索 CREATE VIRTUAL TABLE q_fts USING fts5( paper_id UNINDEXED, no UNINDEXED, stem_text, answer_text, knowledge, tokenize unicode61 );写入时把题干和答案拆开放knowledge存空格分隔的标签串。这里有个坑FTS5 内置的unicode61对中文是按字切分的搜「拉普拉斯」能命中但搜「拉氏变换」这种同义说法就悬。要按词召回就在写入前用分词器预切一遍。import sqlite3, jieba def to_index_text(s: str) - str: # cut_for_search 会额外切出细粒度词提升短词召回 return .join(jieba.cut_for_search(s)) conn sqlite3.connect(signals_bank.db) conn.execute(CREATE VIRTUAL TABLE IF NOT EXISTS q_fts USING fts5( paper_id UNINDEXED, no UNINDEXED, stem_text, answer_text, knowledge, tokenizeunicode61)) for rec in load_all_items(papers/jsonl): # 上一章产出的结构化数据 conn.execute( INSERT INTO q_fts VALUES (?,?,?,?,?), (rec[paper_id], rec[no], to_index_text(rec[stem]), to_index_text(rec.get(answer, )), .join(rec.get(knowledge, []))) ) conn.commit() # 查询找所有涉及卷积的题按卷子排序 rows conn.execute( SELECT paper_id, no, stem_text FROM q_fts WHERE q_fts MATCH ? ORDER BY paper_id, no, (卷积,) ).fetchall()to_index_text只作用于入库文本查询词也要走同一个函数否则两侧切分粒度不一致会导致漏召回。ORDER BY里用paper_id, no而不是rank是因为复习时更想按年份顺序看知识点演化而不是看相关度。4.3 知识点打标把题目映射到课程主线自动打标先做关键词命中再对未命中的题做人工补标。下面是覆盖《信号与系统》主线的映射表可以直接喂给标注脚本。知识点标签命中关键词典型题型convolution卷积、h(t)、冲激响应求零状态响应fourier_series傅里叶级数、三角形式、指数形式周期信号展开fourier_transform傅里叶变换、频谱、幅度谱求频谱并画图sampling抽样、采样定理、奈奎斯特判断能否无失真恢复laplace拉普拉斯、s 域、收敛域求 X(s) 与 ROCz_transformZ 变换、z 域、单位圆求 X(z) 与收敛域stability稳定、极点、右半平面系统稳定性判定state_space状态方程、状态变量求状态转移矩阵打标脚本先在题干里做关键词扫描命中多个就写多个标签一个都没命中的丢进review。这一步别追求全自动五套卷子总共也就一两百道题人工过一遍的时间和调参的时间差不多但准确率高得多。5. 进阶抽题组卷、去重与质量校验5.1 按知识点配比组卷有了标签表组卷就是一次带配额的抽样查询——每组知识点抽固定数量优先抽没做过或做错过次数多的题。-- 每个知识点抽 3 道优先抽历史错误次数高的题 SELECT k.knowledge, q.paper_id, q.no, q.stem_text FROM q_fts q JOIN item_stat k ON k.paper_id q.paper_id AND k.no q.no WHERE q.knowledge MATCH ? ORDER BY k.wrong_count DESC, RANDOM() LIMIT 3;ORDER BY k.wrong_count DESC, RANDOM()的顺序不能反先按错题优先同权重再随机保证每次组的卷子既针对薄弱点又不完全重复。item_stat是一张普通表记录做题次数、错误次数和最近练习时间和 FTS 表用paper_id no关联。5.2 三个必查的质量指标题库跑起来之后最怕的是静默错误题目缺了后半段、答案配错了题号、同一道题被重复入库。加一段校验三项都能量化。指标计算方式预警阈值题干完整度以「。」「」结尾的题干占比低于 85% 要回查切分答案配平率有 answer 字段的题 / 总题数低于 90% 查答案区解析重复率题干 MinHash 相似度 0.9 的题对数大于 0 逐对人工确认题干完整度这项最能暴露问题。信号题的题干常以公式结尾不以句号收尾所以阈值不能设太高85% 是个经验值。重复检测用 MinHash 不用精确匹配是因为同一道题在不同年份的卷子上往往只改了一两个数字纯文本比对发现不了。5.3 一个具体技巧把校验固化成一条命令最后落一个make check目标把上面三步串起来五套卷子重跑一次不到十秒。加卷子的时候只把新 PDF 丢进papers/raw其余全自动。check: python scripts/split.py --in papers/jsonl --out data/items.json python scripts/validate.py data/items.json \ --min-complete 0.85 --min-answer 0.90 --dedup-threshold 0.9 python scripts/index.py data/items.json --db signals_bank.db echo OK: 题库校验与索引完成关键在于validate.py用非零退出码报错这样接进 CI 或者在本地随手跑都能拦住坏数据。新卷子进库的边际成本降到一条命令题库才敢持续加。本文还有配套的精品资源点击获取