ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python自动化生成Word文档:以批量学院信息为例

Python自动化生成Word文档:以批量学院信息为例 简介这是一份嘉应学院数学学院人才培养方案文档以数学与应用数学专业师类本科为主体同时涵盖信息与计算科学、数学教育等专业方向适合师范生、考研学生、教务管理者及关注数学教师培养的读者作为教学计划、专业评估或升学规划的参考。资源包仅含1个doc文件约630KB结构完整清晰划分为培养目标与规格、主干学科与核心课程、“平台模块”课程设置、学分分配、实践教学环节及学制安排等模块便于查阅与二次编辑。文档详细列出数学分析、高等代数、解析几何、常微分方程、概率统计、数学教学论等核心课程并规划了教育实习和毕业论文的周数与学分附有课程类别、学时与学分统计表及教育活动时间分配表能够帮助读者快速把握数学与应用数学专业从理论课程到实践训练的整体培养框架。目前已有119人学习下载。1. 收到“嘉应学院数学学院.doc”这个文件名时我意识到这是个自动化问题一个名为“嘉应学院数学学院.doc”的文件在很多校园信息化场景里反复出现要么是从官网复制页面内容手工排成Word要么是汇总各学院师资、专业、成果后统一归档。这类文档的特点是结构固定、数据零散、命名却高度规律比如“XX学院.doc”“XX学院信息.doc”。如果有十几个学院要同时处理手工复制粘贴不仅慢还容易把段落格式和表格边框弄乱。更麻烦的是下次数据一更新整个文档又得从头做一遍。与其每次都用Word手动操作不如把“嘉应学院数学学院.doc”当作一个典型样本写一套用Python生成和批量维护学院信息文档的流程。这套方法只需要requests、python-docx和LibreOffice三个工具就能把数据源变成排版干净、可校对的Word文件并且下次数据变化时只改数据文件脚本不需要动。这类需求在校办、教务处、二级学院行政岗位里相当普遍开发者和运维都能从中看到一条可复用的自动化路径。2. 数据源先行把零散信息整理成可编程的结构2.1 为什么用JSON作为中间层而不是直接爬网页初次接触这个需求的人最容易想到的是写爬虫直接抓嘉应学院数学学院的官网页面然后解析HTML往Word里塞。这个思路能通但不稳页面改版、字段缺失、编码异常都会让脚本崩在中间环节。更推荐的做法是先把信息从网页、Excel或已有文档中抽取出来整理成一个中间层文件比如JSON或CSV。JSON是最好的选择因为学院简介、专业列表、师资名单这类数据天然有嵌套关系用JSON可以保留结构后续用python-docx填充时也只需要按key取值。中间层的存在价值在于它把“数据采集”和“文档生成”彻底解耦。数据更新时只需要重新抓取页面并覆盖JSON不必碰生成文档的代码。反过来文档格式调整时只需要改模板脚本不用重新去抓网页。这种分层思路同样适用于其他场景比如从教务系统导出教师信息、从科研系统拉取项目列表都可以先落成JSON再做下游处理。2.2 用requests从官网抓取学院简介的示例假设嘉应学院数学学院官网的结构是标准的HTML页面简介在#intro节点下师资名单在#teachers表格中。下面这段脚本只做一件事抓取页面提取关键字段落成JSON。import requests from bs4 import BeautifulSoup import json url https://math.jyxy.edu.cn/intro.html resp requests.get(url, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) intro soup.select_one(#intro).get_text(stripTrue) teachers [] for tr in soup.select(#teachers tr)[1:]: tds tr.find_all(td) if len(tds) 3: teachers.append({ name: tds[0].get_text(stripTrue), title: tds[1].get_text(stripTrue), direction: tds[2].get_text(stripTrue) }) data { college: 数学学院, school: 嘉应学院, intro: intro, teachers: teachers } with open(college_info.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)这段代码逻辑不难但有几个参数值得说明。timeout10限制请求等了10秒就放弃避免某个页面卡死整个任务resp.encoding utf-8是强制指定编码很多高校站点没有在响应头里写明charset靠requests猜测经常会猜成GBK导致中文乱码get_text(stripTrue)会把段落首尾空白去掉同时把内部连续空白压缩成单个空格处理简介时尤其好用。最后写JSON时用ensure_asciiFalse这样中文字符直接以可读形式落盘而不是变成\uXXXX转义序列后续排查数据问题时不用再解码。2.3 清洗文本时最容易翻车的三个细节抓下来或复制进来的文本往往带着肉眼看不到的脏数据。第一个坑是特殊空白字符比如\u3000全角空格、\xa0不间断空格混在中文段落里会让Word的对齐看起来忽宽忽窄。处理办法简单用re.sub(r[\u3000\xa0], , text)统一替换成普通空格。第二个坑是图片路径与相对URL官网上的图片地址经常是/uploads/xxx.jpg如果直接写到JSON里后期插入图片时没有绝对路径会找不到文件。抓取时应拼接完整URL转成可下载的绝对地址。第三个坑是换行符混用Windows和Linux下抓到的文本可能同时存在\r\n和\n在Python里统一预处理成\r\n即可。这三个问题单独看都不大但叠加在一起会让最终生成的Word文档显得粗糙。实际项目中我会把清洗逻辑单独封装成一个函数输入原始列表输出清洗后的字符串后续脚本调用同一份清洗逻辑避免每处各写一遍。3. 用python-docx搭出文档骨架标题、段落与表格3.1 最小可用代码生成一个带学院名称的docx数据准备好了接下来就是最核心的部分用python-docx搭建文档结构。首先安装依赖然后写一段最小代码生成带有标题、简介段落和师资表格的word文件。from docx import Document from docx.shared import Pt, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH import json with open(college_info.json, r, encodingutf-8) as f: data json.load(f) doc Document() title doc.add_heading(f{data[school]}{data[college]}, level0) title.alignment WD_ALIGN_PARAGRAPH.CENTER intro_title doc.add_heading(学院简介, level1) doc.add_paragraph(data[intro]) doc.add_heading(师资队伍, level1) table doc.add_table(rows1, cols3) table.style Table Grid hdr table.rows[0].cells hdr[0].text 姓名 hdr[1].text 职称 hdr[2].text 研究方向 for teacher in data[teachers]: row table.add_row().cells row[0].text teacher[name] row[1].text teacher[title] row[2].text teacher[direction] doc.save(嘉应学院数学学院.docx)这段代码里最值得注意的参数是level0和doc.add_heading。level0生成的是文档主标题样式对应Word里的“标题1”但字体更大如果直接用add_paragraph再手动加粗标题不会出现在导航窗格里不利于长文档快速跳转。table.style Table Grid给表格加上全部边框否则默认表格没有可见线条打印出来像纯文本排列不符合归档要求。add_heading(学院简介, level1)让小节标题出现在目录和导航窗格中方便阅读者在大纲视图里来回切换。3.2 插入师资表格并设置列宽与边框上面的表格能跑但列宽完全交给Word默认分配通常三个字段等宽姓名列浪费空间研究方向列挤成一团。实际排版时应该按内容长度分配比例姓名占2.5厘米职称占3厘米研究方向占剩余宽度。from docx.shared import Cm widths [Cm(2.5), Cm(3.0), Cm(9.0)] for row in table.rows: for idx, width in enumerate(widths): row.cells[idx].width width table.autofit False这里有个python-docx容易踩的坑只设置单元格宽度还不够必须同时把table.autofit设为False否则Word打开时会根据内容重新计算列宽设置的值全部失效。另一个细节是列宽的单位用Cm而不是Inches国内打印场景习惯用厘米单位换算起来更直观。表格样式是Table Grid如果后续需要调整边框颜色或粗细得通过table.style下的XML属性修改普通API不直接暴露这一层属于进阶玩法。3.3 样式对象与中文排版参数字体、字号、段落缩进中文文档和英文文档在排版上有很大差异中文正文默认仿宋或宋体三号西文用Times New Roman段落首行缩进两字符标题字号逐级递减。python-docx默认模板是英文风格直接生成的中文文档看起来会很“西式”字体发虚、行距偏大所以必须通过样式对象全局修改。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), 宋体) pf style.paragraph_format pf.line_spacing 1.5 pf.first_line_indent Pt(24)qn(w:eastAsia)是python-docx里一个特殊处理docx底层XML把中文字体存放在w:eastAsia属性中直接用style.font.name设置只能改西文字体中文还是落到默认主题上。必须配合qn设置中文字体名中英文才会分别使用各自的字体。first_line_indent Pt(24)实现首行缩进两个字符因为12磅字号的两字符约等于24磅。用点数而不是“字符数”作为单位的好处是同时兼容不同段落直属样式不会因为标题样式覆盖字体大小而缩进异常。4. 批量生成多学院文档循环、占位符与资源管理4.1 一个脚本处理多个学院的循环控制实际工作里往往不是只生成一个学院而是要一次处理数学学院、物理学院、计算机学院等多个部门。此时数据源应改为一个包含多份JSON的目录或者一个合并的数组。在循环里需要特别小心变量污染每次生成新文档时必须在循环内部新建Document()对象不能复用同一个doc并不断追加内容否则后一个学院会包含前一个学院的数据。import json import glob from docx import Document for json_path in glob.glob(colleges/*.json): with open(json_path, r, encodingutf-8) as f: data json.load(f) doc Document() title doc.add_heading(f{data[school]}{data[college]}, level0) title.alignment WD_ALIGN_PARAGRAPH.CENTER doc.add_heading(学院简介, level1) doc.add_paragraph(data[intro]) doc.add_heading(师资队伍, level1) table doc.add_table(rows1, cols3) table.style Table Grid for teacher in data[teachers]: row table.add_row().cells row[0].text teacher[name] row[1].text teacher[title] row[2].text teacher[direction] output_name f{data[school]}{data[college]}.docx doc.save(output_name) print(f已生成: {output_name})这个循环最大的价值体现在异常隔离上。如果某个学院的JSON里缺了intro字段程序会在那一轮抛出KeyError后面的学院全部停止生成。更稳妥的做法是在循环内套一层try...except记录失败原因后跳过当前文件继续下一个所有失败项统一在最后输出。这样可以避免“一个学院的数据问题阻塞了整个部门的工作”这种低效情况。4.2 插入图片和页眉页脚时要注意的路径问题多数学院文档需要加入院徽或校园照片。插入图片时最常见的坑是路径中存在空格或中文字符导致doc.add_picture报错。解决方案不是去改文件名而是用pathlib.Path处理路径对象再传给add_picture这样从根上规避了字符串拼接和转义问题。from pathlib import Path logo_path Path(assets) / math_logo.png if logo_path.exists(): doc.add_picture(str(logo_path), widthCm(3.0)) doc.paragraphs[-1].alignment WD_ALIGN_PARAGRAPH.CENTERdoc.add_picture默认会在图片下方生成一个段落用doc.paragraphs[-1]拿到这个新段落并设置居中。插入尺寸用widthCm(3.0)控制这样即使原图很大也会被缩放否则几兆像素的图片直接变换为Word页面全宽打印时会非常突兀。图片路径校验必须在插入前进行exists()检查能提前暴露缺失资源而不是等到保存时才发现。页眉页脚的设置逻辑类似doc.sections[0].header可以访问页眉区域在其中添加段落或图片。需要注意每个section的页眉是独立的如果文档被分成了多个section只给第一个加页眉后续section不会自动继承这里不像Word手工操作那样默认“链接到前一条页眉”。4.3 用subprocess调用LibreOffice把docx转为doc标题里最终交付的是.doc后缀而python-docx只能输出.docx。虽然两者都是Word可打开的文件但很多老旧办公系统只接受真正的.doc二进制格式。Linux服务器上最可靠的做法是使用LibreOffice的无头模式进行转换。soffice --headless --convert-to doc 嘉应学院数学学院.docx --outdir output/如果是在Python脚本内部调用用subprocess.run更灵活。import subprocess subprocess.run([ soffice, --headless, --convert-to, doc, 嘉应学院数学学院.docx, --outdir, output/ ], checkTrue)--convert-to doc的转换格式需要LibreOffice环境下安装了Microsoft Word过滤器Debian系中通常由libreoffice-writer包提供。转换后的文件名与源文件相同只是后缀变成.doc所以输出目录必须存在否则soffice静默失败并返回非零退出码。checkTrue让脚本在转换失败时抛出CalledProcessError便于批量任务中及时发现异常。这里有一个额外经验转换之前不要打开同名docx文件LibreOffice本身允许覆盖但某些版本会因文件锁而转换失败表现为生成空的doc文件。5. 文档校验读回docx核对内容验证doc文件真实性与完整性生成文档只是第一步真正让这套自动化流程可信靠的是校验环节。常见的校验包括字数是否合理、表格行数是否与数据源匹配、标题是否齐全。python-docx可以读回文件执行这些检查。from docx import Document def verify_docx(path, expected_teachers): doc Document(path) all_text \n.join(p.text for p in doc.paragraphs) assert 嘉应学院数学学院 in all_text, 缺少学院名称 assert len(doc.tables) 1, 缺少表格 teacher_table doc.tables[0] actual_teachers len(teacher_table.rows) - 1 assert actual_teachers expected_teachers, f师资行数不一致: {actual_teachers} ! {expected_teachers} return True这段校验代码最核心的价值是防止“文档生成了但内容残缺”这类问题。比如doc.paragraphs只能读取段落而doc.tables[0]可以拿到第一个表格。校验时把表格行数减去表头行再与数据源的师资数量比对任何不匹配都会立即触发断言。除了逻辑校验还需要看转换后的.doc文件头。真正的OLE复合文档以D0 CF 11 E0开头而新格式.docx则是50 4B 03 04用如下命令即可分辨。xxd -l 4 output/嘉应学院数学学院.doc如果看到d0 cf 11 e0则表明转换成功如果仍是50 4b则说明Word文件依然以zip容器存储。用xxd -l 4读取前四个字节配合管道脚本可以做批量文件格式巡查。校验通过后整套自动化生成流程才算闭环。这样的做法把“文件生成”升级为“可验证的交付物”无论交付给上级部门还是归档保存都能保证每一份文档内容和格式双达标。后续数据有变动时只需重跑一遍生成与校验脚本十分钟内几十个学院的信息文档就能全部更新完毕。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进