ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大众点评奶茶店铺爬虫与数据分析实战:从数据抓取到聚类回归

大众点评奶茶店铺爬虫与数据分析实战:从数据抓取到聚类回归 简介本资源是面向餐饮行业市场调研与竞品分析场景的数据采集与分析工具包聚焦北京市奶茶店铺这一细分市场适合具备一定Python基础的数据分析学习者与行业研究人员使用。包内共36个文件以27张png可视化图表、2份csv原始数据集、2个html交互地图、1个py爬虫脚本、1个ipynb分析文件及配套说明文档为主压缩包约12.58MB。爬虫代码支持自定义爬取地址与美食种类可灵活调整采集范围原始数据集涵盖店铺名称、地址、评分、评论数、人均消费等字段。分析部分提供完整的ipynb文件覆盖直方图、相关性热力图、DBSCAN聚类、层次聚类树状图、空间密度热力图及回归诊断等模块并附上海与北京两地对比结果。目前已有113人学习读者可据此掌握从数据抓取、清洗到统计建模与可视化的完整链路快速复现一套可迁移的餐饮竞品分析流程。1. 从一份奶茶店铺数据集说起这套爬虫加分析工具到底能干什么打开这份压缩包第一眼看到的不是代码而是一堆命名规整的 CSV 和 PNG。北京、上海两地的奶茶店铺数据各占一份外加一个Dianping_Crawler.py爬虫脚本、一个final_da.ipynb分析笔记以及二十多张已经跑出来的图表。这不是一个教你写爬虫的教程包而是一套已经跑通、结果可复现的完整工作流——从大众点评网页端抓取店铺信息到清洗、聚类、回归、可视化最后落成一份能直接放进市场调研报告里的分析素材。它解决的核心问题是做餐饮行业竞品分析时数据从哪来、怎么结构化、怎么看出门道。适合两类人——一类是想拿现成数据练手pandas、seaborn、sklearn的数据分析新手另一类是需要快速摸清一个城市奶茶店铺分布、价格带、评分与评论数关系的市场调研从业者。代码和数据集都摆在那里改个城市名、换个美食品类就能跑出自己的版本。2. 爬虫脚本拆解从网页请求到 CSV 落盘的完整链路2.1 为什么是 requests BeautifulSoup 而不是 ScrapyDianping_Crawler.py的体量不大从文件命名和常见实现方式推断它走的是requests发请求、BeautifulSoup解析 HTML、csv模块写文件的轻量路线。这个选型很务实目标页面是大众点评的店铺列表页结构相对固定不需要 Scrapy 那套调度器、去重队列和中间件体系。对于单城市、单品类、几千条量级的数据抓取轻量脚本的调试成本更低改起来也快。常见做法是先把请求头伪装好尤其是User-Agent和Referer然后按页码循环构造 URL。大众点评的列表页通常带分页参数比如pageno1、pageno2这样递增。脚本里一般会设一个max_page或者根据返回内容判断是否还有下一页。import requests from bs4 import BeautifulSoup import csv import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.dianping.com/, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(city, keyword, page): # 城市和品类拼进 URLpage 控制翻页 url fhttps://www.dianping.com/{city}/ch10/g{keyword}/p{page} resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return resp.text def parse_shops(html): soup BeautifulSoup(html, html.parser) shops [] for item in soup.select(.shop-list .shop-item): name item.select_one(.shop-name).get_text(stripTrue) star item.select_one(.shop-star).get(title, ) review item.select_one(.review-num).get_text(stripTrue) price item.select_one(.mean-price).get_text(stripTrue) address item.select_one(.shop-address).get_text(stripTrue) shops.append([name, star, review, price, address]) return shops def save_csv(rows, filename): with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([店铺名称, 评分, 评论数, 人均价格, 地址]) writer.writerows(rows)这段代码的逻辑分三层fetch_page负责带伪装头请求页面parse_shops用 CSS 选择器提取字段save_csv用utf-8-sig编码写文件——这个细节很关键不加-sig的话 Excel 打开中文会乱码。参数方面timeout10防止单次请求卡死time.sleep(random.uniform(1, 3))这种随机延时通常要加在翻页循环里降低被限流的概率。2.2 自定义爬取地址与美食种类的实现方式这套工具号称支持自定义地址和品类实现思路一般有两种一种是把城市拼音和品类 ID 做成命令行参数或配置文件另一种是直接在脚本顶部定义变量。从文件结构看Dianping_Crawler.py大概率是后者——改city和keyword两个变量就能切换目标。if __name__ __main__: city beijing # 换成 shanghai 即可抓上海 keyword 奶茶 # 换成 火锅、咖啡 也能跑 all_rows [] for page in range(1, 51): # 抓前 50 页 html fetch_page(city, keyword, page) rows parse_shops(html) if not rows: break # 没有数据说明翻到头了 all_rows.extend(rows) time.sleep(random.uniform(1.5, 3.5)) save_csv(all_rows, f{city}_{keyword}_shops.csv)这里有几个参数需要根据实际情况调整。range(1, 51)控制抓取页数大众点评一个品类通常不会超过 50 页设太大浪费请求设太小数据不够。time.sleep的区间建议不低于 1 秒太激进容易触发验证码。break的判断逻辑是如果某一页解析出来是空列表说明要么翻完了要么被反爬拦截返回了空页面——这两种情况都需要停下来人工检查。注意大众点评的页面结构会不定期调整CSS 选择器可能失效。如果跑出来是空 CSV先手动请求一页看看返回的 HTML 里 class 名有没有变。3. 数据分析笔记实战从 CSV 到聚类、回归与可视化3.1 final_da.ipynb 的整体分析框架final_da.ipynb是整个包里最有价值的部分。它不是简单的df.describe()走一遍而是围绕奶茶店铺的经营特征做了一套完整的探索性分析加建模。从附带的图表文件名可以反推出分析链路六变量直方图看分布、相关性热力图看变量关系、DBSCAN 聚类做店铺分群、层次聚类树状图看价格分层、回归诊断看模型残差、分组箱线图对比高低价组差异。这个框架的合理之处在于先看单变量分布再看变量间相关然后用无监督方法分群最后用回归量化影响因素。对于市场调研场景这套流程能回答几个关键问题——奶茶店集中在哪些区域、人均价格和评分有没有关系、评论数多的店有什么共同特征。import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False df pd.read_csv(北京大众点评奶茶店铺数据.csv, encodingutf-8-sig) df[评论数] pd.to_numeric(df[评论数], errorscoerce) df[人均价格] df[人均价格].str.replace(¥, ).astype(float) df df.dropna(subset[评论数, 人均价格, 评分]) fig, axes plt.subplots(2, 3, figsize(15, 8)) for ax, col in zip(axes.flatten(), [评分, 评论数, 人均价格]): sns.histplot(df[col], kdeTrue, axax) ax.set_title(f{col}分布) plt.tight_layout() plt.savefig(六变量直方图.png, dpi150)这段代码做了三件事读 CSV 时指定utf-8-sig编码避免中文列名乱码把评论数和人均价格从字符串转成数值类型errorscoerce让无法转换的值变成NaN而不是报错用seaborn画分布直方图并叠加核密度曲线。SimHei字体设置是中文环境下的必备操作不设的话图表里中文全是方块。3.2 DBSCAN 聚类与空间分布热力图的参数设置DBSCAN 在这份分析里承担的是店铺分群任务。它的优势是不需要预先指定簇数量能自动识别噪声点——对于奶茶店铺数据这意味着它能找出那些不像任何一群的异常店铺比如价格极高或评论数极低的离群点。from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler features df[[评分, 评论数, 人均价格]].copy() scaler StandardScaler() X_scaled scaler.fit_transform(features) dbscan DBSCAN(eps0.8, min_samples5) df[cluster] dbscan.fit_predict(X_scaled) print(df[cluster].value_counts())关键参数有两个eps是邻域半径min_samples是形成核心点所需的最小样本数。eps0.8是在标准化后的特征空间里设定的标准化让三个量纲不同的变量处于同一尺度否则评论数的数值范围会完全压制评分。min_samples5意味着至少 5 家店在半径内才算一个簇这个值太小会导致碎片化太大则会把小簇吞掉。调参时可以先跑几组eps值看聚类结果的数量和噪声比例选一个业务上说得通的。空间分布热力图则是用folium或pyecharts把经纬度映射到地图上空间分布热力图_店铺密度.html这个文件名说明输出的是交互式 HTML可以直接在浏览器里缩放查看。如果原始 CSV 里没有经纬度字段常见做法是用地址调高德或百度的地理编码接口补全。3.3 回归诊断与分组对比高价组和低价组差在哪基准回归图.png、优化回归图.png、残差诊断图.png这几个文件说明分析里做了回归建模。从残差诊断图_加入营业时间平方项.png这个命名可以推断模型经过了一轮优化——加入了营业时间的平方项可能是发现营业时间与评论数之间存在非线性关系。分组回归的思路是按人均价格把店铺分成高价组和低价组分别跑回归看哪些因素在不同价格带里的影响方向或显著性不同。高价组回归结果.png和低价组回归结果.png就是这两组的结果对比。这种分析对市场调研很有用——如果高价组里评分对评论数的影响更强说明高价店的口碑传播更关键如果低价组里价格敏感度更高那定价策略就要更谨慎。import statsmodels.api as sm # 按中位数分高低价组 median_price df[人均价格].median() df[price_group] np.where(df[人均价格] median_price, 高价, 低价) for group in [高价, 低价]: sub df[df[price_group] group].copy() sub[log_review] np.log1p(sub[评论数]) X sm.add_constant(sub[[评分, 人均价格]]) model sm.OLS(sub[log_review], X).fit() print(f--- {group}组回归结果 ---) print(model.summary())这里对评论数取了log1p变换因为评论数通常呈长尾分布直接回归会受极端值影响。sm.add_constant加截距项model.summary()输出系数、标准误、t 值和 R²。分组回归的样本量会减半如果原始数据只有几百条分组后的统计功效可能不够这时候要谨慎解读 p 值。4. 避坑与排查跑这套代码时最容易翻车的五个地方4.1 爬虫返回空数据或 403现象脚本跑完生成的 CSV 只有表头或者请求直接抛HTTPError: 403。原因大众点评对请求频率和请求头有检测。没有Referer、User-Agent太旧、请求间隔太短都会触发拦截。另一个常见原因是页面结构变了原来的 CSS 选择器匹配不到任何元素。解决先手动用浏览器打开目标页面确认页面能正常访问然后检查请求头是否完整Referer设成https://www.dianping.com/把time.sleep调到 2 秒以上如果还是 403可能需要处理 Cookie常见做法是先从浏览器复制一份有效 Cookie 放进请求头。4.2 CSV 中文乱码现象用 Excel 打开生成的 CSV店铺名称和地址全是乱码。原因csv.writer默认用系统编码写入Windows 中文环境下是 GBK而 Excel 默认按 UTF-8 解析。解决写文件时指定encodingutf-8-sig-sig会写入 BOM 头Excel 识别到 BOM 就会用 UTF-8 解码。如果已经生成了乱码文件用pd.read_csv(file.csv, encodinggbk)读进来再重新存一遍。4.3 人均价格字段无法转数值现象df[人均价格].astype(float)报ValueError。原因原始数据里人均价格带¥符号有些店铺显示暂无报价或空字符串直接转 float 会失败。解决先用.str.replace(¥, )去掉符号再用pd.to_numeric(..., errorscoerce)强制转换无法转换的变成NaN最后dropna或填充。不要用astype(float)硬转一定会翻车。4.4 DBSCAN 聚类结果全是一类或全是噪声现象df[cluster].value_counts()显示只有一个簇标签或者-1噪声占了绝大多数。原因eps设得太小所有点都成了噪声设得太大所有点合并成一簇。另外如果特征没有标准化量纲大的变量会主导距离计算。解决先确认做了StandardScaler然后用 k-距离图辅助选eps——计算每个点到第 k 个最近邻的距离排序后画曲线拐点位置就是合适的eps。min_samples一般设为特征数加一三维特征设 4 到 6 之间比较合理。4.5 回归模型残差不满足正态分布现象残差诊断图.png显示残差 Q-Q 图严重偏离对角线或者残差与拟合值呈现喇叭形。原因因变量分布偏斜、遗漏了重要变量、或者存在异方差。评论数这种长尾变量直接回归很容易出现这个问题。解决对因变量做对数变换np.log1p通常能显著改善检查是否有极端离群值拉偏了模型考虑用稳健回归sm.RLM替代 OLS如果残差图显示非线性模式加入平方项或交互项就像残差诊断图_加入营业时间平方项.png那样。5. 进阶用法把分析模板迁移到其他品类和城市这套工具最实用的地方在于它的可迁移性。Dianping_Crawler.py改两个变量就能抓另一个城市或品类final_da.ipynb改一下文件名和列名就能跑新数据。但迁移时有几个细节决定了你能不能一次跑通。首先是品类关键词的 URL 编码问题。大众点评的品类路径有时候不是直接拼中文而是用品类 ID。如果换成火锅抓不到数据先手动打开大众点评火锅列表页看 URL 里的品类标识是什么直接复制过来用。其次是城市拼音的对应关系。北京是beijing上海是shanghai但有些城市在点评里的拼音和标准拼音不一致比如西安是xian厦门是xiamen。抓之前先在浏览器里确认一下。再就是分析模板的列名适配。final_da.ipynb里用的列名是评分评论数人均价格如果你抓的是其他品类列名可能一样但数据分布会差很多。比如火锅店的人均价格普遍高于奶茶店DBSCAN 的eps参数需要重新调。我一般会先跑一遍df.describe()看数值范围再决定标准化后的eps设多少。# 迁移到新品类时的快速检查清单 new_df pd.read_csv(上海_火锅_shops.csv, encodingutf-8-sig) print(new_df.dtypes) # 确认数值列类型 print(new_df[[评分, 评论数, 人均价格]].describe()) # 看分布范围 print(new_df.isnull().sum()) # 看缺失情况 # 根据新数据的分布重新设 eps from sklearn.neighbors import NearestNeighbors X StandardScaler().fit_transform(new_df[[评分, 评论数, 人均价格]].dropna()) nbrs NearestNeighbors(n_neighbors5).fit(X) distances, _ nbrs.kneighbors(X) distances np.sort(distances[:, -1]) plt.plot(distances) plt.ylabel(5th nearest neighbor distance) plt.show() # 拐点处对应的值就是候选 eps这段代码里的 k-距离图是选eps的常用手段。曲线急剧上升的拐点位置就是数据密度开始稀疏的边界取那个位置的 y 值作为eps聚类效果通常比拍脑袋设一个数好得多。还有一个容易忽略的点大众点评的店铺数据里评论数和人均价格经常有缺失。如果直接dropna可能丢掉三成以上的样本。我的习惯是先把缺失比例算出来如果低于 10% 就删高于 10% 就考虑用中位数填充或者把缺失本身作为一个特征。这个决策没有标准答案取决于你的分析目标——如果只是看整体分布删掉缺失样本影响不大如果要做精细的回归建模缺失值处理方式会直接影响系数估计。从那以后我每次拿到新城市的数据都强制先跑一遍describe()和缺失检查再动聚类和回归的参数。这套流程看起来多花十分钟但能省掉后面反复调参、反复怀疑数据的后悔药时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表