基于Hadoop+PySpark+Scrapy的考研院校推荐系统实战
1. 项目整体架构与技术选型思路1.1 考研院校推荐系统到底解决什么问题每年考研报名人数都在刷新纪录择校这件事已经从“凭感觉”变成了“拼信息”。不同院校的报录比、复试分数线、专业课科目、招生人数每年都在变动靠手工翻官网、刷论坛、问学长不仅效率低而且信息严重滞后。很多考生花了大量时间搜集数据结果不是信息过时就是维度不全最终择校决策建立在片面的信息之上。这个项目做的就是一件事把考研择校这个“信息密集型”任务变成一个“数据驱动”的可量化决策过程。系统通过爬虫自动采集考研相关数据包括各院校的复试分数线、录取人数、报考人数、专业课科目、学校层次、地域分布等信息经过大数据框架清洗统计后基于推荐算法为考生生成个性化的院校推荐列表同时对目标院校的分数线走势做预测给考生一个相对科学、可参考的择校依据。从毕设角度看这个项目覆盖了完整的数据链路——采集、存储、清洗、计算、推荐、展示每一层都有对应的技术栈支撑论文和答辩的可讲内容非常充足。从实际价值看这套架构换一个数据源、换一套推荐逻辑就能复用到高考志愿填报、职业岗位推荐、课程选课推荐等场景扩展性很强。1.2 为什么选Hadoop PySpark Scrapy这套组合很多同学看到这个技术栈的第一反应是“重”觉得一个考研推荐系统用得着上Hadoop吗这个疑问在答辩时也经常被问到。说实话单从数据量来看考研数据即使全量爬下来也就是几千条到几万条的规模确实用不上大数据框架。但毕设项目的评分标准里技术难度和架构完整性占很大比重更重要的是这套组合能够在有限的数据量下完整展示大数据处理的整个流程。Scrapy负责数据采集层它的异步IO机制让爬虫在采集多站点数据时效率远高于requests逐条请求而且Scrapy内置了Item Pipeline、Middleware、Selector等组件契合“框架化开发”的课程要求。Hadoop承担数据存储与基础计算HDFS的分布式文件系统天然适合存放爬虫产生的半结构化、非结构化数据MapReduce虽然写起来繁琐但对于离线批处理任务稳定可靠。PySpark接在Hadoop之上用DataFrame API做ETL清洗和特征工程比纯MapReduce开发效率高一个量级特别适合处理需要多步转换的数据清洗场景。还有一个非常实际的原因这套组合是招聘市场上大数据岗位的高频技能栈。通过这个项目你能把Hadoop生态的HDFS、YARN、ZookeeperSpark生态的RDD、DataFrame、Spark SQL以及爬虫相关的反爬策略、并发控制全部串起来简历上写“独立设计并实现基于HadoopPySpark的数据处理流水线”比写“熟悉大数据框架”有说服力得多。1.3 系统模块划分与数据流向整个系统按照“采集-存储-处理-计算-应用”五个层级划分模块数据流单向流转每一层只依赖下一层的输出这样设计的好处是模块解耦某一层替换实现不影响其他层。采集层部署多台爬虫节点Scrapy爬虫分别针对研招网、学校研究生院官网、各类考研论坛和信息聚合站点抓取数据。原始数据经过Scrapy的Item Pipeline做初步清洗后统一封装为JSON格式通过异步写入方式进入Kafka消息队列。引入Kafka做缓冲层主要是为了让爬虫的写入速度和下游Hadoop的消费速度解耦防止爬虫高峰期大量请求直接压垮NameNode。存储层用HDFS保存全量原始数据按照数据来源和日期做目录分区同时将元数据存入MySQL。这里需要说明一个设计决策HDFS适合大文件顺序读写不适合频繁的小事务查询所以系统用MySQL保存经过清洗的结构化核心数据供推荐服务实时查询HDFS则作为数据仓库保存全量历史数据供离线分析。计算层以PySpark为核心。定时的Spark任务从HDFS读取原始数据完成缺失值处理、字段标准化、重复数据去重等清洗操作然后通过特征工程构造院校特征矩阵和考生画像特征。分数线预测模块也跑在这一层基于历史分数线序列构建时间序列模型和回归模型预测下一年度的复试分数线区间。应用层是一个基于Flask构建的Web服务提供院校信息查询、分数线趋势可视化、个性化院校推荐和分数线预测功能。推荐服务启动时从MySQL加载院校特征矩阵到内存在线计算相似度矩阵为每个考生实时生成Top-N推荐列表。整个数据链路从前端触发到推荐结果返回全流程可追溯方便答辩时进行技术演示。2. 数据采集层核心设计2.1 Scrapy爬虫的站点分析与采集策略考研数据的源头非常分散每个学校的研究生院官网结构都不一样有的甚至没有独立官网信息挂在学校主页的子目录下。如果对每个站点单独写一套爬虫工作量会失控。我当时的做法是把目标站点分为三类分别对应不同的爬虫模板。第一类是结构化较好的站点比如研招网的硕士专业目录查询页面这类站点有统一的查询参数和返回格式适合用FormRequest提交查询条件解析返回的HTML表格或JSON接口。第二类是半结构化的学校研究生院官网页面结构大同小异但细节不同这类站点统一用Scrapy的ItemLoader配合自定义Processor解析把公共字段抽出来定义成基类各学校只需要继承基类并覆盖CSS选择器即可。第三类是论坛、贴吧等非结构化信息源这类站点数据噪声大、反爬较强主要采集带有分数线、报录比关键词的帖子内容后续在PySpark阶段做文本清洗和关键词抽取。采集策略上需要关注两个核心指标爬取速度和请求频率。速度决定了多久能完成一轮全量采集频率则直接影响IP被封的概率。我的做法是设置DOWNLOAD_DELAY为2到3秒同时开启AUTOTHROTTLE自动限速让Scrapy根据目标站点的响应时间动态调整延迟。对需要登录的站点用Scrapy的CookiesMiddleware维护会话部分站点要求验证码时引入打码平台接驳但这类站点数量要严格控制否则成本太高。2.2 动态页面数据获取与Playwright集成方案考研数据中有相当一部分藏在动态渲染的页面里典型的是各院校的历年分数线查询系统页面上通过JavaScript异步加载数据传统Scrapy直接请求只能拿到空壳HTML。解决这个问题的常规路径有两条第一是分析页面的XHR接口直接构造请求获取JSON数据这是最高效的方式但前提是接口没有做签名校验和频率限制第二是用浏览器渲染引擎加载完整页面再提取数据适合接口加密、参数复杂的情况。真实环境中考研院校的分数线接口普遍做了基础的Referer校验和请求频率限制签名算法相对少见。所以我优先选择第一条路径用抓包工具分析接口结构直接请求JSON接口获取数据。但部分学校把分数线做成了图片或复杂的动态表格这时候就需要第二条路径兜底。我在项目中用Scrapy集成Playwright处理动态页面。具体做法是开发一个自定义的Downloader Middleware当Request携带playwrightTrue的meta标记时中间件启动Chromium浏览器加载页面等待指定的CSS选择器出现后提取页面源码返回给Spider解析。需要注意浏览器的启动和销毁非常耗时必须做实例复用否则每条请求都启动一个浏览器实例采集效率会断崖式下降。我的方案是用Playwright的异步API配合一个连接池限定最多同时运行3个浏览器上下文超出部分排队等待。# middleware.py 关键逻辑示例 class PlaywrightMiddleware: def process_request(self, request, spider): if not request.meta.get(playwright): return None # 从连接池中获取浏览器上下文 context yield from spider.browser_pool.get() try: page yield from context.new_page() yield from page.goto(request.url, timeout30000) yield from page.wait_for_selector( request.meta.get(wait_selector), timeout10000 ) content yield from page.content() return HtmlResponse( request.url, bodycontent, encodingutf-8, requestrequest, ) finally: yield from context.close() spider.browser_pool.release(context)这个集成方案有几个细节值得提醒。一是Playwright的浏览器上下文每次使用最好新建不要长驻复用否则页面状态会互相污染。二是动态页面加载时等待的Selector要选得准我通常等待数据容器而不是等待某个具体数据项降低因个别字段缺失导致的超时。三是动态渲染的页面采集速度大约只有静态请求的五分之一所以同一个站点的动态页面和静态页面要分开处理能走接口的绝不走浏览器。2.3 爬虫数据质量控制与入库规范爬虫采集的数据质量直接决定后续推荐算法和预测模型的效果上限这个环节做的清洗越多后面PySpark的压力越小。我在Item Pipeline中配置了三个组件去重过滤器、字段校验器和格式标准化器。去重过滤器基于Scrapy的RFPDupeFilter扩展对Item的关键字段拼接后做MD5用Redis的Set结构判断是否重复。这样做的好处是分布式部署时多台爬虫共享同一个去重集合避免重复采集。字段校验器对每个字段做非空校验和类型校验如果核心字段为空直接丢弃该Item并记录日志防止脏数据进入HDFS浪费存储空间。格式标准化器统一处理日期格式统一转为yyyy-MM-dd、分数格式统一转为浮点数保留一位小数和文本格式去除多余空白和换行符这样下游处理时不需要再做重复劳动。入库环节采用了分库分表的思路。原始JSON数据写入HDFS的/data/raw/目录按采集日期分区每条数据是一个独立的JSON行。清洗后的结构化数据异步写入MySQL按照专业、院校、年份建立联合索引支撑推荐服务的实时查询。这里需要特别说明的是HDFS上的数据必须包含采集时间戳、数据来源URL这两个字段一方面是数据溯源的需求另一方面是论文中做数据分析时的有力素材。# items.py 部分字段定义 class PostgraduateItem(scrapy.Item): school_name scrapy.Field() # 院校名称 school_code scrapy.Field() # 院校代码 major_name scrapy.Field() # 专业名称 major_code scrapy.Field() # 专业代码 year scrapy.Field() # 年份 admission_count scrapy.Field() # 录取人数 apply_count scrapy.Field() # 报考人数 retest_score scrapy.Field() # 复试分数线 retest_rate scrapy.Field() # 复试比例 source_url scrapy.Field() # 数据来源URL crawl_time scrapy.Field() # 采集时间戳2.4 反爬策略与封IP应对经验考研相关站点的反爬强度总体上比电商、社交平台低一个档次但也不能忽视。我遇到的反爬手段主要有四类请求频率限制、User-Agent检测、Cookie校验、动态Token。请求频率限制是最常见的解法和常规方案一致——请求随机延时加代理池轮换。这里重点说代理池公共代理池的可用率波动很大我的方案是自建一个轻量代理池用Redis维护一个代理队列爬虫每次请求前从队列中取出一个代理IP请求失败时标记该代理的失败次数连续失败三次就自动移出队列。代理来源是免费代理网站的定时采集加上少量付费代理接口用质量高的付费代理兜底。User-Agent检测的解法是维护一个UA池随机抽取使用比固定一个Chrome UA安全得多。Cookie校验集中在需要登录才能查看数据的站点我的策略是先用一次浏览器自动化完成登录把Cookie序列化到Redis爬虫启动时直接加载。动态Token比较麻烦通常需要逆向JavaScript我一般优先看有没有JsonP接口或者移动端接口绕开前端校验实在不行才用Playwright渲染方案兜底。还有一条重要的经验爬虫节点的出口IP要离散化。如果所有请求都从一个IP出去多低的频率都会被识别。我在部署时把采集任务分发到多个节点每个节点用不同的网络出口这样即便某个站点触发了风控也只影响一个节点不会拖垮整个采集链路。3. 数据存储与处理方案3.1 Hadoop集群部署与目录规划Hadoop的部署方式要根据可用资源决定。有条件用三台以上服务器做真实集群性能更好演示效果也更有说服力如果只有一台机器就用伪分布式模式在单个节点上分别启动NameNode、DataNode、ResourceManager、NodeManager进程。不管哪种方式安装配置流程基本一致核心在于配置文件的三件套core-site.xml、hdfs-site.xml、yarn-site.xml。以三节点集群为例架构通常是一台master节点运行NameNode和ResourceManager两台slave节点运行DataNode和NodeManager。core-site.xml中设置NameNode的RPC通信地址和HDFS临时目录hdfs-site.xml设置副本数为2集群只有三台时默认3副本会浪费空间和NameNode的元数据目录yarn-site.xml配置ResourceManager的地址和节点管理器的内存参数。这里最容易踩的坑是临时目录和元数据目录的权限问题Hadoop启动时如果没有写入权限会直接报错建议启动前统一执行chown -R hadoop:hadoop /data/hadoop。HDFS目录规划遵循分层管理原则我在根目录下创建了/data/raw、/data/clean、/data/warehouse三个顶层目录分别存放原始数据、清洗后数据和经过聚合分析的仓库数据。/data/raw按采集日期/数据来源二级分区/data/warehouse按业务主题/年份分区。分区策略看起来简单但直接影响后续Spark任务的读取效率如果分区粒度过细会产生大量小文件导致Spark任务频繁进行文件系统操作性能白白损耗。3.2 PySpark ETL清洗流程PySpark在项目中的核心任务是三块数据清洗、特征工程、统计分析。每次定时任务从HDFS读取当天的增量数据经过清洗后写入HDFS的clean层然后进行统计分析生成指标结果最后把结构化结果写入MySQL供Web服务查询。清洗流程的第一步是数据解析把JSON行数据读入DataFrame用from_json函数解析JSON字符串为结构化列。需要注意Scrapy产出的JSON格式偶尔会有非法字符Spark解析时会直接报错我的解决办法是读取时把mode设置为PERMISSIVE把损坏的记录放到单独的列中后续统一处理。# 数据清洗核心逻辑 from pyspark.sql import functions as F from pyspark.sql.types import StructType, StructField, StringType, IntegerType, FloatType schema StructType([ StructField(school_name, StringType()), StructField(school_code, StringType()), StructField(major_name, StringType()), StructField(major_code, StringType()), StructField(year, IntegerType()), StructField(admission_count, IntegerType()), StructField(apply_count, IntegerType()), StructField(retest_score, FloatType()), ]) df spark.read.json(hdfs://master:9000/data/raw/2024/*/) # 去重同一学校同一专业同一年的数据保留最新采集的一条 dedup_df df.dropDuplicates([school_code, major_code, year]) # 缺失值处理复试线缺失的使用同院校同专业近三年的均值填充 fill_df dedup_df.select( school_name, school_code, major_name, major_code, year, F.when(F.col(retest_score).isNull(), F.avg(retest_score).over(Window.partitionBy(school_code, major_code))) .otherwise(F.col(retest_score)).alias(retest_score) )清洗过程中最花时间的是院校名称的规范化。同一个学校在不同网站上的名称可能不一样比如“北京航空航天大学”可能被写成“北航”或“北京航空航天大学北航”如果名称不统一后续的院校聚合统计结果就会非常混乱。我在Spark中维护了一个院校别名映射表用when-otherwise或者UDF进行统一替换这个表的构建来自爬虫采集时的人工整理虽然造表过程繁琐但对后续所有分析环节都有帮助。3.3 数据仓库建模与统计分析清洗完成后的数据进入/data/warehouse层按照星型模型组织。事实表保存院校-专业-年份的维度的分数线和报录数据维度表包括院校维度表学校层次、所在省份、是否为985/211/双一流、专业维度表学科门类、专业评级、时间维度表年份、年份类别。这种建模方式的好处是明确的层次关系后续做任意维度的上卷下钻分析都很方便。统计分析部分产出了三个核心结果表按院校层级统计的平均复试分数线趋势、按学科门类统计的报录比分布、按省份统计的院校热度排行。这些统计结果一方面直接展示在大屏页面上作为辅助参考另一方面作为推荐系统中的院校热度特征输入。实际做的时候很多结论还挺有意思的比如某些非985院校的热门专业报录比甚至超过部分985冷门专业这种信息对考生做性价比判断非常有价值。4. 分数线预测与推荐系统核心4.1 复试分数线预测的模型选型与实现分数线预测是整个项目中最容易出彩也最容易翻车的模块。说容易出彩是因为只要模型逻辑讲清楚、结果展示合理答辩老师都比较认可说容易翻车是因为如果拿到的历史数据量太短任何模型都不可能有太高的准确率。我当时的处理思路是不追求精确预测单一值而是预测一个合理区间同时给出影响因素的重要性排序。模型选型上我对三种方案做了对比实验。方案一是基于时间序列的ARIMA模型把某院校某专业历年的复试线看成一条时间序列用ARIMA捕捉趋势性和季节性。这个方案的问题是考研复试线受政策影响波动较大单纯靠历史数据外推遇到政策改革年份预测偏差会很大。方案二是多元线性回归把报考人数、录取人数、院校层次、专业热度、上一年度分数线等作为特征预测下一年度分数线。这个方案的优点是特征可解释性强能分析各因素对分数线的影响权重。方案三是随机森林回归可以捕捉特征之间的非线性关系但需要的数据量至少是几千条否则过拟合风险很高。最终我选择了多元线性回归作为主模型ARIMA作为对照模型原因很简单数据量在千条量级时线性回归的稳定性和可解释性都更好而且在pySpark的MLlib中实现成本低。核心代码如下from pyspark.ml.feature import VectorAssembler from pyspark.ml.regression import LinearRegression feature_cols [apply_count, admission_count, retest_score_prev, school_level, major_hot, year_index] assembler VectorAssembler(inputColsfeature_cols, outputColfeatures) feat_df assembler.transform(train_df) lr LinearRegression(featuresColfeatures, labelColretest_score) lr_model lr.fit(feat_df) print(R2:, lr_model.summary.r2) print(系数:, lr_model.coefficients) print(特征重要性:, dict(zip(feature_cols, lr_model.coefficients)))实验结果显示对分数线影响最大的特征是上一年度复试线其次是报考人数和院校层次这个结论符合常识也为论文中的归因分析提供了支撑。模型训练时需要注意数据划分不要随机切分要按照年份切分用过去年份训练、最近年份验证才能反映模型在真实预测场景的表现。4.2 考研院校推荐算法设计推荐系统的核心算法采用混合推荐策略用户冷启动阶段使用基于规则的推荐有一定行为数据后切换为基于物品的协同过滤。冷启动问题在考研场景中非常突出——考生第一次使用系统时往往只有本科院校、目标专业、期望城市这几个基础信息没有点击、收藏等行为数据传统协同过滤完全跑不起来。冷启动阶段的推荐逻辑是规则打分根据考生的目标专业匹配开设该专业的院校列表然后按照院校层次、城市GDP水平、前一年度分数线与考生预估分数的差值、报录比等维度加权打分。权重的设置参考了考研论坛上的择校因素调研专业匹配度占30%、院校层次占25%、地域偏好占20%、上岸难度占25%。这套规则把“冲刺院校-适中院校-保底院校”三档推荐区隔开直接输出结构化的推荐结果虽然逻辑简单但胜在可解释性强考生能看到每个推荐理由。# 规则打分核心逻辑 def rule_score(candidate, user_pref): score 0.0 # 专业匹配度直接命中40相近专业30跨专业10 if candidate.major_code user_pref.major_code: score 40 elif candidate.major_category user_pref.major_category: score 30 # 院校层次加分 school_level_score {985: 25, 211: 20, 双一流: 15, 普通本科: 5} score school_level_score.get(candidate.school_level, 5) # 地域偏好 if candidate.province in user_pref.pref_provinces: score 20 # 上岸难度分数线与预估分差值 diff candidate.retest_score - user_pref.estimated_score score 15 if abs(diff) 10 else (10 if diff 0 else 5) return score当考生在系统中有收藏、点击、对比等行为数据后推荐策略切换为基于物品的协同过滤。物品即院校-专业的组合通过Spark的ALS算法计算院校之间的相似度矩阵。ALS交替最小二乘法在PySpark MLlib中有成熟实现核心参数是rank潜在因子数、regParam正则化参数、alpha置信度参数。我调参后确定的经验值是rank10、regParam0.1、alpha1.0在离线评估中RMSE约为0.82这个指标作为论文中的模型评估结果也够用。相似度计算完成后的推荐流程是从考生行为数据中提取最近交互过的一组院校从相似度矩阵中找出每所院校的Top-K相似院校加权汇总后过滤掉已交互和历史分数线超出考生预估分数过多的院校生成最终推荐列表。整条链路的计算在Spark中离线完成推荐结果写回Redis缓存Web服务直接读取响应时间能控制在100毫秒以内。4.3 推荐结果的可解释性与效果评估推荐系统本身是一个算法问题但放到毕业设计里更需要关注展示问题。普通考生并不关心你用的是ALS还是ItemCF他们关心的是“为什么给我推荐这个学校”。所以我为每条推荐结果生成了三条解释理由根据你的目标专业和院校层次偏好推荐、该院校近三年该专业分数线波动较小上岸概率高、该院校在你期望的城市且专业排名靠前。这些理由由推荐日志中的特征贡献值组合生成。离线评估方面我用历史交互数据做了留一法验证从交互记录中随机抽出一组作为测试数据看模型能否在Top-10中命中这组数据。实验结果是命中率约63%作为基于ALS的推荐系统这个指标处于正常水平。如果没有达到预期优先检查的是行为数据的稀疏性——很多考生注册后并没有产生足够的交互行为系统被迫退回冷启动推荐导致协同过滤的效果无法发挥。5. 后端服务与前端可视化实现5.1 Flask后端服务接口设计后端服务采用Flask框架原因是轻量、上手快、部署简单作为毕业设计项目完全够用。接口设计遵循RESTful风格核心接口有五个考生画像管理接口注册、修改偏好、院校信息查询接口按专业、地区、层次筛选、分数线查询与趋势图数据接口、推荐结果获取接口、分数线预测接口。每个接口统一返回JSON格式包含状态码、消息和数据体。# 推荐接口示例 app.route(/api/recommend, methods[POST]) def recommend(): user_id request.json.get(user_id) top_n request.json.get(top_n, 10) strategy request.json.get(strategy, auto) # 优先从缓存读取推荐结果 cached redis_client.get(frec:{user_id}) if cached: return jsonify({code: 0, data: json.loads(cached)}) # 缓存未命中则调用推荐服务 rec_list recommender_service.recommend(user_id, top_n, strategy) redis_client.setex(frec:{user_id}, 3600, json.dumps(rec_list)) return jsonify({code: 0, data: rec_list})接口层需要注意的点是参数校验和异常处理。请求参数在入口做合法性校验比如年份必须是四位数字、分数必须在100到500之间避免脏数据进入业务逻辑服务内部异常统一捕获后转换为结构化错误码返回防止前端拿到不友好的堆栈信息。系统上线后我加了一个全局请求日志中间件记录每个接口的耗时、状态码和异常信息排查问题方便很多。5.2 可视化页面分数线趋势与院校对比前端页面采用Vue ECharts的方案没有用重型UI框架因为项目的重点在后端技术栈前端保持清爽可用即可。核心页面有三个数据总览大屏、院校详情页、个人推荐中心。数据总览大屏用ECharts展示各院校层次的平均分数线趋势折线图、各学科门类的报录比柱状图、各省份院校热度地图。这部分的数据来源是PySpark预计算好的统计结果接口一次返回全部图表所需数据前端按图表类型分发减少了请求交互次数首屏加载速度快很多。院校详情页展示单个院校的完整信息包括基本信息卡片、近五年分数线折线图、报录比趋势图以及该院校相似院校的推荐列表。分数线趋势图对用户选择冲刺院校还是有参考价值的可以看到某个学校分数线是否处于持续上涨通道推测下一年度的竞争热度。个人推荐中心是系统的核心页面展示推荐结果列表每条推荐卡片标明推荐档位冲刺、适中、保底、推荐匹配度分数和推荐理由用户可以对推荐结果反馈“感兴趣”或“不感兴趣”这个反馈会回写到行为日志作为协同过滤的输入数据。系统里的推荐效果是随着使用逐渐变准的把这个机制在页面上讲清楚了用户能理解系统的价值而不是只看到一堆冷冰冰的算法。5.3 前后端联调与部署细节联调阶段遇到最多的问题是跨域和Session管理。前后端分离部署在不同端口前端请求必须做跨域处理Flask后端用flask-cors扩展统一配置允许来源避免在Nginx层做代理转发。Session方面没有用传统Cookie Session而是采用JWT做用户认证登录后前端保存Token请求时放入Authorization头后端用装饰器统一校验部分需要身份的接口加token_required即可。部署采用Docker Compose编排一次性拉起所有服务容器MySQL、Redis、Hadoop如果有条件可以单独用容器跑否则Hadoop部署在宿主机、后端服务、前端Nginx。Docker化部署的好处是环境一致性避免“换了台机器跑不起来”的尴尬答辩演示前提前在演示机上docker-compose up -d全部搞定非常省心。6. 常见问题与排查技巧实录6.1 Hadoop与PySpark配合时的典型故障Hadoop和Spark配合使用时最常遇到的故障集中在资源不足、小文件过多、版本兼容三方面。资源不足的典型表现是Spark任务提交后长时间处于ACCEPTED状态或者运行到一半Executor丢失。排查步骤是先查看YARN的资源监控页面确认可用内存和核数然后检查spark-submit的参数设置executor-memory、executor-cores要和YARN配置匹配。我踩过的坑是yarn.nodemanager.resource.memory-mb设置过低导致多个Executor无法同时启动把参数调大后问题解决。小文件过多是HDFS场景的经典问题。爬虫每次写入的数据量都不大日积月累产生了大量小块文件Spark读取时任务数飙升每个任务只处理几KB数据大部分时间浪费在任务调度上。解决思路是定期用REPARTITION做文件合并或者在写入HDFS时指定coalesce(1)强制输出单文件。这个优化做完后同样数据量的Spark作业耗时从8分钟降到3分钟效果立竿见影。版本兼容问题在Spark读取HDFS时比较隐蔽常见的报错是Incompatible clusterIDs或Unable to load native-hadoop library。前者是因为NameNode的clusterID和DataNode不一致删除各节点的VERSION文件后重新格式化解决后者是本地Hadoop库缺失安装对应版本的libhadoop.so或在SPARK_HOME的conf/spark-env.sh中设置LD_LIBRARY_PATH解决。6.2 Scrapy爬虫采集过程中的异常处理爬虫层面最烦人的问题不是反爬而是踩到反爬策略后的静默失败。如果请求返回了验证码页面而Spider解析时没有匹配到任何Item任务会正常结束但产出为零。如果对这种情况没有监控数据断供几天才会被发现。我的做法是给每个Spider设置产出监控指标解析回调中每提取一条Item就往Redis中的计数器加一跑批任务结束后对比预期值和实际值差值超过阈值时发送报警。虽然这个监控逻辑简单但实际排查问题节省了很多时间。另一个常见问题是代理IP导致的数据错乱。同一个代理IP在短时间内被多个线程复用某些请求会命中别人缓存的路由节点导致请求返回的页面内容不符合预期。尤其是采集多地域的院校数据时IP归属地会影响到部分站点的地域差异化内容。这个问题的解法是代理绑定会话同一个目标站点的请求尽量走同一批IP。6.3 数据质量偏差的排查思路如果发现推荐结果质量异常优先排查的顺序是数据源是否有缺失、清洗逻辑是否有遗漏、特征工程是否合理、算法参数是否需要调整。大部分情况都出在前两层。数据源缺失的例子是部分院校只公布了复试分数线没有公示报考人数导致报录比特征大量缺失我最初的处理是直接丢弃缺失特征结果喜欢冷门专业的考生推荐准确率明显偏低。改成均值填充并增加“数据完整度”标记特征后推荐效果才正常起来。清洗逻辑遗漏的例子是部分院校的专业代码在不同年份发生过调整相同专业在不同年份代码不一致导致时间序列上的专业匹配失败。这个问题的解法是清洗阶段用“院校名专业名”作为联合key而不是单独依赖专业代码。6.4 一套实用的毕设项目Checklist根据这个项目的实际开发经验我整理了一份可复用的Checklist覆盖从开发到答辩的全流程数据源确定后先做一周的试采集确认数据量级和数据质量再决定完整的采集计划Hadoop集群搭建完成后先跑通一个WordCount示例再进入业务逻辑开发明确原始数据、清洗数据、计算结果的分层路径所有任务统一从这里读写推荐算法先实现规则版跑通完整链路后再迭代协同过滤版本所有接口和后端服务保留请求日志答辩演示出问题时能快速回溯项目部署尽量做到Docker-compose一键启动演示前在展示机器上完整测试一遍论文中的架构图、流程图、数据流图与系统实际逻辑保持一致不要在论文和实现之间出现明显偏差这份Checklist里每一条背后都有我踩过的坑。比如第1条最早我拿到题目就急着自己写爬虫花了两天时间把爬虫写好一跑才发现目标站点改版了数据结构和预期完全不同前期工作作废。后来养成了先花时间分析目标站点、做小规模试采集的习惯看起来耽误时间实际是把风险前置暴露了。7. 项目扩展与总结思考这个项目做完之后我最大的体会是技术栈的组合不是功能的叠加。Hadoop、PySpark、Scrapy这三样单独拎出来每个都有对应的小项目可以练习但只有当它们被组织在同一个数据链路里时才能真正理解为什么要做分层、为什么要做缓冲、为什么要做格式统一。如果时间充裕这个项目还有几个值得扩展的方向。一是把推荐算法升级为融合深度学习模型用序列模型捕获考生的动态兴趣变化但前提是样本量足够大否则效果未必超过现在的混合策略。二是引入实时计算框架用Kafka Flink做考生行为日志的实时分析实现“看了这个学校就给推相似的”实时推荐效果。三是把系统从考研场景迁移到更通用的泛教育领域。最后再分享一个经验毕业设计项目的完成质量很大程度上取决于前期需求拆解的细致程度。我在项目启动前用一周时间画出了完整的数据流图、模块图和用例图然后才开始动手写代码。过程中虽然也经历了多次返工但整体路径是清晰的每一个模块做到什么程度、依赖什么接口都在计划之中。这种先规划后开发的节奏比“边写边想”的项目推进方式要稳妥得多写论文的时候前期这些设计文档直接就是论文的核心素材。