基于DeepSeek的美食数据分析系统:从爬虫到可视化与智能推荐全流程

发布时间:2026/10/14 4:47:20
基于DeepSeek的美食数据分析系统:从爬虫到可视化与智能推荐全流程 做这套基于DeepSeek的美食数据分析系统纯粹是因为我自己想吃明白一个城市里到底什么值得吃。项目脱胎于某次逛携程系美食平台时的想法——上面积累了海量的餐厅评分、人均消费、用户评价、菜系标签这些数据零散看没什么感觉但聚合起来再配上大模型解读就是一个能回答“这家人均150的川菜到底值不值”的智能美食助手。整套系统用Python爬虫做数据采集Django做Web框架pandas做清洗聚合ECharts做可视化图表最后把DeepSeek大模型接入让AI自动读评论、做情感分析、输出推荐理由。这不是什么惊天动地的创新但技术链路非常完整从数据采集到数据展示再到自然语言交互每一步都有可以落地的细节。如果你正准备做数据采集、数据分析、Web可视化或者LLM应用相关的项目不管是课程设计、个人作品集还是内部工具这套流程可以直接抄作业。下面我把整体设计、代码实现、踩过的坑一次写清楚。1. 项目整体拆解美食数据系统到底要做成什么样1.1 核心需求与功能清单很多人一开始做数据分析项目容易犯一个毛病爬了一堆数据画了几张图然后呢没有场景没有交互做完自己都不知道给谁看。所以我在动手之前先列了一张需求表明确这套系统必须回答的问题某座城市里哪些餐厅值得去依据是什么不同菜系的人均消费区间长什么样高分餐厅集中在哪些商圈用户的真实评论里都在夸什么、骂什么如果我预算150元以内想吃火锅系统能推荐什么围绕这些问题系统的功能就清晰了数据采集模块抓取餐厅的基础信息店名、菜系、评分、人均、地址和用户评论。数据清洗模块去重、空值处理、价格区间归一化。可视化模块价格分布、菜系占比、评分分布、商圈热度、评论词云。大模型模块接入DeepSeek实现评论情感分析、智能推荐、数据结论总结。这套功能模块并不是拍脑袋定的每一条都对应实际使用场景。比如词云图解决的是“这家店到底哪方面口碑好”的问题智能推荐解决的是“帮我找店”的问题。架构上数据从底向上流动每一层都有明确职责后期加新数据源也方便。1.2 技术栈选型为什么是Django Python DeepSeek技术选型上我做过几次试验最终定下来这套组合下面说说理由。首先是后端框架。很多人觉得做数据分析展示用Flask就够了轻量、上手快。但一旦系统需要用户管理、后台管理、数据库ORM、表单校验这些能力Flask还是要自己拼装一堆扩展。Django自带的Admin后台在调试数据爬虫时非常有用——我可以直接在后台看爬下来的数据长什么样、有没有奇怪的脏记录。而且Django的ORM对复杂查询支持得不错后面前端需要的聚合统计接口写起来很顺手。数据库我选了MySQL理由很简单数据量不算特别大但关系查询多评论表、餐厅表、用户表之间有关联MySQL稳定且生态成熟。如果只是演示SQLite也行但生产环境跑起来会慢一些所以我直接上了MySQL。大模型接入选择DeepSeek主要是两个原因。第一是API调用方式和OpenAI兼容用Python的SDK几行代码就能接入没有太高的学习成本。第二是它的中文理解能力在美食评论这种口语化、甚至掺杂方言和网络用语的文本上表现不错情感分析的效果实测比通用规则好很多。pip install django pandas requests beautifulsoup4 pyecharts openai sqlalchemy上面这条命令装齐了后端、爬虫、分析、图表和大模型SDK所有依赖。pyecharts是我用来生成图表的库它可以在Python端直接生成HTML和JavaScript这样省去了前后端联调的复杂度。2. 数据采集层一套能落地的携程美食爬虫2.1 目标页面的数据构成分析爬虫最忌讳上来就写代码正确做法是先研究页面结构。我打开携程系美食平台的列表页后发现数据存在两种形态一种是页面HTML里直接渲染的餐厅卡片另一种是页面底部的JSON接口里返回的结构化数据。优先解析JSON接口理由很简单数据字段完整、格式规整、不用花大量精力写HTML解析逻辑。一般在浏览器的开发者工具里切到“网络”标签筛选XHR请求就能找到返回餐厅列表的接口。接口返回的字段大致是这样的{ data: [ { poiId: 12345678, name: 某川菜馆, score: 4.7, avgPrice: 128, category: 川菜, address: 某区某路, commentCount: 892, comments: [ {content: 麻辣鲜香就是排队太久, score: 5} ] } ] }有了这个结构再去定义Django的模型就非常轻松了。餐厅表的主键直接用平台的poiId设为唯一键评论单独拆一张表通过外卖名关联这样一对多的关系建模合理后续聚合查询效率也高。2.2 请求策略与反爬规避公开数据可以采集但不代表可以不讲规矩地暴力拉取。我在写爬虫的时候做了一套请求策略这套策略帮我少踩了很多坑。首先是User-Agent伪装。不要用缺省的Python-requests头容易被识别。我的做法是准备一个UA池每次请求随机取一个模拟不同浏览器的访问特征import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Safari/537.36 Edg/119.0.0.0 ] def get_random_ua(): return random.choice(USER_AGENTS)其次请求频率必须控制。我的策略是每爬完一页就随机休眠1到3秒同时监测HTTP状态码。一旦连续出现403或者428立刻停止并等待一段时间。这里有一个很重要的原则不要让服务器误以为你在攻击它。控制频率既是为了保障数据源稳定也是让爬虫活得够久能真正把任务跑完。如果遇到验证码拦截我的建议是不要硬刚。换代理池是一方面另一方面是降低并发数把单次请求的并发从5降到1实测可以明显减少验证码弹出概率。说到底做数据采集的初心是获取数据不是和对方安全团队较劲。2.3 清洗与入库脏数据怎么处理爬下来的数据看似规整实际处理时问题一大堆。最常见的几类餐厅名带有HTML转义符、人均消费字段是字符串“¥128/人”、有的评论为空、有的评分缺失。处理方案我统一在采集后做一层清洗管道所有数据经过清洗再进数据库import pandas as pd import re def clean_price(raw): # 从¥128/人中提取数字 match re.search(r(\d), str(raw)) return int(match.group(1)) if match else 0 def clean_data(df): df df.drop_duplicates(subsetpoi_id, keepfirst) df[avg_price] df[avg_price].apply(clean_price) df[score] pd.to_numeric(df[score], errorscoerce).fillna(0) df df[df[avg_price] 0] return df这里有几个细节需要注意。去重时保留第一批数据因为首次抓取的字段往往最完整价格解析用正则而不是直接类型转换避免报错导致任务中断评分为0的餐厅单独标记做排行榜时自动过滤否则0分餐厅会干扰均值统计。数据入库我用Django的ORM批量创建时用bulk_create一次写入几千条数据只需几秒比逐条insert快一个数量级。当然大批量写入之前要先在数据库里给poi_id建唯一索引否则重复写入会导致异常。3. 可视化层让数据长在页面上3.1 图表组件选型与前后端配合可视化方案我对比过三个方向纯前端ECharts、pyecharts生成HTML、以及后端组合JSON数据交给前端渲染。最终我采用了pyecharts Django模板的方案原因很直接——pyecharts可以在Python里直接生成图表HTML片段然后嵌入Django模板不需要单独维护前端工程。但这里有一个关键的教训不要在视图函数里现算现画。我的做法是提前在数据层把聚合结果算好缓存起来视图函数只负责把已经聚合好的字典数据传给模板。这样页面响应时间能控制在200毫秒以内而不至于每次刷新都触发全表扫描。下面是一个生成价格区间柱状图的例子from pyecharts.charts import Bar from pyecharts import options as opts def price_distribution_chart(): # 数据已经提前聚合bins [(0-50, 120), (50-100, 350), ...] bins get_price_bins_from_cache() bar ( Bar() .add_xaxis([x[0] for x in bins]) .add_yaxis(店铺数量, [x[1] for x in bins]) .set_global_opts( title_optsopts.TitleOpts(title人均消费区间分布), xaxis_optsopts.AxisOpts(name价格区间), yaxis_optsopts.AxisOpts(name店铺数量) ) ) return bar.render_embed()render_embed会返回一段包含基础图表配置的HTML片段直接放在模板里加上必要的JS依赖就能展示。也可以用render_embed加上自定义的js具体看你的模板里有没有全局引入ECharts的依赖。3.2 几个值得抄的指标计算口径做可视化最怕口径混乱。同样的数据平均值和中位数呈现出来的结论可能完全相反。我在项目里定了几个固定的统计口径这里也分享给你人均消费清洗后按中位数统计而不是平均值。因为个别高端餐厅会把均值拉高中位数更真实反映普通用户的消费水平。评分分布按0.5分为一档统计比如4.5到5.0、4.0到4.5这样条形图更平滑不会出现大量评分集中在最右侧的情况。菜系占比前10大菜系单独展示其余归入“其他”避免饼图出现一大片细碎的小扇区。商圈热度取每个商圈下餐厅的评价总数作为热度值评价数量比店铺数更能体现真实活跃度。词云图也做了但这里提醒一句词云图的展示效果很漂亮信息密度却并不高。如果做的是严肃的数据分析报告词云只能作为辅助不能替代结构化的频次统计。我通常会把词云结果和高频词Top20表格一起展示两者相互印证。4. AI大模型层接入DeepSeek的完整方案4.1 SDK调用与统一接口封装接入DeepSeek的第一步是创建一个客户端实例。这里有一点值得注意DeepSeek的API和OpenAI的SDK兼容所以直接安装openai库然后把base_url指向DeepSeek的接口地址即可。from openai import OpenAI client OpenAI( api_key你的API密钥, base_urlhttps://api.deepseek.com ) def chat_with_deepseek(system_prompt, user_prompt, temperature0.3): response client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt} ], temperaturetemperature, max_tokens1024 ) return response.choices[0].message.content调用大模型最忌讳在业务代码里到处写chat调用我封装了一个统一的接口函数所有类型的任务都走这个入口。系统提示词、用户提示词通过参数传入这样后续调整不同场景的提示词不用改调用处的逻辑。温度参数到底怎么设实测下来情感分析、数据总结这类任务我开0.3希望模型输出稳定、更贴合原始数据而智能推荐、文案生成这类任务会调到0.8让输出更有变化、更像人话。4.2 提示词模板评论情感分析的正确姿势大模型接入最容易翻车的就是提示词写得敷衍。一开始我试过直接把几十条评论一股脑扔给模型让它“分析情感倾向”结果返回的内容五花八门。后来我总结出一个好用的提示词模板三段结构第一段给角色第二段给任务约束第三段给输出格式。例如SYSTEM_PROMPT 你是一名深耕餐饮行业多年的数据分析师擅长从用户评价中提取有效信息。 你的任务是分析用户评论判断情感倾向并提取被提及的属性维度。 输出必须严格按照JSON格式不要有任何多余说明。 .strip() USER_PROMPT 请分析以下评论输出JSON包含 - sentiment: positive/neutral/negative - dimensions: 提及的维度数组从中选取口味、环境、服务、性价比、排队、食材、卫生 - summary: 一句话概括用户核心关注点 评论如下 {comments_text} .strip()为什么输出格式这么重要因为后续系统要考虑把情感分析结果入库。如果模型返回的是自由文本下游程序很难解析。强制JSON输出之后配合json.loads解析结果直接入库做统计比如可以统计出“某商圈负面评价中排队是最高频词”这就是有价值的数据结论了。另外提一下注释里的小技巧把comments_text切片处理。一次分析20条评论就够了切多了模型容易抓不住重点输出质量反而下降。我试过一次塞200条结果模型开始胡写像“整体氛围积极向上”这种空话都出来了。4.3 结合检索增强的美食问答如果只是让DeepSeek做情感分析那还不够惊艳。我的系统里加了一个问答功能用户在页面输入“南京东路附近人均100的江浙菜有哪些”系统先做数据库检索把符合条件的餐厅列表找出来再交给DeepSeek生成推荐理由。这里的核心思路是让模型基于真实数据回答而不是让它自己脑补。因为大模型有幻觉问题它可能编造出一个不存在的餐厅名字。所以流程必须是数据库精准检索 - 候选结果整理成文本 - 大模型基于候选文本生成回答。def recommend_restaurants(city, budget_max, cuisine): candidates Restaurant.objects.filter( citycity, avg_price__ltebudget_max, categorycuisine ).order_by(-score)[:5] context \n.join( f{r.name}评分{r.score}人均{r.avg_price}地址{r.address} for r in candidates ) prompt f 用户需求{cuisine}菜预算不超过{budget_max}元城市{city} 候选餐厅数据 {context} 请从中推荐1-2家最合适的餐厅说明推荐理由语言要通俗易懂。 return chat_with_deepseek(你是当地的美食向导。, prompt)这是最朴素的RAG应用——不需要搞复杂的向量数据库和向量检索因为数据本身是结构化的直接靠数据库查询就能完成召回。只有当数据变成非结构化文本时才需要引入向量检索。5. 系统联调中的工程细节5.1 Django视图、路由与异步请求数据有了、图表有了、AI接口也有了接下来就是把这些模块串起来。我画了一个清晰的调用链路浏览器发起请求 - Django路由 - 视图函数 - 数据层查询 - 返回JSON或渲染模板 - 前端展示。Django路由配置比较简单但有几个设计上的细节值得留意。所有数据接口我统一用/api/前缀方便后续做权限控制页面路由用普通名称和数据API分开。这样即使后端接口变了页面路由也不用动。# urls.py urlpatterns [ path(, views.dashboard, namedashboard), path(api/restaurants/, views.restaurant_list, namerestaurant_list), path(api/stats/, views.stats, namestats), path(api/recommend/, views.recommend, namerecommend), ]视图函数里有一个细节涉及AI大模型的接口千万不要做成同步阻塞。DeepSeek接口的响应时间通常是2到4秒如果前端同步等着用户体验非常差。我的做法是先把请求接收下来返回一个“处理中”的占位状态然后用异步任务或者简单的轮询方式来获取结果。Demo阶段最简单的方案是把AI请求放在独立线程里跑状态存数据库前端每隔1秒查一下结果。代码不多但体验截然不同。5.2 部署与性能优化的取舍部署方面我本地开发用的是Django自带的runserver但正式跑起来会换成Gunicorn。一个比较稳妥的组合是Nginx做静态文件服务和反向代理Gunicorn跑Django应用MySQL存数据Redis做缓存。性能优化我做了三个层面的事情第一个层面是数据库层。给餐厅表的city、category、avg_price加上联合索引涉及筛选排序的查询性能提升非常明显。评论表按餐厅查询给poi_id加索引即可。第二个层面是缓存层。图表的聚合结果放在Redis里设置十分钟过期。因为爬虫数据不是实时更新图表数据完全没必要每次请求都重新聚合。实测加上缓存后dashboard接口响应时间从800毫秒降到了150毫秒。第三个层面是数据层。列表页默认只返回前20条配合分页组件让用户自己翻页而不是一次性把所有餐厅数据塞给浏览器。注意Gunicorn worker数量不要拍脑袋定一个固定值。通用公式是2乘以CPU核心数再加1比如服务器是2核就开5个worker。开太多worker会频繁切换上下文内存占用上去但吞吐量不涨。6. 实战问题排查与经验速查6.1 爬虫阶段的高频翻车点做爬虫的人都知道很多坑不是代码逻辑问题而是环境问题。我在这里列几个典型的第一个是编码问题。有些接口返回的JSON里中文显示成\u5e97这类转义字符这不是bug而是JSON序列化时的正常行为。使用requests的resp.json()解析时会自动还原成中文但如果你用resp.text手动处理就要注意编码。最好的办法是直接用resp.json()让它自己处理。第二个是Session保持。携程系页面的登录态是通过Cookie维持的如果你需要对登录后的数据进行爬取第一件事是先用浏览器登录把Cookie复制出来然后用requests的Session对象设置这个Cookie。爬虫程序每次启动都设置一次Cookie比每次请求都带Cookie头更省事。第三个是频率控制失效的问题。我遇到过明明设置了随机延时还是被限流。后来发现是因为爬虫跑在服务器上服务器IP被平台标记了。解决办法是改用代理IP池或者干脆在本机网络环境下跑频率再压低一点。说到底速度慢一点没关系稳定地跑完才是硬道理。第四个是所有爬虫都会遇到的问题——页面改版。接口参数变了、返回结构变了爬虫直接挂掉。我的建议是爬虫模块做一层适配器把网络请求和字段解析解耦改版时只需要改解析器不用动整套任务调度逻辑。6.2 可视化卡顿的排查思路dashboard页面打开慢最开始我怀疑是图表渲染的问题实际排查后发现瓶颈在数据接口的聚合SQL上。比如计算菜系占比时如果直接用ORM遍历所有餐厅数据到Python里做groupby数据量一大就卡死了。正确的做法是把聚合压力放在数据库use group by让MySQL先做分组统计再返回结果给Python。一次查询搞定不需要在Python里进行二次聚合。另外有个小细节pyecharts生成的图表如果数据点太多浏览器渲染会卡顿。解决方案是在生成图表之前对数据进行下采样比如价格分布图是按每一元一个桶还是每十元一个桶决定了柱状图的数据点数量。合理的分桶策略既不影响可读性又能让图表流畅。6.3 大模型接口的边界情况DeepSeek接口接入看起来不难但真正做得得心应手还要处理一些边界情况。超时问题排第一。大模型推理时间不可控高峰期可能5秒、10秒才返回。如果客户端默认超时时间过短就会出现部分请求失败。我给chat调用设置了60秒的超时时间同时封装了自动重试机制遇到网络错误或5xx状态码等待2秒重试一次最多重试3次。第二是Token限制。分析长文本时容易超出上下文窗口。我的处理方案是分段传入比如评论情感分析时每段20条分析完再把结果汇总。如果是一次性总结任务先把原始文本截断到模型支持的Token数以内再交给模型。第三是输出不稳定。明明要求输出JSON有时模型还是会多输出一句解释。我的处理办法是在解析JSON失败时用正则把JSON部分提取出来再解析如果还失败就重试一次。代码里加一层兜底逻辑比让用户看到一片报错强一百倍。6.4 工作流管理的小经验整个项目跑下来我的数据管道大致是这样协同的每天凌晨2点用定时任务触发爬虫爬完自动做清洗入库然后生成聚合缓存等到白天用户访问时看到的都是最新数据。日志系统一定要从一开始就搭好。爬虫记录每轮抓了多少条、失败了多少条AI模块记录每次调用的Token消耗、耗时、成功失败状态。没有日志出了问题只能靠猜排查成本极高。我见过太多项目前期不写日志上线后抓瞎的案例了。维护这套系统最大的体会是数据质量决定分析价值提示词决定AI输出的上限。这两个点一定要花心思打磨。数据清洗做不好后面所有图表和AI分析都建立在沙子上提示词设计不合理再强的模型也发挥不出效果。如果你也想做类似的项目建议先从一个小城市的美食数据入手比如几百家餐厅加几千条评论把全链路跑通后再扩展。这个体量足够练习数据清洗和可视化又不会让爬虫和数据处理的复杂度淹没掉你对整体架构的感知。做这类项目最好的学习方式不是看多少篇教程而是真的把一套流程从头到尾跑一遍。踩坑、修复、优化这些经验只有自己动手才能沉淀下来。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询