尧图精选

基于Python的美食菜谱数据分析可视化系统设计与实现

🕒 发布时间:2026/9/7 14:30:56 📁 来源:尧图网络
每年到了毕业设计开题季都会有一批同学在选什么题上反复纠结。想选带爬虫的怕被说没技术含量想选机器学习的又担心数据量不够、模型跑不出效果想选可视化又觉得单纯画图表太单薄。其实这类需求完全可以合并成一个漂亮的闭环系统——基于Python的美食菜谱数据分析可视化系统正好把爬虫、Django后端、机器学习、数据分析和可视化全部串起来。这个题目的好处在于菜谱数据获取门槛低、字段天然丰富菜名、食材、分类、口味、评分、收藏数既能体现数据处理能力又能展示Web开发和算法应用能力是一个一题串联四大技术栈的典型选题。下面我会按实际开发顺序把整个系统的技术选型、关键代码、建模思路和答辩避坑经验完整拆一遍希望能给正在做类似毕设的同学一些可直接落地的参考。1. 为什么菜谱数据适合做毕设系统价值与技术路线设计1.1 菜谱数据的天生优势与选题逻辑我见过很多同学做某平台数据分析系统最后卡死在一个问题上数据拿不到。要么平台反爬太严要么登录才能看要么返回的数据是乱码。而菜谱类网站的数据开放程度相对高得多绝大多数菜谱详情页都是静态渲染的HTML菜名、食材清单、做法步骤、口味标签、用户评分、收藏量这些关键字段都直接写在页面源码里。这意味着爬虫环节不会耗费你大量时间去对抗反爬可以把主要精力放到后续的数据处理和系统搭建上。另外菜谱数据本身具备极强的分析叙事性。你可以按菜系分析分布规律按食材分析热门搭配按评分和收藏量分析用户偏好甚至可以结合地域分析口味差异。这些分析结果天然适合用图表展示评委看到的不再是一个光秃秃的表格而是一套完整的数据故事。对于本科毕业设计或课程设计来说这种数据能讲出东西来的题目比硬凑一个冷门数据集要容易发挥得多。1.2 系统整体架构与模块分工在动手写代码之前一定要先把系统边界画清楚。我建议把整个系统拆成四个核心模块。第一个是数据采集层负责从目标菜谱网站抓取基础数据包括菜谱名称、分类、所需食材、步骤、口味标签、评分和收藏量。采集到的原始数据通过pandas做缺失值处理、去重、字段格式规范化后统一写入MySQL数据库。第二个是后端服务层采用Django框架实现。Django的价值在于自带ORM、Admin后台和模板渲染引擎。ORM帮我们省去了大量原生SQL的编写工作Admin后台可以直接可视化查看和管理菜谱数据模板引擎则方便后续渲染可视化页面。第三个是算法分析层这是整个系统的亮点。我会用机器学习方法做两类任务一是基于菜谱食材和菜名的文本特征做菜系分类比如区分川菜、粤菜、湘菜二是基于食材相似度或用户协同过滤做菜品推荐。训练好的模型保存成文件由Django实时调用。第四个是可视化层使用ECharts在前端展示分析结果。ECharts通过Ajax从Django接口获取JSON数据渲染成交互式图表。核心流程爬虫采集 → pandas清洗 → MySQL存储 → Django接口 → 机器学习建模 → ECharts可视化这套架构的好处是每一层都职责单一层与层之间通过接口或文件解耦。哪怕某个环节出了问题也不会导致整个系统崩溃。1.3 关键技术选型与版本组合建议技术选型是毕业设计开题报告里被问得最多的问题。我建议采用以下组合全部经过实际验证彼此之间兼容性良好技术组件推荐方案说明语言环境Python 3.9/3.10过新的Python版本可能部分依赖不支持3.10兼容性最稳Web框架Django 4.2 LTS官方长期支持版本配套资料多爬虫requests BeautifulSoup4轻量灵活适合静态HTML页面数据清洗pandas numpy表格化处理数据效率极高数据库MySQL 8.0 pymysql并发和数据量支撑能力比SQLite好机器学习scikit-learn 1.2分类、推荐、评估一站式完成可视化ECharts 5.0 WordCloud交互图表 词云视觉效果丰富前端辅助Bootstrap 5 jQuery快速搭界面ECharts依赖jQuery方便操作DOM这套组合全是用pip就能完成安装的开源组件部署时也不需要额外购买商业服务适合学校实验室或学生个人电脑环境。2. 爬虫模块实战从菜谱网站抓取到结构化数据集2.1 数据源分析与爬取字段设计菜谱类网站有很多豆瓣美食、心食谱、美食杰、下厨房、豆果美食都是常见选择。以下厨房为例它有两个特点一是菜谱列表页是常规的分页URL结构二是详情页里的食材和步骤都以非常规整的DOM节点排列。另一个备选是美食杰它的菜谱分类体系川菜、粤菜、湘菜等比下厨房更清晰采集后做菜系分类时会省掉很多标注工作。我建议在爬虫开发前先花半天时间手工分析目标网站。打开一个详情页按F12进入开发者工具找到菜名、食材、步骤对应的HTML标签结构。这个工作远比直接写代码重要——我见过太多同学爬虫写了一半发现字段定位不稳定最后全部重来。比如食材字段有些网站在li标签里放食材名和用量有些网站则是放在span标签里如果第一版解析逻辑就写错后面数据质量会全线崩盘。设计字段时我给每个菜谱保留了以下信息字段名示例是否需要recipe_name鱼香肉丝必须category川菜必须ingredients猪里脊、木耳、胡萝卜...必须steps1. 切丝 2. 调汁...必须taste微辣、酸甜可选rating8.7可选favorite_count2300可选cover_urlhttp://...可选评分和收藏量这两列不要小看它们可以在后续做热门菜谱分析和评分预测时发挥大作用。2.2 基于requests和BeautifulSoup的爬虫实现我用的方案是requests加BeautifulSoup整个逻辑并不复杂先请求列表页解析出所有菜谱详情页的链接再逐个请求详情页并提取字段。为了控制爬虫速度和避免给目标服务器造成压力我会设置随机延时和请求失败重试。import time import random import requests from bs4 import BeautifulSoup import pandas as pd HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } def get_html(url, retry3): for attempt in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 if resp.status_code 200: return resp.text except Exception: pass time.sleep(random.uniform(1, 3)) return None def parse_list_page(html): 从列表页解析出详情页链接 soup BeautifulSoup(html, html.parser) links [] for a_tag in soup.select(a[href*/recipe/]): href a_tag.get(href) if href and href not in links: links.append(href) return links def parse_detail_page(html): 从详情页提取菜谱核心字段 soup BeautifulSoup(html, html.parser) data {} name_tag soup.select_one(h1) data[recipe_name] name_tag.text.strip() if name_tag else None # 食材解析视网站结构调整选择器 ingredient_tag soup.select_one(.ingredients) if ingredient_tag: items ingredient_tag.select(li) data[ingredients] 、.join([item.text.strip() for item in items]) else: data[ingredients] None # 步骤解析 step_tag soup.select_one(.steps) if step_tag: steps step_tag.select(p) data[steps] .join([step.text.strip() for step in steps]) else: data[steps] None return data这里补充两个非常容易踩的坑。第一resp.encoding utf-8必须显式设置因为某些服务器返回的响应头里没有charset字段requests会根据HTTP头猜测编码猜错就会导致中文乱码。第二BeautifulSoup的CSS选择器要写得有弹性最好通过select_one配合class名获取不要直接用下标索引因为网站改版时最常变的往往就是class名。2.3 爬虫调度、去重和异常数据治理单页爬虫写好后要让系统真正能工作还需要考虑调度效率和数据治理问题。我用了广度优先的思路先从列表页第一页开始解析链接再逐页翻动同时用一个visited集合存放已经处理过的URL防止重复请求。seen_links set() all_recipes [] for page in range(1, 30): list_url fhttps://example-caipu.com/list?page{page} list_html get_html(list_url) if not list_html: continue links parse_list_page(list_html) for link in links: if link in seen_links: continue seen_links.add(link) detail_html get_html(link) if detail_html: item parse_detail_page(detail_html) item[url] link all_recipes.append(item) time.sleep(random.uniform(2, 4))爬完一批数据后一定要用pandas做一次全面清洗。清洗动作包括删除recipe_name或ingredients为空的行将评分列转为浮点数缺失值用该分类均值填充将收藏量或浏览量统一换算成整数对重复菜谱名去重并保留评分高的那条记录。import pandas as pd df pd.DataFrame(all_recipes) df.dropna(subset[recipe_name, ingredients], inplaceTrue) df.drop_duplicates(subset[recipe_name], keepfirst, inplaceTrue) df[favorite_count] pd.to_numeric(df[favorite_count], errorscoerce).fillna(0) df[rating] pd.to_numeric(df[rating], errorscoerce).fillna(0) df.to_csv(recipes_raw.csv, indexFalse, encodingutf-8-sig)编码上有个小细节utf-8-sig会在文件开头写入BOM标记Excel直接用这个CSV时中文不会乱码但如果你要导入MySQL建议改用普通utf-8。导出CSV只是为了方便你在开发阶段看数据真正入库还是通过Django的ORM写入。3. Django后端开发数据建模、ORM查询与接口设计3.1 Django项目初始化与App规划数据清洗完成后就进入系统开发的后端环节。我习惯先创建虚拟环境再装Django避免污染全局Python环境。python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install django4.2 pymysql pandas scikit-learn numpy django-admin startproject food_project cd food_project python manage.py startapp recipeApp命名建议叫recipe或analysis含义清晰。然后在settings.py里注册App并配置MySQL数据库连接。MySQL中要提前建好同名数据库字符集选utf8mb4这样才能完整存下中文和特殊字符。pymysql需要做兼容处理在项目__init__.py中写上import pymysql pymysql.install_as_MySQLdb()3.2 菜谱数据模型设计Django的ORM是开发效率的一大保障。根据前面设计的字段我建了四个核心模型菜系分类表、菜谱主表、食材表和步骤表。菜谱主表与菜系表是多对一关系菜谱与食材是多对多关系。from django.db import models class Category(models.Model): name models.CharField(max_length50, uniqueTrue, verbose_name菜系) class Meta: db_table category class Recipe(models.Model): name models.CharField(max_length200, verbose_name菜谱名称) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue, blankTrue) ingredients models.TextField(verbose_name食材) steps models.TextField(verbose_name做法) taste models.CharField(max_length100, blankTrue, verbose_name口味) rating models.FloatField(default0, verbose_name评分) favorite_count models.IntegerField(default0, verbose_name收藏数) cover_url models.URLField(blankTrue, verbose_name封面图) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table recipe ordering [-favorite_count]这里有一个我踩过坑后总结的经验字段类型尽量精准。ingredients和steps虽然看似可以共用TextField但后续做文本分析时如果你把食材和步骤混在一起特征工程的复杂度会直线上升。我在模型中刻意把它们分成两个字段就是为了机器学习阶段能分别提取特征。3.3 数据导入、ORM查询技巧与API接口模型建好后用一条命令生成并执行迁移然后写一个Django管理命令或脚本把CSV数据批量导入数据库。导入时逐行用ORM的get_or_create写入虽然比原生SQL慢一些但能自动处理重复数据。from recipe.models import Recipe, Category def import_csv(file_path): with open(file_path, encodingutf-8) as f: import csv reader csv.DictReader(f) for row in reader: category, _ Category.objects.get_or_create(namerow[category]) recipe, created Recipe.objects.get_or_create( namerow[recipe_name], defaults{ category: category, ingredients: row[ingredients], steps: row[steps], rating: float(row[rating]), favorite_count: int(row[favorite_count]), }, )批量导入几千条数据也就几十秒足够用了。真正的考试点在于如何把数据通过接口给前端图表用。我倾向于先用Django的JsonResponse返回JSON毕竟ECharts要的就是JSON格式。from django.http import JsonResponse from django.db.models import Count, Avg from recipe.models import Recipe, Category def category_distribution(request): 各菜系菜谱数量占比 data list( Category.objects.annotate(countCount(recipe)) .values(name, count) ) return JsonResponse({data: data}, safeFalse) def top_hot_recipes(request): 收藏量前10的热门菜谱 data list( Recipe.objects.order_by(-favorite_count)[:10] .values(name, favorite_count, rating) ) return JsonResponse({data: data}, safeFalse)如果你的毕设题目要求接口更规范也可以顺手引入Django REST Framework。但就这个题目而言轻量接口已经足够引入重框架反而增加了答辩时被追问的风险。4. 机器学习环节菜系分类、热门预测与推荐系统4.1 菜系分类模型从食材文本预测菜系机器学习是整个系统的加分项也是很多同学最心虚的部分。其实菜谱数据非常适合做传统机器学习任务不需要深度学习也能得到不错的效果。我选择的第一个任务是菜系分类输入菜谱的食材和菜名预测它属于哪个菜系。首先从数据库中读出数据用pandas构造训练集。这里核心是特征工程把每道菜的食材文本和菜名合并成一列然后用TF-IDF向量化。TF-IDF能自动判断哪些词对区分菜系更有价值比如花椒这个词在川菜中出现的频率显著高于其他菜系它就会被赋予更高权重。import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report df pd.DataFrame(list(Recipe.objects.all().values(name, ingredients, category))) df[text] df[name] df[ingredients] df df.dropna(subset[category]) vectorizer TfidfVectorizer(max_features5000, token_patternr\b[\w\u4e00-\u9fa5]\b) X vectorizer.fit_transform(df[text]) y df[category] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model MultinomialNB() model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred))多分类效果取决于数据质量和数据量。如果每个菜系只有三四十道菜朴素贝叶斯的准确率只能到50%左右不用慌这是正常的。想在毕设里拿到更好看的指标可以从两个方向改进一是增加每个菜系的数据量让样本均衡分布二是换成逻辑回归或线性SVM这两种模型在短文本分类上往往比朴素贝叶斯更稳。4.2 模型持久化与Django调用模型训练好之后不能每次请求都重新训练一遍。正确做法是把训练好的模型和向量化器保存到磁盘Django启动时加载一次然后在视图函数中调用。import joblib joblib.dump(model, model/category_model.pkl) joblib.dump(vectorizer, model/tfidf_vectorizer.pkl)在Django中新建ml_service.py负责统一加载模型并对外提供预测接口。import joblib from django.conf import settings _model None _vect None def get_model(): global _model, _vect if _model is None: _model joblib.load(str(settings.BASE_DIR / model / category_model.pkl)) _vect joblib.load(str(settings.BASE_DIR / model / tfidf_vectorizer.pkl)) return _model, _vect def predict_category(name, ingredients): model, vect get_model() text [f{name} {ingredients}] x vect.transform(text) return model.predict(x)[0]这种懒加载写法能显著提升Django开发服务器的启动速度。如果你在文件顶部直接加载模型每次runserver重启都要等好几秒开发体验很差。4.3 基于食材重合度的菜品推荐另一个适合毕设展示的机器学期模块是推荐功能。我采用的是基于食材特征的相似度推荐用户输入我想吃鸡肉系统自动查找食材中包含鸡肉的菜谱并按食材重合度从高到低推荐。def recommend_by_ingredient(input_ingredients, top_n10): recipes Recipe.objects.exclude(ingredients__isnullTrue) vectorizer get_ingredient_vectorizer() input_vec vectorizer.transform([ .join(input_ingredients)]) scores [] for recipe in recipes: # 实际实现时可预先将食材列向量化并存入内存 recipe_vec vectorizer.transform([recipe.ingredients]) sim (input_vec * recipe_vec.T).toarray()[0][0] scores.append((recipe, sim)) scores.sort(keylambda x: x[1], reverseTrue) return [r for r, s in scores[:top_n]]这里要注意逐条计算向量相似度的代码写法在数据量上到几千条时性能就开始吃紧。优化方案有两个一是用TF-IDF一次性把所有菜谱食材向量化变成一个大的稀疏矩阵然后计算输入向量与大矩阵的余弦相似度这样的话运算效率非常高二是把向量化结果缓存到Redis里避免每次请求都重新计算。毕设数据量在万级以内方案一就够了。5. 可视化大屏设计ECharts让数据自己说话5.1 图表选型与页面整体规划可视化设计是整个系统的门面评委第一眼看的是页面效果而不是底层代码。我平时做这类系统时会先画出页面原型再决定每个区域放什么图表。推荐布局是顶部放四个KPI卡片包括菜谱总数、菜系数量、平均评分、总收藏量中间区域放菜系分布柱状图和热门菜谱Top10条形图右侧放食材词云和口味分布饼图底部放评分分布直方图和菜谱发布趋势折线图。图表类型分析内容核心数据字段KPI卡片整体数据概览count, avg柱状图菜系菜谱数量对比category name, count条形图收藏量Top10菜谱recipe name, favorite_count词云热门食材分布ingredients饼图口味占比taste直方图评分分布rating5.2 ECharts与Django模板的集成方式ECharts的接入方式非常简单Django模板通过static标签加载ECharts的JS文件页面用Ajax请求接口拿到JSON数据再注入图表配置。核心代码在Django模板中的写法大概如下。!DOCTYPE html html langzh head meta charsetUTF-8 title美食菜谱数据分析/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script srchttps://code.jquery.com/jquery-3.7.1.min.js/script /head body div idcategoryChart stylewidth: 600px; height: 400px;/div script $.ajax({ url: /api/category-distribution/, method: GET, dataType: json, success: function(res) { var chart echarts.init(document.getElementById(categoryChart)); var names res.data.map(item item.name); var counts res.data.map(item item.count); chart.setOption({ tooltip: {}, xAxis: { type: category, data: names }, yAxis: { type: value }, series: [{ type: bar, data: counts }] }); } }); /script /body /html这里有三个经验值得记录。第一ECharts官方CDN在国内访问速度可能不稳定建议下载到本地static目录引用避免答辩现场网络波动导致图表白屏。第二图表容器必须显式设置宽度和高度否则ECharts初始化时会报错。第三Ajax返回的JSON字段名要和前端保持一致一个最简单的办法是后端用.values(name, count)前端就用item.name和item.count。5.3 词云与交互图表的进阶优化词云能在一屏内直观展示高频食材视觉冲击力强。ECharts官方不直接提供词云组件需要额外引入echarts-wordcloud插件。词云的数据处理要用到分词工具把每道菜的食材字段拼接起来用jieba分词后统计词频只保留名词和常见食材词过滤掉适量少许这类描述性词汇。import jieba from collections import Counter from recipe.models import Recipe all_text .join(Recipe.objects.values_list(ingredients, flatTrue)) words jieba.lcut(all_text) stopwords {适量, 少许, 克, 勺, 个, 做法, 步骤} words [w for w in words if w not in stopwords and len(w) 1] word_counter Counter(words).most_common(50)词云接口返回时要注意ECharts的echarts-wordcloud插件要求数据格式是[{name: 花椒, value: 120}, ...]字段名不能写错否则词不显示。配色和交互方面我建议选一套低饱和度的暖色系主题比如橙黄色系与美食主题比较搭。大屏页面可以加上轮播效果每五秒自动切换某个图表的提示框这样在答辩演示时画面不会一直静止观感更好。6. 答辩前必做模型调优、系统性能与演示环境准备6.1 数据质量与模型精度的隐蔽问题很多同学在训练模型时都会遇到准确率不理想的情况但很少有人意识到首先该排查的是数据质量而不是换算法。我在这类项目上踩过一个大坑导入数据库时菜系字段里混入了家常菜私房菜这类不是按地域划分的标签导致多分类模型出现大量混淆。解决办法是合并或剔除低类别样本比如只保留样本量大于50的菜系。样本不均衡同样会直接影响模型输出。如果川菜有2000道而某小众菜系只有50道朴素贝叶斯会严重偏向川菜。建议用train_test_split时传入stratifyy做分层采样这样训练集和测试集中的类别比例保持一致分类报告里的小类别的F1值也有参考意义。另一个隐蔽问题是中文文本向量化的正则表达式。sklearn默认的token_pattern只匹配字母和数字中文会被当成空白分隔符处理因此分类效果极差。我在前面的代码里特意写了token_patternr\b[\w\u4e00-\u9fa5]\b这一行就是专门用来兼容中文文本的。6.2 系统性能优化与多线程爬虫毕设答辩时系统卡顿是最尴尬的场景之一。为了确保演示流畅要在开发阶段就做好三件小事。数据库层给recipe表的name和category_id字段加索引这样Top10查询和分类统计会明显加快视图层对不频繁变化的统计接口添加Django缓存比如用cache_page装饰器缓存5分钟异步任务层大规模爬虫可以使用concurrent.futures.ThreadPoolExecutor并发抓取把爬取效率提升三到五倍。from concurrent.futures import ThreadPoolExecutor, as_completed def batch_crawl(urls): all_data [] with ThreadPoolExecutor(max_workers5) as executor: future_map {executor.submit(fetch_and_parse, url): url for url in urls} for future in as_completed(future_map): try: result future.result() if result: all_data.append(result) except Exception as e: print(抓取失败, future_map[future], e) return all_data多线程爬虫要注意控制并发数5个线程对大多数菜谱网站来说已经足够线程太多容易被封IP。如果想做得更严谨可以在中间件层配置随机User-Agent池每次请求从池子里随机取一个减少被识别为爬虫的概率。6.3 演示环境与答辩节奏的经验最后聊一个很多人忽略的环节——答辩现场演示。技术做得再好演示环境出了问题印象分会大打折扣。我的建议是准备一台独立的演示笔记本提前装好Python环境、MySQL服务和所有依赖项数据预先导入好模型文件放到正确路径。演示时用python manage.py runserver启动Django本地服务浏览器打开几个事先固定好的页面按顺序讲解爬虫抓取的数据、分析图表和模型预测效果。演示脚本控制在7到10分钟比较合适节奏大致是前两分钟讲选题背景和系统框架中间四分钟演示页面功能和图表最后三分钟展示机器学习模块比如现场输入一个菜名让模型预测菜系。这里有个小技巧预测接口的模型加载需要一点时间第一次点击时容易卡顿我习惯在项目启动后用python manage.py shell预先执行一次预测函数把模型提前加载进内存演示时就非常流畅。根据个人经验还能给这个小系统加一个导出功能把分析结果一键导出成PDF报告很多学校会要求提交设计报告这个功能投评委所好答辩时经常受到表扬。如果后续想继续扩展可以接入更多数据源做不同平台的菜谱对比或者把推荐模块升级成基于用户行为数据的协同过滤算法让系统从展示型进化成可用型。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →