ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

如何用Python+WeasyPrint自动生成网络安全评估报告PDF完整版

如何用Python+WeasyPrint自动生成网络安全评估报告PDF完整版 简介PDF因其版式固定、跨平台一致、可离线查阅等特性成为网络信息安全评估报告的标准交付格式。然而完整版报告包含资产清单、威胁库、脆弱性分析、风险矩阵等结构化信息手工排版耗时且易错。利用HTMLCSS定义版式借助Jinja2模板引擎填充数据再通过WeasyPrint渲染引擎生成PDF可实现评估报告从数据到文档的自动化流水线。该方案支持自定义页眉页脚、书签导航、元数据注入以及风险矩阵图嵌入显著提升报告生成效率与一致性。适用于等保测评、风险评估、安全审计等场景帮助安全团队快速交付可检索、可追溯的合规文档。1. 网络信息安全评估报告的完整版为什么必须落在 PDF 上客户、领导或监管方伸手要评估报告完整版时十有八九是在等一份 PDF。不是 Word也不是内部 Wiki 链接——因为 PDF 把页面版式、字体、图表固定成了不可漂移的形态拿到 Windows 或 macOS 上打开都一样打印和归档也省心。但真正做过的人知道所谓完整版不是把段落凑齐就完事。评估过程产生的资产清单、威胁库、脆弱性匹配表、风险值计算口径、整改建议每一类都必须有固定结构且这些结构在 PDF 里要能检索、能跳转、能离线查阅。这篇文章就按做这一行常用的技术路径展开先定义报告的信息模型再用模板引擎生成 HTML最后用工具链转成带书签和元数据的 PDF。适合需要频繁交付评估报告的安全工程师以及被要求出一份合规报告但不想手工排版的技术负责人。2. 评估报告先搭信息模型章节结构、数据字段与模板选型2.1 网络信息安全评估报告的核心章节与必备字段一份能被称为完整版的评估报告至少要覆盖从资产到风险的完整链路。我一般会固定 6 个主章节项目概述与范围、资产识别与赋值、威胁与脆弱性分析、风险计算与等级判定、整改建议与优先级、附录原始数据和工具输出。这 6 章不是拍脑袋定的它们对应风险评估的基本逻辑通过资产、威胁、脆弱性三个要素计算风险值再结合已有安全措施确定处置优先级。每个章节内部要定义字段。比如资产识别表必须有资产编号、资产名称、所属部门、责任人、可用性/机密性/完整性赋值。威胁与脆弱性分析表必须有威胁源、威胁类型、对应脆弱性编号、脆弱性严重程度、已有控制措施。这些字段必须提前固定因为后面做 PDF 模板、数据库导出、批量生成时字段名就是程序里的变量名。字段没定后面所有自动化都是空谈。2.1.1 字段标准化示例模块字段名英文标识示例值数据类型资产asset_idAS-001字符串资产asset_name核心生产数据库服务器字符串资产confidentiality高枚举(高/中/低)威胁threat_source外部黑客字符串威胁threat_type恶意代码枚举脆弱性vuln_idVUL-2024-001字符串脆弱性severity高危枚举风险risk_value81数值风险risk_level高枚举字段定义好了报告的内容才不会每次写法都不一样。很多团队的直接痛点不是不会写报告而是每个工程师输出一个样式最终合稿时统一格式最耗时间。有了标准字段模板和数据就能分离。2.2 选择报告生成技术栈Word、LaTeX、Markdown、HTML 转 PDF 的取舍生成 PDF 的技术路径不少但各有代价。我梳理过几种常见做法Word 宏/域适合必须用 Office 的环境但自动化程度低目录更新和页眉页脚容易错位批量生成需要操作 COM 接口跨平台基本不可用。LaTeX排版质量极高表格和公式漂亮但对中文支持需要额外配置 xeCJK 和字体学习成本高且团队里不是每个人都愿意写 LaTeX。Markdown Pandoc胜在轻量但复杂页眉页脚、公司 Logo、风险矩阵图混排时控制力不足生成的书签层级经常不符合预期。HTML CSS PDF 渲染引擎这是最接近前端工程师思维的做法。HTML 能自由控制分页、页眉、页脚、标题层级、表格样式还能嵌入图片和矢量图CSS 的page规则能精确控制纸张尺寸和页边距渲染引擎负责把 HTML 转成带书签的 PDF。从维护成本看HTML 模板更适合团队协作前端同事能快速调样式安全工程师只管填数据。所以我一般选这条路径具体引擎用 WeasyPrint 或 Chromium 的 headless 打印模式。两者都能从本地 HTML 生成 PDF且支持 CSS 分页媒体标准。2.2.1 技术栈对比表方案中文支持自动化程度书签/目录图表混排推荐度Word 宏强低弱中不推荐LaTeX xeCJK中中强强适合学术Markdown Pandoc中高中中适合草稿HTML WeasyPrint强高强强推荐HTML headless Chrome强高中强备选选型时还要考虑安装依赖。WeasyPrint 需要系统有 cairo、pango 等原生库Windows 上安装相对麻烦如果公司环境允许用 Docker 镜像是最省事的方式。Chromium headless 则要求服务器有对应内核不过现在 CI 里跑一个无头浏览器也很常见。2.3 用 HTML 定义报告版式一个最小可用的模板骨架定了选型下一步就是写 HTML 模板。模板里不要写死数据而是用占位符。下面是一个最小模板骨架它包含封面、目录占位、章节标题和表格!DOCTYPE html html langzh-CN head meta charsetutf-8 title网络信息安全评估报告/title style page { size: A4; margin: 2.5cm 2cm 2cm 2cm; top-center { content: 网络信息安全评估报告; font-size: 9pt; color: #888; } bottom-right { content: 第 counter(page) 页 / 共 counter(pages) 页; font-size: 9pt; } } body { font-family: Noto Sans CJK SC, Microsoft YaHei, sans-serif; } h1 { color: #1a3e6f; border-bottom: 2px solid #1a3e6f; } h2 { color: #1a3e6f; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #999; padding: 6px 8px; font-size: 10pt; } th { background: #dce6f1; } /style /head body h1网络信息安全评估报告/h1 p报告编号{{ report_id }}br 评估日期{{ eval_date }}br 编制单位{{ org_name }}/p h21. 项目概述/h2 p{{ overview }}/p h22. 资产识别/h2 table trth资产编号/thth资产名称/thth保密性/thth完整性/thth可用性/th/tr {% for asset in assets %} tr td{{ asset.asset_id }}/td td{{ asset.asset_name }}/td td{{ asset.confidentiality }}/td td{{ asset.integrity }}/td td{{ asset.availability }}/td /tr {% endfor %} /table /body /html这个模板里出现了{{ }}和{% for %}它们是 Jinja2 模板引擎的语法。逻辑说明page块定义了 A4 纸张、页边距、页眉和页脚页脚用 CSS 计数器显示当前页码和总页数{{ report_id }}等是数据占位符{% for asset in assets %}会遍历资产列表为每一条数据生成一行表格。参数说明size: A4可换成size: A5或自定义尺寸margin四个值分别对应上、右、下、左边距按 cm 写不影响其他单位页眉页脚里的content属性可以自由改文字counter(pages)是 WeasyPrint 支持的 CSS 计数器。如果团队用 headless Chromepage的counter(pages)支持不完整这时我会在生成脚本里用 PDF 库二次写入页脚。3. 用 Python 自动生成完整版 PDF 评估报告的可复现流程3.1 搭建最小环境安装 Python 依赖与渲染引擎HTML 模板需要渲染引擎。首选 WeasyPrint因为它的分页媒体支持最完整。在 Python 3.9 环境中安装方式如下pip install weasyprint jinja2如果系统缺少原生库会报类似libpango-1.0.so.0: cannot open shared object file的错误。Debian/Ubuntu 上需要先执行apt-get install -y libpango-1.0-0 libcairo2 libgdk-pixbuf2.0-0 libffi-dev逻辑说明WeasyPrint 本身是 Python 包但底层依赖 Pango 负责文本布局Cairo 负责渲染所以需要系统级依赖。参数说明libpango-1.0-0是文本布局引擎缺了会导致中文字体错乱libcairo2是 2D 图形渲染库缺了图片和边框无法绘制。如果你在 Windows 上开发建议直接用 Docker 镜像ghcr.io/ko-build/wkhtmltopdf之类的替代但 WeasyPrint 的官方 Docker 镜像比较老我一般在自己项目的 Dockerfile 里装这些依赖。3.2 编写数据填充脚本从 Excel 或数据库读取评估结果评估报告的数据通常来源于安全扫描工具Nessus/Nexus 等、资产台账或手工评估表。为了自动化我会把数据统一整理成 JSON 结构然后传给 Jinja2 模板。以下是一个真实的数据处理示例import json from jinja2 import Environment, FileSystemLoader # 模拟从扫描工具导出的数据 data { report_id: SEC-2024-008, eval_date: 2024-12-20, org_name: 某省政务云平台, overview: 本次评估覆盖 12 个核心业务系统发现高危漏洞 4 个中危 8 个。, assets: [ {asset_id: AS-001, asset_name: 核心数据库服务器, confidentiality: 高, integrity: 高, availability: 高}, {asset_id: AS-002, asset_name: Web 应用服务器, confidentiality: 中, integrity: 高, availability: 中}, ] } env Environment(loaderFileSystemLoader(templates/)) template env.get_template(report_template.html) rendered_html template.render(data) with open(report.html, w, encodingutf-8) as f: f.write(rendered_html)这段代码的逻辑说明用FileSystemLoader指定模板目录template.render(data)把词典里的report_id、overview、assets等变量替换到模板中最终把渲染后的 HTML 存成中间文件。实际项目中data通常会从扫描仪器的 API 拉取或者从 Excel 用 pandas 读取然后转换成同样的字典结构。参数说明encodingutf-8必须写否则 Windows 下写文件会用 GBK 导致 HTML 里的中文乱码。Environment可以设置autoescapeTrue来防止恶意 HTML 注入如果报告数据可信度较高可以保持默认关闭但最好开启避免意外。3.3 将 HTML 渲染为 PDFWeasyPrint 核心调用与参数配置渲染 HTML 到 PDF 的代码很简单但参数有讲究from weasyprint import HTML HTML(filenamereport.html).write_pdf(网络信息安全评估报告完整版.pdf, stylesheets[style_extra.css])这段代码会读取report.html将其中引用的 CSS 和内置样式合并输出为 PDF。stylesheets参数可以追加额外的 CSS 文件用于覆盖模板里的默认样式。比如某些报告需要额外调整行距可以在style_extra.css里写body { font-size: 10.5pt; line-height: 1.7; } table td { padding: 4px 6px; }逻辑说明HTML(filename...)也可以改成HTML(stringrendered_html)这样就不必先生成中间 HTML 文件。中间文件的好处是调试时可以单独用浏览器打开 HTML 看结构坏了直接检查渲染之前的 HTML。输出路径里的中文文件名是允许的但要注意操作系统文件系统编码Linux 下默认 UTF-8 没问题。参数说明write_pdf没有太多参数但它会扫描 HTML 中的所有link和本地资源。如果模板里有相对路径的图片比如images/risk_matrix.pngWeasyPrint 会以 HTML 文件所在目录为基准解析。如果你使用HTML(string...)需要额外传base_url参数否则图片和外部样式找不全。生成后可以立刻用pdfinfo检查基本信息pdfinfo 网络信息安全评估报告完整版.pdf输出会显示文件页面数、页大小、PDF 版本、作者、创建时间等。这一条命令非常值得养成习惯因为它能快速确认文件没有损坏、页数是否符合预期。4. 风险矩阵图、书签目录与元数据让 PDF 真正“完整”4.1 用 matplotlib 生成风险矩阵图并嵌入报告风险评估报告离不开风险矩阵图。通常横轴是威胁发生可能性纵轴是影响程度颜色块从绿到红表示风险等级。用 Python 绘制并嵌入 PDF比在 HTML 手动画表格更直观。以下代码生成一张 5×5 风险矩阵import matplotlib.pyplot as plt import numpy as np risk_values np.array([ [2, 4, 6, 8, 10], [4, 6, 9, 12, 16], [6, 8, 12, 16, 20], [8, 10, 15, 20, 25], [10, 12, 18, 25, 30] ]) fig, ax plt.subplots(figsize(6, 5)) im ax.imshow(risk_values, cmapRdYlGn_r, aspectauto) ax.set_xticks(range(5)) ax.set_yticks(range(5)) ax.set_xticklabels([极低, 低, 中等, 高, 极高], fontsize9) ax.set_yticklabels([极低, 低, 中等, 高, 极高], fontsize9) ax.set_xlabel(威胁发生可能性) ax.set_ylabel(影响程度) for i in range(5): for j in range(5): ax.text(j, i, risk_values[i, j], hacenter, vacenter, fontsize8) plt.tight_layout() plt.savefig(risk_matrix.png, dpi200)逻辑说明imshow把数值矩阵映射为颜色块cmapRdYlGn_r表示红在数值高处、绿在低处_r反转了默认颜色顺序。aspectauto避免单元格变成正方形导致图片过宽。保存时dpi200是给 PDF 用打印清晰度够且文件不会过大。参数说明风险矩阵的阈值分级不是固定的很多公司有自己的一套算法。我这里的 5×5 只是示例实际应根据《风险评估方法》里的公式计算比如风险值 资产价值 × 威胁可能性 × 脆弱性严重程度然后把风险值映射到 5 档。如果你们用 4×4 或 3×3 矩阵改数组维度和xticklabels即可。在 HTML 模板中嵌入图片只需在需要的位置加img srcimages/risk_matrix.png alt风险矩阵图 stylewidth: 70%; display: block; margin: 0 auto;注意src是相对路径WeasyPrint 会把图片压缩进 PDF 内部所以单独删除这个 PNG 不影响 PDF 显示。4.2 书签与目录让 PDF 能像网页一样跳转没有书签的 PDF只能从头滚动到黑完整的报告必须有目录大纲。WeasyPrint 自动根据 HTML 中的h1到h6生成书签。上面的模板用了h1和h2生成 PDF 后打开侧边栏就能看到两级目录。如果想自定义书签名称可以在 HTML 标签上加bookmark属性h2 bookmark1. 项目概述1. 项目概述/h2逻辑说明bookmark是 WeasyPrint 私有的 HTML 属性它会把书签的显示文字改成属性值。如果不加就用标签文本。建议章节编号写进标题里书签会自然层级分明。但目录页和书签是两回事。目录页是正文前的一页显示各章节页码书签是 PDF 阅读器侧边栏的导航树。做目录页最简单的方式是让 PDF 渲染引擎自动收集页码不过 WeasyPrint 不支持target-counter的跨页引用。我常用的替代方案是目录页用 CSS 的lead或者直接手写链接。手写时用a href#section11. 项目概述/a并在对应章节标题上加ida href#section11. 项目概述/a h2 idsection11. 项目概述/h2这样在 PDF 里点击目录文字会跳转到对应章节。这是 Web 原生的锚点行为WeasyPrint 会保留。4.3 元数据、加密与权限设置生成后的 PDF 默认没有作者、标题等信息这不利于归档和检索。WeasyPrint 会读取 HTML 的title作为 PDF 标题但其他元数据需要借助 Python 的pikepdf库在生成后注入pip install pikepdffrom pikepdf import Pdf, Encryption pdf Pdf.open(网络信息安全评估报告完整版.pdf) with pdf.open_metadata() as meta: meta[dc:title] 网络信息安全评估报告完整版 meta[dc:creator] 安全运营中心 meta[pdf:Keywords] 网络信息安全, 评估报告, 风险矩阵 meta[xmp:CreateDate] 2024-12-20T18:00:0008:00 pdf.save(report_with_metadata.pdf, encryptionEncryption(ownerowner-pass, useruser-pass)) pdf.close()逻辑说明open_metadata()以可编辑方式打开 XMP 元数据可以直接赋值。dc:title是 Dublin Core 标准pdf:Keywords是 PDF 的定制字段。保存时通过encryption参数设置密码owner是所有者密码用于控制权限user是打开文档需要的密码。如果不想加密可以省略encryption参数。参数说明建议把元数据写入放在 PDF 渲染完成后而不是写入 HTML因为 HTML 元数据没有统一标准。加密等级默认是 RC4现在推荐传Encryption(owner..., user..., algorithmAES-256)但要确认阅读器兼容性。归档型报告我会设 owner 密码而不设 user 密码这样任何人都能打开但只有知道 owner 密码的人能修改权限。5. 网络信息安全评估报告 PDF 验收三个必调参数与易错点5.1 用 pdftotext 验证报告文字可检索性完整版不能是扫描图片。很多团队用 WPS 或在线转换器把 Word 导出成 PDF文本保留了检索能力但用某些设计软件生成 PDF 时文字被转成矢量曲线或图片导致 CtrlF 搜不到关键词。验收时必须检查pdftotext 网络信息安全评估报告完整版.pdf - | grep 风险值如果有输出说明文字层存在。如果没有说明 PDF 是纯图像或字体未嵌入。另一点是字体内嵌用pdffonts查看pdffonts 网络信息安全评估报告完整版.pdf输出中每一行对应一种字体Type 列如果是Type3或未嵌入打印时可能替换字体。正常应该是TrueType或CID TrueType并带有embedded标记。WeasyPrint 默认会嵌入本地能找到的字体所以我会在模板 CSS 中明确指定Noto Sans CJK SC并确保系统已安装。5.2 三个必调参数行距、页边距、图片压缩渲染报告时最容易出问题的是分页一个章节标题出现在页底表格被切到下一页行间距过大导致总页数虚高。我通常固定三个参数line-height: 1.61.8正文常规使用1.6表格内1.2page { margin: 2.5cm 2cm 2cm 2cm; }不要小于2cm装订或打印时白边不够图片导出dpi200统计分析图表dpi150超过 300 会无意义增大 PDF 体积。这三个参数不是拍脑袋定的。行距小于1.5时中文长段落读起来密集大于2时整个报告页数会多 20%。页边距和装订线需要协调如果客户要求双面打印左边的边距最好设到2.5cm。图片方面Web 截图类图片用 PNG 保真风险矩阵这类矢量图形建议存 SVG 再嵌入或直接在matplotlib里savefig成 PDF 矢量格式。5.3 版本管理源文件与 PDF 的一致性这是最后一层保障。评估报告有修改周期改一版数据就要重新生成 PDF最怕的是 PDF 已经是新版但源 Excel 还是旧版。我一般会在报告副标题里写版本 v2.1并在生成脚本里用 Git 提交哈希作版本号import subprocess commit subprocess.check_output([git, rev-parse, --short, HEAD]).decode().strip() data[version] fv2.1-{commit}然后在 HTML 封面页展示这个版本。这样 PDF 的元数据、正文和代码仓库能一一对应。最后再跑一次pdfinfo确认页数用pdftotext抽查 23 个关键词几乎所有交付坑都能避开。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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