简介这是一份面向大数据初学者与Hadoop实践者的完整项目资料围绕全国各省市酒店数据的分析与处理展开帮助读者掌握分布式存储与MapReduce编程的核心流程。资源共79个文件以21个Java源码与21个class编译文件为主体辅以7个xml配置、6组part-r-00000与_success输出结果、2个csv数据文件及properties、txt说明文档压缩包约758KB结构清晰便于按模块查阅。项目以hotel.csv为数据源涵盖酒店ID、省市、星级、价格等字段通过HDFS分布式存储与Java编写的Map与Reduce任务完成酒店总数统计、省市分布、平均房价等聚合分析并配有说明文档讲解数据清洗、任务实现与结果解读。已有2096人学习下载适合作为课程设计、毕业项目或大数据入门练手案例读者可据此理解Hadoop集群配置、作业提交与结果分析的完整链路积累海量数据处理经验。1. 全国酒店数据上 Hadoop从 2000 万行 CSV 到可查询宽表手头拿到一份全国各省市酒店数据两千万行上下字段包含酒店名称、省市、地址、评分、评论数、价格区间、开业时间。用 Excel 打开直接卡死用 pandas 单机跑一次 group by 要等好几分钟内存一紧张就 OOM。这类量级正好卡在「单机勉强能跑、但跑得难受」的尴尬区间也是 Hadoop 数据分析项目最典型的练手场景。这篇笔记讲的就是把这份全国酒店数据落到 Hadoop 上从伪分布式搭建、HDFS 入库、MapReduce 清洗、Hive 建仓到 Spark 做多维分析走一遍能复现的链路。适合正在做 hadoop 课程设计、数据分析项目实践或者想拿真实业务数据练手 HDFS、MapReduce、Hive、Spark 的人。不追求集群规模一台 8G 内存的机器就能跑通全流程重点是把每一步的参数和坑讲清楚。2. 环境选型与 Hadoop 伪分布式搭建单机也能跑通全链路2.1 为什么这个项目用伪分布式而不是 Docker 或真集群做酒店数据分析第一步不是写代码而是决定跑在哪。常见三种选择真集群、Docker 镜像、伪分布式。真集群要三台以上机器对个人练手成本太高hadoop 的 docker 镜像启动快但 HDFS 数据卷、端口映射、NameNode 格式化这几步在容器里排查起来反而绕伪分布式在一台机器上把 NameNode、DataNode、ResourceManager、NodeManager 全跑起来配置文件和真集群几乎一致出问题能直接看日志最适合「基于 hadoop 的」这类单机项目。我一般会选伪分布式理由是它把分布式的心智模型完整保留下来了你依然要配 core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml依然要格式化 NameNode依然要通过 HDFS 上传数据。等这套跑顺了再迁到真集群只是改主机名和副本数的事。酒店数据两千万行、压缩后大概几百 MB伪分布式完全扛得住。版本上Hadoop 3.x 是当前主流JDK 用 8 或 11 都行Hadoop 3.3 之后对 JDK 11 支持较好。不要用 JDK 17 直接上反射相关的报错会让你怀疑人生。2.2 伪分布式最小配置四个 XML 文件怎么改先确认 SSH 免密登录本机这是启动脚本的前提# 生成密钥并授权本机免密 ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys # 验证第一次会问 yes/no之后应免密进入 ssh localhostssh-keygen的-P 表示空密码方便脚本调用authorized_keys权限必须是 600否则 sshd 会拒绝使用。验证那一步如果还要输密码后面 start-dfs.sh 会卡在输入密码上。接着改etc/hadoop/core-site.xml指定 HDFS 的默认文件系统和 NameNode 地址configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/opt/module/hadoop/data/tmp/value /property /configurationfs.defaultFS是客户端连 HDFS 的入口端口 9000 是约定俗成改成别的也行但要同步。hadoop.tmp.dir一定要显式指定默认落在 /tmp 下机器重启后数据丢失NameNode 元数据没了就得重新格式化这是新手最常见的翻车点。hdfs-site.xml里把副本数设为 1伪分布式只有一个 DataNode设 3 会一直报副本不足configuration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/opt/module/hadoop/data/name/value /property property namedfs.datanode.data.dir/name value/opt/module/hadoop/data/data/value /property /configurationdfs.replication设 1 是伪分布式的关键很多人照抄集群配置写 3结果 DataNode 一直报「could only be replicated to 0 nodes」。name.dir 和 data.dir 分开存放方便单独清理。mapred-site.xml指定 MapReduce 跑在 YARN 上configuration property namemapreduce.framework.name/name valueyarn/value /property /configurationyarn-site.xml配 ResourceManager 和 NodeManager 的辅助服务configuration property nameyarn.resourcemanager.hostname/name valuelocalhost/value /property property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /property /configurationyarn.nodemanager.aux-services必须是mapreduce_shuffle写错 MapReduce 任务会卡在 shuffle 阶段不动。2.3 格式化与启动顺序错了就得重来配置改完先格式化 NameNode再启动# 格式化只能执行一次 hdfs namenode -format # 启动 HDFS start-dfs.sh # 启动 YARN start-yarn.sh # 验证进程 jpsjps应该看到 NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager 五个进程。少一个就去logs/目录看对应日志。格式化只能做一次重复格式化会导致 DataNode 的 clusterID 和 NameNode 不一致报「Incompatible clusterID」解决办法是清空 data 目录重新格式化。启动后浏览器访问localhost:9870看 HDFS 界面localhost:8088看 YARN 界面。这两个端口是 Hadoop 3.x 的2.x 是 50070 和 8088别照旧教程找错端口。3. 酒店数据入库与 MapReduce 清洗把脏 CSV 变成规整数据3.1 数据先看清字段、编码、缺失值三件事酒店数据常见来源是爬虫导出或平台开放数据字段大致是酒店 ID、名称、省份、城市、地址、评分、评论数、最低价、开业年份。入库前先本地抽样看几行重点确认三件事编码是不是 UTF-8、分隔符是不是逗号、有没有字段内嵌逗号。# 看前 5 行确认分隔符和表头 head -5 hotels.csv # 看编码file 命令能识别 UTF-8 和 GBK file -i hotels.csv # 统计行数确认量级 wc -l hotels.csv如果file -i显示charsetiso-8859-1或 GBK说明是 GBK 编码直接传 HDFS 后 Hive 查询会乱码。先用iconv -f GBK -t UTF-8 hotels.csv hotels_utf8.csv转码。字段内嵌逗号的情况用 Python 的 csv 模块重新导出比 sed 替换靠谱。3.2 上传 HDFS分块、压缩与 distcp 参数数据规整后传到 HDFS# 建目录 hdfs dfs -mkdir -p /warehouse/hotel/raw # 上传大文件会自动分块 hdfs dfs -put hotels_utf8.csv /warehouse/hotel/raw/ # 查看块信息 hdfs fsck /warehouse/hotel/raw/hotels_utf8.csv -files -blocksHDFS 默认块大小 128MB两千万行 CSV 大概几百 MB会切成几个块。hdfs fsck能看到块分布和副本状态副本数应该是 1。如果后续要跨集群搬数据hadoop distcp是标准工具常用参数-m控制并发 map 数-update只拷贝变化的文件-delete让目标与源保持一致。单机项目用不上 distcp但面试常问知道-m默认是 20 就行。3.3 MapReduce 清洗过滤缺失评分和异常价格原始数据里评分缺失、价格为 0 或负数、评论数为负的记录要清掉。写一个 MapReduce 程序做清洗Mapper 逐行解析Reducer 直接输出合格记录。// HotelCleanMapper.java public class HotelCleanMapper extends MapperLongWritable, Text, Text, NullWritable { private Text outKey new Text(); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line value.toString(); // 跳过表头 if (line.startsWith(hotel_id)) return; String[] fields line.split(,); // 字段数不足直接丢弃 if (fields.length 9) return; try { double score Double.parseDouble(fields[5]); long comments Long.parseLong(fields[6]); double price Double.parseDouble(fields[7]); // 评分 0-5价格必须为正评论数非负 if (score 0 || score 5) return; if (price 0) return; if (comments 0) return; outKey.set(line); context.write(outKey, NullWritable.get()); } catch (NumberFormatException e) { // 数值解析失败丢弃该行 } } }Mapper 的四个泛型分别是输入 key、输入 value、输出 key、输出 value。这里输出用NullWritable是因为清洗只做过滤不需要聚合省去序列化开销。split(,)对字段内嵌逗号无能为力如果数据里地址字段含逗号得换成 CSV 解析库这是清洗阶段最容易忽略的坑。Driver 里指定输入输出路径// HotelCleanDriver.java public static void main(String[] args) throws Exception { Configuration conf new Configuration(); Job job Job.getInstance(conf, hotel clean); job.setJarByClass(HotelCleanDriver.class); job.setMapperClass(HotelCleanMapper.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(NullWritable.class); // 没有 Reduce 阶段Map 直接输出 job.setNumReduceTasks(0); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); }setNumReduceTasks(0)表示纯 Map 任务适合过滤类作业省掉 shuffle 开销。输入输出路径从命令行参数传方便复用。输出目录必须不存在否则报FileAlreadyExistsException这是 MapReduce 的硬性规定。打包提交# 编译打包 mvn clean package # 提交作业 hadoop jar hotel-clean.jar com.example.HotelCleanDriver \ /warehouse/hotel/raw /warehouse/hotel/clean提交后去 YARN 界面看任务状态日志里能看到 map 完成百分比。清洗后的数据在/warehouse/hotel/clean下是 part-m-00000 这样的文件。4. Hive 建仓与多维分析用 SQL 回答业务问题4.1 建外部表挂载清洗后数据清洗后的数据用 Hive 建表查询最方便。建外部表删表不删数据CREATE EXTERNAL TABLE hotel_clean ( hotel_id STRING, hotel_name STRING, province STRING, city STRING, address STRING, score DOUBLE, comments BIGINT, price DOUBLE, open_year INT ) ROW FORMAT DELIMITED FIELDS TERMINATED BY , STORED AS TEXTFILE LOCATION /warehouse/hotel/clean;EXTERNAL关键字让表和数据解耦误删表不会丢 HDFS 上的文件。FIELDS TERMINATED BY ,要和实际分隔符一致错了整列变 NULL。字段类型按实际数据定score 用 DOUBLEcomments 用 BIGINTopen_year 用 INT。建完先验证行数和抽样SELECT COUNT(*) FROM hotel_clean; SELECT * FROM hotel_clean LIMIT 10;如果 COUNT 是 0 但 HDFS 上有文件多半是 LOCATION 路径写错或分隔符不匹配。4.2 分区表优化按省份分区减少扫描全国数据按省份查询频繁建分区表能大幅减少扫描量CREATE TABLE hotel_partitioned ( hotel_id STRING, hotel_name STRING, city STRING, address STRING, score DOUBLE, comments BIGINT, price DOUBLE, open_year INT ) PARTITIONED BY (province STRING) STORED AS ORC; -- 开启动态分区 SET hive.exec.dynamic.partitiontrue; SET hive.exec.dynamic.partition.modenonstrict; INSERT INTO TABLE hotel_partitioned PARTITION (province) SELECT hotel_id, hotel_name, city, address, score, comments, price, open_year, province FROM hotel_clean;分区字段 province 不能出现在表定义里要放在PARTITIONED BY中。动态分区默认是 strict 模式要求至少一个静态分区这里全动态所以要改成 nonstrict。ORC 格式比 TEXTFILE 省空间、查询快适合做分析表。4.3 三个典型分析 SQL省市均价、评分分布、开业趋势第一个各省酒店均价和数量SELECT province, COUNT(*) AS hotel_cnt, ROUND(AVG(price), 2) AS avg_price FROM hotel_partitioned GROUP BY province ORDER BY avg_price DESC;第二个评分区间分布看数据质量SELECT CASE WHEN score 4.5 THEN 4.5以上 WHEN score 4.0 THEN 4.0-4.5 WHEN score 3.5 THEN 3.5-4.0 ELSE 3.5以下 END AS score_level, COUNT(*) AS cnt FROM hotel_partitioned GROUP BY CASE WHEN score 4.5 THEN 4.5以上 WHEN score 4.0 THEN 4.0-4.5 WHEN score 3.5 THEN 3.5-4.0 ELSE 3.5以下 END;第三个按开业年份看酒店增长SELECT open_year, COUNT(*) AS cnt FROM hotel_partitioned WHERE open_year BETWEEN 2000 AND 2024 GROUP BY open_year ORDER BY open_year;这三个 SQL 覆盖了聚合、分桶、时间趋势是数据分析项目里最常被问到的三类。跑之前先SET hive.exec.paralleltrue;让无依赖的阶段并行能快一些。5. Spark 接手复杂分析从 Hive 表到可视化结果5.1 为什么 Hive 之后还要上 SparkHive 适合固定报表但酒店数据里有些分析用 SQL 写起来别扭比如计算每个城市酒店评分的标准差、做价格和评论数的相关性、按省市做 TopN 排名。这些用 Spark DataFrame 或 RDD 更顺手而且 Spark 能直接读 Hive 表不用重新导数据。Spark 读 Hive 需要把 hive-site.xml 放到 Spark 的 conf 目录或者用enableHiveSupport()。本地模式跑就行不用起 Spark 集群。5.2 PySpark 读 Hive 表做省市排名from pyspark.sql import SparkSession from pyspark.sql.functions import col, avg, count, stddev, desc, rank from pyspark.sql.window import Window # 开启 Hive 支持 spark SparkSession.builder \ .appName(hotel_analysis) \ .enableHiveSupport() \ .getOrCreate() # 读 Hive 分区表 df spark.sql(SELECT * FROM hotel_partitioned) df.cache() # 多次用缓存起来 # 每个城市的酒店数、均价、评分标准差 city_stat df.groupBy(province, city).agg( count(*).alias(hotel_cnt), avg(price).alias(avg_price), avg(score).alias(avg_score), stddev(score).alias(score_std) ).filter(col(hotel_cnt) 10) # 过滤样本太少的城市 # 按省份内均价排名 window Window.partitionBy(province).orderBy(desc(avg_price)) city_rank city_stat.withColumn(price_rank, rank().over(window)) city_rank.show(20)enableHiveSupport()让 Spark 能访问 Hive 元数据。cache()把 DataFrame 放内存后面多次聚合不用重复读 HDFS。stddev算评分标准差能看出一个城市酒店质量是否参差。Window.partitionBy做分组排名比自连接高效。filter掉酒店数少于 10 的城市避免小样本拉偏排名。5.3 价格与评论数相关性分析# 价格和评论数的皮尔逊相关系数 corr df.stat.corr(price, comments) print(f价格与评论数相关系数: {corr:.4f}) # 按价格区间看平均评论数 price_bucket df.withColumn( price_level, when(col(price) 200, 200以下) .when(col(price) 500, 200-500) .when(col(price) 1000, 500-1000) .otherwise(1000以上) ).groupBy(price_level).agg( count(*).alias(cnt), avg(comments).alias(avg_comments) ).orderBy(price_level) price_bucket.show()stat.corr直接算两列相关系数省得自己写公式。when/otherwise做分桶比 SQL 的 CASE WHEN 在代码里更易读。结果可以导出成 CSV 给可视化用city_rank.toPandas().to_csv(city_rank.csv, indexFalse)toPandas把 Spark DataFrame 转成 pandas数据量小的时候没问题几百万行以上会 OOM那种情况直接df.write.csv写 HDFS 再取。6. 避坑与排查酒店数据项目里最容易翻车的五件事6.1 格式化两次导致 DataNode 起不来现象start-dfs.sh后 jps 只有 NameNode没有 DataNode日志报Incompatible clusterID。原因重复执行hdfs namenode -formatNameNode 生成了新 clusterIDDataNode 还留着旧的。解决停掉所有进程清空dfs.datanode.data.dir和dfs.namenode.name.dir指向的目录重新格式化一次再启动。格式化只做一次记牢。6.2 中文乱码编码没转就上传现象Hive 查询出来省份、城市全是问号或方块。原因原始 CSV 是 GBK直接传 HDFSHive 按 UTF-8 解析。解决上传前用iconv -f GBK -t UTF-8转码或者建表时指定SERDEPROPERTIES (serialization.encodingGBK)。推荐转码一劳永逸。6.3 字段内嵌逗号导致列错位现象Hive 查询某些行城市字段变成了地址的一部分列整体右移。原因地址字段里有逗号split(,)或 Hive 默认分隔符把它当成了列分隔。解决清洗阶段用 Python csv 模块重新导出把内嵌逗号字段加引号或者换用\t、|等不会出现在内容里的分隔符。这是酒店数据最常见的脏数据问题。6.4 动态分区报错strict 模式限制现象INSERT INTO ... PARTITION报Dynamic partition strict mode requires at least one static partition column。原因Hive 动态分区默认 strict要求至少一个静态分区。解决执行SET hive.exec.dynamic.partition.modenonstrict;并且这个设置要在同一个会话里重连就失效脚本里要重新设。6.5 Spark 读 Hive 表报找不到元数据现象spark.sql(SELECT * FROM hotel_partitioned)报Table or view not found。原因Spark 没找到 hive-site.xml连的是内置的 Derby 元数据库不是 Hive 的 MySQL 元数据。解决把 Hive 的 hive-site.xml 复制到 Spark 的 conf 目录确保hive.metastore.uris指向正确的地址或者启动hive --service metastore后在 Spark 里配spark.hadoop.hive.metastore.uris。本地练手最省事的办法是复制配置文件。7. 把分析结果落成可复用的指标表跑通全链路之后真正有价值的是把常用指标固化成表下次来新数据直接覆盖分区就行。我一般会建三张结果表dws_province_stat存省市维度统计dws_score_dist存评分分布dws_open_trend存开业趋势。用 Hive 的INSERT OVERWRITE按天或按批次覆盖配合调度工具定时跑。-- 省市统计结果表ORC 存储 CREATE TABLE IF NOT EXISTS dws_province_stat ( province STRING, hotel_cnt BIGINT, avg_price DOUBLE, avg_score DOUBLE ) STORED AS ORC; INSERT OVERWRITE TABLE dws_province_stat SELECT province, COUNT(*) AS hotel_cnt, ROUND(AVG(price), 2) AS avg_price, ROUND(AVG(score), 2) AS avg_score FROM hotel_partitioned GROUP BY province;INSERT OVERWRITE会清空目标表再写适合全量重算。如果数据是按天增量改成INSERT INTO配合分区。ORC 格式压缩比高查询时列裁剪效果好做结果表比 TEXTFILE 合适。验证结果表有个小技巧拿 Hive 的结果和 Spark 的结果对一遍两边算出来的 avg_price 差在小数点后两位以内说明链路没问题。我习惯在 Spark 里再算一次同样的聚合df.groupBy(province).agg(avg(price))和 Hive 表 join 对比差异大的省份单独查原始数据多半是脏数据没清干净。还有一个容易忽略的点酒店数据里同一家酒店可能有多条记录不同平台、不同时间抓取做统计前要去重。用ROW_NUMBER() OVER (PARTITION BY hotel_id ORDER BY comments DESC)取评论数最多的那条或者按 hotel_id 加名称做 distinct。不去重的话省市酒店数会虚高均价也会被重复记录拉偏。这个坑我在第一次做的时候踩过统计出来某省酒店数比实际多了三成查了半天才发现是重复数据。最后说个习惯每次改完 SQL 或 Spark 代码先在小分区上跑比如WHERE province北京确认逻辑对了再全量跑。全量两千万行跑一次几分钟但逻辑错了重跑就是浪费。把中间结果落成临时表出问题能回溯到哪一步比从头再来省事得多。希望帮到你。本文还有配套的精品资源点击获取