
前阵子某个月底我照例要把十来套数据库的巡检结果整理成 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( host10.0.0.5, usermonitor, passwordxxxx, connect_timeout3 ) 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, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2)细节上我做了几个特殊处理连接超时设为 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 再转 WordCSV 的问题在于没有层级结构关联性强的指标比如连接数和最大连接数在 CSV 里就是两列需要通过列名约定位子扩展性和可读性都不如 JSON。而且 JSON 本身是树形结构和报告章节天然对应渲染层写起来非常顺。这里正好接上你最近在 Linux 上看到的一个技巧一键获取文件名并生成列表。巡检脚本跑完会产生一堆 JSON 文件想批量交给报告脚本处理一条命令就搞定for f in check_*.json; do python gen_report.py $f; done或者更“现代”一点直接利用 find 拿到完整路径列表配合 xargs 传给 Pythonfind ./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, encodingutf-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(数据库巡检报告, level0) # 关键点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(rows3, cols2) 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] dbaexample.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, fattachment; filename{file_path}) msg.attach(part) # 具体 smtp 连接配置按公司邮件服务器来加上这段之后整个流程就变成了定时任务跑巡检采集 → 生成 Word → 自动发邮件。人只需要在收到邮件后看一眼有问题再去数据库里验证大量重复劳动直接消失了。5.2 这套方案还能复用到哪些场景最后聊点心得体会。这个“采集到 JSONJSON 转 Word”的架构虽然是我做数据库巡检时搭起来的但后来我发现它几乎能套用到所有“定期生成报告”的场景。比如服务器巡检把数据库采集换成psutil或者读取/proc/meminfo就能生成服务器资源周报再比如业务日报只需要把数据源的 SQL 换成业务指标查询Word 模板改成业务口径就变成一份面向老板的业务日报生成器。核心思想都一样把数据和展示解耦数据层输出统一格式展示层只负责排版渲染。我个人在实际操作中的一个体会是自动化并不等于“什么都让脚本干”而是“让脚本干它擅长的事让人干人擅长的事”。数据采集和排版格式化是脚本的强项识别异常、判断风险还是要靠 DBA 的经验。所以我的脚本里保留了人工补充备注的接口给机器留了“不可控空间”这样生成的报告既高效又不至于完全失去人的判断。如果你也想搞一套我的建议是从最小版本开始先选一个你日常最花时间的报告类型用今天的脚本方案跑通不要一开始就追求完美排版等链路跑顺了再慢慢调样式、加自动发送、接提醒。只要迈出第一步你就能感受到“一键生成”的爽感。