尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

美团外卖数据分析系统实战:Scrapy采集、Hive数仓与Spark计算全链路

美团外卖数据分析系统实战:Scrapy采集、Hive数仓与Spark计算全链路 简介本资源为一份基于Python的美团外卖数据分析系统学位论文文档面向计算机相关专业学生、数据分析初学者及需要完成课程设计或毕业设计的人群帮助读者理解从数据采集到可视化展示的完整项目实现路径。压缩包内仅含1个docx文件大小约376KB内容涵盖项目背景、关键技术点与核心功能实现结构完整可直接作为论文写作与系统开发的参考模板。文档重点讲解Scrapy框架抓取店铺、菜品与评价数据Django搭建前端界面并借助Hive完成数据清洗、Spark执行订单分析、用户行为分析与店铺评价分析同时采用协同过滤算法实现个性化推荐。读者可从中获取系统架构设计思路、模块划分方式、数据库存储方案以及论文摘要与章节组织范例适合用于快速搭建同类分析系统或撰写相关学术文档。目前已有317人学习具备一定的参考热度与实用价值。1. 从一份论文文档说起这套美团外卖数据分析系统到底在解决什么问题外卖平台每天产生的订单、评价、菜品浏览记录本质上是典型的高维稀疏数据。很多同学拿到“基于 Python 的美团外卖数据分析系统”这类论文选题时第一反应是直接上 Django 写增删改查结果做出来的东西只是一个后台管理页面跟“数据分析”四个字没什么关系。这份论文文档的价值在于它把 Scrapy 采集、Hive 清洗、Spark 计算、Django 可视化串成了一条完整链路而不是把技术栈当装饰品堆在“相关技术”章节里。它适合两类人一类是正在做课程设计或学位论文、需要一套能跑通的数据分析系统骨架的在校生另一类是已经会写 Python 爬虫和 Web 应用但没把离线数仓和推荐算法接进业务系统的初中级工程师。下面按数据从抓取到落库、从离线计算到前端展示的顺序拆开讲重点放在参数怎么设、任务怎么调、哪里容易翻车。2. Scrapy 采集层字段设计、翻页策略与反爬节奏控制2.1 为什么采集层要先定 Schema 再写 Spider论文里提到用 Scrapy 抓取店铺、菜品、评价三类数据但没写字段结构。实际动手时如果 Spider 边写边加字段后面 Hive 建表、Spark 计算、Django 展示都要跟着改返工成本极高。常见做法是先按业务分析目标倒推字段比如要做“热门菜品销量排行”菜品表至少要有shop_id、dish_name、price、month_sales、rating、crawl_date六个字段。下面是一个可直接复用的 Item 定义字段命名统一用下划线避免 Hive 建表时再转换# items.py import scrapy class DishItem(scrapy.Item): shop_id scrapy.Field() # 店铺唯一标识用于关联店铺表 shop_name scrapy.Field() # 店铺名称便于人工核对 dish_name scrapy.Field() # 菜品名称 price scrapy.Field() # 当前售价单位元 month_sales scrapy.Field() # 月销量字符串需后续转 int rating scrapy.Field() # 菜品评分可能为空 crawl_date scrapy.Field() # 采集日期分区字段字段说明里最关键的是crawl_date。离线数仓按天分区是标准做法Hive 表用dt做分区键Spark 读取时只加载指定分区能省掉大量 I/O。month_sales在页面上常带“月售1000”这类文本采集时保留原始字符串清洗阶段再用正则提取数字不要在 Spider 里做复杂转换否则解析失败会直接丢数据。2.2 翻页与请求节流的参数配置美团外卖的列表页通常有分页参数常见形式是page或offset。Scrapy 默认并发是 16直接跑很容易触发限流。在settings.py里至少要调这几个参数# settings.py 关键配置 DOWNLOAD_DELAY 1.5 # 每个请求间隔 1.5 秒 CONCURRENT_REQUESTS_PER_DOMAIN 2 # 单域名并发降到 2 AUTOTHROTTLE_ENABLED True # 开启自动限速 AUTOTHROTTLE_START_DELAY 1.0 # 起始延迟 AUTOTHROTTLE_MAX_DELAY 10.0 # 最大延迟遇到 429 时自动拉长 RETRY_TIMES 3 # 失败重试 3 次DOWNLOAD_DELAY和CONCURRENT_REQUESTS_PER_DOMAIN是一对需要联调的参数。延迟设太小、并发设太高短时间内请求数上去了但被封的概率也上去了延迟设太大采集周期拉长论文实验部分的数据量可能不够。我一般先用DOWNLOAD_DELAY1.5、并发 2 跑 100 个请求观察返回状态码如果出现 403 或 429再把延迟往上加。提示采集前先确认目标站点的 robots.txt 和使用条款控制请求频率只采集公开可见的页面数据不要绕过登录或验证机制。2.3 数据落地先存 JSON Lines 再入 MySQL论文里写“采集后形成 pandas 保存然后使用 MySQL 保存”这个顺序在实际工程里可以优化。Scrapy 直接写 MySQL 会在管道里引入数据库连接开销采集速度受数据库写入影响。更稳的做法是先输出 JSON Lines 文件再用独立脚本批量入库# 运行 Spider输出到文件 scrapy crawl dish_spider -o dishes_20250101.jsonl -t jsonlines # 批量导入 MySQL用 LOAD DATA 比逐条 INSERT 快一个量级 mysql -u root -p meituan_analysis -e LOAD DATA LOCAL INFILE dishes_20250101.jsonl INTO TABLE raw_dish FIELDS TERMINATED BY \t LINES TERMINATED BY \n (shop_id, shop_name, dish_name, price, month_sales, rating, crawl_date); -t jsonlines指定输出格式为每行一个 JSON 对象方便后续按行处理。LOAD DATA的字段分隔符要和文件实际格式一致JSON Lines 默认是逗号分隔的 JSON直接导入需要先用 Python 转成制表符分隔的文本或者改用pandas.read_json读入后再to_sql。这一步的取舍是文件导入快但格式要求严pandas 导入灵活但内存占用高数据量在百万行以内用 pandas 完全够用。3. Hive 与 Spark 离线计算从原始表到分析指标3.1 Hive 建表与分区设计原始数据进 Hive 之前要先建外部表并指定分区。外部表的好处是删表不删数据方便反复调试-- 建原始菜品表按采集日期分区 CREATE EXTERNAL TABLE IF NOT EXISTS ods_dish ( shop_id STRING, shop_name STRING, dish_name STRING, price STRING, month_sales STRING, rating STRING ) PARTITIONED BY (dt STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t STORED AS TEXTFILE LOCATION /user/hive/warehouse/ods/dish; -- 加载某一天的数据 ALTER TABLE ods_dish ADD PARTITION (dt2025-01-01) LOCATION /user/hive/warehouse/ods/dish/dt2025-01-01;PARTITIONED BY (dt STRING)把日期从普通字段提升为分区键查询时WHERE dt2025-01-01只扫描对应目录不会全表扫描。STORED AS TEXTFILE适合调试阶段生产环境一般换成 ORC 或 Parquet压缩比和查询速度都更好。price和month_sales在原始表里保持 STRING 类型清洗阶段再转换这样即使某天数据格式异常也不会导致加载失败。3.2 Spark 清洗与指标计算清洗逻辑用 Spark SQL 写最直观核心是把文本销量转成数值、过滤异常价格、按店铺聚合# spark_clean.py from pyspark.sql import SparkSession from pyspark.sql.functions import regexp_extract, col, avg, sum as _sum spark SparkSession.builder \ .appName(MeituanDishClean) \ .enableHiveSupport() \ .getOrCreate() # 读取指定分区 df spark.sql(SELECT * FROM ods_dish WHERE dt2025-01-01) # 清洗提取销量数字价格转 double过滤价格异常 cleaned df.withColumn( sales_num, regexp_extract(col(month_sales), r(\d), 1).cast(int) ).withColumn( price_num, col(price).cast(double) ).filter( (col(price_num) 0) (col(price_num) 1000) ) # 按店铺聚合菜品数、平均价格、总销量 shop_stats cleaned.groupBy(shop_id, shop_name).agg( avg(price_num).alias(avg_price), _sum(sales_num).alias(total_sales) ) shop_stats.write.mode(overwrite).saveAsTable(dws_shop_stats) spark.stop()regexp_extract(col(month_sales), r(\d), 1)从“月售1000”里提取出“1000”第三个参数 1 表示取第一个捕获组。filter里的价格区间是经验值外卖菜品单价超过 1000 元的极少过滤掉能排除采集错误。saveAsTable把结果写入 Hive 表Django 后续直接查这张表即可不需要再连 Spark。3.3 协同过滤推荐的数据准备论文提到用协同过滤做推荐核心是构建“用户-菜品”评分矩阵。外卖场景下没有显式评分时常用下单次数或浏览次数作为隐式反馈数据源字段用途订单表user_id, dish_id, order_count构建评分矩阵浏览表user_id, dish_id, view_count补充隐式反馈菜品表dish_id, shop_id, category物品特征评分矩阵用 Spark 的mllib计算物品相似度from pyspark.mllib.recommendation import ALS, Rating # ratings 是从 Hive 读出的 (user_id, dish_id, score) 三元组 ratings spark.sql(SELECT user_id, dish_id, score FROM dws_user_dish_score) \ .rdd.map(lambda r: Rating(r[0], r[1], float(r[2]))) # 训练 ALS 模型rank 是隐因子维度iterations 是迭代次数 model ALS.train(ratings, rank10, iterations10, lambda_0.01) # 为指定用户推荐 5 个菜品 recs model.recommendProducts(userId1001, num5)rank10表示用 10 个隐因子描述用户和菜品值越大拟合能力越强但越容易过拟合lambda_0.01是正则化系数控制模型复杂度。外卖数据稀疏度高rank 一般取 10 到 50 之间先用 10 跑通流程再根据离线评估指标调整。4. Django 可视化与接口层把离线结果接到前端4.1 模型定义与数据库路由Django 的 ORM 默认连 MySQL但分析结果表在 Hive 里。常见做法是在 MySQL 里建一张结果同步表Spark 任务跑完后用INSERT OVERWRITE或 JDBC 把结果写回 MySQLDjango 只读 MySQL# models.py from django.db import models class ShopStat(models.Model): shop_id models.CharField(max_length64, primary_keyTrue) shop_name models.CharField(max_length128) avg_price models.FloatField() total_sales models.IntegerField() stat_date models.DateField() class Meta: db_table dws_shop_stats managed False # 表由 Spark 任务创建Django 不管理managed False告诉 Django 不要为这个模型生成迁移文件表结构由外部任务维护。stat_date字段用于前端按日期筛选配合 ECharts 的时间轴组件可以做趋势图。4.2 接口编写与 ECharts 数据格式对接前端用 ECharts 展示店铺销量排行后端接口返回 JSON 数组即可# views.py from django.http import JsonResponse from .models import ShopStat def shop_ranking(request): date request.GET.get(date, 2025-01-01) qs ShopStat.objects.filter(stat_datedate).order_by(-total_sales)[:10] data { names: [s.shop_name for s in qs], sales: [s.total_sales for s in qs], } return JsonResponse(data)order_by(-total_sales)[:10]取销量前十负号表示降序。返回的names和sales两个数组直接对应 ECharts 柱状图的 x 轴和 y 轴数据前端不需要再做转换。接口用GET传日期参数方便前端加日期选择器。4.3 推荐接口的调用时机推荐结果不适合实时计算ALS 模型训练一次可能要几分钟。常见做法是每天凌晨跑一次 Spark 任务把每个用户的 Top-N 推荐写入 MySQL 推荐表Django 接口直接查表def recommend(request): user_id request.GET.get(user_id) recs UserRecommend.objects.filter(user_iduser_id).order_by(-score)[:5] return JsonResponse({dishes: [r.dish_name for r in recs]})这样接口响应时间在毫秒级不会因为模型计算拖慢页面。推荐表加(user_id, score)联合索引查询效率更高。5. 排错与调优几个容易踩的坑5.1 采集数据为空或字段缺失Spider 跑完发现 JSON 文件是空的先检查parse方法的 XPath 或 CSS 选择器是否匹配到了页面结构。用scrapy shell交互式调试最快scrapy shell https://example.com/list?page1 # 进入交互后测试选择器 response.css(.dish-name::text).getall() response.xpath(//span[classprice]/text()).get()如果选择器返回空列表说明页面结构变了或者请求被重定向到了验证页。检查response.status和response.url确认拿到的是目标页面。5.2 Spark 任务内存溢出清洗大分区时常见java.lang.OutOfMemoryError。调参方向有三个提高 executor 内存、增加分区数、减少单次处理数据量。spark-submit \ --executor-memory 4g \ --num-executors 4 \ --conf spark.sql.shuffle.partitions200 \ spark_clean.pyspark.sql.shuffle.partitions默认是 200数据量小的时候反而因为分区过多导致调度开销大可以降到 50数据量大时提到 400 以上让每个分区处理的数据块更小。--executor-memory根据集群实际资源调整不要超过单节点物理内存的 80%。5.3 Django 查询慢的定位方法接口响应超过 1 秒时先用 Django Debug Toolbar 看 SQL 执行时间或者直接在 MySQL 里EXPLAINEXPLAIN SELECT shop_name, total_sales FROM dws_shop_stats WHERE stat_date 2025-01-01 ORDER BY total_sales DESC LIMIT 10;如果type列显示ALL说明走了全表扫描需要在stat_date和total_sales上建联合索引CREATE INDEX idx_date_sales ON dws_shop_stats (stat_date, total_sales DESC);索引顺序很重要stat_date放前面因为它是等值过滤total_sales放后面用于排序这样 MySQL 可以直接利用索引有序性省掉 filesort。5.4 推荐结果重复或冷启动新用户没有历史行为ALS 无法给出个性化推荐。兜底策略是返回全局热销榜用total_sales排序取 Top-N。另外检查训练数据里是否有重复的(user_id, dish_id)记录ALS 对重复评分敏感训练前用distinct()去重。最后一章收在一个具体技巧上把 Spark 任务的结果写回 MySQL 时用mode(overwrite)会清空整张表再写入如果任务中途失败表里数据就没了。更稳的做法是先写到临时表成功后再RENAME TABLE原子替换或者用INSERT OVERWRITE配合分区字段只覆盖当天分区。这个细节在论文里通常不会写但实际部署时能避免半夜被叫起来恢复数据。本文还有配套的精品资源点击获取
返回列表