基于Hadoop与PySpark的农产品推荐系统设计与实现
2026/9/15 2:48:48 网站建设 项目流程

每年到毕业季,计算机专业的同学都在为选题发愁。系统做大了吧,怕做不完毕不了业;做小了吧,又怕答辩的时候没有亮点被老师问住。如果你正在这个阶段,我强烈建议你看看"大数据+推荐系统+爬虫"这个组合,尤其是落到农产品这个细分领域。这篇博文我会完整拆解一个已经跑通的设计方案:基于Hadoop + PySpark + Scrapy的农产品推荐系统,从数据采集、存储清洗、推荐引擎到可视化大屏,一条链路全部打通。不管你是想参考选题、借鉴架构,还是准备照着重写一遍,这篇文章应该都能给你实在的帮助。

先说这个项目的整体形态:Scrapy负责从公开的农产品信息网站上爬取品种、价格、产地、热度等数据;Hadoop HDFS作为底层存储;PySpark负责离线清洗、特征加工和推荐计算;最后通过一个Web后台和可视化大屏把结果展示出来。它覆盖了大数据毕业设计最常见的几个考察点——分布式存储、分布式计算、数据采集、数据可视化、算法应用,该有的都有了,而且每个模块都可以单独拿出来展开讲。

1. 选题拆解:为什么"农产品+大数据推荐"是毕业设计的稳妥选择

1.1 农产品数据场景的特殊价值

农产品跟普通电商商品有一个本质区别:它的数据维度更散、信息更不透明。同一个西红柿,不同产地、不同批次、不同批发市场,价格差异可能非常大。而消费者在选购时往往只看到超市货架上的一个价格标签,完全不知道这个价格在同类产品里处于什么水平,也不知道当前这个季节到底什么菜性价比最高。

把这种信息差做成一个推荐系统,逻辑就非常顺:系统采集农产品数据,分析价格规律、产地分布、季节性变化,再结合用户的历史浏览和偏好,给用户推荐"你可能想买/想关注的农产品"。这个场景既贴近民生,又有真实的数据复杂性,写进毕业论文里,研究意义和社会价值都比"基于某某数据的某某推荐"要有说服力得多。

1.2 技术栈组合的考量:这套选型为什么"能打"

很多同学的毕设选型有一个误区:只选自己最熟练的技术,最后做出来的东西虽然能跑,但是技术上没有可写的亮点。还有一类同学反过来,什么新用什么,结果做到一半发现自己根本hold不住,无奈返工。

这套方案选Hadoop + PySpark + Scrapy,其实是在"毕设考察点"和"工程可实现性"之间取了一个很好的平衡:

  • Hadoop:虽然业界现在很多场景已经转向更轻量的大数据组件,但Hadoop依然是绝大多数高校大数据课程的核心内容。毕设里用Hadoop,答辩时老师天然有话说、有得问,你也好回答。
  • PySpark:这是加分项。它意味着你不只是在Hadoop上跑MapReduce,而是用了更现代的内存计算框架。同时PySpark的API对Python开发者极其友好,写起来比Java版MapReduce不知道舒服多少倍。
  • Scrapy:它是Python爬虫领域的事实标准,稳定性高、文档全、扩展性强。用Scrapy爬数据,工作量可控,而且"爬虫+反爬"这个点本身就很有话题性。

这三个组件串起来,就构成了一条完整的大数据处理流水线:采集 -> 存储 -> 清洗 -> 计算 -> 应用。这个架构无论写进论文还是PPT,都可以画成一个漂亮的架构图,每一层都有对应的技术点可讲。

2. 第一层:Scrapy农产品爬虫——数据从哪来、怎么稳定地拿

2.1 目标网站分析与爬虫策略设计

爬虫第一步不是写代码,而是确定数据源。做农产品项目,目标站点一般有两类:一类是批发市场类的价格信息网站,数据以"品种+市场+价格"为主;另一类是B2B农产品电商平台,数据以"店铺+产品+价格+销量"为主。两种各有优劣:价格信息网站结构简单、反爬弱、但字段少;B2B平台字段丰富、更有推荐场景,但反爬较强、数据噪音也大。

我建议毕设场景下优先选择前者作为主数据源,因为你的核心精力应该放在"存储+计算+推荐"上,而不是耗在爬虫对抗上。当然,如果指导老师要求数据量必须足够大,可以再补充一两个数据源做交叉验证。

爬虫的架构我建议这样设计:

# 项目结构规划 agriculture_spider/ ├── scrapy.cfg ├── requirements.txt ├── agriculture/ │ ├── __init__.py │ ├── items.py # 定义数据字段结构 │ ├── middlewares.py # 下载中间件(代理、UA轮换) │ ├── pipelines.py # 数据管道(清洗、去重、落库) │ ├── settings.py # 爬虫配置文件 │ └── spiders/ │ └── product_spider.py # 核心爬虫

2.2 页面解析与字段设计

以批发价格类网站为例,列表页通常展示产品名称、品类、最低价、最高价、产地、发布日期。详情页可能还有规格、单位、历史价格走势。解析的时候直接上Scrapy的Selector(基于XPath或CSS),比BeautifulSoup+requests的组合要快,而且支持链式提取,代码好维护。

字段设计是很多人容易忽略的地方。提前想清楚最终要做什么分析和推荐,爬的时候就把字段定义到位,能省掉后面大量的返工时间。我当时设计的items.py大概长这样:

import scrapy class ProductItem(scrapy.Item): product_id = scrapy.Field() # 产品唯一标识 name = scrapy.Field() # 产品名称 category = scrapy.Field() # 品类(叶菜类/根茎类/果菜类等) origin = scrapy.Field() # 产地 market = scrapy.Field() # 批发市场名称 low_price = scrapy.Field() # 最低价(元/斤) high_price = scrapy.Field() # 最高价(元/斤) avg_price = scrapy.Field() # 平均价 unit = scrapy.Field() # 单位 spec = scrapy.Field() # 规格 update_time = scrapy.Field() # 发布时间 crawl_time = scrapy.Field() # 爬取时间

字段这里有几个细节值得注意:

  • avg_price不要用前端展示的字段,最好用(low_price + high_price) / 2自己算,因为不同网站的均价口径不一样,有的含税有的不含税,统一自己算才能保证数据一致。
  • crawl_timeupdate_time分开存,为的是后面做数据新鲜度分析。推荐系统里,一个商品的"信息新鲜度"可以直接影响推荐权重,这是做好几个同类产品之后学到的经验。
  • 产地、市场这种文本字段,爬下来是什么样就存成什么样,清洗阶段再统一做规范化。不要想着在爬虫里做太多清洗,爬虫的任务是"拿得全",清洗的任务是"变得净"。

2.3 反爬应对与爬虫稳定性

农产品信息类网站的反爬通常很基础,最常见的限制就是请求频率和User-Agent。应对手段也不用太复杂:

  • UA轮换:准备一个常见UA列表,在Downloader Middleware里随机选择。
  • 访问间隔:在settings.py中设置DOWNLOAD_DELAY = 1.5,或者在爬虫代码里用time.sleep()控制。1.5秒是个比较稳妥的值,太快容易被封IP,太慢爬几十万条数据要等太长时间。
  • 错误重试:把RETRY_ENABLED打开,设置RETRY_TIMES = 3。对临时性的连接超时,重试能解决大部分问题。
# settings.py 关键配置 DOWNLOAD_DELAY = 1.5 RANDOMIZE_DOWNLOAD_DELAY = True USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' ROBOTSTXT_OBEY = False CONCURRENT_REQUESTS = 16 RETRY_ENABLED = True RETRY_TIMES = 3 DOWNLOAD_TIMEOUT = 15

这里有个非常重要但容易被学生忽略的点:爬虫跑起来之后,一定要做断点续爬的规划。Scrapy本身带有JOBDIR参数,支持暂停和恢复:

scrapy crawl product -s JOBDIR=crawls/product

如果你打算做一个数据量上万的完整爬虫,建议先小批量测试(比如先跑100条),确认数据质量和解析逻辑没问题,再全量开跑。

如果你爬取的网站包含动态加载的iframe,比如农产品大宗交易平台常见的内嵌行情页面(这类页面数据往往是异步加载的),Scrapy标准下载器就无能为力了。处理方案有两个:一是抓包找到数据接口,直接请求JSON接口,这是最优解;二是集成Playwright或Selenium做浏览器渲染。根据我实际操作的经验,90%的情况下都能抓到直接返回数据的Ajax接口,完全没必要上重型的浏览器自动化,后者又会带来IP封禁问题。

3. 第二层:Hadoop HDFS存储与PySpark清洗——数据落地与加工

3.1 数据入HDFS的流程设计

爬虫落地的数据先进入一个中转区域,我建议用本地MySQL或者直接以CSV/JSON文件形式暂存,之后通过脚本统一上传到HDFS。为什么不直接从爬虫写到HDFS?因为爬虫是持续运行的,逐条写HDFS会产生大量小文件,而HDFS对海量小文件的元数据管理效率很低,这是Hadoop一个非常经典的"反模式"。

更合理的做法是:爬虫按天分批次产出文件,每天定时/手动上传一次HDFS。上传方式直接用命令行工具:

hdfs dfs -mkdir -p /user/agriculture/raw/product/20240616 hdfs dfs -put /data/crawl/20240616/product_*.json /user/agriculture/raw/product/20240616/

这样按日期分目录存储,后面做增量处理、数据回溯都很方便。如果服务器上安装了Hive,还可以在这个路径上直接建一张外部表,把HDFS上的JSON文件映射成结构化表,查询时用SQL就行。毕设场景下不强制用Hive,但如果你会的话,答辩时能加不少印象分。

3.2 PySpark的清洗策略

数据上了HDFS之后,脏数据问题接踵而至。农产品数据的脏,脏得很典型:

  • 价格字段里有"暂无"、"面议"、"--"这类非数字字符串;
  • 产地字段同一省份写法五花八门,比如"山东寿光"、"山东省寿光市"、"寿光";
  • 品种名称不统一,"土豆"和"马铃薯"混用;
  • 重复数据,同一天同一市场的同一品种爬了两遍;
  • 异常价格,比如某些字段把"元/公斤"和"元/斤"混在一起存。

这些脏数据如果直接喂给推荐算法,结果会非常离谱。我的清洗方案用PySpark来实现,核心逻辑分四步:

from pyspark.sql import SparkSession from pyspark.sql.functions import col, when, udf, regexp_replace from pyspark.sql.types import DoubleType # 初始化Spark Session spark = SparkSession.builder \ .appName("product_data_clean") \ .config("spark.sql.warehouse.dir", "hdfs://localhost:9000/user/hive/warehouse") \ .getOrCreate() # 读取HDFS上的JSON原始数据 df = spark.read.json("hdfs://localhost:9000/user/agriculture/raw/product/20240616/") # 清洗1:价格字段转为数值类型,无法转换的置空 df = df.withColumn("low_price", col("low_price").cast(DoubleType())) df = df.withColumn("high_price", col("high_price").cast(DoubleType())) df = df.withColumn("avg_price", col("avg_price").cast(DoubleType())) # 清洗2:过滤掉价格异常的数据(均价低于0或高于1000元的视为异常) df = df.filter(col("avg_price").isNotNull()) df = df.filter((col("avg_price") > 0) & (col("avg_price") < 1000)) # 清洗3:产地字段统一去掉省市后缀 df = df.withColumn("origin", regexp_replace(col("origin"), "省|市$", "")) # 清洗4:按唯一业务键去重 df = df.dropDuplicates(["market", "name", "crawl_time"])

关于这些清洗逻辑,有几条我想单独拎出来讲:

价格上限1000元这个阈值不是拍脑袋定的。我后来分析过数据分布,正常情况下农产品每斤价格在0.1到200元之间,超过1000的有两种可能:一是单价和总价混了(比如一箱苹果30斤,总价150,字段却写成150元/斤),二是部分高档进口水果确实贵,但那不是大众消费品。做这种过滤之前,最好先跑一个describe()看一下数据分布,再决定阈值,不要一上来就盲目过滤。

去重键的选择要谨慎。"市场+名称+爬取时间"只是我自己这个项目场景下的一个去重组合。如果你的数据源里有产品ID,直接用ID去重是最稳的。农产品数据有个特殊的麻烦——完全不同的产地和批次下,同一种产品会有不同的规格和价格,所以去重必须结合多个维度,建议"名称+规格+市场"作为业务键,时间维度用"当天"做窗口。

3.3 为什么清洗计算要放到PySpark而不是爬虫里

这个问题在答辩时极有可能被问到,想清楚了你就能答得漂亮。

从技术上讲,爬虫进程本质上是单机I/O密集型的,主要瓶颈在请求网络和解析HTML,把CPU密集型的清洗工作塞进爬虫进程,会拖慢爬取速度,还会让爬虫和数据清洗的耦合度变高——爬虫挂了清洗逻辑跟着停,清洗改了爬虫也得跟着动。

更重要的是,清洗操作往往需要全量数据来做判断。比如你要识别并合并产地的同义地名("寿光"和"寿光市"),单看一条数据是判断不了的,需要看整个数据集分布。PySpark跑在Hadoop集群上,面对的是全量数据,做这类"全局视野"的操作得天独厚。

4. 第三层:推荐系统落地——用户到底该看什么农产品

4.1 推荐场景的重新定义

先泼一盆冷水:大部分毕设场景下,你没有真实的用户行为数据,没有几万用户几百万条点击日志。所以不要一上来就整复杂的矩阵分解或者深度推荐模型,没有数据支撑,模型跑不起来,论文里也只能泛泛而谈。

农产品推荐系统在毕设这个约束下,更合适的定位是"基于内容的召回 + 规则化的个性化排序"。

推荐场景可以这样定义:系统里有若干农产品(数据来自爬虫),有若干用户(来自系统注册表),用户可以对农产品产生浏览、收藏、加购行为。系统根据用户的历史行为,推荐他"可能感兴趣的农产品"。没有真实行为数据没关系,可以在本地模拟生成一批合理的用户行为数据来驱动算法,论文里明确说明"采用模拟数据完成方法验证"就可以,这在学术研究中也是常见做法。

4.2 混合推荐策略:协同过滤+基于内容的双通道

我一直建议在毕设里把召回设计成两条路同时走:一条是基于用户的协同过滤,一条是基于内容的相似推荐。最后融合排序。这样推荐的解释性更强,答辩时也更有的聊。

协同过滤通道用物品协同过滤(Item-CF),它比用户协同过滤在前景上更好解释、也更稳定。核心逻辑是"喜欢这个农产品的用户,也喜欢那些农产品"。代码用PySpark的ALS(交替最小二乘法)做,这是Spark MLlib里最成熟的推荐算法。

from pyspark.ml.recommendation import ALS from pyspark.ml.evaluation import RegressionEvaluator # 准备行为数据:userId, productId, rating(可以是浏览量/收藏/购买的加权分) ratings = spark.read.csv("hdfs://localhost:9000/user/agriculture/behavior/", header=True) # 拆分为训练集和测试集 (train, test) = ratings.randomSplit([0.8, 0.2], seed=42) # ALS模型训练 als = ALS( maxIter=10, regParam=0.1, userCol="userId", itemCol="productId", ratingCol="rating", coldStartStrategy="drop" ) model = als.fit(train) # 为所有用户生成Top-N推荐 userRecs = model.recommendForAllUsers(20)

这里面注意coldStartStrategy="drop",这个参数如果漏了,预测时遇到新用户或者新物品会给NaN,后续代码直接就崩了。

基于内容的通道根据农产品的品类、产地、价格区间构建特征向量,计算相似度。比如用户经常浏览"西红柿",而且偏好"山东产"的,那系统会把产地为山东的其他果菜类产品推给他。这里不需要复杂的深度语义模型,用简单的TF-IDF或者类别特征拼接,然后算余弦相似度就够了。

from pyspark.ml.feature import StringIndexer, OneHotEncoder, VectorAssembler from pyspark.ml.linalg import Vectors # 构建农产品特征向量:品类独热编码 + 地区编码 + 价格分箱 category_encoder = OneHotEncoder(inputCol="category_index", outputCol="category_vec") assembler = VectorAssembler( inputCols=["category_vec", "origin_index", "price_bucket"], outputCol="features" )

然后对每个用户最近交互过的N个农产品,取它们的内容相似商品作为候选,再跟Item-CF的候选集合并,最终用得分加权排序。

4.3 推荐结果的工程化输出

算法计算完成之后,结果要落到存储里供Web端调用。两种常见方案:

  • 结果写入Redis,用user:{userId}:rec_list作为key,value存JSON数组,Web端查询时直接读缓存,性能好。
  • 结果写回HDFS,适合做离线的批量导出和进一步分析。

毕设项目我建议用Redis,既能体现技术栈的丰富度,实际开发调试也很方便。数据结构大概长这样:

[ {"product_id": "p1001", "name": "红富士苹果", "score": 0.98, "reason": "你常买的水果类", "origin": "山东"}, {"product_id": "p1088", "name": "烟台樱桃", "score": 0.94, "reason": "同类用户也在关注", "origin": "山东"} ]

注意我加了一个reason字段。用户在页面上看到"为什么推荐这个"的时候,点击率会有明显变化。做产品讲究解释性,推荐系统更是如此。毕业设计里把这个字段放进去,答辩时也是一个容易被老师肯定的亮点。

5. 第四层:可视化与Web端——数据怎么让人看懂

5.1 图表本身不产生价值,指标才产生价值

农产品可视化的关键不在于你用了多炫酷的图表库,而在于你是否选择了合适的分析指标。我看过太多的毕设大屏,满屏ECharts图表,实际全是炒鸡模糊的"数据展示",没有任何有洞察力的分析结论,答辩时经不起追问。

在农产品这个场景下,有几个真正值得做进大屏的分析主题:

分析主题核心指标图表形态分析价值
市场价格总览各类农产品当日均价、周涨跌幅折线图+柱状图回答"今天什么菜贵/便宜"
产地分布各省份产品数量、价格带分布地图+散点图回答"好产品从哪里来"
品类结构各品类产品数占比、平均价饼图/环形图回答"平台品类结构是否合理"
价格异常预警超出历史均值±2倍标准差的产品表格+高亮回答"有没有异常价格波动"
用户行为概览注册用户数、总行为数、活跃度数字翻牌+趋势图回答"系统整体使用情况如何"

价格预警这个是我比较得意的一个点。算每个农产品近30天的价格均值和标准差,然后用今天跟均值做比较:

from pyspark.sql.functions import stddev, avg, col price_stats = df.groupBy("product_id").agg( avg("avg_price").alias("price_mean"), stddev("avg_price").alias("price_std") ) # 标记异常:当日价格超出均值±2个标准差 result = df.join(price_stats, "product_id") \ .withColumn("is_anomaly", (col("avg_price") > col("price_mean") + 2 * col("price_std")) | (col("avg_price") < col("price_mean") - 2 * col("price_std")))

这个逻辑很简单,做出来效果却非常好——大屏上"爆涨爆跌农产品TOP10"这个模块,既直观又有应用价值。

5.2 大屏与后台的联调方案

可视化大屏通常有几类实现方式,毕设里我建议选"Vue + ECharts",轻量、开发快、生态好。大屏页面通过调用后端接口获取统计数据,后端从HDFS或者处理好的结果表里读数据返回JSON。

一个容易踩的坑是后端查询速度。如果你的大屏页面上有个指标要实时从HDFS上跑一个全局聚合,那响应时间可能是秒级甚至分钟级。大屏上放一个一直在转圈的loading图,演示效果会非常尴尬。

正确做法是:把大屏需要的指标提前用PySpark离线计算好,写入MySQL或Redis,Web后端只做简单的查询转发。离线和在线分离,这是真正的大数据系统架构思维,也是答辩时值得拿出来讲的设计决策。

6. 代码之外的交付物:文档、PPT、演示视频的配合

6.1 论文(LW文档)的写作策略

做毕设,代码只是其中一半的分数,论文占另一半。写论文的时候,我建议按这条主线走:提出问题(农产品信息不透明)-> 设计解决方案(基于大数据采集与推荐的系统)-> 技术细节(分章节详细描述)-> 实验验证(推荐效果评估、系统功能测试)。这本身就是一个经典的研究闭环。

技术描述上注意两点:一是每个组件的选型理由要写,比如"选用Hadoop作为存储平台因为其适合海量离线数据的批量处理";二是推荐算法的评估必须用数据说话,离线的时候算一下RMSE、召回率、精确率,画个曲线放进去,比写一千字主观描述都有说服力。

6.2 PPT与讲解视频的展示技巧

答辩PPT不需要把所有代码贴上去,但要画好两张图:系统架构图数据流图。系统架构图从上到下画:数据采集层 -> 数据存储层 -> 数据处理层 -> 算法引擎层 -> 应用展示层;数据流图就画爬虫数据怎么一步一步流转到最终的推荐结果。这两张图画好了,答辩的时候照着讲,思路会非常清晰。

讲解视频的做法,我强烈推荐分段录制,而不是一个视频从头讲到尾。分成4段:环境启动与数据采集 -> 数据处理流程 -> 推荐算法演示 -> 可视化大屏展示。每段控制在5分钟以内。这样做,哪怕现场出问题,也可以放某一段视频来兜底,演示不翻车。

6.3 我在实际部署中踩过的坑

最后分享几个我在这类项目里真实踩过的坑,希望你能绕过去:

第一个是Hadoop和本机端口冲突。HDFS的NameNode默认用9000端口,但很多本地开发框架也爱用9000(比如PHP-FPM、Docker的一些服务),一旦冲突NameNode直接起不来。解决方式很土但很实用:装好Hadoop之后立刻把core-site.xml里的端口改掉,改成9001或者8020。

第二个是PySpark和Anaconda的兼容性问题。如果你的Python是用Anaconda管理的,直接pip install pyspark然后在jupyter里from pyspark.sql import SparkSession,经常会出现java.io.IOException: Cannot run program "python3"的报错。这是因为PySpark会在环境变量里找python,但找不到。解决办法是在代码里显式指定:

import os os.environ["PYSPARK_PYTHON"] = "/usr/bin/python3" os.environ["JAVA_HOME"] = "/usr/local/jdk8"

这段代码要放在import pyspark之前。这三个变量不提前配好,你会在环境问题上浪费一整个晚上。

第三个坑是HDFS上的文件块大小和副本数。默认128MB块,3副本,这个配置对毕设的小数据量(比如几GB)其实非常浪费。如果只是本地伪分布式的环境,建议把副本数调成1,块大小调小一点,既能节省磁盘又能加快任务执行。修改路径在hdfs-site.xml

<property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.blocksize</name> <value>64m</value> </property>

6.4 如果你想继续扩展

有些人做完毕设就万事大吉了,但如果你有精力,这个系统其实有几个非常好的扩展方向。

第一个,把实时价格监控加上。现在做的是离线批处理,加一个Kafka + Spark Streaming的实时处理通道之后,数据流的完整度会提升一个量级,可以做到"今天上午的价格变动,下午就能进大屏"。但这个难度明显偏高,除非你基础很扎实,否则不建议在毕设阶段贸然上。

第二个,引入深度语义模型。比如用BERT对农产品介绍文本做特征抽取,替代我之前用的TF-IDF。效果会有提升,但对设备和数据量的要求也上来了,毕设阶段可以放在"展望"部分写。

第三个,也是我现在回头看最推荐的一个方向:把这个系统当成一个学习的载体,而不只是一个毕设。把爬虫做得更健壮,把数据处理脚本写得更规范,把推荐算法的评估做得更严谨。你的简历上写"设计并实现了一个基于Hadoop的大数据推荐系统"和"用Python写了个爬虫"完全是两个重量级的项目描述,面试官看到前者,至少愿意多问你两个问题。

以上这些经验,都是我在跑通这个项目的过程中真实遇到的问题和对应的解决方案。如果你要动手做,建议给自己留足两个月左右的完整时间,第一个月把爬虫和Hadoop环境搞定,第二个月做Spark处理和推荐,最后留两周给可视化、论文、PPT和录制视频,整体节奏会比较从容。代码跑的流畅是一回事,能跟答辩老师清楚讲明白系统每个模块为什么这么设计,那才是真正拿高分的底牌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询