Python自动生成数字信号处理实验报告:FFT、滤波器与docx排版实践 📅 发布时间:2026/9/17 16:29:13 👁 浏览次数: 简介这是一份北京理工大学数字信号处理课程的实验报告文档面向信息工程、电子通信等专业本科生及需要完成DFT相关实验的学习者用于理解离散傅里叶变换原理并完成信号频谱分析的实验任务。压缩包内仅含1个docx文件大小约795KB文档结构完整包含实验目的、实验原理、实验内容、MATLAB代码与结果截图等模块可直接作为实验报告的撰写参考。报告围绕DFT与DTFT的关系展开涵盖补零对频谱分辨率的影响、由DFT恢复DTFT的方法以及利用DFT分析连续时间信号频谱时采样间隔、截取长度与频域采样点数的选取原则并讨论频谱混叠、栅栏效应与频谱泄露等典型问题。实验部分给出多个具体案例如对序列计算4点与64点DFT并对比幅度谱相位谱、分析复合正弦波信号的频率分辨率、以DFT近似分析指数衰减信号频谱等配套FFT与矩阵运算的可运行代码。目前已有1351人学习下载适合作为课程实验的对照范例与复习材料。1. 从实验数据到 docx数字信号处理实验报告的交付形态实验室里跑完一组 FFT屏幕上有波形、有频谱、有滤波器频响真正卡住人的往往是最后一步把这些东西整理成一份格式合规、公式和图都能正常打开的 docx。数字信号处理实验报告的难点很少在算法本身而在于几类异构信息要按同一套规范落进同一份文档——采样率、点数、窗函数这些参数要成表幅频特性和相频特性要成图差分方程和系统函数要成公式最后还要满足学院给的页边距、字体、章节编号要求。手写排版一次两次还行一个学期六七个实验重复劳动很快就会吃掉调试代码的时间。这份文档的合理做法是把它当成一个构建产物计算脚本负责产出数据和图片模板脚本负责把数据渲染成 docx。适合三类人刚接触数字信号处理实验、需要一份能直接改的生成脚本的本科生带实验课、需要统一收作业格式的助教以及想把 MATLAB、Python 计算流程和文档产出打通的人。下面按「算数据 → 搭骨架 → 塞图和公式 → 批量与排错」的顺序走一遍每步都给可复制的代码和参数说明。2. 数字信号处理实验的计算内核FFT、滤波器与数据落盘实验报告里的每个数字都要能追溯到一次计算所以先把计算脚本固化下来让 docx 生成脚本只读中间结果而不是重新算一遍。这样同一组参数不会在两个脚本里写出两个值。2.1 采样率、点数与观测时长三个量先定死数字信号处理实验最常见的事故是频率刻度对不上。根源通常在 fs、N、T 三个量里有一个是随手写的。三者满足 N fs × T改任意一个都会影响频率分辨率 Δf fs / N。做频谱分析时Δf 决定了两个相邻谱线能否分开取值太小会让 50 Hz 和 55 Hz 混成一根峰。import numpy as np fs 1000.0 # 采样率 Hz必须大于信号最高频率的 2 倍 T 1.0 # 观测时长 s N int(fs * T) # 采样点数决定频率分辨率 df fs/N 1 Hz t np.arange(N) / fs # 时间轴用 arange 而不是 linspace避免端点重复 f0, f1 50.0, 300.0 # 两个测试频率f1 fs/2 才不混叠 x 0.8 * np.sin(2 * np.pi * f0 * t) 0.3 * np.sin(2 * np.pi * f1 * t)逻辑说明np.arange(N)/fs生成的是 N 个等间隔采样点步长 1/fs和采样定理的定义严格对应换成linspace(0, T, N)会得到步长 T/(N-1)频率刻度会有微小偏移。参数上fs 取 1000 Hz 时奈奎斯特频率是 500 Hz测试信号最高 300 Hz留了余量。如果实验要求观察混叠把 f1 改成 700 Hz再画频谱就能看到它折叠到 300 Hz 处这是报告里很值得写的一段分析。2.2 用 numpy.fft 做谱分析并避开幅值刻度错幅值刻度错是实验报告扣分的高频项。np.fft.fft输出的幅值和实际信号幅值差一个 N 倍关系取单边谱时还要再乘 2直流分量不乘。把这几个系数写成注释比事后对着图猜要省事。import numpy as np from scipy import signal X np.fft.rfft(x) # 实数信号用 rfft只返回正频率部分 freq np.fft.rfftfreq(N, d1 / fs) # 对应的频率轴单位 Hz amp np.abs(X) / (N / 2) # 单边幅值谱先除 N 再乘 2 amp[0] / 2 # 直流分量只除 N不乘 2 # 加窗后再算一次比较频谱泄露的差异 win signal.get_window(hann, N) amp_win np.abs(np.fft.rfft(x * win)) / (N / 2) amp_win[0] / 2逻辑说明rfft只算正频率长度是 N//21比fft省一半内存也省掉手动截半的步骤。rfftfreq(N, d1/fs)里的 d 是采样间隔别传成 fs否则频率轴会差几个数量级。加窗那段用来做对比实验矩形窗下 50 Hz 附近的旁瓣衰减慢换成汉宁窗后旁瓣压低但主瓣变宽报告里把两组曲线画在同一张图上分析部分就有内容可写。提示加窗会降低信号总能量做幅值比较时要乘窗函数的相干增益汉宁窗约 0.5否则加窗后的峰值看起来比实际小一半。2.3 IIR 与 FIR 滤波器设计iirfilter / firwin 的参数怎么改滤波器设计是数字信号处理实验的核心环节报告里要给出阶数、截止频率、通带波纹、阻带衰减这几项以及幅频响应曲线。IIR 用双线性变换法FIR 用窗函数法两套代码都留着方便做同一指标的对比。设计方法调用函数关键参数相位特性阶数需求巴特沃斯 IIRsignal.iirfilterN、Wn、ftype、fs非线性低切比雪夫 I 型 IIRsignal.iirfilter加 rp 通带波纹非线性更低窗函数法 FIRsignal.firwinnumtaps、cutoff、window严格线性高等波纹 FIRsignal.firlsnumtaps、bands、desired严格线性中等from scipy import signal fs, fp, fst 1000.0, 120.0, 200.0 # IIR4 阶巴特沃斯低通截止 120 Hz b, a signal.iirfilter(N4, Wnfp, btypelow, ftypebutter, fsfs, outputba) w_iir, h_iir signal.freqz(b, a, worN2048, fsfs) # FIR65 点汉明窗低通截止取通带与阻带的中点 taps signal.firwin(numtaps65, cutoff(fp fst) / 2, fsfs, windowhamming, pass_zerolowpass) w_fir, h_fir signal.freqz(taps, worN2048, fsfs)逻辑说明iirfilter的 Wn 在传了 fs 之后直接写 Hz不传 fs 就写归一化频率Nyquist 为 1这是最容易搞错的一处。outputba返回传递函数的分子分母系数方便用freqz算频响如果后面要做零相位滤波改成outputsos配合sosfiltfilt更稳。FIR 的cutoff取 160 Hz 是因为窗函数法在通带和阻带之间有一段过渡带取中点能让通带边缘的衰减更均匀。numtaps取 65 是奇数保证是 I 型线性相位阶数越高过渡带越窄但计算量和群延迟也越大。报告里把两者频响画在一起注明各自阶数和群延迟对比就很清楚。2.4 把计算结果落成生成脚本能读的中间格式计算和排版拆成两个脚本后中间格式要同时容纳数组、标量和字符串。np.savez存数组最省事元信息单独放一个 JSON两边都能读。import json import numpy as np np.savez(dsp_data.npz, fsfs, NN, tt, xx, freqfreq, ampamp, amp_winamp_win, w_iirw_iir, h_iirh_iir, w_firw_fir, h_firh_fir) meta {title: 基于 FFT 的频谱分析与 IIR/FIR 滤波器设计, fs: fs, N: N, f0: f0, f1: f1, iir_order: 4, fir_taps: 65} with open(dsp_meta.json, w, encodingutf-8) as f: json.dump(meta, f, ensure_asciiFalse, indent2)逻辑说明npz是压缩的二进制容器按关键字取数避免用位置索引。JSON 里放的是要写进报告正文的标量用ensure_asciiFalse保证中文标题直接可读indent2方便人工核对。这样排版脚本只要 import 两个文件不需要 import scipy运行环境干净很多。3. 用 python-docx 搭出实验报告的骨架排版脚本的第一目标是让学院模板里的每一项都能用代码表达标题层级、正文样式、表格边框、页边距、页眉页码。python-docx 覆盖了其中大部分剩下的靠直接操作 OOXML 补。3.1 Document 对象与章节层级映射实验报告一般是「一、实验目的 / 二、实验原理 / 三、实验内容与步骤 / 四、实验结果与分析 / 五、结论」这种结构和 Word 的 Heading 1 到 Heading 3 一一对应。把章节名和层级写进配置脚本就能按顺序生成。from docx import Document from docx.shared import Pt, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH doc Document() # 页边距按学院模板通常上下 2.54 cm、左右 3.17 cm for section in doc.sections: section.top_margin Cm(2.54) section.bottom_margin Cm(2.54) section.left_margin Cm(3.17) section.right_margin Cm(3.17) doc.add_heading(一、实验目的, level1) doc.add_paragraph(掌握 FFT 的幅值刻度换算方法理解频谱泄露与窗函数的关系。, styleNormal) doc.add_heading(1.1 实验原理, level2) doc.add_paragraph(离散傅里叶变换将有限长序列映射到频域……, styleNormal)逻辑说明add_heading的 level 参数决定样式名Heading 1 / Heading 2不是数字层级写 0 会变成 Title 样式。页边距必须在保存前设置doc.sections是一个列表多节文档要逐个设。正文段落统一用styleNormal不要用add_paragraph()之后调paragraph_format逐个改样式集中管理才能在模板换版时一处生效。3.2 中文字体rFonts 的 eastAsia 属性才是关键只设font.name的话中文在 Word 里会回落到默认字体字号和行距跟着变导出 PDF 后更难对齐。原因是 OOXML 把西文和东亚字符的字体分开存必须同时写w:ascii和w:eastAsia。from docx.oxml.ns import qn from docx.shared import Pt style doc.styles[Normal] style.font.name Times New Roman # 只影响西文和数字 style.font.size Pt(12) style.element.rPr.rFonts.set(qn(w:eastAsia), 宋体) # 中文字体 # 行距固定 1.5 倍段前段后按模板微调 pf style.paragraph_format pf.line_spacing 1.5 pf.space_before Pt(0) pf.space_after Pt(0)逻辑说明style.element.rPr是样式底层的 XML 节点rFonts上挂着w:ascii、w:hAnsi、w:eastAsia、w:cs四个属性python-docx 只暴露了前两个的快捷写法东亚字体要自己用qn拼命名空间。数字和公式里的变量用 Times New Roman中文用宋体这是实验报告最常见的组合。如果模板要求正文小四号12 pt而标题用三号16 pt把所有字号写成常量放在文件开头后续调整只改一处。3.3 三线表把实验参数和结果对照着放数字信号处理实验报告里表格出现频率很高采样参数表、滤波器指标表、不同窗函数的性能对比表。学院模板一般要求三线表也就是只有顶线、底线和表头下的分隔线中间不画竖线。from docx.enum.table import WD_TABLE_ALIGNMENT from docx.oxml.ns import qn from docx.oxml import OxmlElement def set_border(cell, edge, sz8, valsingle): tcPr cell._tc.get_or_add_tcPr() borders tcPr.find(qn(w:tcBorders)) if borders is None: borders OxmlElement(w:tcBorders) tcPr.append(borders) el OxmlElement(fw:{edge}) el.set(qn(w:val), val) el.set(qn(w:sz), str(sz)) borders.append(el) rows [(参数, 取值, 说明), (采样率 fs, 1000 Hz, 大于 2 倍最高频率), (点数 N, 1024, 频率分辨率 0.98 Hz), (IIR 阶数, 4, 巴特沃斯低通), (FIR 点数, 65, 汉明窗线性相位)] table doc.add_table(rowslen(rows), cols3) table.alignment WD_TABLE_ALIGNMENT.CENTER for i, row in enumerate(rows): for j, text in enumerate(row): table.cell(i, j).text text for c in table.rows[0].cells: # 表头下框线 set_border(c, bottom, sz8) for c in table.rows[-1].cells: # 表格底线 set_border(c, bottom, sz8)逻辑说明python-docx 没有三线表的现成 API只能往单元格的tcPr里注入w:tcBorders。sz的单位是 1/8 磅8 大约是 1 磅符合大多数模板要求。表格宽度默认自适应如果模板要求固定列宽在写入文字前给每个单元格设cell.width否则 WPS 打开时列宽会跳。实验报告的表格建议在表上方加一行表题用add_paragraph加居中格式不启用 Word 的题注域避免编号错乱。3.4 页眉页脚与页码字段页码在很多模板里是硬性要求用 python-docx 加页码需要插入域代码因为add_paragraph只能写静态文本。from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.enum.text import WD_ALIGN_PARAGRAPH footer doc.sections[0].footer p footer.paragraphs[0] p.alignment WD_ALIGN_PARAGRAPH.CENTER run p.add_run() for tag, attr in [(w:fldChar, {w:fldCharType: begin}), (w:instrText, None), (w:fldChar, {w:fldCharType: end})]: el OxmlElement(tag) if attr: for k, v in attr.items(): el.set(qn(k), v) if tag w:instrText: el.set(qn(xml:space), preserve) el.text PAGE run._r.append(el)逻辑说明页码是 Word 的域结构是「开始标记 域指令 结束标记」三段PAGE两侧的空格不能省否则某些版本解析不出指令。页眉同理把PAGE换成STYLEREF Heading 1就能自动显示当前章节名。注意域只在打开文档时计算人工检查要先用 Word 或 WPS 打开一次让它渲染直接读 XML 看不到数字。4. 波形图与公式进 docx图片插入、OMML 与常见渲染坑计算脚本产出的图和数据要变成报告里的插图和公式这一步最容易出现「在编辑器里好看、交上去变形」的问题。4.1 matplotlib 出图参数dpi、figsize 与中文字体插图清晰度和中文字体是两个独立的坑。dpi 决定位图分辨率figsize 决定物理尺寸两者共同决定插入 Word 后的显示大小。import matplotlib matplotlib.use(Agg) # 无显示环境必须指定后端 import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] # 中文 plt.rcParams[axes.unicode_minus] False # 负号正常显示 fig, ax plt.subplots(figsize(6.0, 3.0), dpi300) ax.plot(freq, amp, lw0.9, label矩形窗) ax.plot(freq, amp_win, lw0.9, label汉宁窗) ax.set_xlabel(频率 f / Hz) ax.set_ylabel(幅值 / V) ax.set_xlim(0, 500) ax.grid(alpha0.3, linestyle--) ax.legend() fig.tight_layout() fig.savefig(spectrum.png, dpi300, bbox_inchestight)逻辑说明figsize(6, 3)对应 6 英寸宽按 1 英寸 2.54 cm 换算约 15.2 cm加bbox_inchestight裁掉空白边缘后和 A4 正文宽度接近。dpi300是印刷级文件大小约几百 KB够用设成 600 会让 docx 体积迅速膨胀。SimHei在部分 Linux 环境没有改成Noto Sans CJK SC更通用字体缺失时中文会显示成方框这一点在出图后要立刻打开 PNG 确认。4.2 add_picture 的尺寸换算与图注编号插入图片时width参数用的是 EMU 或Cm、Inches对象直接写像素数会被当成 EMU图片会小到看不见。from docx.shared import Cm from docx.enum.text import WD_ALIGN_PARAGRAPH p doc.add_paragraph() p.alignment WD_ALIGN_PARAGRAPH.CENTER p.add_run().add_picture(spectrum.png, widthCm(14.5)) cap doc.add_paragraph() cap.alignment WD_ALIGN_PARAGRAPH.CENTER cap.add_run(图 4-1 矩形窗与汉宁窗的幅值谱对比).font.size Pt(10.5)逻辑说明正文宽度 21 cm 减去左右页边距 6.34 cm 剩 14.66 cm图宽取 14.5 cm 留一点余量避免顶到边距。图注单独一段、居中、小一号字编号手工写而不是用SEQ域因为实验报告章节不多手工编号更可控。如果同一张图要跨脚本复用把文件名和编号写进 JSON 配置排版时按顺序取。4.3 OMML 公式手写 XML 与 LaTeX 转换两条路Word 的原生公式格式是 OMMLpython-docx 不提供写入接口只能自己拼 XML 或者用第三方转换。实验报告里公式数量有限手写最关键的两三个、其余用文本表达式是性价比最高的做法。from docx.oxml import parse_xml from docx.oxml.ns import nsdecls def add_omml(doc, xml_body): para doc.add_paragraph() xml fm:oMathPara {nsdecls(m, w)}m:oMath{xml_body}/m:oMath/m:oMathPara para._p.append(parse_xml(xml)) return para # 系统函数 H(z) (b0 b1 z^-1) / (1 a1 z^-1) frac ( m:f m:numm:rm:tb0 b1 z/m:t/m:r m:sSupm:em:rm:tz/m:t/m:r/m:e m:supm:rm:t-1/m:t/m:r/m:sup/m:sSup/m:num m:denm:rm:t1 a1 z/m:t/m:r m:sSupm:em:rm:tz/m:t/m:r/m:e m:supm:rm:t-1/m:t/m:r/m:sup/m:sSup/m:den /m:f ) add_omml(doc, m:rm:tH(z) /m:t/m:r frac)逻辑说明m:oMathPara是独立成行的公式容器m:oMath是行内公式二者不能混用。m:f表示分式m:num和m:den是分子分母m:sSup是上标结构m:e放底数、m:sup放指数。nsdecls(m, w)会自动补上命名空间声明缺了它 Word 会直接报文档损坏。如果公式量大且已经在 LaTeX 里写好另一条路是先用转换工具把 LaTeX 转成 MathML再用 Office 自带的 MML2OMML.XSL 做一次 XSLT效果更规整但要多一个依赖和一次文件读取。两条路都可行公式少于十个时手写反而更快。注意OMML 里不要出现全角括号和中文标点Word 解析时会报「文档中的 XML 无效」排查起来很费时间。公式内的文字统一用半角。4.4 图片与公式在 WPS、Word 里的渲染差异同一份 docx 在两个软件里打开常见差异有三处一是字体回落WPS 找不到宋体时用自带的替代字体行距跟着变二是表格边框WPS 对默认w:tblBorders的解释比 Word 宽松三线表可能多出竖线三是公式老版本 WPS 对 OMML 的渲染位置会偏移几个像素。应对方法很朴素交作业前用目标软件各打开一次把页数、图的位置、表格线扫一遍。如果学院只收 PDF用同一台机器、同一个软件导出避免在 A 电脑用 Word 导出、B 电脑用 WPS 打开再导出。另外文档里所有图片都用嵌入而不是链接链接图片换台电脑就变成红叉这一点在复制到 U 盘前必须确认。5. 批量生成与排错多份数字信号处理实验报告的一致性校验一个学期的实验报告如果有五六份格式漂移几乎必然发生因为每次都是手改。把模板和内容彻底分开用同一份脚本跑多组数据是控制一致性最直接的办法。5.1 模板加数据驱动一次产出多组实验记录把第 2 章的npz和 JSON 当成输入把第 3、4 章的排版逻辑包成一个函数参数只留实验编号和输出路径。每组实验对应一个数据目录脚本遍历目录批量生成章节编号和图号按目录顺序自动递增。import json from pathlib import Path from docx import Document def build_report(data_dir: Path, out_path: Path, index: int): meta json.loads((data_dir / dsp_meta.json).read_text(encodingutf-8)) doc Document() doc.add_heading(f实验{index}{meta[title]}, level1) doc.add_paragraph( f采样率 fs {meta[fs]} Hz点数 N {meta[N]} f频率分辨率 {meta[fs] / meta[N]:.2f} Hz。 ) # 后续按需插入表格、图片和公式 doc.save(out_path) for i, d in enumerate(sorted(Path(data).iterdir()), start1): build_report(d, Path(fout/实验{i}.docx), i)逻辑说明sorted()保证生成顺序和目录名一致编号不会乱把index传进函数而不是在函数内部计数是为了让标题里的编号和文件名严格对应。如果某个实验的图数量不同把图和表的清单也放进 JSON函数按清单循环避免为每个实验写一个分支。5.2 docx 打不开、无法预览、WPS 默认新建 docx 的排查顺序生成出来的 docx 出问题排查要按从外到内的顺序走先确认文件本身完好再怀疑软件配置。docx 本质是个 ZIP 包用解压工具打开就能看到word/document.xml这一步能排除大部分「文件损坏」。现象常见原因处理方式打开提示文档损坏OMML 命名空间缺失或标签未闭合解压 docx用 XML 工具校验 document.xml资源管理器无法预览 docx预览处理器未安装或文件关联异常安装完整版 Office 组件或改用缩略图模式WPS 不能默认新建 docx.docx 关联被其他办公软件占用系统默认应用里把 .docx 指回 WPS 或 Word图片显示红叉用了链接图片或路径含中文改为嵌入图片输出目录避免中文和空格保存时报权限错误目标文件正被 Word 或 WPS 打开关闭已打开的文档或输出到新文件名中文字体变成方框系统缺少指定字体换用 Noto Sans CJK SC 或微软雅黑PermissionError是批量脚本最常撞到的一个原因是上一轮生成的 docx 还开着。稳妥写法是输出文件名带时间戳或者在写入前捕获异常并跳过而不是让整个批次中断。另外文件名里不要出现空格和中文很多脚本环境处理路径时不做转义会在这里翻车。5.3 用哈希校验交叉核对数据与文档最后一层验证是确认文档里的数字确实来自计算脚本。把关键指标算一个哈希或者直接做数值比对比人工读一遍可靠。import hashlib import json import numpy as np d np.load(data/exp1/dsp_data.npz) # 取前 8 个频谱幅值保留 6 位小数后拼接 sig ,.join(f{v:.6f} for v in d[amp][:8]) digest hashlib.md5(sig.encode()).hexdigest() meta json.loads(open(data/exp1/dsp_meta.json, encodingutf-8).read()) print(meta[title], digest)逻辑说明取前 8 个幅值是因为峰值附近的数值最能反映参数是否一致浮点格式化到 6 位可以消除平台差异带来的末位抖动。把哈希写进报告附录或者提交记录下次数据被误改时能立刻发现。如果同一组参数要在两份文档里出现用同一个 npz 文件不要从 docx 里反向抄数字——这一步看着多余实际能省掉大量返工。本文还有配套的精品资源点击获取