简介:本资源是一套面向本科毕业设计与图像取证初学者的完整Django Web系统实现方案,聚焦数字图像真实性分析与篡改检测技术落地。项目以Python为核心,采用Django构建稳健后端,HTML+CSS+JS实现交互式前端界面,MySQL支撑图像元数据、取证结果及用户管理等结构化存储,配套说明文档、技术文献(LW)与答辩PPT,覆盖从开发部署到成果汇报的全流程需求。压缩包共265个文件,含25个HTML页面、42个JS脚本、25个核心Py模块、17个CSS样式表及75个GIF动图(多用于界面反馈与流程演示),另有SQL建库脚本、字体资源与PNG/JPG示例图像,整体16.96MB,结构清晰、模块分离明确。目前已有53人学习下载,可直接运行调试、深入理解Django ORM与图像取证业务逻辑耦合方式,并复用文档中的环境配置清单、接口设计说明与常见报错解决方案。
1. 这不是普通网页截图工具:Django+HTML图像取证系统解决的是“谁改了页面、何时改、改了哪一行”的硬需求
当你在运维后台发现用户提交的订单页突然多出一个可疑支付按钮,或教育平台的考试页面被注入了未授权的跳转脚本,靠肉眼比对HTML源码已完全失效——页面可能被动态渲染、CDN缓存、服务端模板拼接层层掩盖。此时,“图像取证”不是指PS修图鉴定,而是将HTML结构、CSS样式、DOM树状态、资源加载时序等维度固化为可哈希、可比对、可溯源的数字指纹。本系统用Python Django构建服务端核心,前端采用标准<!doctype html><html lang="zh-cn">结构配合Bootstrap快速搭建响应式取证界面,MySQL存储每次抓取的DOM快照、资源哈希、时间戳及操作日志。它面向安全审计员、合规检查工程师和Web质量保障人员,不依赖浏览器插件或远程SaaS服务,所有HTML解析、差异计算、快照生成均在本地Django进程内完成,关键参数如DOM深度阈值、CSS选择器白名单、资源加载超时均可代码级控制。新手能30分钟部署起一个带数据库的取证服务,老手则可通过重写HtmlSnapshotProcessor类接入自定义的XPath规则或Canvas像素比对逻辑。
2. 从零启动:用Django 4.2+Bootstrap 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 django==4.2.13 mysqlclient==2.2.4 requests==2.31.0 beautifulsoup4==4.12.2提示:
mysqlclient需系统级依赖,Ubuntu执行sudo apt-get install python3-dev default-libmysqlclient-dev build-essential;CentOS用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_mode='STRICT_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_zone='+00:00'", }, 'TEST': { 'CHARSET': 'utf8mb4', 'COLLATION': 'utf8mb4_unicode_ci', } } }参数说明:
charset='utf8mb4'确保emoji和特殊符号正确存储;time_zone='+00: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_length=2000, db_index=True) title = models.CharField(max_length=500, blank=True) dom_hash = models.CharField(max_length=64, db_index=True) # SHA256 of normalized HTML resource_hashes = models.JSONField(default=dict) # {"script.js": "a1b2c3...", "style.css": "d4e5f6..."} screenshot_path = models.CharField(max_length=500, blank=True) # Optional visual proof created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) 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_delete=models.CASCADE, related_name='diffs_as_base') target_snapshot = models.ForeignKey(HtmlSnapshot, on_delete=models.CASCADE, related_name='diffs_as_target') diff_html = models.TextField() # Unified diff format of DOM tree changed_nodes = models.JSONField(default=list) # [{"tag": "div", "attr": "id", "old": "old-id", "new": "new-id"}] is_critical = models.BooleanField(default=False) # Auto-flagged if <script> or form action changed created_at = models.DateTimeField(auto_now_add=True) class AuditLog(models.Model): user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) action = models.CharField(max_length=100) # "capture", "compare", "export" target_url = models.URLField(max_length=2000, blank=True) details = models.JSONField(default=dict) # {"status": "success", "duration_ms": 1245} created_at = models.DateTimeField(auto_now_add=True)执行迁移:
python manage.py makemigrations python manage.py migrate2.3 构建Bootstrap 5.3前端界面:标准化取证工作流
在core/templates/core/下创建base.html,严格遵循热词中的<!doctype html><html lang="zh-cn">结构:
<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>HTML图像取证系统</title> <!-- Bootstrap 5.3 CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- 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 class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="{% url 'home' %}">🔍 HTML取证</a> <div class="navbar-nav ms-auto"> <a class="nav-link" href="{% url 'capture_form' %}">抓取新快照</a> <a class="nav-link" href="{% url 'compare_form' %}">比对快照</a> <a class="nav-link" href="{% url 'snapshot_list' %}">历史记录</a> </div> </div> </nav> <main class="container mt-4"> {% if messages %} {% for message in messages %} <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert"> {{ message }} <button type="button" class="btn-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(string=lambda 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, headers=headers, timeout=15) 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( url=url, title=soup.title.string.strip() if soup.title else "无标题", dom_hash=dom_hash, resource_hashes=resource_hashes, screenshot_path="" # 后续可集成Puppeteer生成 ) # Step 5: 记录审计日志 AuditLog.objects.create( user=request.user if request.user.is_authenticated else None, action='capture', target_url=url, 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', pk=snapshot.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(), name='home'), path('capture/', views.CaptureView.as_view(), name='capture_form'), path('capture/<int:pk>/', views.SnapshotDetailView.as_view(), name='snapshot_detail'), # ... 其他路由 ]参数说明:
timeout=15防止恶意长连接阻塞;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>...</ins><del>...</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(keepends=True), target_text.splitlines(keepends=True), fromfile='base', tofile='target', 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(pk=base_id) target_snap = HtmlSnapshot.objects.get(pk=target_id) diff_result = compare_snapshots( base_snap.dom_hash, # 实际应传HTML内容,此处简化 target_snap.dom_hash ) SnapshotDiff.objects.create( base_snapshot=base_snap, target_snapshot=target_snap, diff_html=diff_result["diff_html"], changed_nodes=diff_result["changed_nodes"], is_critical=diff_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_name='htmlsnapshot', index=models.Index(fields=['url', '-created_at'], name='idx_url_latest'), ), migrations.AddIndex( model_name='htmlsnapshot', index=models.Index(fields=['dom_hash', '-created_at'], name='idx_domhash_recent'), ), migrations.AddIndex( model_name='snapshotdiff', index=models.Index(fields=['base_snapshot', 'target_snapshot'], name='idx_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应为ref,key为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), name='dispatch') 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 = f"dom_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 class="row mb-4"> <div class="col-12 col-md-8"> <h2>快照详情:{{ object.url }}</h2> <p class="text-muted">抓取时间:{{ object.created_at|date:"Y-m-d H:i:s" }} | DOM哈希:{{ object.dom_hash|slice:":16" }}...</p> </div> <div class="col-12 col-md-4 text-md-end"> <a href="{% url 'compare_form' %}?base={{ object.pk }}" class="btn btn-primary"> <i class="bi bi-arrow-left-right"></i> 与另一快照比对 </a> </div> </div> <div class="row"> <div class="col-12 col-lg-6"> <div class="card mb-4"> <div class="card-header bg-light"> <h5 class="mb-0">页面结构摘要</h5> </div> <div class="card-body"> <ul class="list-group list-group-flush"> <li class="list-group-item d-flex justify-content-between"> <span>标题</span> <span class="fw-bold">{{ object.title|default:"未设置" }}</span> </li> <li class="list-group-item d-flex justify-content-between"> <span>Script数量</span> <span class="fw-bold">{{ object.resource_hashes|length|add:"0" }}</span> </li> <li class="list-group-item d-flex justify-content-between"> <span>最后更新</span> <span class="fw-bold">{{ object.updated_at|timesince }}前</span> </li> </ul> </div> </div> </div> <div class="col-12 col-lg-6"> <div class="card mb-4"> <div class="card-header bg-light"> <h5 class="mb-0">资源哈希列表</h5> </div> <div class="card-body p-0"> <ul class="list-group list-group-flush"> {% for resource, hash_val in object.resource_hashes.items %} <li class="list-group-item"> <small class="text-muted">{{ resource|truncatechars:30 }}</small> <div class="badge bg-secondary float-end">{{ hash_val|slice:":8" }}</div> </li> {% endfor %} </ul> </div> </div> </div> </div> <div class="row"> <div class="col-12"> <div class="card"> <div class="card-header bg-light"> <h5 class="mb-0">DOM结构预览(截取前200字符)</h5> </div> <div class="card-body"> <pre class="bg-dark text-light p-3 rounded"><code>{{ 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_verified=True) # 需在模型中添加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 class="badge 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(default=False)。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_mode='STRICT_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(pk=pk) # 构建PDF专用HTML模板 html_content = f""" <!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>HTML取证报告 - {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 class="header"> <h1>HTML图像取证报告</h1> <p>URL: {snapshot.url} | 抓取时间: {snapshot.created_at}</p> </div> <div class="container mt-4"> <h2>DOM结构摘要</h2> <p><strong>标题:</strong> {snapshot.title}</p> <p><strong>DOM哈希:</strong> {snapshot.dom_hash}</p> <h2>原始HTML(截取)</h2> <pre style="background:#f8f9fa;padding:15px;overflow-x:auto;">{snapshot.html_content[:1000]}</pre> </div> <div class="footer"> 报告生成时间: {timezone.now()} | 系统版本: Django {django.get_version()} </div> </body> </html> """ pdf_file = HTML(string=html_content).write_pdf() response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="forensic_report_{pk}.pdf"' return response关键点:
weasyprint支持中文需指定SimSun或Noto Sans CJK SC字体;<pre>标签保留HTML原始格式;Content-Disposition: attachment强制下载而非浏览器打开;django.get_version()写入版本号满足合规审计要求。此PDF可直接作为等保测评或ISO27001证据提交。本文还有配套的精品资源,点击获取