前阵子某个月底,我照例要把十来套数据库的巡检结果整理成 Word 报告发给团队和上级。说实话,最烦人的根本不是巡检本身,而是巡检完之后的“手工组装”:打开 Excel 看数据、把关键指标复制到 Word、调整表格列宽、统一字体字号、改页码……一套流程下来,光排版就能耗掉大半天。后来我实在忍不住了,花了两个晚上写了一套自动化脚本,从此“数据库巡检”到“Word报告生成”之间再也不用人工搬运。这篇文章就把这套方案完整拆给你看,包括架构思路、指标设计、关键代码、踩坑实录,哪怕你没有写过一行代码,照着思路也能让工具人自己干苦力。
1.1 手工写报告,到底浪费在哪
先说个扎心的事实:大多数团队的数据库巡检报告,写法和五年前没什么区别。无非是登录数据库执行几条命令,把结果复制到文本里,再粘贴进 Word 调格式。单套库可能只花二十分钟,但当你手里有 MySQL、PostgreSQL、Oracle 混着十几套实例时,纯粹的数据搬运和格式调整就能吃掉一个下午。
我统计过自己手工写报告的时间分布:真正跑巡检命令只占 30%,从原始输出里捞关键指标占 20%,剩下的 50% 全部耗在 Word 排版上——对齐表格、加粗表头、把小数点后六位的数字改成两位、调整页边距和行距。这些动作毫无技术含量,但偏偏又必须做,因为一份连表格都撑出页面边界的报告,递出去只会显得不专业。
所以“告别手工整理”这件事,核心不是简化巡检,而是把“从原始数据到成稿报告”这段路自动化。巡检命令该跑还是跑,但跑出来的结果不再经过人工复制粘贴,而是直接喂给脚本,由脚本完成排篇布局、格式规范和内容填充。
1.2 自动化链条:采集、转换、渲染三层分离
我最初的想法很粗暴:写一个脚本,又是连数据库又是生成 Word,一把梭。但写到一半就发现不行,因为巡检数据源太杂,有 SQL 查询结果、有系统命令输出、还有需要人工填写的备注信息,全揉在一个脚本里,改一处就得动全局,维护成本太高。
后来我参考了后端开发里常见的分层思路,把整个流程拆成三段:
- 采集层:负责连数据库、跑巡检 SQL、收集系统信息,最终输出结构化的 JSON 文件。这一层只关心“数据准不准”,不关心报告长什么样。
- 转换层:把 JSON 文件映射成报告所需的指标项,比如把“缓冲命中率 0.9971”变成“缓存命中率 99.71%”,把原始字节数换算成 GB。这一层只关心“数据怎么表达”。
- 渲染层:读取处理好的指标,用 python-docx 生成 Word 文档,负责字体、表格、段落、页眉页脚。这一层只关心“长得好不好看”。
串联起来就是一条命令:先执行巡检采集脚本生成 JSON,再执行报告生成脚本读取 JSON 并输出 docx。中间任何一层出了问题,都可以单独调试,不会互相拖累。这也是我后来敢在报告模板里不断折腾字体和样式的原因——改渲染层的代码,完全不会影响数据准确性。
1.3 技术选型:为什么是 Python + python-docx
选 Python 不需要多解释,数据库连接有成熟的 PyMySQL、psycopg2 库,处理 JSON 有内置的 json 模块,最关键的是有 python-docx 这个库,专门用来操作 Word 文档,能创建段落、表格、标题,还能设置字体和样式。
python-docx 的能力边界需要提前说清楚:它擅长从零生成结构规整的 Word 文档,也能读取和修改已有的 docx 文件,但做不到像 VBA 那样对文档进行非常细粒度的排版控制。也就是说,如果你要生成一份完全自定义、带复杂页眉页脚和封面设计的报告,python-docx 也能做到,但需要你多写一些样式代码。好在我日常的巡检报告结构比较固定,无非是标题、表格、结论段落,python-docx 完全够用。
还有一套备选方案是用 Pandoc 把 Markdown 转成 Word,我也试过,Markdown 写起来确实快,但如果报告里有很多列数不固定的表格,Pandoc 的表格样式控制起来非常吃力。相比之下,python-docx 对每个单元格都能单独操作,适合做精细控制。
2. 巡检指标设计与数据采集脚本
2.1 巡检指标怎么选:宁精勿滥
报告不是数据堆砌,给老板看的报告尤其如此。我见过有人把SHOW GLOBAL STATUS的几百行输出全塞进 Word 里,结果就是一份看起来“很专业”但没人会认真读的流水账。真正有效的巡检报告,应该回答这几个问题:系统现在健康吗?有哪些隐患?需不需要处理?
所以我的指标清单只保留这些类别,每类挑三到五个关键项:
- 实例基础信息:数据库版本、运行时长、字符集、端口号。这类指标用于确认巡检对象的基本盘。
- 连接与会话:当前连接数、最大连接数、活跃会话数、Threads_running。连接数逼近上限是生产事故的前兆。
- 事务与锁:当前活跃事务数、锁等待次数、阻塞会话ID。用于发现长时间的锁竞争。
- 慢查询:慢查询条数、最慢 SQL 的执行时间、慢查询日志大小。这是 SQL 性能问题的直接证据。
- 存储空间:数据目录剩余空间、单表最大的几个表、binlog 占用空间。
- 性能关键指标:Buffer Pool 命中率、QPS、TPS、InnoDB 行读次数。
- 主从复制状态(如果是从库或主从架构):复制延迟秒数、Slave_SQL_Running 状态、中继日志大小。
这套指标组合覆盖了“可用性、性能、容量”三个巡检维度。既不会少到漏掉问题,也不会多到让人抓不住重点。每次巡检结果里,我还会让脚本自动生成一句健康度结论,比如“实例运行平稳,无明显异常”或“连接数已达上限的 80%,建议扩容或排查连接泄漏”,这句话直接放在报告开头,省得阅读报告的人自己去揣摩。
2.2 采集脚本的实现思路
采集脚本我用 Python 写,连接 MySQL 用的是 PyMySQL,连不上时会把错误信息也写进 JSON,绝不中断整个巡检流程。核心思路是维护一个字典,把每个类别的指标查完后塞进去,最后统一json.dump到文件。
伪代码结构是这样的:
import pymysql, json, socket, time def get_conn(): return pymysql.connect( host="10.0.0.5", user="monitor", password="xxxx", connect_timeout=3 ) def collect_basic(cursor, result): cursor.execute("SELECT VERSION()") result["basic"]["version"] = cursor.fetchone()[0] cursor.execute("SHOW GLOBAL STATUS LIKE 'Uptime'") result["basic"]["uptime_s"] = int(cursor.fetchone()[1]) def collect_conn(cursor, result): cursor.execute("SHOW GLOBAL STATUS LIKE 'Threads_connected'") result["connection"]["threads_connected"] = int(cursor.fetchone()[1]) cursor.execute("SHOW VARIABLES LIKE 'max_connections'") result["connection"]["max_connections"] = int(cursor.fetchone()[1]) def main(): result = { "db_host": socket.gethostname(), "check_time": time.strftime("%Y-%m-%d %H:%M:%S"), "basic": {}, "connection": {}, "status": {} } try: conn = get_conn() with conn.cursor() as cursor: collect_basic(cursor, result) collect_conn(cursor, result) except Exception as e: result["error"] = str(e) finally: with open("check_mysql_prod_01.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2)细节上我做了几个特殊处理:连接超时设为 3 秒,避免某台库宕机时巡检脚本挂住;采集失败时不是抛异常退出,而是把错误写进 JSON 的error字段,让报告生成脚本在 Word 里单独显示“采集失败”的提示;密码这类敏感信息不硬编码在脚本里,而是放环境变量。
2.3 JSON 中间文件的结构与命名规范
JSON 是采集层和渲染层之间的“协议”,结构设计直接影响后续生成的复杂度。我的做法是:每个实例对应一个 JSON 文件,文件名带上库名和时间,比如check_mysql_prod_01_20250115.json,这样即使批量跑了几十套库,也不会互相覆盖。
JSON 内部按类别分块,每块是一个字典:
{ "db_host": "mysql-prod-01", "db_type": "MySQL", "check_time": "2025-01-15 10:30:22", "basic": { "version": "8.0.32", "uptime_s": 604800 }, "connection": { "threads_connected": 128, "max_connections": 500, "threads_running": 4 }, "performance": { "buffer_pool_hit_rate": 0.9961, "qps": 1520.5, "tps": 38.2 }, "replication": { "slave_io_running": "Yes", "slave_sql_running": "Yes", "delay_seconds": 0 }, "storage": { "data_free_mb": 20480, "top_big_table": "orders" } }这里有个容易被忽略的点:JSON 里存的是原始数值,比如 uptime 用秒、buffer_pool_hit_rate 用小数。换算成“天”“百分比”的工作留给渲染层。好处是采集脚本不用关心展示逻辑,以后想换成“小时”只需要改渲染层,不用重新采集。
你可能会问,为什么不用数据库直接生成 CSV 再转 Word?CSV 的问题在于没有层级结构,关联性强的指标(比如连接数和最大连接数)在 CSV 里就是两列,需要通过列名约定位子,扩展性和可读性都不如 JSON。而且 JSON 本身是树形结构,和报告章节天然对应,渲染层写起来非常顺。
这里正好接上你最近在 Linux 上看到的一个技巧:一键获取文件名并生成列表。巡检脚本跑完会产生一堆 JSON 文件,想批量交给报告脚本处理,一条命令就搞定:
for f in check_*.json; do python gen_report.py "$f"; done或者更“现代”一点,直接利用 find 拿到完整路径列表,配合 xargs 传给 Python:
find ./checks -name "check_*.json" -print0 | xargs -0 -I {} python gen_report.py {}这样不管是手动跑一下还是挂到 crontab 里定时执行,都能做到“新数据一到,报告自动生成”,整套流水线完全不需要人守在现场。
3. Word 报告自动生成实操细节
3.1 报告模板与内容结构设计
动手写代码之前,先把报告长什么样想清楚。一份好的巡检报告,结构应该像体检报告一样清晰:先给结论,再列明细,最后给建议。我的模板固定为六个章节:
- 封面区:报告标题、巡检系统名、巡检时间、巡检人(脚本写死为“自动巡检系统”)。
- 巡检概览:一段文字描述本次巡检的总体结论,后面是一个汇总表格,展示实例基础信息和关键健康指标。
- 详细信息:按指标类别分小节,每个小节配一个表格。这一章是报告主体,阅读者可以直接定位到关心的维度。
- 主从复制状态(只有主从架构才显示):复制延迟、IO 线程和 SQL 线程状态。
- 风险提示与建议:脚本根据指标阈值自动生成建议,比如“连接数达到上限的 80%”“慢查询数较上周增长两倍”。
- 附录:巡检命令清单、采集时间、采集脚本版本号。便于审计时追溯数据来源。
这套结构的好处是:领导只需要看概览和建议,DBA 可以翻详细信息,审计的人看附录。不同角色各取所需,不会互相干扰。
3.2 python-docx 排版关键代码
python-docx 生成 Word 的核心操作有三个:设置文档默认字体、插入标题和段落、插入表格并设置样式。我直接放一段简化但可运行的核心代码,说明几个关键点:
from docx import Document from docx.shared import Pt, RGBColor, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn import json data = json.load(open("check_mysql_prod_01_20250115.json", encoding="utf-8")) doc = Document() # 关键点1:设置正文默认字体,中英文都要设置 style = doc.styles["Normal"] style.font.name = "Calibri" style.font.size = Pt(10.5) style._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑") # 关键点2:插入一级标题 doc.add_heading("数据库巡检报告", level=0) # 关键点3:插入巡检概览段落 p = doc.add_paragraph() run = p.add_run(f"巡检实例:{data['db_host']}") run.bold = True p.paragraph_format.space_after = Pt(6) # 关键点4:插入表格 table = doc.add_table(rows=3, cols=2) table.style = "Table Grid" table.rows[0].cells[0].text = "指标" table.rows[0].cells[1].text = "数值" table.rows[1].cells[0].text = "数据库版本" table.rows[1].cells[1].text = data["basic"]["version"] table.rows[2].cells[0].text = "连接数" table.rows[2].cells[1].text = str(data["connection"]["threads_connected"]) doc.save("巡检报告_mysql_prod_01.docx")这段代码跑完,就能得到一份最基础的 Word 报告。但实际使用中,我很快发现几个必须处理的坑:中文字体不生效、表格宽度超页面、表头没有加粗底纹。这些问题不解决,生成的报告只能算“半成品”。我把完整解决方案放到第四节统一说,因为每个问题都是我踩过坑之后才总结出来的。
3.3 一键执行:文件列表批量处理
单实例的脚本能跑通以后,接下来就是把“跑一次巡检”和“生成一份报告”串成一条命令。我把整个流程写成 shell 脚本,挂在 Linux 的 crontab 里,每周一早上八点自动执行:
#!/bin/bash # 每周巡检报告生成脚本 cd /opt/db_check # 清理上周的中间文件 find ./checks -name "check_*.json" -mtime +7 -delete # 第一步:批量执行采集,这里用数组维护实例清单 for DB_HOST in "10.0.0.5:mysql-prod-01" "10.0.0.6:mysql-prod-02" "10.0.0.7:pg-prod-01"; do HOST=${DB_HOST%%:*} NAME=${DB_HOST##*:} python collect_db.py --host $HOST > ./checks/check_${NAME}_$(date +%Y%m%d).json 2>/dev/null done # 第二步:批量生成报告,关键就是这句话 find ./checks -name "check_*.json" -print0 | xargs -0 -I {} python gen_report.py {} # 第三步:把生成的 docx 统一挪到 report 目录 mkdir -p ./reports mkdir -p ./reports 2>/dev/null find ./reports -name "*.docx" -mtime +30 -delete这个脚本的核心在于,不用手工维护一份“哪些 JSON 对应报告”的映射表,而是用find一次性拿到所有待处理的文件列表,再逐个喂给报告生成脚本。你记得开头提到的“一键获取文件名并生成列表”那个技巧吗?就是这个思路,只不过把“列表”从屏幕输出换成了程序的输入参数。这就是自动化流水线的精髓:把人工枚举步骤砍到零。
如果你用的是 Windows 服务器,也不用担心,PowerShell 里有对应的Get-ChildItem和ForEach-Object,思路完全一样。
4. 常见问题与排查技巧实录
4.1 中文字体设置不正确
先说一个几乎所有新手都会踩的坑:python-docx 里给font.name设置“微软雅黑”,生成的 Word 里中文却还是宋体。原因是 Word 的中文字体需要通过w:eastAsia属性单独指定,只设置 ASCII 字体名是不生效的。
正确写法是:
from docx.oxml.ns import qn run.font.name = "微软雅黑" run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")我建议在设置Normal段落样式时就把中文字体一次配好,而不是每个 run 单独设置,否则代码冗长且容易漏掉某些动态插入的文本。另外要注意,如果后续用add_heading()插入标题,标题使用的是 Heading 样式,不是 Normal,需要单独设置 Heading 1 到 Heading 3 的字体,否则标题可能显示为默认的西文字体。
4.2 表格样式与宽度控制
生成的表格如果列宽不设,Word 默认会按内容自适应。问题在于当其中一列是超长路径或者大段描述文字时,表格会被撑出页面边界,非常难看。解决这个问题有两个办法。
第一种,手动设置表格总宽度和每列比例。python-docx 里可以这样操作:
from docx.shared import Cm table.autofit = False widths = [Cm(5), Cm(4), Cm(8)] for row in table.rows: for idx, width in enumerate(widths): row.cells[idx].width = width第二种更省事:直接把表格风格设置为“网格型”,然后调整页面方向为横向(portrait 是竖向,landscape 是横向)。不过通常巡检报告用竖向就能放下,所以优先选择控制列宽。
我个人的习惯是:数值类列宽设 3 到 4 厘米,名称类列设 5 到 6 厘米,描述类列设 7 厘米以上,这样在 A4 纸上看起来最舒服。
4.3 采集数据缺失怎么兜底
生产环境不比测试环境,经常会遇到某些指标查不出来:比如这次监控账号权限没给足,SHOW SLAVE STATUS直接报错;或者实例刚重启过,某些状态变量被重置为 0。这些情况如果不在脚本里处理,生成的报告就会出现“0”或者干脆报 KeyError 崩溃。
我的方案是两重保险:第一重,采集层捕获异常后依然生成 JSON,只是把异常信息写入error字段;第二重,渲染层在读取每个指标时用dict.get(key, "N/A")而不是dict[key],拿不到数据就显示“N/A”,同时在概览里标红提示“部分指标采集失败,详情见附录”。
这样即使某台库临时抽风,报告还是能顺利生成,阅读者也能一眼看到哪些数据缺失,不会误把“N/A”当成真实值。
4.4 报告更新与二次编辑问题
自动生成的 Word 报告,难免会有需要人工补充备注的时候,比如这次巡检发现某个慢查询和业务发布有关,想在报告里加一段说明。但问题来了:下次自动生成时,如果直接覆盖同名文件,手工备注就全丢了。
我建议把生成文件名带上时间戳,比如巡检报告_mysql_prod_01_20250115.docx,而不是固定成巡检报告_mysql_prod_01.docx。另外,在报告末尾加一个“历史版本记录”表格,每次自动生成时读一下当前目录已有的同名实例报告,把上一份的报告时间追加到记录里。这样既保留历史,又不会因为覆盖而丢失信息。
如果你确实需要每次生成时保留人工批注,一个可行方案是把批注写进 JSON 里一个专门的comment字段,采集时读取人工维护的备注文件,合并到 JSON 中,再由渲染层生成到报告对应位置。等于让人工备注作为“配置”存在,而不是直接编辑 Word。
5. 扩展玩法与实际收益
5.1 从一键生成到自动分发
跑通“Word 报告一键生成”之后,很多人会自然而然想到下一步:报告生成之后怎么送出去?我当时的做法是再接一个邮件发送脚本,把生成的 docx 作为附件,定时发给指定收件人。
import smtplib from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.mime.text import MIMEText def send_report(file_path, to_addr): msg = MIMEMultipart() msg["Subject"] = "数据库巡检周报" msg["From"] = "dba@example.com" msg["To"] = to_addr with open(file_path, "rb") as f: part = MIMEBase("application", "octet-stream") part.set_payload(f.read()) part.add_header("Content-Disposition", f"attachment; filename={file_path}") msg.attach(part) # 具体 smtp 连接配置按公司邮件服务器来加上这段之后,整个流程就变成了:定时任务跑巡检采集 → 生成 Word → 自动发邮件。人只需要在收到邮件后看一眼,有问题再去数据库里验证,大量重复劳动直接消失了。
5.2 这套方案还能复用到哪些场景
最后聊点心得体会。这个“采集到 JSON,JSON 转 Word”的架构,虽然是我做数据库巡检时搭起来的,但后来我发现它几乎能套用到所有“定期生成报告”的场景。
比如服务器巡检,把数据库采集换成psutil或者读取/proc/meminfo,就能生成服务器资源周报;再比如业务日报,只需要把数据源的 SQL 换成业务指标查询,Word 模板改成业务口径,就变成一份面向老板的业务日报生成器。核心思想都一样:把数据和展示解耦,数据层输出统一格式,展示层只负责排版渲染。
我个人在实际操作中的一个体会是:自动化并不等于“什么都让脚本干”,而是“让脚本干它擅长的事,让人干人擅长的事”。数据采集和排版格式化是脚本的强项,识别异常、判断风险还是要靠 DBA 的经验。所以我的脚本里保留了人工补充备注的接口,给机器留了“不可控空间”,这样生成的报告既高效,又不至于完全失去人的判断。
如果你也想搞一套,我的建议是从最小版本开始:先选一个你日常最花时间的报告类型,用今天的脚本方案跑通,不要一开始就追求完美排版,等链路跑顺了再慢慢调样式、加自动发送、接提醒。只要迈出第一步,你就能感受到“一键生成”的爽感。