1. 毕设选题与技术选型的来龙去脉毕业设计这件事每个经历过的人都知道选题选好了后面是康庄大道选不好逻辑绕来绕去代码改了又改最后答辩还可能被问得哑口无言。我当时选“Django微博热搜数据分析与可视化系统”这个题目前后纠结了一个多星期认真想过它到底值不值得做、能不能做出来、答辩能不能讲清楚。先说结论这个题目非常适合作为计算机科学与技术、软件工程、大数据相关专业的毕业设计。理由有三点数据获取门槛低但有质感。微博热搜是公开页面数据不需要用户登录、不需要授权结构相对稳定非常适合练习数据采集。相比“爬取电商商品价格”“爬取招聘网站职位”这类同样常见的题目微博热搜天生带有“实时性”和“话题性”分析出来的结果直观易懂答辩时更好讲故事。技术栈覆盖面完整。一个完整的系统需要爬虫采集、数据清洗入库、后端API、定时任务调度、可视化展示甚至缓存、部署刚好能把大学四年学的东西串起来。用Django做后端把爬到的数据通过ECharts展示成图表技术深度和广度都能体现。分析维度灵活。同一套数据可以只做基础的热搜排行榜也可以做热度趋势预测、话题类别聚类、词频分析想往深了做有空间想控制工作量也有退路。1.1 为什么选Django而不是Flask、FastAPI或SpringBoot很多同学在框架选型上会纠结我的经验是毕业设计选框架首要标准不是“技术最新”而是“生态成熟、资料多、你自己使得顺手”。如果这个项目是你第一次完整做前后端联调那更是如此。Django的优势非常明显自带Admin后台。爬虫采集到的数据可以直接在后台查看、修改、删除相当于白送了一套数据管理界面。这在答辩演示时特别加分也让开发期调试省了大量时间。ORM Migrations体系完整。定义好模型之后执行迁移表结构自动建好不用手写SQL建表脚本。和MySQL、SQLite的衔接都很顺。内置模板引擎。可视化系统如果走服务端渲染路线Django模板直接渲染HTML页面不需要额外搭前端工程。就算后面改用前后端分离Django写一套JSON API也不费劲。中间件、信号、缓存框架、认证体系都是现成的用得上的时候拿来即用。Flask和FastAPI我也简单评估过Flask太轻很多功能要自己拼第三方库适合写小型服务但做完整系统时工作量大FastAPI性能确实好自带的API文档也很漂亮但在毕业设计场景里面它的性能优势其实体现不出来反而Django生态里的现成组件更多。SpringBoot是另一个极端Java体系完整但开发效率低、环境配置重对一个以Python为主线的毕设来说没必要。1.2 整体架构怎么设计整个系统我采用的架构如下采集层Python脚本/APScheduler定时任务 | v 数据存储层MySQL / SQLiteDjango ORM管理 | v 后端服务层Django AppAPI接口 Admin后台 | v 展示层HTML页面 ECharts图表 / 可选Vue分离采集层独立存在不混在Django的请求处理流程里。这样设计的原因是采集任务本质上是“主动抓取外部数据”和“被动响应前端请求”是两种完全不同的生命节奏。混在一起写容易导致某个请求响应变慢甚至因为爬虫阻塞影响页面访问。后端层我只用了Django自带的django-admin和django-rest-framework。其实纯用Django原生JsonResponse写几个接口也完全够用但要考虑答辩时老师可能会问“如果前端有多个端怎么办”DRF的路由、序列化、分页机制会让你回答得更从容。展示层刚开始我打算直接写Django模板原生JavaScript后来因为要做词云和大屏效果还是改成了Django提供JSON接口 前端页面异步加载的模式。这样图表刷新、交互筛选都好做代码逻辑也更清晰。2. 数据采集把微博热搜稳定抓回本地爬虫是整套系统的数据入口也是最容易出幺蛾子的环节。很多同学写爬虫能跑通一次就高兴得不行但实际一跑就是几小时甚至几天问题全在“稳定性”上。2.1 数据源分析微博热搜的页面地址是s.weibo.com/top/summary打开开发者工具F12切到Network面板刷新页面就能看到一个名为/ajax/side/hotSearch的接口返回的是JSON格式数据。这个接口会给出一批热搜词及其排名、热度值、分类标签等信息比直接解析HTML方便太多。我们没有做任何绕过机制的事情只是请求了一个公开的接口地址通过常规的requests库带上基本请求头User-Agent、Referer去访问。需要注意请求频率必须控制通常两次抓取之间至少间隔几十秒尤其不要像压力测试一样高频请求。添加请求头时不要伪造太多字段保持和浏览器请求一致即可。接口里返回的JSON结构可能随页面改版有变动采集代码要写成“容错式”字段缺失时不要直接崩溃。2.2 采集脚本的核心实现下面是我当时写的核心采集函数的简化版本。这里用的是requests库解析JSON然后把结果存入数据库。import requests import json from datetime import datetime from django.utils import timezone from myapp.models import HotSearchItem, HotSearchSnapshot def fetch_hot_search(): url https://s.weibo.com/ajax/side/hotSearch headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Referer: https://s.weibo.com/, } try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() data resp.json() except Exception as e: print(f[ERROR] 请求失败: {e}) return real_data data.get(data, {}).get(realtime, []) if not real_data: print([WARN] 没有获取到实时热搜数据接口结构可能已变化) return # 创建一次快照 snapshot HotSearchSnapshot.objects.create(snapshot_timetimezone.now()) bulk_list [] for item in real_data: word item.get(word) or item.get(word_scheme) or if not word: continue heat item.get(raw_hot) or 0 rank item.get(rank) or 0 label item.get(label_name) or bulk_list.append( HotSearchItem( snapshotsnapshot, rankrank, wordword, heatheat, labellabel, ) ) HotSearchItem.objects.bulk_create(bulk_list, batch_size50) print(f[INFO] 采集完成共写入 {len(bulk_list)} 条热搜)这段代码虽然简单但有几个细节值得你注意第一HotSearchSnapshot和HotSearchItem是分开的两张表。每次采集只创建一个快照记录所有热搜词都挂在快照下面。为什么因为热搜的排名和热度是随时间变化的如果只存“当前有哪些热词”而没有“这是哪一刻的热词”后面做趋势分析、做榜单变化追踪就完全无从下手。第二bulk_create批量写入。我的实测经验是50条热搜逐条save()会触发50次数据库插入而bulk_create一次就能完成速度差别非常明显。毕设数据量虽然不大但好的习惯要养起来等数据量到几十万条时差距就会放大。第三timeout10必须写。不写timeout的话网络异常时requests会一直挂着定时任务可能卡死。2.3 采集失败时的真实场景爬虫跑到第三天我开始收到空数据告警。打开日志一看接口返回的JSON结构变了realtime字段变成了空列表但也没有报错。实际上那天微博正好做了一次页面改版把热搜接口的字段名调整了。这种问题第一次遇到会觉得很坑但处理方式其实很简单采集程序必须做最少字段校验和结构兼容。我在代码里加了data.get(data, {}).get(realtime, [])这种容错写法即使结构变了也只是返回空列表不会直接抛异常导致整个脚本崩溃。同时日志里打出了警告信息方便及时发现问题。还有一个常见的坑是raw_hot热度值偶尔会缺失。微博热搜词条中有些带有“新”“热”“爆”标签的词条热度为0有些则没有热度值。我在代码里做了or 0兜底避免插入数据库时报NOT NULL约束错误。2.4 关于合规和频率控制的个人看法写爬虫一定要搞清楚边界。我做的只是请求公开接口、解析公开数据、用于个人学习和毕业设计展示没有抓取任何用户个人信息也没有做任何绕过反爬机制的操作。这是底线问题也是答辩时老师可能问到的点你要能清楚地讲出来。另一个原则是频率克制。我当时设置的采集间隔是5分钟一次实际用下来发现对于毕设场景其实1小时一次完全够用。微博热搜虽然变化快但粒度是“小时级”就已经能看出趋势了。采集频率太高不仅给对方服务器造成压力你自己也会因为IP被临时限制而头疼。3. 数据建模与Django ORM的落地细节数据模型设计是整个项目的地基。很多同学在这个环节容易偷懒结果分析阶段发现字段不够用、表结构不合理又回过来改模型、迁移数据库非常痛苦。3.1 表结构设计我最终设计的核心表有两张这里直接给出简化版的模型定义from django.db import models class HotSearchSnapshot(models.Model): snapshot_time models.DateTimeField(db_indexTrue, verbose_name快照时间) class Meta: db_table hot_search_snapshot verbose_name 热搜快照 def __str__(self): return f快照 {self.snapshot_time:%Y-%m-%d %H:%M:%S} class HotSearchItem(models.Model): snapshot models.ForeignKey( HotSearchSnapshot, on_deletemodels.CASCADE, related_nameitems, verbose_name所属快照, ) rank models.IntegerField(verbose_name排名) word models.CharField(max_length200, db_indexTrue, verbose_name热搜词) heat models.BigIntegerField(default0, verbose_name热度值) label models.CharField(max_length50, blankTrue, default, verbose_name标签) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table hot_search_item verbose_name 热搜条目 ordering [rank]设计思路拆开讲快照表和条目表分离。快照表代表“某个时间点抓取到的整个榜单”条目表代表“该时间点的一个具体热搜词”。一对一的关系是一个快照有50条左右条目。这样设计的好处是能准确重建“某一时刻的完整热搜榜”想要知道“某个词第一次出现是什么时候”“某两个词是不是同时上过榜”都很好查。db_indexTrue加在常用查询字段上。snapshot_time、word都是查询高频字段。比如后面做“按天聚合统计词频”“查询某个词的所有历史排名”时没有索引会很慢。虽然毕设数据量可能只有几千几万条索引效果不明显但这是规范性问题。on_deletemodels.CASCADE。删掉某次快照时其下所有条目自动删除避免残留大量孤儿数据。heat用BigIntegerField。热搜热度值虽然一般不会超过千万但用BigInteger保证不会溢出后面做历史趋势图时更稳。3.2 Django的MTV模式在模型层带来的帮助在写模型过程中我越来越认同Django MTV模式的价值尤其在做毕设这种需要快速迭代的项目时Model层只需要定义数据模型迁移命令自动生成表结构、索引。Template层Django模板直接渲染HTML方便展示数据。当然我做的是前后端分离的JSON接口模板只用于后台页面。View层视图函数把模型数据序列化后返回JSON逻辑清晰。答辩时如果被问到“MTV模式到底有什么作用”你可以直接结合自己的项目讲Model负责数据定义与数据库交互Template负责页面展示View是连接两者的业务逻辑层。分离让代码职责清晰修改一个环节不影响另外两个环节。这种回答比背概念要具体得多。3.3 高频ORM操作示例在分析和统计阶段我写了不少ORM查询。挑几个典型场景分享一下。查询某天所有出现在热搜榜上的词from django.db.models import Count from django.utils import timezone def get_daily_hot_words(date): start timezone.make_aware(datetime.combine(date, datetime.min.time())) end timezone.make_aware(datetime.combine(date, datetime.max.time())) return ( HotSearchItem.objects .filter(snapshot__snapshot_time__range(start, end)) .values(word) .annotate(countCount(id)) .order_by(-count)[:100] )这里用到valuesannotate组合实现按热搜词分组统计“出现在多少次快照中”这个指标能反映一个词的热度持续度。查询某个词的历史排名变化画趋势图用def get_word_rank_history(word): return ( HotSearchItem.objects .filter(wordword) .order_by(snapshot__snapshot_time) .values_list(snapshot__snapshot_time, rank, heat) )注意这里用values_list直接返回元组列表省去对象封装速度快方便前端直接使用。删除过期的旧快照数据清理def clean_old_data(days7): from datetime import timedelta deadline timezone.now() - timedelta(daysdays) HotSearchSnapshot.objects.filter(snapshot_time__ltdeadline).delete()delete()会级联删除所属的热搜条目一条语句清理两张表很方便。我自己在ORM使用上踩过的坑主要有两个一是忘了filter返回的是QuerySet而不是列表重复使用同一个QuerySet时要注意它可能因为被重新计算而引发额外SQL查询二是get_or_create如果并发调用可能产生竞态不过毕设场景基本用不到要真用了还是建议写try/except兜底。3.4 数据模型演进的实际经历项目做到中期我想加一个“热搜词分类”的功能比如把“明星”“社会”“体育”分类。最初设计时没考虑分类字段后来只能通过迁移添加python manage.py makemigrations python manage.py migrateDjango的迁移系统会检测模型变化并生成映射脚本这比我之前手写SQL维护表结构要省心得多。也是从这次改动开始我意识到一个好的ORM和数据迁移体系对开发效率的提升有多明显。建议做毕设时初期不要追求把所有字段一次性设计完美先把核心跑通后面通过迁移迭代完善是常态。4. 后端API与任务调度让数据自己跑起来数据有了模型有了接下来怎么把数据变成可视化看板这就要靠后端API把数据喂给前端。同时采集任务不能每次手动跑要用定时任务自动化。4.1 API接口设计我做了下面几个核心接口全部返回JSON格式供前端ECharts图表调用接口路径方法功能说明/api/top/current/GET获取当前最新一次快照的热搜TOP20/api/top/history/GET获取指定热搜词的历史排名与热度趋势/api/stats/daily/GET按天统计热词出现次数返回TOP50/api/stats/wordcloud/GET返回近N小时热搜词及其热度值用于词云展示/api/stats/rank-change/GET返回最新快照较上一快照的排名升降变化当前榜单接口的核心实现使用DRF的APIViewfrom rest_framework.views import APIView from rest_framework.response import Response from myapp.models import HotSearchSnapshot, HotSearchItem class CurrentTopView(APIView): def get(self, request, *args, **kwargs): latest_snapshot ( HotSearchSnapshot.objects .order_by(-snapshot_time) .first() ) if not latest_snapshot: return Response({items: [], error: 暂无数据}) items ( HotSearchItem.objects .filter(snapshotlatest_snapshot) .order_by(rank)[:20] .values(rank, word, heat, label) ) return Response({ snapshot_time: latest_snapshot.snapshot_time, items: list(items), })这里的关键点是每次都只查最新的那一次快照不要把所有数据一次性查出来再切片。虽然数据量小的时候看不出区别但这种写法逻辑清晰也能保证查询效率。4.2 定时采集方案APScheduler定时任务方案我当时对比了好几个Django自带的manage.py crontab受系统环境限制Windows计划任务也可以调用管理命令但配置不方便最后我用了APScheduler理由很简单可以在Python代码里直接定义任务调度和Django共用一套环境。支持interval间隔调度、cron表达式调度灵活度高。在Django的App配置里启动时注册调度器即可不用额外部署Celery、Redis队列那些重组件。调度器初始化代码# scheduling.py from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger from django.conf import settings from myapp.utils.crawler import fetch_hot_search import logging logger logging.getLogger(__name__) def start_scheduler(): if settings.DEBUG: # 开发环境下每2分钟跑一次方便调试 trigger IntervalTrigger(minutes2) else: # 生产环境每10分钟跑一次 trigger IntervalTrigger(minutes10) scheduler BackgroundScheduler() scheduler.add_job(fetch_hot_search, triggertrigger, idfetch_hot_search, replace_existingTrue) scheduler.start() logger.info(定时采集任务已启动)然后在App的apps.py里注册启动class MyappConfig(AppConfig): default_auto_field django.db.models.BigAutoField name myapp def ready(self): from . import scheduling if not os.environ.get(DJANGO_RUNNING_MIGRATE): scheduling.start_scheduler()这个方案有几个要注意的坑。第一ready()方法在manage.py migrate时也会执行如果采集任务启动时数据库表还没建好会报错。所以我用环境变量做一个简单的开关只在运行server时不跳过。第二本地开发时如果想立刻看到效果建议把间隔调短一些比如2分钟一次采集完刷新页面就能看到新数据。第三正式部署后建议看一下日志确认调度器真的启动了不要等到答辩前才发现数据停在几天前没更新。开发环境里我还加了一个手动触发按钮。在Admin后台或者自定义的一个页面上点击“立即采集”按钮就能手动调用fetch_hot_search()函数非常方便做演示和测试。4.3 Redis缓存接入项目初期我没加缓存后来发现一个问题首页的“当前热搜TOP20”接口每次加载都要查数据库而且每次取的还都是最新的快照根本没必要每次实时查。于是我把这个接口的响应缓存到了Redis里缓存60秒。Django接入Redis的方式不复杂pip install django-redis然后在settings.py里配置CACHES { default: { BACKEND: django_redis.cache.RedisCache, LOCATION: redis://127.0.0.1:6379/1, OPTIONS: { CLIENT_CLASS: django_redis.client.DefaultClient, } } }接口里改成from django.core.cache import cache class CurrentTopView(APIView): def get(self, request, *args, **kwargs): cache_key api:current_top:latest result cache.get(cache_key) if result is not None: return Response(result) # 查询逻辑... cache.set(cache_key, data, timeout60) return Response(data)关于Redis可视化工具开发时我用的Another Redis Desktop Manager界面简洁能直接查看key的过期时间和值内容调试缓存问题很方便。Windows下也可以用Redis官方自带的redis-cli命令行工具看keys、查value都够用。4.4 数据导出StreamingHttpResponse的实际场景有一个功能是“导出当前热搜数据为CSV文件”这里我用到了Django的StreamingHttpResponse。很多教程里讲这个类都只讲“大文件流式下载”比较抽象在我这个项目里场景就很具体当你想把历史一周的所有热搜条目完整导出给老师或者写论文用时数据量可能上万条。如果一次性生成字符串再返回内存占用会比较大用流式响应就能逐条生成、逐条返回体验更好。import csv from django.http import StreamingHttpResponse def export_csv(request): from myapp.models import HotSearchItem response StreamingHttpResponse( csv_generator(), content_typetext/csv; charsetutf-8, ) response[Content-Disposition] attachment; filenamehot_search.csv return response def csv_generator(): yield snapshot_time,rank,word,heat,label\n items ( HotSearchItem.objects .select_related(snapshot) .order_by(-snapshot__snapshot_time) ) for item in items.iterator(chunk_size200): row f{item.snapshot.snapshot_time.isoformat()},{item.rank},{item.word},{item.heat},{item.label}\n yield row这里Content-Disposition指定了下载的文件名content_type必须同时设置charsetutf-8否则用Excel打开CSV时会中文乱码。这两个参数组合在很多教程里都只是提了一嘴“用于指定响应类型和文件名”真正生产环境里它就是保证文件能正常打开的关键。5. 可视化看板从数据到图表的最后一公里数据链路打通后最后一步就是把冷冰冰的数字变成让人看得懂、有冲击力的图表。这一步从技术上不算难但要做得好看、好用也有一些门道。5.1 前端方案怎么选我在前端方案上试了三条路线纯Django模板 原生JavaScript ECharts最简单适合时间紧、不想引入Node构建链路的项目。Django AdminLTEBootstrap模板界面专业后台管理风格适合“管理系统”方向。Django Vue前后端分离最灵活但需要额外搭建Vue工程数据交互也复杂一些。最终我选了第一种原因是毕设评审的重心在后端设计前端只要清晰直观就足够。ECharts是百度开源的可视化图表库配置简单、文档丰富、示例多在词云、热力、柱状图、折线图方面表现都很好。如果你前端有一定基础也可以选“Django Vue ECharts”的组合用Vue管理图表的数据绑定和组件状态交互体验更好。前端选型不是大问题关键是后端数据结构要稳定接口字段统一。5.2 核心图表设计我的看板最终实现了五张图第一张当前热搜TOP10横向条形图这张图最简单把最新快照的TOP10热搜词按热度从高到低展示主要用来展示“此时此刻大家在关注什么”。async function renderTop10() { const resp await fetch(/api/top/current/); const data await resp.json(); const items data.items.slice(0, 10); const option { title: { text: 当前热搜TOP10, left: center }, tooltip: {}, grid: { left: 3%, right: 8%, bottom: 3%, containLabel: true }, xAxis: { type: value }, yAxis: { type: category, data: items.map(i i.word).reverse() }, series: [{ type: bar, data: items.map(i i.heat).reverse(), itemStyle: { color: #ff6f61 } }] }; const chart echarts.init(document.getElementById(chart-top)); chart.setOption(option); }第二张热搜词热度趋势折线图选择某个热搜词后展示它在过去一段时间内排名和热度的变化。这里用到了前面get_word_rank_history接口的数据。async function renderTrend(word) { const resp await fetch(/api/top/history/?word${encodeURIComponent(word)}); const data await resp.json(); const chart echarts.init(document.getElementById(chart-trend)); chart.setOption({ title: { text: “${word}”热度趋势, left: center }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.times }, yAxis: [{ type: value, name: 热度值 }, { type: value, name: 排名, inverse: true }], series: [ { name: 热度值, type: line, data: data.heats, smooth: true }, { name: 排名, type: line, yAxisIndex: 1, data: data.ranks, smooth: true } ] }); }折线图维度选择很有讲究。排名这个指标是“逆序”的第1名数值最小最热所以我把排名放到了第二个y轴并设置inverse: true让第一名出现在图表上方。这个细节虽然不大但答辩时如果有老师问到“你的图表怎么体现热度对比”会是一个加分项。第三张词云图词云用ECharts官方扩展echarts-wordcloud实现。把最近若干次快照里出现的所有热搜词按照热度值映射成词云中词的大小。const resp await fetch(/api/stats/wordcloud/); const data await resp.json(); const chart echarts.init(document.getElementById(chart-wordcloud)); chart.setOption({ series: [{ type: wordCloud, shape: circle, left: center, top: center, width: 90%, height: 90%, sizeRange: [12, 60], rotationRange: [0, 0], textStyle: { color: () rgb(${Math.round(Math.random() * 255)}, ${Math.round(Math.random() * 255)}, ${Math.round(Math.random() * 255)}), }, data: data.items // [{name: 热搜词, value: 热度值}] }] });词云从视觉上很有冲击力第一眼看到“谁是大热点”一目了然用来做展示页的视觉中心非常合适。第四张排名升降榜将最新快照和前一条快照比对展示哪些词排名上升了、哪些下降了、哪些是新上榜的。这张表用纯HTML表格渲染加上上升/下降箭头的文本标签。第五张按天热词频次统计柱状图用get_daily_hot_words的结果展示某天所有词出现在快照中的次数反映热度持续性。5.3 大屏适配和实时刷新如果你的选题方向是“可视化大屏”这里给你一点实在建议。大屏和普通网页的差别核心在于写满、不用滚动、自动刷新。不用滚动意味着所有图表要在一个屏幕内展示布局要用flex或grid划分网格而不是HTML流式布局。自动刷新指用setInterval定时请求接口更新图表数据。比如每1分钟刷新一次当前榜单和升降榜。注意如果每次都chart.setOption(option)图表内部会保留旧状态需要设置notMerge: true彻底替换数据setInterval(async () { const resp await fetch(/api/top/current/); const data await resp.json(); const items data.items.slice(0, 10); chart.setOption({ yAxis: { data: items.map(i i.word).reverse() }, series: [{ data: items.map(i i.heat).reverse() }] }, true); }, 60000);大屏背景一般用深色底图表配色也相应调整。ECharts默认主题是浅色的在大屏上对比度不够你可以用registerTheme注册一个深色主题或者直接在option里覆盖背景色、文本色、轴线颜色。5.4 图表数据异常的处理图表页加载时如果接口返回空数据或者接口报错前端一定要有兜底提示否则页面上一片空白答辩时非常尴尬。我在每个fetch后面都加了try...catch和空数据判断try { const data await resp.json(); if (!data.items || data.items.length 0) { document.getElementById(chart-top).innerHTML div classempty-tip暂无数据请稍后再试/div; return; } } catch (e) { console.error(图表数据加载失败:, e); document.getElementById(chart-top).innerHTML div classempty-tip页面加载异常请刷新/div; }6. 部署上线与Windows环境实测开发完了不是终点能部署上线才算一个完整的项目。我自己的开发环境是Windows所以部署方案用的是Waitress Nginx的组合。这个组合和传统的runserver、gunicorn不太一样值得记录一下。6.1 开发环境与生产环境配置分离项目根目录下我创建了settings_dev.py和settings_prod.py通过环境变量切换。分离的主要原因是SQLite和MySQL的配置不同、DEBUG开关不同、静态文件路径不同。开发环境用SQLite零配置、文件型数据库开发调试很方便。生产环境我换成了MySQL原因后面讲。settings_prod.py里的关键配置DEBUG False ALLOWED_HOSTS [your_server_ip, www.example.com] DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: hotsearch_db, USER: hotsearch_user, PASSWORD: your_secure_password, HOST: 127.0.0.1, PORT: 3306, } } STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles为什么不继续用SQLite因为生产环境里面定时任务每10分钟写一次数据库一天144次快照写入SQLite的写锁机制在高频写入场景下可能出现“database is locked”的报错体验很差。MySQL的并发写入能力好得多。6.2 Waitress Nginx怎么个事Django官方建议在生产环境不要使用runserver因为它性能弱、不安全。传统方案是Linux下用gunicorn或uwsgi但我在Windows上开发装gunicorn会失败所以选了Waitress。Waitress是一个纯Python实现的WSGI服务器跨平台、能在Windows上稳定运行。安装pip install waitress启动方式也简单waitress-serve --listen127.0.0.1:8000 myproject.wsgi:application如果你想后台静默运行可以写一个run_prod.pyfrom waitress import serve from myproject.wsgi import application serve(application, host127.0.0.1, port8000, threads8)Nginx放在前面做反向代理和静态文件服务。浏览器访问Nginx的80端口Nginx把动态请求转发给后端的Waitress服务同时直接返回Django的静态文件。Nginx配置要点server { listen 80; server_name your_server_ip; location /static/ { alias /path/to/your/project/staticfiles/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }为什么要把静态文件交给Nginx而不是Django自己处理因为Django处理静态文件时每次都要走Python进程性能很差且文件稍大一点就明显变慢。Nginx就直接返回磁盘上的文件几乎零开销。启动顺序也要注意先启动Waitress进程再启动Nginx。如果Nginx在Waitress之前启动访问时会出现502 Bad Gateway。排查时可以netstat -ano | findstr :8000确认8000端口是否确实被Waitress监听。6.3 生产环境下的定时任务我一开始把APScheduler放在了Django进程里跟随启动本地开发没问题但生产环境会遇到一个潜在风险如果用到多进程部署多个worker进程会各自启动一个调度器导致同一个采集任务被重复执行多次。我在部署时只用了一个Waitress进程所以不会重复。但如果你以后用多进程部署建议把采集任务拆成独立的脚本用系统的计划任务Windows Task Scheduler或cron来调度这样更稳妥。6.4 静态文件收集部署前必须执行python manage.py collectstatic这个命令会把所有App里的静态文件复制到STATIC_ROOT指定的目录供Nginx访问。漏掉这一条是新手常犯的错误——本地开发时runserver能自动处理静态文件部署后却全是404。7. 项目开发中踩过的坑完整的排查链路每个项目都有踩坑史这个项目也不例外。挑几个印象深刻的坑把当时的排查过程完整梳理一遍对你可能比任何知识分享都有用。7.1 中文乱码MySQL连接层面解决某天查看数据库里热搜词发现全部变成了一串???当时有点慌。排查链路是这样的第一步先查采集脚本打印的日志。日志里打印的JSON是正常的中文说明数据源没问题。第二步查Django shell里HotSearchItem.objects.first().word发现读出来也是???。说明写入时就已经乱码了。第三步查MySQL编码。用命令SHOW VARIABLES LIKE character_set%发现character_set_database是latin1不是utf8mb4。问题根源清楚了MySQL默认数据库编码不对。解决办法是删掉数据库重建创建时指定编码CREATE DATABASE hotsearch_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;同时Django连接串里也加上OPTIONSDATABASES { default: { ENGINE: django.db.backends.mysql, OPTIONS: { charset: utf8mb4, }, } }改完之后重新采集中文完全正常。这个坑的教训是数据库层面要刻意确认编码不要想当然。尤其是Windows MySQL安装时默认配置很容易导致中文字符集不对。7.2 快照去重与“热搜词是否变化”的误判有一天我发现排名升降榜上无数个“新上榜”完全不对。排查后发现是采集脚本写了个replaceTrue的逻辑导致部分快照重复保存但实际上可能是同一条数据被重复插入。具体来说我的HotSearchSnapshot表没有加唯一约束每次跑采集都会新建一条快照无论当前榜单和上一次差多少。而图表模块又要拿最新两条快照做差集重复快照会污染结果。解决方式是加了一个简单判断如果当前接口返回的TOP10和数据库里最新一条快照的TOP10完全一致就跳过本次写入减少数据冗余。latest_snapshot HotSearchSnapshot.objects.order_by(-snapshot_time).first() if latest_snapshot: latest_words list( HotSearchItem.objects.filter(snapshotlatest_snapshot) .order_by(rank) .values_list(word, flatTrue) ) current_words [i.get(word) for i in real_data][:50] if latest_words current_words: print([INFO] 榜单未变化跳过本次采集) return这个改动一举两得减少数据库写入次数也让排名升降分析更准确。7.3 时间字段与前端显示的时区偏差采集到的快照时间在页面上显示比本地时间快了8小时。查了一圈才发现是Django的USE_TZ设置的问题。Django默认USE_TZ True存入数据库的时间是UTC时间模板渲染时会转换成本地时区但如果前端是直接拿JSON里的ISO字符串渲染这个字符串是UTC的前端不会自动做时区转换。解决方案有两种在settings.py里设置TIME_ZONE Asia/Shanghai同时在接口序列化时把时间转成字符串。前端拿时间后手动new Date(value)让浏览器自动转换为本地时间。我选择的是在后端统一转换后输出from django.utils import timezone local_time timezone.localtime(snapshot.snapshot_time) snapshot_time_str local_time.strftime(%Y-%m-%d %H:%M:%S)这样就保证了不管数据库存的是什么接口一律输出东八区格式化好的时间字符串前端展示零心智负担。7.4 ECharts图表在容器隐藏时初始化尺寸为0大屏页有个Tab切换从“当前热点”切到“历史趋势”时第二个Tab里的折线图只有一条线贴着左侧非常奇怪。排查后发现原因ECharts初始化时如果容器是display: none状态图表的初始宽度是0后面切换到显示状态时不会自动重算尺寸。解决方式是在Tab切换时调用chart.resize()document.querySelectorAll(.tab-btn).forEach(btn { btn.addEventListener(click, () { setTimeout(() { trendChart.resize(); wordCloudChart.resize(); }, 50); }); });这个坑属于纯前端范畴但做可视化项目基本都会遇到提前记住能省半天时间。7.5 Windows部署时Waitress启动失败的进程占用问题生产环境部署那天Waitress一直启动失败报错信息是地址被占用。用netstat -ano | findstr :8000一看之前一个没关干净的Python进程还占着8000端口。在Windows上杀进程用taskkill /PID 12345 /F再重新启动Waitress就正常了。另外提醒一下Windows后台运行Python服务不太方便可以参考我用一个简单的启动脚本封装waitress-serve命令配合计划任务开机启动比手动敲命令稳定得多。8. 拓展方向与个人体会系统的基础功能做完之后我给自己留了几个可选的进阶方向虽然毕设主体没有完全实现但和老师深入探讨时给思路加分不少。第一接入更多的数据源。微博热搜只代表微博站内的热度可以扩展接入某平台热榜、短视频平台热榜做“多平台热点聚合分析”。后端数据结构基本不用改只要给HotSearchSnapshot加一个source字段就行。第二增加热度预测模型。学术上这种基于时间序列的热度变化数据很适合做预测分析。可以用自回归、滑动平均或者更进阶的时序预测模型预测未来某个词的热度走势。无论做不做得到高精度设计思路完整就能在论文里讲故事。第三词云和话题聚类。对一周、一个月的热搜词做jieba分词再用聚类把高频词归成“体育”“娱乐”“科技”等类别做成话题类别占比饼图。这个方向对专业深度要求不高但非常体现数据分析能力。最后分享一点个人体会做这个毕业设计我最大的收获不是学会了Django怎么用而是真正体验了“数据从产生到展现的全流程”。爬虫采集、数据建模、定时调度、接口设计、可视化展示每个环节单独看都不难串起来做完整你会对一套系统的运行方式建立直观认识。这个过程比背十遍概念都有效。数据量变大以后还可以试试引入Elasticsearch做全文检索、用Kafka做流式采集不过那是另一个量级的故事了。先把当前这套跑稳定你就能在毕业答辩时挺直腰板说这是我独立设计并实现的完整数据分析系统。
