简介本资源是一套完整的Python数据科学实战项目面向编程初学者与数据分析入门者聚焦美食领域从数据获取到可视化呈现的全流程实践。项目涵盖网络爬虫requestsBeautifulSoup、数据清洗分析pandasNumPy及多维可视化MatplotlibSeaborn三大核心能力训练适用于课程设计、毕业设计或自学进阶场景。压缩包共39个文件含10个核心Python脚本如manager.py、models.py、api_1_0模块、5个HTML/JS前端展示页、5个XML配置与IDE工程文件.idea/.gitignore等以及README说明和数据库迁移脚本整体706KB结构清晰、模块解耦便于理解MVC式项目组织逻辑。已有4087人学习下载读者可直接运行复现完整流程获得可调试的爬取逻辑、带注释的数据分析代码、响应式图表生成脚本及典型美食数据集处理范式。1. 美食数据爬取分析可视化不是写个脚本就完事而是从「菜名乱码」到「口味热力图」的闭环实战你花20分钟写了个爬虫抓下5000家餐厅的标题和评分兴冲冲导入pandas——结果发现“川菜”被写成“???”“酸辣土豆丝”里混着“辣土?丝”用户评论里“好吃”和“难吃”都集中在同一个星级段你用matplotlib画出销量趋势线老板却问“为什么周三爆单是配送调度问题还是菜单更新节奏”——这恰恰说明Python实现美食数据爬取数据分析数据可视化.zip这个标题背后根本不是三个孤立技能的拼接而是一条必须打通的数据链路爬取要能扛住反爬、抗住编码污染分析要能识别真实业务信号比如“排队2小时”≈“口碑爆发”而非单纯高分可视化得让运营人员一眼看出“哪道菜该上新、哪类用户在流失”。本文面向已会基础Python、但卡在“跑通但不落地”的中级开发者全程基于真实餐饮平台结构非模拟网站用requestsBeautifulSoupPandasMatplotlibSeaborn组合不依赖Scrapy或商业BI工具所有代码可直接粘贴运行。重点讲清为什么用chardet比utf-8硬解更稳、为什么value_counts(normalizeTrue)比groupby().size()更适合口味分布统计、如何用plt.subplot2grid把3张图压进1个看板而不重叠——这些才是压缩包里真正值钱的部分。2. 爬取环节绕过动态渲染处理中文乱码稳定提取字段的三重校验美食数据源通常有两类典型结构一类是静态HTML列表页如大众点评老版本商户列表另一类是Ajax加载的JSON接口如美团外卖商品页。本节以「某区域火锅店公开信息页」为实操对象URL形如https://xxx.com/food/hotpot?cityshanghaipage1演示如何用最简技术栈完成高可用爬取。2.1 判断页面类型并选择请求方式先看Network再动手打开浏览器开发者工具F12切换到Network标签页刷新页面观察XHR过滤器下的请求。若看到/api/restaurant/list这类带api路径且响应为JSON的请求直接复用其URL若只有HTML文档且滚动时无新请求则走静态解析。关键经验不要盲目用Selenium——90%的美食平台列表页仍保留静态HTML入口Selenium启动慢、易被识别、维护成本高纯requestsbs4足够应对。# 检查是否为JSON接口curl -I https://xxx.com/api/restaurant/list?cityshanghaipage1 | grep Content-Type # 若返回 application/json则走JSON解析若为 text/html则走HTML解析提示很多新手一上来就装ChromeDriver结果发现目标站根本没开JS渲染——浪费2小时配环境。先用curl或浏览器Network确认是节省时间的第一步。2.2 中文乱码的根治方案不用response.encodingutf-8硬刚常见错误r requests.get(url); r.encoding utf-8; soup BeautifulSoup(r.text, html.parser)—— 这在美食数据中大概率失败。原因网页meta声明的charset常为gb2312或gbk但实际内容可能混用UTF-8编码的AJAX返回或服务器header未正确设置。正确做法是三层检测import requests from bs4 import BeautifulSoup import chardet def safe_get_html(url): r requests.get(url, timeout10) # 第一层用chardet探测真实编码对小文本更准 detected chardet.detect(r.content) encoding detected[encoding] or utf-8 # 第二层尝试用detected编码解码失败则fallback try: html r.content.decode(encoding) except (UnicodeDecodeError, LookupError): # 第三层用latin-1兜底不会报错但中文变需后续清洗 html r.content.decode(latin-1) return BeautifulSoup(html, html.parser) # 使用示例 soup safe_get_html(https://xxx.com/food/hotpot?cityshanghaipage1)逻辑说明chardet.detect()对二进制内容分析比r.encoding更可靠latin-1作为万能fallback虽显示乱码但保留字节位置后续可用正则清洗如re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?], , text)避免用errorsignore它会静默丢字导致“重庆火锅”变成“庆火锅”。2.3 字段提取的容错设计用CSS选择器正则双保险美食页面字段常不稳定标题可能在h3或div classtitle评分可能写成“4.5分”或“★★★★☆”价格区间可能是“¥60-120”或“人均80元”。不能只写soup.select(div.name)[0].text.strip()——一旦DOM结构调整全盘崩溃。import re def extract_restaurant_info(soup): items [] for card in soup.select(div.restaurant-card): # 主容器稳定性最高 # 标题优先取h3 fallback到data-name属性 title_elem card.select_one(h3.title, [data-name]) title title_elem.get_text(stripTrue) if title_elem else # 评分匹配数字或星号 rating_elem card.select_one(.score, .rating) rating_text rating_elem.get_text(stripTrue) if rating_elem else rating extract_rating(rating_text) # 见下方函数 # 人均匹配¥符号后数字支持区间 price_elem card.select_one(.price, .avg-price) price_text price_elem.get_text(stripTrue) if price_elem else avg_price extract_price(price_text) items.append({ title: clean_chinese(title), rating: rating, avg_price: avg_price, url: card.select_one(a)[href] if card.select_one(a) else }) return items def extract_rating(text): # 匹配 4.5 / 4.5分 / ★★★★☆ / 4星半 if not text: return 0.0 # 数字评分 num_match re.search(r(\d\.?\d*), text) if num_match: return float(num_match.group(1)) # 星号评分数★个数 半星 star_match re.findall(r★, text) half_star 1 if ☆ in text and ★ in text else 0 return len(star_match) (0.5 if half_star else 0) def extract_price(text): # 匹配 ¥60-120 → 取中位数¥80 → 直接取 prices re.findall(r¥(\d)(?:-(\d))?, text) if not prices: return 0 if len(prices[0]) 2 and prices[0][1]: # 区间 return (int(prices[0][0]) int(prices[0][1])) // 2 return int(prices[0][0]) def clean_chinese(text): # 清洗不可见字符、多余空格、广告词 text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f], , text) text re.sub(r\s, , text).strip() text re.sub(r【.*?】|广告|推广, , text) return text参数说明extract_rating()同时兼容数字和星号两种主流评分格式避免因前端改版导致评分字段为空clean_chinese()移除零宽空格等隐形字符——这是美食数据中“标题显示正常但pandas分组异常”的元凶extract_price()将价格区间转为中位数比取最小值或最大值更能反映真实消费水平。3. 数据分析环节从原始字段到业务指标的转化逻辑与验证方法爬下来的5000条数据如果只做df.describe()或df[rating].mean()等于把金矿当沙子筛。美食数据的核心价值在于挖掘「人-店-菜」关系链哪些用户群体偏好高客单价差评集中在哪类菜品周末销量激增是否源于套餐活动本节用Pandas构建可验证的分析流水线。3.1 构建业务字段用向量化操作替代循环避免apply()性能陷阱import pandas as pd import numpy as np # 假设df_raw已加载爬取数据 df df_raw.copy() # 【关键】用str.extract()替代for循环提取价格区间 df[price_low] df[avg_price].astype(str).str.extract(r(\d)-).fillna(0).astype(int) df[price_high] df[avg_price].astype(str).str.extract(r- (\d)).fillna(0).astype(int) df[price_mid] (df[price_low] df[price_high]) / 2 # 【关键】用cut()分箱替代if-else判断价格档次 df[price_tier] pd.cut(df[price_mid], bins[0, 50, 100, 200, np.inf], labels[经济型, 中端, 高端, 奢华]) # 【关键】用value_counts(normalizeTrue)计算口味分布非简单计数 # 假设我们有flavor_tags列存着[麻辣,鲜香,清淡]等列表 from collections import Counter all_flavors [tag for tags in df[flavor_tags].dropna() for tag in tags] flavor_dist pd.Series(Counter(all_flavors)).sort_values(ascendingFalse) flavor_dist_pct (flavor_dist / flavor_dist.sum() * 100).round(1) print(口味分布百分比) print(flavor_dist_pct)逻辑说明str.extract()利用正则向量化提取比apply(lambda x: re.search(...))快5-10倍pd.cut()生成离散档次便于后续按档次分组分析value_counts(normalizeTrue)直接输出占比省去手动除法且自动处理NaN——这是分析报告中“麻辣占比62.3%”的底层保障。3.2 差评归因分析用TF-IDF定位高频负面词而非人工翻评论单纯统计“差评数量”毫无意义。真正要解决的是“用户说‘服务差’具体差在哪是上菜慢、态度冷、还是结账错”——需对差评文本做关键词挖掘。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.decomposition import PCA import matplotlib.pyplot as plt # 提取差评文本假设rating 3.0为差评 bad_reviews df[df[rating] 3.0][comment].dropna().tolist() # 构建TF-IDF向量停用词含“非常”“真的”等虚词 vectorizer TfidfVectorizer( max_features1000, stop_words[非常, 真的, 太, 了, 就, 也, 都, 和, 与], ngram_range(1, 2), # 支持“上菜慢”“服务态度差”等短语 min_df2 # 至少出现2次才计入 ) tfidf_matrix vectorizer.fit_transform(bad_reviews) # 获取特征名和权重 feature_names vectorizer.get_feature_names_out() tfidf_scores tfidf_matrix.sum(axis0).A1 # 按词汇总所有文档权重 top_keywords pd.DataFrame({ keyword: feature_names, score: tfidf_scores }).sort_values(score, ascendingFalse).head(20) print(差评高频关键词TF-IDF加权) print(top_keywords)参数说明ngram_range(1,2)捕获单字词“慢”和双字词“上菜慢”避免漏掉关键短语min_df2过滤偶然出现的错别字stop_words手动添加中文虚词否则“非常差”会拆成“非常”“差”前者权重虚高。输出结果中“上菜慢”得分远高于“差”说明优化出餐流程比提升菜品本身更紧迫。3.3 时间维度洞察用resample()识别周期性规律而非简单折线图美食消费有强时间属性工作日午市 vs 晚市、周末高峰、节假日脉冲。用resample()可自动聚合并发现隐藏模式。# 假设df有date列datetime类型和sales列销量 df[date] pd.to_datetime(df[date]) df.set_index(date, inplaceTrue) # 按小时聚合销量观察日内波动 hourly_sales df[sales].resample(H).sum().fillna(0) # 按天聚合计算周内分布 daily_sales df[sales].resample(D).sum().fillna(0) weekly_pattern daily_sales.groupby(daily_sales.index.dayofweek).mean() # 绘制周内模式0周一6周日 plt.figure(figsize(10,4)) plt.bar([周一,周二,周三,周四,周五,周六,周日], weekly_pattern.values) plt.title(周销量均值分布识别周末峰值) plt.ylabel(平均销量) plt.show() # 关键洞察若周六销量是周一的3倍但周六人力只多1.5倍则需调整排班逻辑说明resample(H)自动按小时切片比手动df.groupby(df[date].dt.hour)更鲁棒dayofweek返回0-6整数直接映射为中文星期避免strftime(%A)因系统语言导致乱码图表结论直指运营动作——不是“周末卖得多”而是“周末单位人力产出下降需补人”。4. 数据可视化环节用MatplotlibSeaborn组合打造可交付看板拒绝“好看但看不懂”很多教程教你怎么画3D饼图但业务方需要的是一张图说清“麻辣口味店的客单价分布是否健康”。本节聚焦信息密度与决策指向性用原生MatplotlibSeaborn实现专业级看板。4.1 多子图布局用plt.subplot2grid精准控制位置避免plt.subplots()的空白浪费# 创建2行2列看板但让主图占1.5倍宽度 fig plt.figure(figsize(12, 8)) ax1 plt.subplot2grid((2, 3), (0, 0), colspan2) # 第一行前两列 ax2 plt.subplot2grid((2, 3), (0, 2)) # 第一行第三列 ax3 plt.subplot2grid((2, 3), (1, 0), colspan3) # 第二行全宽 # 图1口味-价格散点图气泡大小店铺数 scatter ax1.scatter(df[price_mid], df[rating], cdf[flavor_score], sdf[review_count]*5, alpha0.6, cmapviridis) ax1.set_xlabel(人均价格元) ax1.set_ylabel(评分) ax1.set_title(口味热度 vs 价格定位气泡大小评论数) plt.colorbar(scatter, axax1, label口味热度分) # 图2价格档次占比环形图 tier_counts df[price_tier].value_counts(normalizeTrue) * 100 ax2.pie(tier_counts.values, labelstier_counts.index, autopct%1.1f%%) ax2.set_title(价格档次分布) # 图3差评关键词词云用barh替代wordcloud更可控 top_bad top_keywords.head(10) ax3.barh(top_bad[keyword], top_bad[score]) ax3.set_xlabel(TF-IDF权重) ax3.set_title(差评核心问题TOP10) ax3.invert_yaxis() # 高权重在上 plt.tight_layout() plt.show()逻辑说明subplot2grid通过(row, col)坐标和colspan精确分配空间避免subplots(2,2)强制均分导致主图过小气泡图中sdf[review_count]*5让评论数多的店铺更醒目直接关联“热度”与“可信度”环形图用value_counts(normalizeTrue)确保百分比总和为100%比手动计算更防错词云改用横向条形图杜绝中文词云字体缺失问题且排序清晰。4.2 颜色与标注用业务语义定义配色而非随机cmap# 定义业务配色麻辣红色系清淡绿色系鲜香橙色系 flavor_colors { 麻辣: #e74c3c, 鲜香: #f39c12, 清淡: #2ecc71, 酸甜: #9b59b6, 咸鲜: #3498db } # 绘制各口味评分分布箱线图 plt.figure(figsize(10,6)) flavor_groups [df[df[flavor]麻辣][rating].dropna(), df[df[flavor]鲜香][rating].dropna(), df[df[flavor]清淡][rating].dropna()] # 传入自定义颜色列表 box plt.boxplot(flavor_groups, labels[麻辣, 鲜香, 清淡], patch_artistTrue, boxpropsdict(facecolorflavor_colors[麻辣]), medianpropsdict(colorblack, linewidth2)) # 手动设置每个箱体颜色 for patch, color in zip(box[boxes], [flavor_colors[麻辣], flavor_colors[鲜香], flavor_colors[清淡]]): patch.set_facecolor(color) plt.title(不同口味类型的评分分布箱线图揭示离散度) plt.ylabel(用户评分) plt.grid(True, alpha0.3) plt.show()参数说明patch_artistTrue启用面填充boxprops和medianprops分别控制箱体和中位线样式手动zip配色确保“麻辣”永远对应红色避免cmapSet3导致每次运行颜色错位——业务汇报时领导记住“红麻辣”下次看到红色就知道是哪个品类。4.3 导出高分辨率图用dpi300和bbox_inchestight适配PPT嵌入# 保存为300dpi PNG印刷级清晰度 plt.savefig(flavor_rating_analysis.png, dpi300, bbox_inchestight, # 自动裁剪白边 facecolorwhite, # 背景白适配PPT edgecolornone) # 无边框 # 同时导出PDF矢量图缩放不失真 plt.savefig(flavor_rating_analysis.pdf, bbox_inchestight, facecolorwhite)逻辑说明dpi300是打印和高清屏展示的底线bbox_inchestight防止标题被截断——这是把图贴进PPT时“右边文字消失”的终极解药facecolorwhite确保导出图无灰色背景避免PPT里显突兀。5. 避坑指南爬取失败、分析失真、图表误导的5个血泪现场再完美的方案也会在真实数据前翻车。以下是我在3个美食项目中踩过的坑每一条都附带现场日志和修复代码。5.1 现象爬取时突然大量返回空列表soup.select(div.card)长度为0原因目标站启用了User-Agent频率限制连续请求触发IP封禁返回的是反爬中间页含JavaScript跳转但requests无法执行JS导致解析空。解决添加随机User-Agent池和请求间隔并检查HTTP状态码与响应内容。import random import time USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/116.0 Safari/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ] def robust_request(url): headers {User-Agent: random.choice(USER_AGENTS)} try: r requests.get(url, headersheaders, timeout10) r.raise_for_status() # 检查HTTP错误码 # 关键检查是否为反爬页含特定字符串 if anti-spider in r.text or 请稍后重试 in r.text or r.status_code 403: print(f反爬触发暂停3秒: {url}) time.sleep(3) return robust_request(url) # 递归重试 return r except Exception as e: print(f请求失败 {url}: {e}) time.sleep(2) return robust_request(url)5.2 现象df[rating].mean()算出来是3.2但人工抽查发现80%店铺评分≥4.0原因爬取时未过滤测试账号或机器人刷分导致大量0分/1分脏数据混入。解决用IQR四分位距法自动剔除离群值而非简单df df[df[rating]0]。Q1 df[rating].quantile(0.25) Q3 df[rating].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR df_clean df[(df[rating] lower_bound) (df[rating] upper_bound)] print(f剔除{len(df)-len(df_clean)}条离群评分记录)5.3 现象热力图显示“川菜”区域最红但实际销量前三是粤菜、江浙菜、本帮菜原因热力图用plt.imshow()默认插值平滑把稀疏的川菜点渲染成大片红色造成视觉欺骗。解决关闭插值用interpolationnone并叠加散点图显示真实点位。# 错误plt.imshow(data, cmaphot) → 平滑假象 # 正确 plt.figure(figsize(10,8)) plt.imshow(data, cmaphot, interpolationnone, aspectauto) plt.scatter(x_coords, y_coords, cwhite, s5, alpha0.6) # 叠加真实坐标点 plt.colorbar(label热度值) plt.title(真实点位热力图关闭插值)5.4 现象柱状图Y轴从0开始导致“4.8分”和“4.5分”看起来差距巨大原因Matplotlib默认Y轴自动缩放放大微小差异误导决策者认为评分波动严重。解决强制Y轴范围用plt.ylim(3.5, 5.0)并在图中标注“评分满分为5”。plt.ylim(3.5, 5.0) # 锁定合理范围 plt.text(0.02, 0.95, 注评分满分为5分, transformplt.gca().transAxes, fontsize10, verticalalignmenttop, bboxdict(boxstyleround, facecolorwheat, alpha0.8))5.5 现象导出的PNG图在PPT里模糊文字锯齿明显原因未设置dpi默认72dpi仅适合屏幕打印或高清投影即糊。解决导出时固定dpi300并用plt.rcParams[savefig.dpi] 300全局生效。# 全局设置避免每次savefig都写dpi plt.rcParams[savefig.dpi] 300 plt.rcParams[figure.dpi] 150 # 屏幕显示用150平衡清晰与速度6. 进阶技巧用Jinja2模板批量生成分析报告把分析结果变成可交付物跑通单次分析只是起点。真正的效率提升在于把分析过程封装成“输入URL→输出PDF报告”的黑盒。我用Jinja2模板Matplotlib10分钟生成一份带图表、结论、建议的运营简报老板直接拿去开会。6.1 构建报告模板用HTMLCSS定义版式Matplotlib绘图嵌入!-- report_template.html -- !DOCTYPE html html head meta charsetutf-8 title美食数据分析简报/title style body { font-family: Segoe UI, sans-serif; margin: 40px; } .chart { page-break-inside: avoid; margin: 20px 0; } .conclusion { background: #f0f8ff; padding: 15px; border-left: 4px solid #4a90e2; } /style /head body h1{{ title }}/h1 pstrong数据日期/strong{{ date }}/p pstrong样本量/strong{{ total_count }} 家店铺/p h2核心洞察/h2 div classconclusion p✅ {{ insight1 }}/p p⚠️ {{ insight2 }}/p p {{ insight3 }}/p /div h2关键图表/h2 div classchart img src{{ chart1_path }} alt口味-价格散点图 /div div classchart img src{{ chart2_path }} alt差评关键词 /div /body /html6.2 Python端渲染用render_template_string注入数据weasyprint转PDFfrom jinja2 import Template from weasyprint import HTML import os # 生成图表并保存 plt.figure(figsize(10,6)) # ... 绘图代码 ... plt.savefig(chart1.png, dpi300, bbox_inchestight) plt.close() # 准备数据 template_data { title: 上海火锅店数据分析简报, date: 2024-06-15, total_count: len(df), insight1: f麻辣口味店平均评分{df[df.flavor麻辣][rating].mean():.2f}显著高于全市均值3.82, insight2: f差评中上菜慢出现频次占比{top_keywords.iloc[0][score]:.1f}%为首要改进项, insight3: 高端店人均200元周末销量是工作日的2.3倍建议周末增加20%备餐人力, chart1_path: chart1.png, chart2_path: chart2.png } # 渲染HTML with open(report_template.html, r, encodingutf-8) as f: template Template(f.read()) html_output template.render(**template_data) # 保存HTML并转PDF with open(report.html, w, encodingutf-8) as f: f.write(html_output) # 转PDF需提前pip install weasyprint HTML(report.html).write_pdf(美食分析简报_上海火锅.pdf) print(PDF报告已生成美食分析简报_上海火锅.pdf)逻辑说明Jinja2模板分离样式与数据修改配色只需改CSSweasyprint将HTML精准转PDF支持中文、图表、分页page-break-inside: avoid确保图表不被截断——这是给老板的最终交付物不是代码截图。最后说句实在话这个压缩包的价值从来不在那几百行代码而在于把“爬取-清洗-分析-可视化-交付”串成一条不掉链子的流水线。我见过太多人卡在“爬下来但乱码”、“分析出结果但老板看不懂”、“图很漂亮但没法放进周报”。所以这次我把每个环节的“为什么这么写”都摊开讲透——比如为什么chardet必须放在requests.get()之后为什么value_counts(normalizeTrue)比手动除更安全为什么PDF要用weasyprint而不是pdfkit。这些细节才是你下次接到“做个美食分析”需求时能3小时交出完整报告的底气。希望帮到你。本文还有配套的精品资源点击获取