
考研出分那几天我身边好几个二战的朋友都在反复查分数线一边查一边纠结这个分数到底能不能进复试要不要换个学校与其靠感觉和运气不如拿数据说话。这个项目就是干这件事的——用 hadoopsparkhive 搭一套考研分数线预测与推荐系统前端用爬虫采集历年分数线、招生目录和院校信息后端用 Spark 做特征工程和分数预测再用推荐算法帮忙筛出合适的院校专业最后把结果落到可视化大屏上。整套链路做下来既把分布式计算入门了也把推荐系统和数据分析的实战流程走通了。这篇文章我尽量按真实项目落地顺序写从爬虫、数仓、预测、推荐到可视化每一层都会给可复现的方案和踩坑记录。适合三类人看正在做大数据课程设计但不知道从哪下手的人、准备大数据岗位面试想补实战案例的人、以及对推荐系统和数据仓库有兴趣的初学者。1. 项目全景与整体设计1.1 这个系统到底要解决什么考研选校这件事表面上是查分数本质上是做预测和匹配。你要回答两个问题以我现在的能力和复习状态考某个学校某个专业有多大机会如果分数不够理想有没有备选方案歌里唱“三分天注定七分靠打拼”但数据能帮我们把那三分也尽量算清楚。项目的数据基础来自公开渠道历年考研初试分数线、招生专业目录、报录比、复试门槛、院校口碑等。特殊之处在于这些数据不是结构化整齐地放在一张表里而是分散在不同网页、PDF、Excel 表格中。所以采集层必须做一套爬虫把这些零散数据收回来清洗成统一格式后放进 HDFS存储和计算层用 Hive 做数据仓库建模用 Spark 做特征工程与模型训练应用层用推荐算法和可视化系统把结果输出给用户。1.2 技术选型背后的取舍技术栈看起来是“大炮打蚊子”但课程设计和真实项目之间的差距恰恰就在这套链路里。Hadoop负责分布式文件存储。考研数据虽然不算海量但把课程设计放在伪分布式的 Hadoop 上运行可以最直观理解 HDFS 的 block、副本、NameNode/DataNode 机制也能体验真实离线数仓的存储方式。Hive负责数据仓库建模。用类 SQL 语言做 ETL比手写 MapReduce 效率高太多。Hive 的分区、分桶、存储格式优化也是面试高频题实战完印象很深。Spark负责计算和机器学习。特征工程、模型训练、ALS 推荐都在 Spark 上跑利用内存计算让迭代过程大大加快特别适合跑推荐算法里的多轮迭代。爬虫用 Python 的 requests lxml属于最轻量但足够出效果的方案。比起 Scrapyrequests 更好调试也更适合在课程设计里快速跑通。可视化Flask 提供后端接口ECharts 做前端图表简单直观不引入过重的前端框架。有一点要提前说明如果你只是想在本地做预测直接把数据放进 pandas 就行根本不需要 Hadoop。但课程设计的评分点和面试考核点恰恰是这个分布式处理的“过程”本身所以项目架构要完整而不是只追求模型准确率。2. 数据采集层爬虫设计与数据规整2.1 先想清楚要采哪些数据开始写爬虫前先列数据清单。这一点比写代码更重要。我最终把数据分成三张核心主题表表名字段示例用途院校信息表学校名称、所在省份、办学层次985/211/双一流、研究生院网址作为推荐系统的商品维度专业目录表学校、学院、专业代码、专业名称、考试科目、拟招生人数构建候选集和特征历年分数线表年份、学校、专业代码、政治线、英语线、专业课线、总分线训练预测模型的核心标签数据源选公开的研招信息网站、学校研究生院官网尽量避免需要登录才能看的内容。这里也提示一句爬虫不是“能爬到就算赢”要遵守目标网站的 robots 协议、控制请求频率、只采集公开数据。课程设计里可以设置请求间隔至少保证服务端和前端都保持基本负载这是一个从业者该有的基本素养。2.2 requests XPath 的落地写法采集代码选择了 requests lxml 的组合。很多人问为什么不用 BeautifulSoup我的理由很实际这些页面大多是列表页和详情页HTML 结构非常有规律XPath 的表达式比 BeautifulSoup 的对象链式查找更接近“数据路径”的感觉写起来短调试也快。但你要注意XPath 中拿文本和拿属性是两回事。一个常见坑是页面里的分数不在text()里而在某个节点的子节点组合里。比如复试分数线列表同一行里有“学术学位”和“专业学位”两种分数直接//td[2]/text()很容易拿到 None。这种时候要用string(.)或者先取节点再.xpath(string(.))把节点内所有文本一次性抽出来做清洗。下面是我实际用过的采集片段做了简化但完整保留了解析套路import requests from lxml import html import time import csv HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } def parse_fraction_page(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 tree html.fromstring(resp.text) rows tree.xpath(//table[classscore-table]//tr) data_list [] for row in rows: # 注意当列里还有嵌套节点时优先用 string(.) cells row.xpath(./td) if len(cells) 6: continue year cells[0].xpath(string(.)).strip() major_code cells[1].xpath(string(.)).strip() school cells[2].xpath(string(.)).strip() total_score cells[3].xpath(string(.)).strip() political cells[4].xpath(string(.)).strip() english cells[5].xpath(string(.)).strip() data_list.append({ year: year, major_code: major_code, school: school, total_score: total_score, political: political, english: english, }) return data_list if __name__ __main__: url https://example.edu.cn/score_list result parse_fraction_page(url) with open(score_raw.csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesresult[0].keys()) writer.writeheader() writer.writerows(result) print(采集完成共, len(result), 条)这里的string(.)就是很多人搜的python xpath 爬虫 text 函数相关技巧。它解决的是 XPath 默认text()只取直接子文本、不取子孙文本的问题。在你处理表格、富文本页面时这个函数能救命。2.3 清洗规则和落地到 HDFS采集完的原始数据还处于“能看但是不能用”的状态常见问题包括分数是字符串“360分”而不是数字 360同一个学校名称在不同年份叫法不一样“—”、“/”、“暂无”这类特殊值混在数值列里。清洗规则我建议写成独立的脚本不要在采集脚本里顺手做。理由很简单采集脚本往往要反复调试如果清洗逻辑混在里面一旦某个页面结构改了你就得整个脚本重跑。把清洗单独拆出来可以做到“采一次、洗多次”。清洗之后的数据统一存储为 CSV 或 JSON 文件再用 HDFS 命令上传hdfs dfs -mkdir -p /warehouse/ods_exam_score hdfs dfs -put score_clean.csv /warehouse/ods_exam_score/如果数据量再大一点可以用 Flume、Sqoop 或者直接通过 Spark 批量读入。课程设计场景下hdfs dfs -put已经足够。上传后别忘了用hdfs dfs -ls确认一下目录和文件列表很多新手会把文件传错位置导致后面 Hive 建表时发现路径不对。3. Hadoop Hive 数据仓库建设3.1 伪分布式搭建与组件整合这一步是整个项目里环境成本最高的一步。为了能完整跑通流程我用虚拟机装了三台 CentOS 节点其中一台作为主节点另两台作为数据节点。如果你只是学习和演示完全可以用一台机器做 hadoop 伪分布式搭建然后在同一台机器上运行 Hive 和 Spark这样能省掉大量网络配置问题。伪分布式的关键词是“单机多进程”也就是把 NameNode、DataNode、ResourceManager、NodeManager 都跑在同一台机器上。你需要在core-site.xml里指定 HDFS 地址在hdfs-site.xml里设置副本数为 1。很多环境问题都出在 hosts 文件没配置、SSH 免密没打通、JAVA_HOME 没写对这三件事上。我当时在 Hadoop 上还做了 zookeeper 整合目的不是做高可用集群而是为了让 Hive 的元数据服务和 HDFS 之间有更好的协调体验。Zookeeper 的部署本身不复杂下载压缩包、改 zoo.cfg、启动 quorum 模式。但如果你不做高可用这一步可以往后放先把 Hive 和 Spark 跑通再考虑。安装 Hive 3.1.3 时要注意版本匹配Hive 3.1.3 和 Hadoop 3.x 兼容没什么问题但和 Spark 的集成依赖hive-site.xml里的hive.metastore.uris。建议把 Hive 的 Metastore 指向 MySQL而不是默认的 Derby。用 MySQL 做元数据库不仅稳定也能让你更清楚地看到 Hive 在元数据层做了什么。3.2 Hive 分层表设计数据仓库基本都会按 ODS、DWD、ADS 三层设计这个分层逻辑复试也会问。考研系统虽然不算大规模数仓但分层的习惯会直接决定后续开发效率。ODS 层存放从 HDFS 上传的原始数据表结构和源文件保持一致字段少做处理。DWD 层对原始数据做清洗、脱敏、标准化形成细粒度的事实表。ADS 层面向应用的汇总统计表比如“历年分数线统计表”、“专业分数排名表”。DWD 层的建表语句我会写成这样CREATE EXTERNAL TABLE IF NOT EXISTS dwd_exam_score ( year INT, school STRING, province STRING, school_level STRING, major_code STRING, major_name STRING, total_score FLOAT, political_score FLOAT, english_score FLOAT, enroll_plan INT ) PARTITIONED BY (dt STRING) STORED AS PARQUET LOCATION /warehouse/dwd_exam_score;这里有几个强烈的个人建议。第一外部表优先于内部表因为元数据删除不会意外把数据文件干掉。第二STORED AS PARQUET直接写不要用默认的 TEXTFILEParquet 的列式存储在后续 Spark 读取时性能会好很多。第三用分区字段dt而不是按年份分区因为年份在预测时是特征不应该被当成分区维度锁死。3.3 数据入库与小文件治理数据从 ODS 进到 DWD最直接的办法是写 Hive SQL 做INSERT OVERWRITE。但常见问题是如果上游文件很多Hive 会产生大量小文件。小文件会让 NameNode 内存压力变大也会让 Spark 读取时的任务数激增。课程设计里也要有这个意识因为面试官非常喜欢问“hive 优化小文件”的问题。我实际用过两种治理方式。第一种把多个小文件先合并到一个目录再一次性 load第二种在INSERT OVERWRITE前设置SET hive.merge.tezfilestrue或SET spark.sql.shuffle.partitions来控制输出文件数量。用 Spark 跑 Hive SQL 时关注参数如下SET spark.sql.shuffle.partitions4; SET spark.sql.adaptive.enabledtrue; SET spark.sql.adaptive.coalescePartitions.enabledtrue;这四个参数看起来不起眼却能让 Spark 写回 Hive 表时少生成 90% 的小文件。对课程设计来说这条优化路径本身就是加分项。Hive SQL 里还有一个小技巧统计总分的中位数可以用percentile_approx(total_score, 0.5)。这个函数在数据量大时能给出近似值性能比percentile好得多而且能处理浮点型字段。热门搜索词里反复出现 hive percentile_approx说明很多人卡在这里其实是没理解它的使用场景。4. Spark 特征工程与考研分数线预测4.1 从 Hive 读数据并构建特征Spark 里读 Hive 表最简单的方式是启动 SparkSession 时开启 Hive 支持spark-submit \ --master yarn \ --deploy-mode cluster \ --conf spark.sql.catalogImplementationhive \ predictor.py对应代码里from pyspark.sql import SparkSession spark SparkSession.builder \ .appName(exam_score_predictor) \ .enableHiveSupport() \ .config(hive.metastore.uris, thrift://node01:9083) \ .config(spark.sql.warehouse.dir, hdfs://node01:9000/user/hive/warehouse) \ .getOrCreate() df spark.sql( SELECT year, school, province, school_level, major_code, total_score, political_score, english_score, enroll_plan FROM dwd_exam_score WHERE dt 2024-01-01 )如果你的数据源除了 Hive 表之外还有 JSON 文件我额外提醒一点Spark 读 JSON 时的细节比想象中多。最常见的问题是 JSON 某个字段有时是数组、有时是字符串或者嵌套 JSON 跨多行。此时要用spark.read.option(multiline, true).json(path)否则会丢数据。另一个问题是字段类型推断不准建议读完以后.printSchema()先看一眼。特征工程这一步决定了预测质量的上限。我最终使用了以下特征年份作为连续特征用来捕捉分数线逐年上涨/波动的趋势。学校层次985、211、双一流、普通院校转成有序编码等于给学校分档。专业大类编码把计算机、电子、机械等专业映射到编码列让模型能捕捉专业差异。省份招生热度统计这个省份招生单位数量的均值反映报考热度。政治英语单科线有些专业卡单科线比总分更狠加入后模型能更好解释总分变化。4.2 为什么选随机森林而不是神经网络预测考研分数线本质上是个小样本回归问题不需要堆深度学习模型。深度学习在表格数据上的优势并不明显而且特征量只有十几个跑神经网络反而容易过拟合。我用 RandomForestRegressor 做主力模型原因有两点对异常值不敏感非线性的交互关系也能捕捉训练完可以拿到特征重要度写出可解释的分析报告。代码大致是这样from pyspark.ml.feature import VectorAssembler from pyspark.ml.regression import RandomForestRegressor from pyspark.ml.evaluation import RegressionEvaluator feature_cols [ year, school_level_index, major_index, province_heat, political_line, english_line, enroll_plan ] assembler VectorAssembler( inputColsfeature_cols, outputColfeatures ) train_data assembler.transform(df) rf RandomForestRegressor( featuresColfeatures, labelColtotal_score, numTrees200, maxDepth6, seed42 ) model rf.fit(train_data)训练完以后用model.featureImportances看一下哪些特征贡献大。实际结果里政治英语单科线的权重往往会超过年份这说明分数线的上涨不是均速的而是由公共科目线的抬升拉动的。这一点写进分析报告里比单纯报一个 RMSE 有说服力得多。4.3 评估和预测结果的保存回归评估不能只看准确率。我在测试集上分别计算 RMSE、MAE、R2三个指标一起看。RMSE 对离群值敏感如果你的测试集里有“某学校某专业突然热门导致分数线暴涨”这种极端样本RMSE 会很难看这时候 MAE 才是更贴近用户感知的指标。模型预测完的新分数线需要写回 Hive 的一张结果表方便推荐系统和可视化层读取。prediction_df.write \ .mode(overwrite) \ .format(parquet) \ .insertInto(ads_score_prediction)这里的insertInto使用起来挺轻松但要求表结构已经提前建好字段类型最好和 DataFrame 保持一致否则会在边角处报错。Spark 任务跑的时候看一眼内存分配用--executor-memory 2G通常已经足够不用贪多盲目加大内存可能让容器资源空转。5. 考研推荐系统设计5.1 推荐链路从“猜”到“算”推荐系统没有想象中的神秘。你打开视频网站首页刷到一堆内容背后就是推荐算法在工作。把这个逻辑移植到考研场景无非是把视频换成学校和专业组合。我的推荐链路分三块候选召回根据用户的本科专业、所在地域、目标专业大类从专业目录表里捞出一批候选院校。这个阶段讲究“广”不要过早过滤。特征过滤把用户预估分数代入预测模型得到这个学校专业的“录取概率”。概率太低的直接排除概率适中的保留。精排排序用综合打分排序分数只是其中一项还要加上学校层次、专业实力、地理位置等加权项。比如用户本科学计算机目标城市是杭州预算分数 350。候选集就能先锁定开设计算机相关专业的院校再用预测分数生成每个学校的录取概率最后按“冲刺、稳妥、保底”三档输出。这套逻辑写起来不复杂但比简单查分数线实用得多。5.2 ALS 协同过滤实现用户个性匹配如果数据足够多我更推荐用 Spark MLlib 自带的 ALS 算法做协同过滤。ALS 的思想是把“用户-院校”的交互矩阵分解成两个低维矩阵用隐含因子表示用户偏好和院校属性。但考研场景有个特殊性用户和院校的“交互”数据很稀疏。你不可能像视频网站那样让用户点一万次收藏和观看。所以我在 ALS 之前先构造了一个伪交互矩阵用以下行为生成交互值用户收藏某学校3 分用户点击查看某专业详情1 分用户把某学校加入冲刺列表5 分。把这些行为数据放到 ALS 里训练得到校级相似度和用户偏好向量。代码框架如下from pyspark.ml.recommendation import ALS als ALS( userColuser_id, itemColschool_id, ratingColscore, rank10, maxIter10, regParam0.1, coldStartStrategydrop ) als_model als.fit(interaction_df) user_recs als_model.recommendForAllUsers(10)冷启动是这一章的重点坑。新用户没有任何行为数据ALS 根本算不出候选。我的解决方式是做“属性回退”用户填写本科院校和预估分数后先用规则生成一波通用候选同地区优先、同专业优先、分数梯度覆盖。等到用户产生真实点击和收藏之后再慢慢切换到 ALS 个性化结果。这套“规则打底 模型增强”的方案比纯 ALS 在各阶段都更稳。6. 可视化分析与大屏展示6.1 图表设计要回答业务问题做可视化最忌讳的是“把图塞满页面”。每一个图表都应该回答一个具体问题。我当时把大屏分成四个区域总览区历年分数线趋势折线图展示整体涨跌。对比区不同省份、不同专业大类的分数箱线图解决“哪里更卷”的问题。预测区用户输入预估分后展示目标院校的预测录取分数和推荐清单。个人匹配区雷达图展示用户偏好和院校特征的匹配度比如学术氛围、城市发展、竞争压力三个维度。选型上后端用 Flask 起几个只读接口前端用 ECharts 渲染。Flask 接口从 Hive 的 ADS 表取数后先缓存在 Redis 或 MySQL避免每刷新一次页面就重新起 Spark 任务。Spark 是离线计算引擎不是实时数据库这一点要想清楚。6.2 后端接口和图表联动一个典型的 Flask 接口看起来像这样from flask import Flask, jsonify import pymysql app Flask(__name__) def query_ads(sql): conn pymysql.connect( hostnode01, userroot, password123456, databaseexam_sys, charsetutf8mb4 ) cursor conn.cursor() cursor.execute(sql) cols [desc[0] for desc in cursor.description] rows [dict(zip(cols, row)) for row in cursor.fetchall()] cursor.close() conn.close() return rows app.route(/api/trend) def trend(): rows query_ads( SELECT year, AVG(total_score) AS avg_score FROM ads_score_prediction GROUP BY year ORDER BY year ) return jsonify(rows) if __name__ __main__: app.run(host0.0.0.0, port5000)前端用 ECharts 的ajax拉数据setOption刷新图表。重点是把年份作为筛选条件前端切年份时后端只查对应分区Hive/MySQL 的过滤效率都能提高。如果以后想真正做到“爬虫可视化界面”和实时大屏可以加一套调度工具比如 Airflow 每天晚上定时跑 Spark 任务把预测结果写入 MySQL。这样页面白天看到的永远是昨天训练好的结果而不是每次请求都现场跑模型。7. 常见问题与排错实录7.1 环境配置高频故障现象原因解决办法Hadoop 启动后没有 DataNode格式化 NameNode 后 data 目录没清干净删除 dfs.namenode.name.dir 下的 current 目录重新格式化Hive 连不上 Metastorehive.metastore.uris 配置错误检查 thrift 服务是否启动端口默认 9083Spark 读 Hive 表全是 NULLSparkSession 没有 enableHiveSupport()加.config(hive.metastore.uris, thrift://node01:9083)任务报 InputSplit 过大或过小HDFS block 数分配不合理理解 InputSplit 是 map 任务处理的数据切片不是 block 本身可以结合文件大小重设 split 大小这里插一句hadoop 面试题里常考“在一个运行的 Hadoop 任务中什么是 InputSplit”。简单说InputSplit 是 MapReduce 框架对输入数据的逻辑切片它与 HDFS block 有关但不必完全相等。你可以把它理解成给 Map 任务下发的工作单Block 是物理存储单位InputSplit 是计算口径。理解了这个调参时才能知道到底在调什么。7.2 爬虫和推荐场景的坑爬虫坑主要是页面解析不稳定。用text()拿不到内容时我推荐先print(tree.xpath(//table//tr[1]//td[1]//text()))看看原始文本结构再用string(.)处理。IP 被封则优先检查请求频率加time.sleep(random.uniform(1, 3))是最稳妥的降速方式。推荐算法的坑更多在评估。ALS 里如果把所有交互数据一起训练很容易在测试集上表现虚高因为模型知道了用户之前看过什么。正确做法是按时序切分用户前 80% 的行为做训练后 20% 做验证。课程设计不需要做太复杂但要在报告里写清楚你是这么做的这能明显加分。7.3 写完这套系统后的一点个人体会最后说点踩过几次坑之后才完全想明白的东西。做这类大数据课程设计技术栈再全也不如一条“能自圆其说的业务故事”重要。我最早想的是无脑堆组件Hadoop、Spark、Hive、Flume、Kafka 全上结果环境折腾两周业务数据还没进 HDFS。后来砍掉一切对业务没有直接贡献的组件只保留爬虫、数仓、预测、推荐、可视化这条主链路项目才真正跑起来。这门课的收获不在于我调了几个模型而在于我把整条链路从头到尾走了一遍。遇到过 Hive 建表查不到数据、Spark 堆内存溢出、XPath 解析文本返回空、推荐测试集过拟合等各种问题每一个坑都对应一个具体的调参或改代码动作。这种“数据从网页到 HDFS再从 Hive 到 Spark最后可视化呈现”的完整过程比单纯背十个算法公式有价值得多。如果你们项目组还想继续往下扩展可以考虑加一个定时调度模块把爬虫到模型训练做成一键全流程也可以把前端做大屏之外的移动端适配让用户输入自己的预估分就能实时看到推荐结果。但那是下一步的事先把当前链路跑顺了再谈扩展。