Django HTML图像取证系统:DOM哈希与结构化差异比对
发布时间:2026/9/16 17:04:27来源:尧图网络
简介本资源是一套面向本科毕业设计与图像取证初学者的完整Django Web系统实现方案聚焦数字图像真实性分析与篡改检测技术落地。项目以Python为核心采用Django构建稳健后端HTMLCSSJS实现交互式前端界面MySQL支撑图像元数据、取证结果及用户管理等结构化存储配套说明文档、技术文献LW与答辩PPT覆盖从开发部署到成果汇报的全流程需求。压缩包共265个文件含25个HTML页面、42个JS脚本、25个核心Py模块、17个CSS样式表及75个GIF动图多用于界面反馈与流程演示另有SQL建库脚本、字体资源与PNG/JPG示例图像整体16.96MB结构清晰、模块分离明确。目前已有53人学习下载可直接运行调试、深入理解Django ORM与图像取证业务逻辑耦合方式并复用文档中的环境配置清单、接口设计说明与常见报错解决方案。1. 这不是普通网页截图工具DjangoHTML图像取证系统解决的是“谁改了页面、何时改、改了哪一行”的硬需求当你在运维后台发现用户提交的订单页突然多出一个可疑支付按钮或教育平台的考试页面被注入了未授权的跳转脚本靠肉眼比对HTML源码已完全失效——页面可能被动态渲染、CDN缓存、服务端模板拼接层层掩盖。此时“图像取证”不是指PS修图鉴定而是将HTML结构、CSS样式、DOM树状态、资源加载时序等维度固化为可哈希、可比对、可溯源的数字指纹。本系统用Python Django构建服务端核心前端采用标准!doctype htmlhtml langzh-cn结构配合Bootstrap快速搭建响应式取证界面MySQL存储每次抓取的DOM快照、资源哈希、时间戳及操作日志。它面向安全审计员、合规检查工程师和Web质量保障人员不依赖浏览器插件或远程SaaS服务所有HTML解析、差异计算、快照生成均在本地Django进程内完成关键参数如DOM深度阈值、CSS选择器白名单、资源加载超时均可代码级控制。新手能30分钟部署起一个带数据库的取证服务老手则可通过重写HtmlSnapshotProcessor类接入自定义的XPath规则或Canvas像素比对逻辑。2. 从零启动用Django 4.2Bootstrap 5.3搭建可运行的HTML取证服务框架2.1 初始化Django项目并配置MySQL后端Django 4.2是当前LTS版本对Python 3.8兼容性最佳且内置异步视图支持后续扩展资源抓取。首先创建虚拟环境并安装核心依赖python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows pip install django4.2.13 mysqlclient2.2.4 requests2.31.0 beautifulsoup44.12.2提示mysqlclient需系统级依赖Ubuntu执行sudo apt-get install python3-dev default-libmysqlclient-dev build-essentialCentOS用yum install python3-devel mysql-devel gcc。若遇编译失败可临时改用pymysql在settings.py中DATABASES配置里将ENGINE: django.db.backends.mysql改为django.db.backends.mysql并添加OPTIONS: {init_command: SET sql_modeSTRICT_TRANS_TABLES}。创建项目并配置MySQL连接django-admin startproject htmlforensics . python manage.py startapp core在settings.py中修改数据库配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: html_forensics_db, USER: forensics_user, PASSWORD: StrongPass123!, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET SESSION time_zone00:00, }, TEST: { CHARSET: utf8mb4, COLLATION: utf8mb4_unicode_ci, } } }参数说明charsetutf8mb4确保emoji和特殊符号正确存储time_zone00:00强制UTC时区避免跨时区比对误差TEST块为单元测试准备独立字符集防止测试污染生产数据。2.2 设计核心模型存储HTML快照、差异记录与元数据core/models.py定义三个关键模型覆盖取证全链路from django.db import models from django.contrib.auth.models import User class HtmlSnapshot(models.Model): url models.URLField(max_length2000, db_indexTrue) title models.CharField(max_length500, blankTrue) dom_hash models.CharField(max_length64, db_indexTrue) # SHA256 of normalized HTML resource_hashes models.JSONField(defaultdict) # {script.js: a1b2c3..., style.css: d4e5f6...} screenshot_path models.CharField(max_length500, blankTrue) # Optional visual proof created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: ordering [-created_at] indexes [ models.Index(fields[url, -created_at]), models.Index(fields[dom_hash]), ] class SnapshotDiff(models.Model): base_snapshot models.ForeignKey(HtmlSnapshot, on_deletemodels.CASCADE, related_namediffs_as_base) target_snapshot models.ForeignKey(HtmlSnapshot, on_deletemodels.CASCADE, related_namediffs_as_target) diff_html models.TextField() # Unified diff format of DOM tree changed_nodes models.JSONField(defaultlist) # [{tag: div, attr: id, old: old-id, new: new-id}] is_critical models.BooleanField(defaultFalse) # Auto-flagged if script or form action changed created_at models.DateTimeField(auto_now_addTrue) class AuditLog(models.Model): user models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, blankTrue) action models.CharField(max_length100) # capture, compare, export target_url models.URLField(max_length2000, blankTrue) details models.JSONField(defaultdict) # {status: success, duration_ms: 1245} created_at models.DateTimeField(auto_now_addTrue)执行迁移python manage.py makemigrations python manage.py migrate2.3 构建Bootstrap 5.3前端界面标准化取证工作流在core/templates/core/下创建base.html严格遵循热词中的!doctype htmlhtml langzh-cn结构!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 titleHTML图像取证系统/title !-- Bootstrap 5.3 CSS -- link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.3/dist/css/bootstrap.min.css relstylesheet !-- Custom styles for forensic UI -- style .diff-added { background-color: #d4edda; } .diff-removed { background-color: #f8d7da; } .snapshot-card { transition: transform 0.2s; } .snapshot-card:hover { transform: translateY(-2px); } /style /head body nav classnavbar navbar-expand-lg navbar-dark bg-dark div classcontainer a classnavbar-brand href{% url home %} HTML取证/a div classnavbar-nav ms-auto a classnav-link href{% url capture_form %}抓取新快照/a a classnav-link href{% url compare_form %}比对快照/a a classnav-link href{% url snapshot_list %}历史记录/a /div /div /nav main classcontainer mt-4 {% if messages %} {% for message in messages %} div classalert alert-{{ message.tags }} alert-dismissible fade show rolealert {{ message }} button typebutton classbtn-close>import hashlib import re from bs4 import BeautifulSoup, Tag from urllib.parse import urlparse def normalize_html_for_fingerprint(html_content: str, url: str ) - str: 标准化HTML用于生成稳定DOM哈希 - 移除注释、空格、动态属性data-*、aria-* - 统一script/style标签位置 - 替换绝对URL为相对路径基于url参数 - 删除内联事件处理器onclick等 soup BeautifulSoup(html_content, html.parser) # 移除HTML注释 for comment in soup.find_all(stringlambda text: isinstance(text, str) and text.strip().startswith(!--)): comment.extract() # 移除所有data-*和aria-*属性不影响结构但常含动态ID for tag in soup.find_all(True): attrs_to_remove [attr for attr in tag.attrs.keys() if attr.startswith((data-, aria-))] for attr in attrs_to_remove: del tag[attr] # 删除内联事件处理器 event_attrs [onclick, onload, onerror, onsubmit] for attr in event_attrs: for tag in soup.find_all(attrs{attr: True}): del tag[attr] # 标准化script和style标签顺序全部移到head末尾 head soup.head or soup.find(head) or soup scripts soup.find_all(script) styles soup.find_all(style) for script in scripts: script.extract() head.append(script) for style in styles: style.extract() head.append(style) # URL标准化将绝对URL转为相对路径仅限同域 if url: base_domain urlparse(url).netloc for tag in soup.find_all([img, link, script, iframe]): for attr in [src, href, data-src]: if tag.has_attr(attr) and tag[attr].startswith(http): parsed urlparse(tag[attr]) if parsed.netloc base_domain: tag[attr] parsed.path (? parsed.query if parsed.query else ) # 移除多余空白保留标签间必要换行 normalized str(soup).replace(\n, ).replace(\t, ).replace( , ) return re.sub(r\s, , normalized).strip() def generate_dom_hash(html_content: str, url: str ) - str: 生成DOM结构哈希忽略非结构化内容 normalized normalize_html_for_fingerprint(html_content, url) return hashlib.sha256(normalized.encode(utf-8)).hexdigest()逻辑说明normalize_html_for_fingerprint是取证准确性的基石。它不删除script标签本身因可能含关键逻辑但移除其src属性中的动态参数>import json import time import requests from django.shortcuts import render, redirect from django.http import JsonResponse from django.views import View from django.contrib import messages from django.utils import timezone from core.models import HtmlSnapshot, AuditLog from core.utils.html_processor import generate_dom_hash, normalize_html_for_fingerprint class CaptureView(View): def get(self, request): return render(request, core/capture_form.html) def post(self, request): url request.POST.get(url, ).strip() if not url.startswith((http://, https://)): messages.error(request, 请输入以 http:// 或 https:// 开头的有效URL) return redirect(capture_form) try: # Step 1: 获取HTML内容带超时和UA headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } response requests.get(url, headersheaders, timeout15) response.raise_for_status() # Step 2: 生成DOM哈希 dom_hash generate_dom_hash(response.text, url) # Step 3: 提取关键资源URL并计算哈希简化版实际可异步 resource_hashes {} soup BeautifulSoup(response.text, html.parser) for tag in soup.find_all([script, link, img]): if tag.has_attr(src): resource_hashes[tag[src]] pending # Placeholder elif tag.has_attr(href) and tag.name link: resource_hashes[tag[href]] pending # Step 4: 保存快照 snapshot HtmlSnapshot.objects.create( urlurl, titlesoup.title.string.strip() if soup.title else 无标题, dom_hashdom_hash, resource_hashesresource_hashes, screenshot_path # 后续可集成Puppeteer生成 ) # Step 5: 记录审计日志 AuditLog.objects.create( userrequest.user if request.user.is_authenticated else None, actioncapture, target_urlurl, details{ status: success, dom_hash: dom_hash[:8], content_length: len(response.text), duration_ms: int((time.time() * 1000)) } ) messages.success(request, f抓取成功DOM哈希: {dom_hash[:12]}...) return redirect(snapshot_detail, pksnapshot.pk) except requests.exceptions.RequestException as e: messages.error(request, f网络请求失败: {str(e)}) return redirect(capture_form) except Exception as e: messages.error(request, f处理失败: {str(e)}) return redirect(capture_form)core/urls.py注册路由from django.urls import path from core import views urlpatterns [ path(, views.HomeView.as_view(), namehome), path(capture/, views.CaptureView.as_view(), namecapture_form), path(capture/int:pk/, views.SnapshotDetailView.as_view(), namesnapshot_detail), # ... 其他路由 ]参数说明timeout15防止恶意长连接阻塞User-Agent模拟真实浏览器避免被反爬dom_hash[:12]在消息中显示前12位便于人工核对AuditLog记录完整上下文供事后追溯。此版本暂未实现资源文件哈希需额外HTTP请求但resource_hashes字段已预留JSON结构后续可扩展为Celery异步任务。3.3 构建结构化差异比对引擎定位到HTML标签级变更core/utils/diff_engine.py实现基于DOM树的精准比对from bs4 import BeautifulSoup from difflib import unified_diff import json def compare_snapshots(base_html: str, target_html: str) - dict: 比较两个HTML快照返回结构化差异 返回格式: { diff_html: ins.../insdel.../del, changed_nodes: [{tag: div, attr: class, old: old, new: new}] } base_soup BeautifulSoup(base_html, html.parser) target_soup BeautifulSoup(target_html, html.parser) # 提取纯文本用于unified_diff快速粗筛 base_text base_soup.get_text() target_text target_soup.get_text() diff_lines list(unified_diff( base_text.splitlines(keependsTrue), target_text.splitlines(keependsTrue), fromfilebase, tofiletarget, lineterm )) # 深度DOM比对遍历所有标签检查属性变更 changed_nodes [] base_tags list(base_soup.find_all(True)) target_tags list(target_soup.find_all(True)) # 简化策略按标签名和位置匹配实际生产需XPath或唯一ID min_len min(len(base_tags), len(target_tags)) for i in range(min_len): base_tag base_tags[i] target_tag target_tags[i] # 检查标签名是否一致 if base_tag.name ! target_tag.name: changed_nodes.append({ tag: element, position: i, old: f{base_tag.name}, new: f{target_tag.name} }) continue # 检查属性差异 for attr in set(base_tag.attrs.keys()) | set(target_tag.attrs.keys()): old_val base_tag.attrs.get(attr, None) new_val target_tag.attrs.get(attr, None) if old_val ! new_val: changed_nodes.append({ tag: base_tag.name, attr: attr, old: str(old_val), new: str(new_val) }) # 自动标记高危变更 is_critical any( node[tag] script or (node[tag] form and node[attr] action) or (on in node.get(attr, ) and node[old] and node[new]) for node in changed_nodes ) return { diff_html: .join(diff_lines), changed_nodes: changed_nodes, is_critical: is_critical } # 在views.py中调用示例 def compare_snapshots_view(request): if request.method POST: base_id request.POST.get(base_id) target_id request.POST.get(target_id) base_snap HtmlSnapshot.objects.get(pkbase_id) target_snap HtmlSnapshot.objects.get(pktarget_id) diff_result compare_snapshots( base_snap.dom_hash, # 实际应传HTML内容此处简化 target_snap.dom_hash ) SnapshotDiff.objects.create( base_snapshotbase_snap, target_snapshottarget_snap, diff_htmldiff_result[diff_html], changed_nodesdiff_result[changed_nodes], is_criticaldiff_result[is_critical] ) # ... 渲染结果页面关键点compare_snapshots函数分两层——unified_diff提供文本级变更概览DOM遍历定位到具体标签和属性。is_critical标志自动识别script插入、表单action篡改、内联事件注入三类高危行为无需人工判断。生产环境应替换base_snap.dom_hash为实际存储的HTML内容需在HtmlSnapshot模型中增加html_contentTextField并限制大小。4. 部署与性能调优MySQL索引优化、Django缓存策略与Bootstrap响应式适配4.1 MySQL查询加速为高频取证操作建立复合索引针对HtmlSnapshot模型的典型查询模式按URL查最新快照、按DOM哈希查重复项在core/migrations/中添加索引迁移# migrations/0002_add_indexes.py from django.db import migrations class Migration(migrations.Migration): dependencies [ (core, 0001_initial), ] operations [ migrations.AddIndex( model_namehtmlsnapshot, indexmodels.Index(fields[url, -created_at], nameidx_url_latest), ), migrations.AddIndex( model_namehtmlsnapshot, indexmodels.Index(fields[dom_hash, -created_at], nameidx_domhash_recent), ), migrations.AddIndex( model_namesnapshotdiff, indexmodels.Index(fields[base_snapshot, target_snapshot], nameidx_diff_pair), ), ]执行迁移后验证索引生效-- 登录MySQL执行 SHOW INDEX FROM core_htmlsnapshot; -- 应看到 idx_url_latest 和 idx_domhash_recent EXPLAIN SELECT * FROM core_htmlsnapshot WHERE url https://example.com ORDER BY created_at DESC LIMIT 1; -- type应为refkey为idx_url_latest参数说明idx_url_latest使SELECT ... WHERE url ? ORDER BY created_at DESC LIMIT 1查询从全表扫描降至O(log n)idx_domhash_recent支持快速检测重复快照如监控页面是否被篡改idx_diff_pair避免比对历史记录时出现笛卡尔积。所有索引均加-created_at实现倒序索引符合取证场景“查最新”的高频需求。4.2 Django缓存策略减少HTML解析与数据库IO开销在settings.py中配置Redis缓存推荐比数据库缓存快10倍CACHES { default: { BACKEND: django.core.cache.backends.redis.RedisCache, LOCATION: redis://127.0.0.1:6379/1, OPTIONS: { CLIENT_CLASS: django_redis.client.DefaultClient, } } } # 视图级缓存快照详情页缓存1小时 from django.views.decorators.cache import cache_page from django.utils.decorators import method_decorator method_decorator(cache_page(60 * 60), namedispatch) class SnapshotDetailView(DetailView): model HtmlSnapshot template_name core/snapshot_detail.html为HTML解析结果添加函数级缓存# core/utils/html_processor.py from django.core.cache import cache def generate_dom_hash_cached(html_content: str, url: str ) - str: cache_key fdom_hash_{hashlib.md5((html_content[:1000] url).encode()).hexdigest()} cached cache.get(cache_key) if cached: return cached result generate_dom_hash(html_content, url) cache.set(cache_key, result, 300) # 缓存5分钟 return result注意cache_page装饰器作用于整个视图响应适合静态详情页cache.get/set用于计算密集型函数cache_key用html_content[:1000]截断避免KEY过长300秒缓存期平衡新鲜度与性能。若无Redis可降级为LocMemCache仅限单进程开发环境。4.3 Bootstrap 5.3响应式增强适配取证报告的多屏查看需求在core/templates/core/snapshot_detail.html中利用Bootstrap栅格系统重构报告布局{% extends core/base.html %} {% load static %} {% block content %} div classrow mb-4 div classcol-12 col-md-8 h2快照详情{{ object.url }}/h2 p classtext-muted抓取时间{{ object.created_at|date:Y-m-d H:i:s }} | DOM哈希{{ object.dom_hash|slice::16 }}.../p /div div classcol-12 col-md-4 text-md-end a href{% url compare_form %}?base{{ object.pk }} classbtn btn-primary i classbi bi-arrow-left-right/i 与另一快照比对 /a /div /div div classrow div classcol-12 col-lg-6 div classcard mb-4 div classcard-header bg-light h5 classmb-0页面结构摘要/h5 /div div classcard-body ul classlist-group list-group-flush li classlist-group-item d-flex justify-content-between span标题/span span classfw-bold{{ object.title|default:未设置 }}/span /li li classlist-group-item d-flex justify-content-between spanScript数量/span span classfw-bold{{ object.resource_hashes|length|add:0 }}/span /li li classlist-group-item d-flex justify-content-between span最后更新/span span classfw-bold{{ object.updated_at|timesince }}前/span /li /ul /div /div /div div classcol-12 col-lg-6 div classcard mb-4 div classcard-header bg-light h5 classmb-0资源哈希列表/h5 /div div classcard-body p-0 ul classlist-group list-group-flush {% for resource, hash_val in object.resource_hashes.items %} li classlist-group-item small classtext-muted{{ resource|truncatechars:30 }}/small div classbadge bg-secondary float-end{{ hash_val|slice::8 }}/div /li {% endfor %} /ul /div /div /div /div div classrow div classcol-12 div classcard div classcard-header bg-light h5 classmb-0DOM结构预览截取前200字符/h5 /div div classcard-body pre classbg-dark text-light p-3 roundedcode{{ object.html_content|truncatechars:200|linebreaks }}/code/pre /div /div /div /div {% endblock %}逻辑说明col-12 col-md-8在小屏堆叠、中屏分栏col-lg-6在大屏并列显示摘要与资源列表list-group-flush去除边框提升信息密度float-end将哈希缩略值右对齐truncatechars:200防止长HTML撑破容器。所有class均来自Bootstrap 5.3官方文档无自定义CSS确保跨设备一致性。5. 进阶技巧用Django Admin定制取证工作台与MySQL全文检索加速URL搜索5.1 扩展Django Admin为安全审计员定制快照管理面板core/admin.py中重写Admin类暴露取证关键字段并添加批量操作from django.contrib import admin from django.urls import reverse from django.utils.html import format_html from core.models import HtmlSnapshot, SnapshotDiff, AuditLog admin.register(HtmlSnapshot) class HtmlSnapshotAdmin(admin.ModelAdmin): list_display [url_link, title, dom_hash_short, created_at, resource_count] list_filter [created_at, url] search_fields [url, title, dom_hash] date_hierarchy created_at readonly_fields [dom_hash, created_at, updated_at] actions [mark_as_verified] def url_link(self, obj): return format_html(a href{} target_blank{}/a, obj.url, obj.url[:50] ...) url_link.short_description URL def dom_hash_short(self, obj): return obj.dom_hash[:12] ... dom_hash_short.short_description DOM哈希 def resource_count(self, obj): return len(obj.resource_hashes) resource_count.short_description 资源数 admin.action(description标记为已验证人工确认无篡改) def mark_as_verified(self, request, queryset): updated queryset.update(is_verifiedTrue) # 需在模型中添加is_verified布尔字段 self.message_user(request, f成功标记{updated}个快照为已验证) admin.register(SnapshotDiff) class SnapshotDiffAdmin(admin.ModelAdmin): list_display [base_snapshot, target_snapshot, is_critical_badge, created_at] list_filter [is_critical, created_at] search_fields [base_snapshot__url, target_snapshot__url] def is_critical_badge(self, obj): return format_html(span classbadge bg-{}{}/span, danger if obj.is_critical else success, 高危 if obj.is_critical else 常规) is_critical_badge.short_description 风险等级技巧说明list_display中url_link生成可点击链接dom_hash_short截断显示避免表格过宽search_fields启用全文搜索但默认MySQL全文索引需手动开启见下节admin.action添加批量标记功能审计员可一次确认多个快照。is_verified字段虽未在原始模型定义但这是取证流程必需的状态标记建议在HtmlSnapshot中追加is_verified models.BooleanField(defaultFalse)。5.2 启用MySQL全文索引让URL和标题搜索毫秒级响应在MySQL中为core_htmlsnapshot表的url和title字段添加FULLTEXT索引-- 进入MySQL执行 ALTER TABLE core_htmlsnapshot ADD FULLTEXT(url, title); -- 验证索引创建 SHOW INDEX FROM core_htmlsnapshot WHERE Key_name url_title;在Django中启用全文搜索需修改settings.py# settings.py DATABASES[default][OPTIONS][init_command] SET SESSION sql_modeSTRICT_TRANS_TABLES # 在views.py中使用 from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank # 仅PostgreSQL # MySQL替代方案使用raw SQL或第三方库mysql-fulltext-search由于Django ORM原生不支持MySQL全文搜索语法采用Raw SQL方式# core/views.py from django.db import connection def search_snapshots(request): query request.GET.get(q, ).strip() if not query: return render(request, core/search_results.html, {snapshots: []}) with connection.cursor() as cursor: cursor.execute( SELECT id, url, title, dom_hash, created_at FROM core_htmlsnapshot WHERE MATCH(url, title) AGAINST(%s IN NATURAL LANGUAGE MODE) ORDER BY created_at DESC LIMIT 20 , [query]) rows cursor.fetchall() snapshots [ { id: row[0], url: row[1], title: row[2], dom_hash: row[3][:12] ..., created_at: row[4] } for row in rows ] return render(request, core/search_results.html, {snapshots: snapshots})参数说明MATCH(url, title) AGAINST(... IN NATURAL LANGUAGE MODE)启用MySQL原生全文搜索比LIKE %query%快100倍LIMIT 20防止单次查询过载created_at DESC确保最新快照优先。此SQL直接操作底层表绕过ORM限制是MySQL环境下最可靠方案。5.3 生成取证报告PDF用WeasyPrint将HTML快照导出为合规存档安装WeasyPrint并配置pip install weasyprint # Ubuntu需额外安装依赖 sudo apt-get install libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libcairo2在core/views.py中添加导出视图from weasyprint import HTML from django.http import HttpResponse def export_snapshot_pdf(request, pk): snapshot HtmlSnapshot.objects.get(pkpk) # 构建PDF专用HTML模板 html_content f !doctype html html langzh-cn head meta charsetutf-8 titleHTML取证报告 - {snapshot.url}/title style body {{ font-family: SimSun, Noto Sans CJK SC, sans-serif; }} .header {{ background-color: #0d6efd; color: white; padding: 20px; }} .footer {{ border-top: 1px solid #dee2e6; padding: 10px; font-size: 0.8em; }} /style /head body div classheader h1HTML图像取证报告/h1 pURL: {snapshot.url} | 抓取时间: {snapshot.created_at}/p /div div classcontainer mt-4 h2DOM结构摘要/h2 pstrong标题:/strong {snapshot.title}/p pstrongDOM哈希:/strong {snapshot.dom_hash}/p h2原始HTML截取/h2 pre stylebackground:#f8f9fa;padding:15px;overflow-x:auto;{snapshot.html_content[:1000]}/pre /div div classfooter 报告生成时间: {timezone.now()} | 系统版本: Django {django.get_version()} /div /body /html pdf_file HTML(stringhtml_content).write_pdf() response HttpResponse(pdf_file, content_typeapplication/pdf) response[Content-Disposition] fattachment; filenameforensic_report_{pk}.pdf return response关键点weasyprint支持中文需指定SimSun或Noto Sans CJK SC字体pre标签保留HTML原始格式Content-Disposition: attachment强制下载而非浏览器打开django.get_version()写入版本号满足合规审计要求。此PDF可直接作为等保测评或ISO27001证据提交。本文还有配套的精品资源点击获取
网站建设高端定制企业官网