每年三四月份,毕设群里就开始热闹起来。农产品推荐系统这个题目,几乎年年都有,看起来就是 Hadoop、PySpark、Scrapy 这几个技术名词的堆叠,但真正动手做的时候,里面的细节多到让人头皮发麻。我这个项目从爬虫采集、离线清洗、统计分析到推荐模型、可视化大屏,整条链路完整跑通,配套的代码、论文、PPT 和讲解视频也都备齐了。今天把这套东西从零到一的过程彻底捋一遍,重点讲讲每个环节为什么这么设计、哪些地方容易翻车,给准备走大数据方向毕设的同学一条可以照抄的落地路径。
这不是一个普通的管理系统。它的核心是一条完整的大数据流水线:Scrapy 负责从公开农产品信息网站抓取价格、产地、销量等数据,Hadoop 提供分布式文件存储底座,PySpark 完成离线清洗、统计以及推荐模型训练,最后用可视化大屏把分析结果呈现出来,并把推荐结果通过 Web 接口输出给用户。无论你是第一次接触大数据项目,还是已经搭过一些简单应用,这篇文章里提到的思路和踩坑经验,都能帮你少走不少弯路。
1. 项目到底要做什么:需求拆解与技术选型
1.1 从毕设题目到功能清单
拿到这种题目,第一件事不是急着打开终端敲命令,而是把题目拆成能落地交付的功能模块。我当时给自己列了一张清单,刚开始很粗,后来越拆越细,最后变成了这样:
- 农产品数据采集:用 Scrapy 从公开信息源抓取农产品名称、品类、产地、价格、销量、发布时间等数据。
- 数据存储与预处理:原始数据落 HDFS,用 PySpark 做清洗、去重、格式标准化。
- 统计分析:产出价格趋势、产地分布、品类占比、市场销量排行等指标。
- 推荐系统:基于用户行为数据训练协同过滤模型,输出个性化农产品推荐列表。
- 可视化大屏:用图表把统计结果展示出来,兼顾答辩演示效果。
- Web 后端:提供推荐接口和图表数据接口,打通前后端链路。
这个需求拆解的过程非常关键。很多同学一上来就写代码,结果做着做着发现功能边界模糊,比如推荐系统到底要不要做用户注册登录,可视化大屏要展示多少张图,这些在开题阶段没想清楚,后期改起来特别痛苦。我当时是把每个模块的输入、输出、数据流向都画在纸上,虽然不能叫架构图,但至少心里有数,知道这套系统从数据进来到最终展示要走哪几步。
也有同学会问:“是不是一定要把 Hadoop 和 PySpark 都放进去?”说实话,如果只做一个小型推荐网站,MySQL 加 Python 就足够了。但这是大数据方向的毕设,题目本身已经限定了技术栈,所以这些组件必须出现在正确的位置上。关键是不要让它们变成摆设,而是让每个组件都能解释清楚它解决了什么问题。
1.2 为什么是 Hadoop + PySpark + Scrapy 这个组合
技术选型这块,我想说点真心话。这个组合在真实的大数据开发岗位中确实常见,但在毕设场景下,它更多是为了模拟企业级数据处理链路。Scrapy 是 Python 生态里最成熟的爬虫框架,支持异步并发、中间件扩展、管道处理,用它抓取农产品数据再合适不过。Hadoop 提供 HDFS 分布式存储,虽然我的数据量远远达不到“分布式”的规模,但把原始数据按目录结构放到 HDFS 上,再配合 Hive 做查询分析,整条链路就有了大数据项目的骨架。
PySpark 的价值体现在两层。第一层是数据清洗和统计,用 DataFrame API 处理几万条或几十万条数据非常顺手,而且能直接读写 HDFS,不需要像传统方案那样先下载到本地再处理。第二层是机器学习,PySpark 自带的 MLlib 里有 ALS 协同过滤算法,这是推荐系统的核心,可以直接拿来训练模型、生成推荐列表,不用自己去实现矩阵分解。
Scrapy 放在最前面,是因为整个系统的数据源头是它。爬虫爬不到数据,后面所有模块都是空转。我在选型时对比过 requests 加 BeautifulSoup 的方案,那个方案写起来简单,但遇到需要并发抓取、请求重试、数据去重、管道存储时就捉襟见肘。Scrapy 的架构天然把这些能力都包含了,定义好 Item 和 Pipeline,后期扩展特别方便。
还有一个现实问题:环境怎么搭。Hadoop 和 Spark 直接装在虚拟机上非常容易翻车,系统路径、Java 版本、权限配置任何一个环节出错都能耗掉半天。我后来尝试了 Docker 镜像方案,直接拉一个已经配好 Hadoop 的基础镜像,再手动调整配置,省去了大量重复操作。这里给一个建议:如果毕设只要求跑通流程,伪分布式模式完全够用,不需要搭建多节点集群,否则光是节点通信和资源调度的问题就能让人崩溃。
2. 数据从哪来:Scrapy 爬虫的完整落地细节
2.1 爬取目标与字段设计
爬虫要做得好,首先要明确抓什么、存什么。农产品推荐系统需要的数据,核心是商品信息和价格信息。我当时把目标设定为公开的农产品价格信息网站和批发市场数据页面,这类站点数据更新及时、字段结构相对固定,适合用于学习和研究。字段设计上,我最终确定了这样一组:
- name:农产品名称,比如“西红柿”“黄瓜”“富士苹果”。
- category:品类,用于分类统计和推荐时的内容匹配。
- origin:产地,用于产地分布分析和用户偏好判断。
- price:单价,单位统一为元/公斤,遇到元/斤的页面要主动转换。
- unit:计量单位,保留原始单位,防止转换出错。
- market:所属市场,用于市场维度的对比。
- date:发布日期,用于时间序列分析。
- sales_volume:销量,部分站点没有这个字段,没有就直接置 0。
字段设计看起来简单,其实坑很多。比如价格单位不统一、产地信息为 null、同一个商品在不同市场名称写法不一样,这些都会在后续 PySpark 清洗阶段变成麻烦。我的经验是:在爬虫阶段就尽量把数据规范化,能用规则转换的就在 Pipeline 里转换,不要把所有脏数据都丢给离线任务,否则清洗逻辑会变得极其复杂。
我见过不少同学把日期字段存成“2025-03-18 14:23:00”,看起来没问题,但到了按天做统计时,还得先截断再分组。我当时直接统一成 yyyy-MM-dd 格式,省了后面很多事。品类字段也是类似,需要建立一套基础分类映射,比如把“西红柿”“番茄”归到“茄果类”,不然统计出来的品类占比会很零散。
2.2 Scrapy 框架搭建与核心代码
Scrapy 项目结构并不复杂,核心是 items.py、pipelines.py 和 spiders 目录下的爬虫文件。我建的项目叫 agri_scraper,目录里最重要的几个文件按职责拆分得很清楚。先定义一个 Item,把所有字段声明出来,这样后续 Pipeline 处理时能清楚地知道数据长什么样:
import scrapy class AgriProductItem(scrapy.Item): name = scrapy.Field() category = scrapy.Field() origin = scrapy.Field() price = scrapy.Field() unit = scrapy.Field() market = scrapy.Field() date = scrapy.Field() sales_volume = scrapy.Field()Spider 的核心逻辑是解析页面,提取目标字段。下面是一个简化的示例,实际页面结构需要根据目标站点调整 CSS 选择器或 XPath :
import scrapy from agri_scraper.items import AgriProductItem class AgriPriceSpider(scrapy.Spider): name = "agri_price" allowed_domains = ["example-agri.com"] start_urls = ["https://example-agri.com/price/list"] def parse(self, response): for row in response.css("tr.item-row"): item = AgriProductItem() item["name"] = row.css("td.name::text").get() item["category"] = self.classify(row.css("td.name::text").get()) item["origin"] = row.css("td.origin::text").get() item["price"] = self.parse_price(row.css("td.price::text").get()) item["unit"] = "元/公斤" item["market"] = row.css("td.market::text").get() item["date"] = response.url.split("date=")[-1] item["sales_volume"] = 0 yield item next_page = response.css("a.next::attr(href)").get() if next_page: yield response.follow(next_page, callback=self.parse)这段代码里有两个细节值得注意。第一是 classify 方法,它负责把原始名称归到统一的品类体系里,内部就是一组关键词映射表。第二是 parse_price 方法,因为页面上的价格可能是“3.5元/斤”“2元/kg”这种非标准格式,必须提前写解析函数,统一转成浮动数值。做爬虫的核心不是把数据拿回来,而是拿回来的数据能直接给下游用。
Pipeline 负责把数据写到存储系统。我当时的做法是先写入 MySQL,再通过同步脚本每天将新增数据导出为 CSV 并上传到 HDFS。这个流程比直接在 Pipeline 里调用 HDFS API 更稳定,因为爬虫的写入频率很高,直接走 HDFS 容易产生大量小文件,后续处理反而低效。Pipeline 里还会做简单的合规校验,比如价格大于 0 且不超过某个上限,明显异常的数据直接丢弃。
2.3 反爬机制与动态页面抓取的实战处理
反爬是爬虫项目里绕不开的话题,但想强调一句:我们只采集公开数据、只用于学习和研究,必须遵守目标站点的访问频率要求和 robots.txt 声明,不要给目标服务器造成压力。在这个前提下,常见的几个反爬策略可以这样应对:
- User-Agent 轮换:维护一个 UA 列表,在 Downloader Middleware 中随机选择。很多站点只会检查 UA 是否为常见浏览器。
- 请求间隔:通过 DOWNLOAD_DELAY 设置 1 到 3 秒的下载延迟,可以显著降低被封风险。比设置并发数更实在。
- 失败重试:RETRY_TIMES 设为 3,遇到 502、503 时自动重试,而不是直接跳过。
- 动态页面处理:部分农产品价格页面是 iframe 嵌进去的,直接请求当前 URL 只能拿到空壳。我引入了 scrapy-playwright 来渲染动态内容,等 iframe 加载完成后再提取数据。
scrapy-playwright 的集成并不复杂,核心是在 settings.py 中开启 Playwright 支持,然后在 Request 中指定 meta 参数:
import scrapy class DynamicSpider(scrapy.Spider): name = "agri_dynamic" def start_requests(self): yield scrapy.Request( url="https://example-agri.com/dynamic-price", meta={"playwright": True, "playwright_include_page": True}, callback=self.parse ) async def parse(self, response): page = response.meta["playwright_page"] # 等待 iframe 渲染完成后提取 await page.wait_for_selector("iframe") frame = page.frames[1] rows = await frame.query_selector_all("tr") # 继续处理 rows这种异步处理方式在动态页面里非常管用,特别是当数据被嵌在多层 iframe 中时,普通请求根本拿不到。但要注意,启动 Playwright 后爬虫运行速度会明显下降,所以我的策略是:普通静态页面走 Scrapy 原生请求,只有遇到动态 iframe 时才用 Playwright,二者搭配,效率和安全之间取一个平衡。
爬虫写完后,一定要单独验证数据质量。我的做法是抓取完当天数据后,打开 MySQL 表看几个抽样记录,确认价格字段没有空值、日期字段没有乱码、品类字段都有归属。这些检查看着琐碎,但确实能为后面省下大把时间。
3. 数据怎么存怎么算:Hadoop + PySpark 离线链路
3.1 HDFS 目录设计与数据入库流程
Hadoop 环境搭好之后,第一件事是规划 HDFS 目录。这个看似不起眼,实际上体现了一个人对大数据工程的理解。我采用的目录结构是这样的:
- /agri/raw:存放从 MySQL 导出的原始数据,按日期分区,比如 /agri/raw/2025-03-18/。
- /agri/clean:存放 PySpark 清洗后的数据,同样按日期分区。
- /agri/stats:存放统计结果,比如品类价格汇总、市场排行。
- /agri/reco:存放推荐模型输出的结果,供 Web 接口读取。
为什么要按日期分区?因为农产品数据是典型的时间序列数据,每天的爬取结果只有当天完整,后续需要按天做增量处理和回溯分析。如果所有数据堆在一个目录里,统计时全表扫描,不仅慢,而且逻辑混乱。按日期分区后,PySpark 读取时可以直接指定分区路径,处理效率和数据组织都清晰很多。
数据入库流程上,我选择的是“先 MySQL 后 HDFS”。爬虫 Pipeline 写入 MySQL 之后,每天定时执行一个 Shell 脚本,把前一天的数据导出为 CSV,再用 hdfs dfs -put 命令上传到 HDFS。这里有一个小细节:CSV 导出时建议去掉表头,或者统一在 PySpark 读取时指定 header=True,避免后续处理时多出一行脏数据。
有的同学可能觉得绕了一圈有点多余,为什么不直接让爬虫写 HDFS?直接写的问题是爬虫产生的记录是逐条的,每条写一个小文件会造成 HDFS 上文件数量爆炸,影响 NameNode 性能和 Spark 读取效率。先攒到 MySQL 再批量导出,正好规避了这个问题。这也是为什么我说工程方案要选“最合理的”而不是“看起来最直接的”。
3.2 PySpark 数据清洗与统计分析
数据进入 HDFS 之后,就到了 PySpark 的主场。清洗这一步做得好不好,直接决定后续统计和推荐模型的质量。我当时写了一个离线任务,主要做这几件事:
- 删除价格为空或小于等于 0 的记录。
- 按字段组合去重,比如 name、market、date 三者相同视为重复。
- 统一日期格式,清洗非法日期。
- 品类字段标准归一化,把同义名称归到统一品类。
- 对价格做异常值过滤,比如超过该品类历史均值 3 倍的数据认为是录入异常。
Python 代码示例如下:
from pyspark.sql import SparkSession from pyspark.sql.functions import col, to_date, avg, count spark = SparkSession.builder \ .appName("AgriCleanAndStats") \ .getOrCreate() df = spark.read.csv("hdfs://localhost:9000/agri/raw/2025-03-18/", header=True, inferSchema=True) # 清洗:去空值、去重复、过滤异常 clean_df = df.filter(col("price").isNotNull()) \ .filter(col("price") > 0) \ .dropDuplicates(["name", "market", "date"]) # 标准化日期 clean_df = clean_df.withColumn("date", to_date(col("date"), "yyyy-MM-dd")) # 按品类统计日平均价格 daily_stats = clean_df.groupBy("category", "date") \ .agg(avg("price").alias("avg_price")) \ .orderBy("date") daily_stats.write.csv("hdfs://localhost:9000/agri/stats/daily_price/", header=True, mode="overwrite")清洗逻辑里的去重非常重要,因为爬虫可能因为重试机制把同一页面抓了两次,或者同一个商品在列表页和详情页各出现一次。按 name、market、date 三元组去重,是我根据农产品数据的实际情况摸索出来的,比简单的全部字段去重更可靠。
统计分析是为了支撑可视化大屏和推荐系统。除了日平均价格,我还会计算每个市场的品类销量排行、产地分布占比、周度价格环比变化等。PySpark 的 DataFrame API 写这类聚合逻辑非常顺手,一个 groupBy 加 agg 就能搞定。有一个经验是:不要在 HDFS 上反复读取原始数据,应该把清洗后的结果持久化到 /agri/clean 目录,后续的统计和推荐都从 clean 目录读取,这样整体运行时间能减少一半以上。
3.3 推荐算法实现:从 ALS 协同过滤到混合策略
推荐系统是这个项目的技术亮点,也是答辩时老师最容易追问的部分。我采用的是两层设计:第一层是 PySpark MLlib 的 ALS 协同过滤,第二层是基于农产品属性的内容推荐,用来解决冷启动问题。
ALS 算法需要用户对物品的评分数据。真实场景下,农产品电商平台的用户行为通常是隐式的,比如浏览、收藏、加入购物车、下单等,并不会直接给出 1 到 5 分的评分。所以我做了一层转化:浏览记 1 分,收藏记 3 分,下单记 5 分,按用户和商品聚合生成评分矩阵。代码实现不算复杂:
from pyspark.ml.recommendation import ALS from pyspark.ml.evaluation import RegressionEvaluator ratings = spark.read.csv("hdfs://localhost:9000/agri/clean/user_behavior/", header=True, inferSchema=True) train, test = ratings.randomSplit([0.8, 0.2], seed=42) als = ALS(userCol="user_id", itemCol="product_id", ratingCol="rating", coldStartStrategy="drop", maxIter=10, regParam=0.1, rank=10) model = als.fit(train) # 评估模型效果 evaluator = RegressionEvaluator(metricName="rmse", labelCol="rating", predictionCol="prediction") predictions = model.transform(test) rmse = evaluator.evaluate(predictions) print(f"RMSE: {rmse}") # 为每个用户生成Top10推荐 user_recs = model.recommendForAllUsers(10) user_recs.write.csv("hdfs://localhost:9000/agri/reco/als_top10/", header=True, mode="overwrite")ALS 调参是个实践活。maxIter 决定了训练迭代次数,太小模型欠拟合,太大耗时增加且可能过拟合;regParam 控制正则化强度,值越大模型越保守;rank 是隐因子个数,影响模型的表达能力。我跑了一组对比实验,最后把正则参数定为 0.1,隐因子数定位 10,RMSE 稳定在 0.8 左右,对于毕设项目完全够用。
冷启动问题怎么解决呢?新用户没有任何行为记录,ALS 无法给他推荐。我用的是基于属性的内容推荐:根据用户注册时选择的偏好品类或当前浏览的商品,在农产品数据中找相似商品。相似度的计算基于品类、产地、价格区间、季节性等特征,用余弦相似度排序。这一段逻辑用 PySpark 实现起来也不难,核心是把商品特征转成向量,然后计算向量之间的相似度。
值得注意的是,推荐结果不能只给用户看几个商品名就完事了,还需要附上推荐理由,比如“因为您浏览过西红柿,所以推荐同品类的新鲜番茄”。这也是提升答辩演示效果的一个重要设计,能让评委直观感受到推荐系统是在“思考”的。
4. 数据怎么看怎么推:可视化大屏与 Web 端实现
4.1 可视化大屏的图表选型与设计
可视化部分是整个项目中最直观、答辩时最容易出彩的模块。我当时的思路不是堆图表数量,而是围绕农产品推荐这个主题,把最有价值的几个分析维度展示出来。最终大屏上放了四类图表:
- 折线图:农产品价格趋势,让人一眼看到不同品类的价格变化情况。
- 柱状图:销量排行,展示当前市场最受欢迎的农产品Top10。
- 饼图:品类占比,说明农产品类别分布情况。
- 地图:产地分布,用散点图展示农产品来源地。
前端可视化我选用的是 ECharts,支持 Vue 和 React,图表类型丰富且交互体验好。如果你希望更快搭建一个数据大屏页面,也可以参考 AVue-Data 这类大屏模板方案,它内置了很多大屏布局和图表组件,省去自己写 CSS 和栅格布局的时间。不过我的建议是:毕设还是至少自己手写一个图表组件,这样才能在答辩时说清楚每个图表的配置逻辑。
大屏设计里有一个容易被忽视的点:数据从哪来。不能把 PySpark 的统计结果直接扔给前端展示,正确的做法是统计结果落库,大屏通过后端接口读取。我当时的流程是:PySpark 统计完写入 MySQL,后端提供 /api/stats/price-trend 等接口,前端页面加载时调用接口拿数据,再填充到 ECharts 的 option 中。这样明确了数据流向,也方便后期更换数据源。
4.2 后端 API 与前后端对接
后端我用的是 FastAPI,轻量、自带接口文档、支持异步,很适合毕设项目。接口设计上,我分成两类:一类是可视化大屏的统计接口,一类是推荐系统的推荐接口。
统计接口返回的是大屏图表需要的数据,比如价格趋势接口,返回按日期排序的品类和均价数组;销量排行接口,返回商品名称和销量排名的列表。推荐接口稍微复杂一点,需要接收用户ID,返回推荐商品列表和推荐理由。FastAPI 的代码示例:
from fastapi import FastAPI from pydantic import BaseModel import pymysql app = FastAPI() DB_CONFIG = { "host": "localhost", "user": "agri", "password": "agri123", "database": "agri_db", "charset": "utf8mb4" } @app.get("/api/stats/price-trend") def price_trend(category: str = "茄果类", days: int = 30): conn = pymysql.connect(**DB_CONFIG) cursor = conn.cursor() sql = """ SELECT date, avg_price FROM daily_price WHERE category = %s ORDER BY date DESC LIMIT %s """ cursor.execute(sql, (category, days)) rows = cursor.fetchall() cursor.close() conn.close() return [{"date": r[0].strftime("%Y-%m-%d"), "avg_price": r[1]} for r in rows] @app.get("/api/recommend/{user_id}") def recommend(user_id: int, top_n: int = 10): # 读取PySpark生成的推荐结果表,返回TopN商品和推荐理由 return {"user_id": user_id, "items": [...]}后端接口写的多了之后,有个体会:代码本身不难,难的是把数据准备到位。推荐结果表是 PySpark 离线生成的,但如果用户行为数据每天都在更新,推荐结果也要定时重算。我当时写了一个计划任务,每天凌晨自动跑一遍清洗、统计和推荐任务,把结果同步到 MySQL。这样前端任何时候打开,都能看到当天最新的推荐数据。
前后端联调的时候,最容易出问题的是字段类型不匹配。比如 PySpark 输出的小数在 MySQL 里可能变成 Decimal,JSON 序列化时容易出问题。解决办法是统一在后端做类型转换,或者建表时就设置好字段类型为 float 或 double,减少转换层的意外。
4.3 推荐结果的落库与用户行为闭环
推荐系统不是一次性任务。用户看了推荐商品之后,产生了新的浏览或下单行为,这些行为数据应该被采集回来,作为下一轮模型训练的输入,形成一个完整的闭环。我在 Web 端加了一个简单的行为上报接口:用户点击某个推荐商品时,前端发送一条事件记录到后端,后端写入行为日志表。
采集的这些行为数据,最终会进入离线清洗流程,和爬虫原始数据一起处理,生成新的评分矩阵,再训练新模型。虽然毕设阶段用户量有限,但这个闭环设计在答辩时很加分,因为它体现的是工程思维,而不只是“做了一个算法演示”。
为了保证大屏加载速度和推荐接口的响应速度,我还在后端加了一层 Redis 缓存。热点统计接口的查询结果缓存 5 分钟,推荐列表缓存 30 分钟。这样一来,即使 PySpark 离线任务还没有跑完,接口也能及时返回上一次的缓存结果,避免大屏打开时白屏或长时间等待。
5. 毕设路上最常见的坑:问题排查实录
5.1 PySpark 环境与 Python 进程的经典报错
PySpark 的报错几乎是每个做大数据毕设的同学都会遇到的“开门红”。最经典的就是这条:pyspark cannot run program "python3" error=13, Permission denied。原因是 Spark 在启动 Executor 时需要调用系统 Python 解释器,但当前用户对 python3 没有执行权限,或者 PYSPARK_PYTHON 环境变量没有正确设置。
排查思路是:先执行 which python3 确认解释器路径,然后看该路径是否有执行权限;接着在 Spark 配置中强制指定解释器:
export PYSPARK_PYTHON=/usr/bin/python3 export PYSPARK_DRIVER_PYTHON=/usr/bin/python3如果仍然不行,检查 Spark 运行用户是否有权限访问该路径,必要时用 chmod 修复权限。还有一个易踩的坑是:本地明明有 Python3,但 Spark 运行在三台虚拟机集群上,其他节点没有安装 Python3,也会报类似错误。我的建议是调试阶段尽量用伪分布式或者单机模式,等流程完全通顺了再考虑集群,否则排查跨节点环境问题会非常痛苦。
另一个高频报错是 jar does not exist or is not a normal file: /usr/local/hadoop/share/hadoop/m... 这类信息看着像是缺 jar,实际多半是 Hadoop 配置文件中的路径有问题,或者环境变量没有 source 到当前会话。我遇到过的情况是改了 Hadoop 安装目录后,hadoop-env.sh 里的 HADOOP_HOME 还是旧路径,导致很多辅助 jar 找不到。修复方式就是逐项检查 core-site.xml、hdfs-site.xml、yarn-site.xml 中的路径配置,然后重新 source /etc/profile。
5.2 Scrapy 爬不到数据?先查这三个地方
Scrapy 爬虫跑完后只显示 proceed finished with exit code 0,日志里没有任何 item 输出,这是新手最容易懵的场景。程序确实正常执行了,但什么都没抓到,看到“finished”这个词让人误以为成功了,实际上 parse 方法里一个 item 都没产生。这类问题,我排查的顺序固定是三步:
第一步,查看目标页面内容是否和预期一致。很多站点会有反爬验证页或登录跳转,返回的 HTML 里根本没有商品列表。在爬虫里临时加一条 print(response.text[:500]),看看返回的响应是什么,能快速定位是不是被重定向了。
第二步,检查 CSS 选择器或 XPath 是否匹配。这一步最容易踩坑,因为页面结构可能调整过,之前写的选择器已经失效。用 Scrapy Shell 交互式调试是最快的办法,直接 scrapy shell "目标URL",然后反复测试选择器,确认能选中数据再更新代码。
第三步,确认请求头是否完整。部分站点校验 Referer 或 Cookie,请求头不全会返回空白列表。在 start_requests 里补全 headers,或者用中间件统一配置,能解决大部分问题。
如果上述三步都没问题,那就要看是不是被限流了。设置 DOWNLOAD_DELAY 和重试机制之后,再次运行往往就有数据了。总之,exit code 0 不代表成功,一定要在日志里看到 item 的数量。
5.3 Hadoop 集群稳定性的几个实战注意事项
Hadoop 在毕设阶段一般跑的伪分布式模式,虽然只有一台机器,但依然有一堆小坑。第一个是磁盘空间问题,HDFS 默认副本数是 3,即使只有一台机器,它也会把每个数据块复制三份副本,如果集群只有一个 DataNode 且磁盘空间不大,存几千条数据可能还无所谓,但一旦数据量上来,很快就会出现“Insufficient disk space”。我的方案是把副本数改小,在 hdfs-site.xml 中设置 dfs.replication 为 1。
第二个是文件小文件爆炸。Scrapy 爬虫如果直接把逐条数据写成小文件上传 HDFS,NameNode 会不堪重负。前面我也提过,先攒到 MySQL 再批量导出成一个或几个大 CSV,是更合理的做法。这个经验是我实际运行一周后对比发现,再多的代码技巧也救不了小文件带来的元数据压力。
第三个是 HDFS 扩容和磁盘清理。毕设后期数据量大了,会有 “HDFS 服务器扩容” 的需求,但虚拟机上加磁盘相对麻烦,更快的办法是清理不用的中间数据。我在 HDFS 上会定期删除 /agri/raw 下超过两周的原始数据,保留 /agri/clean 和 /agri/stats 的产物。因为原始数据在 MySQL 中还有留存,HDFS 里的中间副本删掉也不影响恢复。
还有一个容易被忽略的问题:Java 版本。Hadoop 3.x 对 Java 8 的支持最稳,有些同学系统里装的是 Java 11 或 17,启动时会出现各种奇怪的类加载错误。我的建议是直接用 Hadoop 官方文档推荐的 JDK 版本,不要贪新求快,毕设环境稳定压倒一切。整个大数据组件家族之间版本匹配是真正的玄学,保持所有组件大版本一致能减少很多兼容性烦恼。
6. 论文、PPT 与视频讲解:让毕设完整交付
6.1 论文怎么写才能撑起这个题目
论文是毕设的重要交付物,很多同学代码跑通了但对写作无从下手。我的经验是论文结构严格按标准模板走,但内容上要突出“技术的落地逻辑”。一般框架是绪论、相关技术介绍、需求分析、系统设计、系统实现、系统测试、总结与展望。重点在系统设计和实现上,大量放体系结构描述、流程图、截图和核心代码,让评委能清晰看到你做了什么。
论文里技术方案部分,不能只是罗列 Scrapy 是什么、Hadoop 是什么,而要说清楚你的系统里这些组件是怎么组织的。比如爬虫部分写“基于 Scrapy 的农产品数据采集模块”,需要有爬虫流程图、管道处理说明、反爬策略分析。推荐系统部分写“基于 ALS 和内容推荐的混合推荐策略”,要有评分矩阵构建过程、模型评估指标、冷启动解决方案。这些都是能体现工作量和技术深度的内容。
写作过程中,我建议图表>文字。同一个模块,一张系统流程图比三段文字描述更直观。但图不要用随机截屏凑数,要有设计感,统一配色、统一字体,这些细节在答辩加分项里非常明显。另外,论文中的代码不要全部贴大段,只需保留关键方法和逻辑,注释要清楚,评审老师通常重点关注代码片段是否解决了某个具体问题。
6.2 PPT 与视频讲解的准备思路
PPT 的逻辑和论文不同,论文是面面俱到,PPT 是讲一个故事:问题是什么、用了什么方案、效果怎么样。我的 PPT 控制在 18 页以内,顺序是:项目背景与意义、需求分析、技术选型、系统架构、爬虫模块、数据存储与处理、推荐算法、可视化展示、系统测试、总结。每一个模块尽量配一张截图,比如爬虫运行日志、HDFS 文件列表、PySpark 执行结果、可视化大屏页面,这样讲起来有凭有据。
讲解视频也不要一边讲一边录,那样会磕磕绊绊。我的做法是先写一个讲解脚本,按模块口播,然后分段落屏,最后用剪映拼接。视频控制在 15 到 20 分钟,前面讲背景 2 分钟,技术架构 3 分钟,重点放在爬虫和推荐系统的实现 8 分钟,最后演示大屏和推荐效果 5 分钟。这样既不冗长,也把项目的亮点全部覆盖。
答辩现场还有一个容易被忽略的点:提前准备好“如果环境跑不起来”的备选方案。我在演示时从不现场运行爬虫,而是播放录好的演示视频,因为现场网络、目标站点状态都不可控。推荐接口也准备了 Mock 数据作为兜底,一旦真实环境出问题,切到 Mock 数据也能完成演示。这种冗余准备,是答辩顺利通过的重要保障。
最后再说一点
整套系统做下来,我最大的体会是:毕设项目不是技术越炫越好,而是每一步都要能自圆其说。Hadoop 为什么存在、PySpark 解决什么问题、推荐结果怎么来的、数据流怎么闭环,这些在答辩时被追问的深度,往往取决于开发时对方案的理解程度。我带着这套系统去答辩时,老师最感兴趣的不是代码量,而是我解释“为什么要先 MySQL 再 HDFS”和“冷启动怎么处理”这两个点。所以说,多问自己几个为什么,比多敲几千行代码更有价值。