贵州菜价这东西,真要做成毕设,难点从来不在"Django"或者"HTML"这些词本身,而在于怎么把一条"采集—清洗—存储—展示"的完整链路串起来,还得在论文里讲出技术含量。我见过太多人卡在爬虫拿到数据之后就不知道怎么继续,也有人把可视化做成了静态页面,答辩时一问数据从哪来就露馅。这篇就把我从页面分析到系统上线、再到论文整理的全过程拆开讲,包括那些踩完才明白的细节。
1. 这个毕设的真实难点:不是爬虫,而是把整条链路跑通
1.1 为什么选菜价作为爬虫可视化课题:需求与数据特征
先说选题。菜价这个方向在毕设里其实是被低估的,很多人觉得"爬个菜价太简单",但真正做进去会发现,它天然具备一套完整的数据挖掘演示场景:数据源公开、更新频繁、结构化程度中等偏下、带明显的时间属性和地域属性。这些特征决定了你可以在论文里名正言顺地讨论爬虫策略、数据清洗、增量更新和可视化设计,每一个环节都有东西可写。
而且菜价数据的实时性和民生关联度高,答辩时老师容易理解,不像是爬一些冷门技术站点,还得先解释业务背景。贵州菜价还有一个隐性优势:贵州是一个高原山区省份,蔬菜价格受季节、运输、产地影响波动明显,这给可视化分析提供了很多"能讲出故事"的角度——比如夏季叶菜价格为什么低、冬季反季节蔬菜为什么贵、贵阳和遵义的价格差异怎么来的。这些分析点写进论文,工作量显得扎实,不是硬凑字数。
1.2 系统整体架构:数据采集、存储、展示三层如何衔接
整个系统的架构我用的是最经典的三层拆分,但没有盲目堆技术。采集层用Python的requests加BeautifulSoup,存储层用SQLite起步、后期可切MySQL,展示层是Django模板加ECharts。选择这套组合的理由很简单:每一层都有成熟的社区方案,出了问题容易搜到答案,而且每一层的代码量都能在论文里作为独立章节呈现。
架构上的关键决策是我把爬虫模块单独拆成一个独立的Python包,而不是塞在Django的视图函数里。 这样做的原因有两个:第一,爬虫需要频繁调试,独立的包可以在命令行直接运行,不用每次启动Django服务;第二,Django的ORM模型需要在爬虫里使用,独立包内部依然可以通过django.setup()加载项目配置,互不干扰。很多毕设翻车都是因为把爬虫逻辑写死在视图里,一旦网站改版,整个系统都要跟着改。
层与层之间的数据流转我也做了明确约定:爬虫只负责产出结构化字典列表,不直接写数据库;数据库操作统一放到一个services模块里;视图函数只做请求处理和模板渲染。这样每一层都能单独写单元测试,论文里"系统设计"这一章就有东西可写了,而不是放几个截图了事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 爬虫模块设计与数据清洗:从网页到结构化数据的过程
2.1 页面分析与数据源选择:先搞清楚网站结构再动手
写爬虫之前,第一步不是写代码,而是花一天时间把目标网站的结构摸清楚。我当时选了贵州本地的一个农产品信息平台,首页有每日菜价栏目,列表页展示了品种名称、市场名称、最低价、最高价、平均价和发布时间。用浏览器开发者工具看了一遍,确认数据是服务端渲染的纯HTML,不是AJAX动态加载,这意味着用requests就能拿到完整数据,不需要上Selenium这种重型方案。
这里有个经验:动手爬之前先在浏览器里测试一下不同参数组合下的URL变化。 列表页通常有分类和页码参数,比如?category=vegetable&page=2,搞清楚这些参数的规律,后面写循环抓取就顺理成章。另外要注意站点是否有robots协议——这是毕设论文里"合规性分析"章节的素材,也是体现你作为开发者基本素养的地方。我当时的做法是限定抓取频率(每次请求间隔3秒),并且只抓取公开的行情数据,绝不去触碰登录态、接口加密之类的东西。这不算技术难题,但论文里写上"遵守robots协议、控制请求频率、不抓取非公开数据"这几句话,答辩时印象分会不一样。
2.2 批量、增量、垂直三类爬虫在这个系统里的实际取舍
我的论文里专门有一节讨论爬虫类型的选择,正好对应你在需求调研时搜到的那个分类框架。这里我把三种类型的实际取舍展开说一下:
批量型爬虫就是"一口气抓完",适用于首次初始化数据。我首次运行时抓取了近一年的历史价格(列表页翻了几十页),目的是让数据库有足够的数据量,折线图才画得出来。增量型爬虫是后续日常运行的核心——每天只抓取当天新增的价格记录,避免重复数据,降低站点压力。垂直型爬虫的意义在于只聚焦"蔬菜价格"这一个垂直领域,不做全网爬取,页面解析规则简单明确。
三种类型不是相互替代的关系,而是互补:批量负责冷启动,增量负责热更新,垂直限定边界。实现时我用同一个Spider基类,通过传入不同的mode参数来控制是"从头抓"还是"仅增量"。核心逻辑是:抓取之前先查询数据库里该品种加市场加日期是否已存在,存在就跳过,不存在才插入。 这个去重逻辑看起来简单,但如果不做,定时任务跑一周之后数据膨胀两倍,折线图全被噪声吃掉。
2.3 数据清洗与入库:把"2.5元/斤"变成数值字段
原始页面里拿到的价格字段是文本格式,比如"2.50元/斤",还有的单位是"元/公斤"。这种文本不能直接入库,否则做不了数值计算和排序。清洗思路分三步:
第一步是单位统一。我用正则表达式把数字部分提取出来,再根据单位字段做倍数转换,统一转成"元/公斤"。正则写法很直接:re.search(r'([0-9]+\.?[0-9]*)', text)。第二步是异常值过滤。有些条目价格明显不合理,比如"0.00元/斤"或者"999.99元/斤",直接丢弃。第三步是品种名归一化。同一蔬菜在不同页面可能叫"白萝卜"和"萝卜(白)",我在清洗层维护了一个别名映射字典,把所有变体映射到统一名称。
入库之前,数据已经是一个干净的字典列表,格式大概是:
python复制{
"vegetable": "白萝卜",
"market": "贵阳农产品物流园",
"low_price": 1.8, # 元/公斤
"high_price": 2.6,
"avg_price": 2.2,
"price_date": "2025-01-15"
}
这个结构直接对应Django模型字段,bulk_create批量插入的效率也高。清洗这块我栽过跟头:一开始把清洗逻辑写在爬虫函数内部,后来发现页面结构调整时,改清洗逻辑就得连带爬虫一起改。后来单独抽了一个cleaners.py,每个品种的清洗规则做成独立的函数,测试起来方便多了。
3. Django后端:数据模型、定时采集与API接口
3.1 数据模型设计:三张核心表如何支撑全部功能
Django的ORM是我选它做后端的主要原因,模型写好了,数据库表结构、查询API、后台管理界面一次性全有了。我这套系统只用了三张核心表,但每张都物尽其用:
第一张是Vegetable(蔬菜品种表),字段包括名称、分类、单位。为什么单独建一张表而不是直接存字符串?因为品种与价格记录是一对多关系,单独建表后按品种聚合查询、以及做品种去重都会高效很多。第二张是Market(市场表),存市场名称和城市归属,贵州的菜价是按市场发布的,后续地图可视化需要城市维度的聚合数据。第三张是PriceRecord(价格记录表),这是核心事实表,外键关联品种和市场,存低价、高价、均价和日期。
关键设计点是给PriceRecord加了一个Meta类里的联合唯一约束:
python复制class PriceRecord(models.Model):
vegetable = models.ForeignKey(Vegetable, on_delete=models.CASCADE)
market = models.ForeignKey(Market, on_delete=models.CASCADE)
low_price = models.DecimalField(max_digits=6, decimal_places=2)
high_price = models.DecimalField(max_digits=6, decimal_places=2)
avg_price = models.DecimalField(max_digits=6, decimal_places=2)
price_date = models.DateField()
class Meta:
unique_together = ("vegetable", "market", "price_date")
这个联合唯一约束是增量爬虫去重的第二道保险——就算爬虫逻辑里忘了判断重复,数据库层面也会报错阻止插入,Django还会抛出IntegrityError。有了这道保险,数据质量就有保障了。
3.2 定时采集任务:用APScheduler替代Celery的务实选择
实时菜价系统不能只靠手动运行爬虫,得有个定时任务每天自动抓取。我知道业界大多数方案是Celery加Redis,但毕设场景我明确建议别上这个组合——配置复杂、需要额外启动worker进程、部署到服务器时还容易出环境问题。我用的是APScheduler的BackgroundScheduler,把它挂在一个独立的Django App里。
启动方式是在apps.py的ready()方法里调用调度器:
python复制from apscheduler.schedulers.background import BackgroundScheduler
from django_apscheduler.jobstores import DjangoJobStore
def start_scheduler():
scheduler = BackgroundScheduler()
scheduler.add_jobstore(DjangoJobStore(), "default")
scheduler.add_job(
run_spider,
trigger="cron",
hour=9,
minute=30,
id="daily_price_spider",
replace_existing=True,
)
scheduler.start()
注意用django_apscheduler这个库,它能把任务执行记录存到数据库里,后台可以看到每次定时任务跑没跑、成功还是失败。这对论文的"系统测试"章节很有用,直接截图定时执行日志,比嘴上说"定时任务正常运行"有力得多。
调度逻辑里还做了个异常兜底:如果某天的数据源网站崩溃或者网络异常,爬虫会抛出异常并且自动发送一条日志记录,调度器捕获异常后不中断,第二天继续正常执行。
3.3 视图与API:页面数据传递的两种方式
这一节对后续可视化特别关键。Django视图向模板传递数据有两种方式:服务端渲染和JSON接口。我两种都用了,分别对应不同的页面需求。
对于首页的简要概览(今日总品种数、最高价品种、最低价品种、平均价),我用服务端渲染,视图函数里查好数据直接渲染到模板的图表配置项里。对于需要用户交互的页面(比如选择日期范围看走势、切换品种做对比),我用JSON接口返回ECharts需要的数据格式。视图写法大致如下:
python复制def price_trend_api(request):
vegetable_id = request.GET.get("vegetable_id")
days = int(request.GET.get("days", 30))
end_date = timezone.now().date()
start_date = end_date - timedelta(days=days)
records = (
PriceRecord.objects
.filter(vegetable_id=vegetable_id, price_date__range=(start_date, end_date))
.order_by("price_date")
)
data = {
"dates": [r.price_date.strftime("%Y-%m-%d") for r in records],
"avg_prices": [float(r.avg_price) for r in records],
}
return JsonResponse(data)
这里有个小坑:DecimalField取出来的值是Decimal类型,直接塞进JSON会报TypeError,所以记得转成float。ECharts只认JavaScript的数值类型,模板端接到的数据必须是数字。
4. 可视化页面的具体实现:ECharts如何讲好菜价数据的故事
4.1 ECharts图表选型:折线图、柱状图、地图各自扮演什么角色
可视化部分我用的是ECharts,原因就俩字:成熟。配置项文档全、社区案例多、各种图表类型开箱即用,而且纯前端渲染,不需要后端额外引库。
首页大屏我放了三个图表,每个图表对应一个分析维度:
第一个是折线图,展示某个品种近30天的平均价格走势。这个图的任务是"讲故事"——价格是涨了还是跌了,波动大不大。折线图适合展示连续时间序列的趋势,一眼能看出价格变化的节奏。配置上我加了tooltip(悬浮提示框)和dataZoom(缩放组件),用户可以拉拽查看特定时间段,交互感直接提升一个档次。
第二个是柱状图,展示当日价格最高的10个品种。柱状图的任务是"排名对比",横向排列时品种名更容易看清。当时我还考虑过用条形图代替,但柱状图在竖直空间里展示10个品种更合适,而且可以通过label配置在柱子上直接显示数值,页面截图放到论文里也清楚。
第三个是贵州地图,分城市展示菜价水平。这个最有视觉冲击力,也是答辩时的加分项。ECharts地图需要先注册贵州省的地图GeoJSON数据,我用的贵州各市州的边界数据,爬虫采集数据里带了市场所在城市,聚合出各城市平均菜价后映射到map图表上。
4.2 模板与前端交互:Django模板语法如何和JS数据对接
这一块是我在实际编码中走过弯路的地方。一开始我把ECharts的数据直接写在模板的<script>标签里,用Django模板语法硬编码数据,结果页面一刷新数据变了,模板里的JS代码也要跟着重新渲染,调试起来非常痛苦。
后来我改成页面初始加载时用服务端渲染注入初始数据,用户交互时通过fetch请求JSON接口动态更新图表。模板里的核心片段大致是这样:
html复制<script>
const initialData = JSON.parse('{{ initial_data_json|safe }}');
// 后续用 initialData 初始化图表
</script>
视图函数里对应的处理是:
python复制def dashboard(request):
initial_data = get_dashboard_data()
context = {
"initial_data_json": json.dumps(initial_data, ensure_ascii=False),
}
return render(request, "dashboard.html", context)
为什么要用|safe过滤器?因为Django模板默认会转义HTML实体,JSON里的引号会被转成",直接放进JS里就报语法错误。用json.dumps序列化之后,配合ensure_ascii=False保证中文不乱码,再看看模板里的|safe放行,就能安全地把数据交给前端JS了。
4.3 交互细节:日期筛选、品种切换、价格排行的实现思路
可视化系统不能只是几个静态图表,得让用户能"玩起来"。我做了三个交互功能,每个都不复杂,但组合起来体验完全不一样。
日期筛选:页面顶部放一个日期范围选择器,用户选定起止日期后,页面内的所有图表都重新请求对应时间段的JSON接口。实现方式是给每个图表数据请求函数加一个参数,日期变化时重新调用chart.setOption()并传入新数据。这里有个细节:ECharts更新数据时,如果只想更新series数据,应该在setOption里加上notMerge: true参数,否则旧数据和数据会产生叠加残留。
品种切换:折线图旁边放一个下拉选择框,列出所有蔬菜品种。选中某个品种后,折线图重新请求该品种的价格走势。实现逻辑不复杂,但要注意筛选框的默认值要和服务端初始渲染的图表数据一致,否则用户进来看到图表显示的是白菜,下拉框却默认选中萝卜,体验就很割裂。
价格排行:柱状图旁边加一个排序方式切换(按均价升序或降序),点击按钮后重新请求接口,接口层加上order_by参数即可。这个功能虽然简单,但能让用户从"今天最贵的菜是哪些"和"最便宜的菜是哪些"两个维度看数据,也算为论文的"系统功能设计"增加了一个功能点。
5. 调试、部署与论文写作中最容易忽略的细节
5.1 中文编码与数据异常的排查:最常见的坑
这个坑我印象太深了,几乎每个做爬虫的人都会遇到。第一次运行爬虫,控制台输出的中文全是乱码,数据入库后页面显示的问号。排查过程分三层:
第一层是网页编码。目标站点的页面返回的可能是gbk编码,而requests默认会用apparent_encoding猜测,猜错就乱码。解决办法是显式指定:
python复制resp = requests.get(url, headers=headers)
resp.encoding = resp.apparent_encoding # 或者直接写死 'utf-8' / 'gbk'
第二层是Python文件的声明编码。Python 3默认文件编码是UTF-8,但如果你的.py文件里包含中文字符,要确保编辑器保存时用的是UTF-8 with BOM或者Unicode。否则运行时报SyntaxError: Non-UTF-8 code starting with,这个错误在Windows上特别常见。
第三层是数据库和Django的编码配置。SQLite默认就支持UTF-8,一般不用改。但如果你切换了MySQL,就要确保建库时指定了utf8mb4字符集,否则中文索引会报错,而且Emoji符号(比如某些特殊字符)存不进去。
排查顺序建议:先看请求拿到的原始内容,再看清洗后的数据,最后看数据库里存的值。 用print调试时,遇到乱码先别急着改数据库,先确认原始数据就是对的,否则后面的处理全白搭。
5.2 Django部署到服务器:从虚拟环境到可访问站点
毕设演示时,如果你只是在自己电脑上跑python manage.py runserver,其实也够了。但如果想让老师在实验室任何一台电脑上都能访问,就得部署到服务器上。我最初用一台云服务器(配置很低,1核2G)踩了不少坑,分享一下最小的可行路径。
第一步,把项目代码传到服务器。用git或scp都行,但上传前一定删掉db.sqlite3和migrations目录,到服务器上再重新执行python manage.py migrate,避免本地数据库和服务器之间产生冲突。这是我的血泪教训,本地sqlite里有一堆测试数据,传到服务器后和线上数据混在一起,后来清理花了好久。
第二步,创建虚拟环境并安装依赖。用python -m venv venv创建虚拟环境,然后激活它,再安装Django、requests、BeautifulSoup、APScheduler这些依赖。注意Python版本,Django 4.x要求Python 3.10以上,如果你的服务器自带的是Python 3.8,得先升级。安装完依赖后,别忘了执行python manage.py collectstatic,Django的静态文件需要统一收集到STATIC_ROOT目录,否则CSS和JS都加载不出来。
第三步,用反向代理把Django跑起来。生产环境不能直接裸跑runserver,我用的是Gunicorn配合Nginx。Gunicorn启动命令大概是这样:
bash复制gunicorn config.wsgi:application --bind 0.0.0.0:8000
Nginx里配一个location /转发到8000端口,location /static/指向collectstatic收集好的目录。如果不会配Nginx,也可以用宝塔面板,图形界面里把Python项目配置好,Nginx配置是自动生成的,适合毕设演示这个级别。
第四步,别忽略ALLOWED_HOSTS。部署后浏览器访问会报DisallowedHost错误,原因就是Django默认只允许localhost访问。在settings.py里设置:
python复制ALLOWED_HOSTS = ["*"] # 毕设演示用,生产环境建议写具体域名或IP
5.3 论文结构与答辩准备:工作量如何呈现
终于说到论文了。很多人的毕设系统做得不错,但论文写得像流水账,导致答辩时被问"你的创新点是什么"就很尴尬。我的经验是,论文结构可以参考这样安排:绪论(背景与意义)、相关技术介绍、需求分析、系统设计、系统实现、系统测试、总结与展望。其中有两处最能体现实力和工作量。
第一处是"需求分析"章节。别只写"用户需要一个菜价查询系统"这种废话,要画用例图、写功能需求和非功能需求,还要写明数据采集的合规性和时效性约束。我当时专门写了一节"爬虫策略需求分析",把批量、增量、垂直三种策略的使用场景和切换条件都写了进去。这一段直接决定了系统设计章节的内容有没有依据。
第二处是"系统测试"章节。一定要有测试用例表格:用例编号、功能描述、操作步骤、预期结果、实际结果。别只写"测试通过"四个字,要写清楚每一步。爬虫模块我写了"增量爬取测试"和"重复数据去重测试",定时任务写了"调度执行测试"和"异常恢复测试"。这些测试脚本和日志截图放到论文里,工作量的视觉效果就出来了。
答辩时常被问到的问题,提前准备:
- 这个系统相比普通菜价网站有什么优势?(答:自动采集、结构化存储、多维度可视化、定时更新)
- 爬虫被封IP了怎么办?(答:控制请求频率、设置User-Agent池、使用代理池——说明实现思路即可,不需要真的写完整代理池)
- 数据准确性怎么保证?(答:联合唯一约束防重复、异常值过滤、单位统一)
最后提醒一点:不要在答辩时说自己"爬了某个电商平台的数据",一是违法违规,二是可能超出毕设选题范围。我的系统定位是"公开的农产品信息平台数据聚合展示",这个定位在合规性上站得住脚,论文里也能自圆其说。
做这个系统最大的体会是,毕设不是"技术选型越高级越好",而是"每一层都有明确的数据流和清晰的验证方式"。爬虫拿不到数据,后续全是空中楼阁;数据进了库但可视化展示不出来,前面的功夫白费。从选型到实现坚持了"每一步都能跑通、每一个结果都能验证"这个原则,整个过程虽然有一些波折,但最后系统上线那一刻,看到叶菜价格曲线、品种排行和地图上的颜色变化都在正常更新,那种满足感还是很值得的。
如果你也正在做类似的爬虫可视化毕设,我建议先花三天时间把数据源、数据库表结构和页面交互流程确定下来,然后再写代码。定好边界之后,后面每一步都是在填已规划好的坑,而不是边写边找方向。另外,把爬虫的代码写优雅一点、注释写清楚一点,这些在论文里都是可以展示的"工程素养"。
