用 Scala 来写 Spark 清洗逻辑,(因为 Scala 是强类型的,代码更严谨,且是 Spark 的原生语言)。
一、 数据仓库整体设计
数据来源:
数据通过爬虫(如 Python 脚本)获取,来源包括智联招聘、Boss 直聘、拉勾网等。
分层逻辑:
- ODS 层 (原始数据层):存储爬虫抓下来的原始 CSV/JSON 数据,保持原貌,按天分区。
- DWD 层 (明细数据层):核心清洗层。(用spark清洗)将"15k-25k"这样的字符串薪资转换为数字,统一城市名称,清洗学历和经验要求。
- DWS 层 (汇总数据层):按业务主题聚合。例如按"城市"、"学历"、"公司性质"进行统计。
- ADS 层 (应用数据层):直接面向报表的数据。例如"大数据岗位薪资城市排行榜"、"学历对薪资的影响分析"。
**二、**总体工作流 (Workflow)
- 爬虫 (Python):
- 抓取数据 -> 存为 JSON Lines 文件(jobs.json,一行一个 JSON 对象)。
- ODS 层 (Hive):
- 建表存原始 JSON 字符串。
- LOAD DATA 加载数据。
- 清洗环节 (Spark - Scala):
- 读取 ODS 表 -> 解析 JSON -> 清洗数据 (正则/类型转换) -> 生成 DataFrame。
- 将结果写入 DWD 表。
- DWD 层 (Hive):
- 只负责建表(定义最终结构),接受 Spark 写入的数据。
- DWS/ADS 层 (Hive SQL):
- 写 SQL 聚合分析。
三、各层详细操作指南(此处为操作示例,仅作参考)
Step 1: ODS 层 (保持原样**,**操作数据存储层)
设计目标:原样存储爬虫抓取的岗位数据,防止数据丢失。
数据格式:文本文件,以逗号或制表符分隔。
这一层不需要变,还是存原始 JSON 串。
-- Hive SQL
CREATE TABLE IF NOT EXISTS ods.ods_job_csv (
json_record STRING COMMENT '原始数据'
)
PARTITIONED BY (dt STRING)
STORED AS TEXTFILE;
-- 如果表已存在则删除
DROP TABLE IF EXISTS ods.ods_job_csv;
-- 创建符合CSV结构的表
CREATE TABLE IF NOT EXISTS ods.ods_job_csv (
position_name STRING COMMENT '职位名称',
company_name STRING COMMENT '公司名称',
experience_req STRING COMMENT '工作经验要求',
education_req STRING COMMENT '学历要求',
work_location STRING COMMENT '工作地点',
salary STRING COMMENT '薪酬',
publish_time STRING COMMENT '发布时间',
job_description STRING COMMENT '职位描述或任职要求',
update_time STRING COMMENT '更新时间',
city_district STRING COMMENT '城市地区',
company_welfare STRING COMMENT '公司福利',
company_size STRING COMMENT '公司规模',
company_type STRING COMMENT '公司类型',
company_tags STRING COMMENT '公司标签',
detail_json STRING COMMENT '区经验学历'
)
COMMENT '原始大数据岗位数据ODS层'
PARTITIONED BY (dt STRING)
STORED AS TEXTFILE;
-- 加载数据
LOAD DATA LOCAL INPATH '/data/job_all.csv' INTO TABLE ods.ods_job_csv PARTITION(dt='2025-12-10');
Step 2: DWD 层 (只建表)
先定义好清洗后的格式。
-- Hive SQL CREATE TABLE IF NOT EXISTS dwd.dwd_job_detail (
job_name STRING,
company_name STRING,
min_salary DOUBLE,
max_salary DOUBLE,
city STRING, education STRING )
PARTITIONED BY (dt STRING) STORED AS PARQUET; -- 推荐 Parquet
Step 3: Spark 清洗 (核心 - Scala 实现)
需要创建一个 Scala Object(例如 JobDataEtl.scala)。
Scala 处理 JSON 的核心逻辑:
ODS 里是一列字符串,我们需要用 Spark SQL 的内置函数 get_json_object 把变成多列。
package com.yourproject.etl`
`import org.apache.spark.sql.{SparkSession, SaveMode}`
`import org.apache.spark.sql.functions._`
`object JobDataEtl {`
` def main(args: Array[String]): Unit = {`
` // 1. 初始化 SparkSession (支持 Hive)`
` val spark = SparkSession.builder()`
` .appName("JobDataCleaning_Scala")`
` .enableHiveSupport() // 必须开启`
` .getOrCreate()`
` import spark.implicits._ // 导入隐式转换,可以使用 $"column" 写法`
` val dt = "2025-10-07" // 可以通过 args(0) 传入`
` // 2. 读取 ODS 层数据`
` val odsDf = spark.sql(s"SELECT json_record FROM ods.ods_job_json WHERE dt='$dt'")`
` // 3. 解析 JSON & 数据清洗`
` // 假设 JSON 结构是: {"job": "大数据开发", "salary": "15-25K", "company": "阿里", "city": "杭州"}`
` val cleanedDf = odsDf`
` // 3.1 第一步:先把 JSON 字段抠出来`
` .select(`
` get_json_object($"json_record", "$.job").as("raw_job_name"),`
` get_json_object($"json_record", "$.salary").as("raw_salary"),`
` get_json_object($"json_record", "$.company").as("company_name"),`
` get_json_object($"json_record", "$.city").as("raw_city"),`
` get_json_object($"json_record", "$.education").as("raw_education")`
` )`
` // 3.2 第二步:对字段进行精细化清洗 (正则、类型转换)`
` .withColumn("min_salary", regexp_extract($"raw_salary", "(\\d+)-", 1).cast("double") * 1000)`
` .withColumn("max_salary", regexp_extract($"raw_salary", "-(\\d+)K", 1).cast("double") * 1000)`
` .withColumn("job_name", trim($"raw_job_name")) // 去除首尾空格`
` .withColumn("city", split($"raw_city", "-").getItem(0)) // "北京-朝阳" -> "北京"`
` .withColumn("education", `
` when($"raw_education".contains("本"), "本科")`
` .when($"raw_education".contains("硕"), "硕士")`
` .otherwise("其他")`
` )`
` // 3.3 第三步:选出最终符合 DWD 表结构的列`
` .select(`
` $"job_name",`
` $"company_name",`
` $"min_salary",`
` $"max_salary",`
` $"city",`
` $"education"`
` )`
` // 4. 写入 DWD 层`
` // 注意:Spark 写入 Hive 分区表时,最好配置 dynamic partition`
` spark.conf.set("hive.exec.dynamic.partition", "true")`
` spark.conf.set("hive.exec.dynamic.partition.mode", "nonstrict")`
` // 将数据写入 DWD 表的指定分区`
` // 这里的 logic 是:我们在 DataFrame 里没有 dt 列,但要写到 dt='$dt' 分区`
` // 方法 A:直接 insertInto (要求 DataFrame 列顺序和 Hive 表完全一致,除了分区列)`
` println(s"正在写入数据到 DWD 层,分区: $dt ...")`
` cleanedDf.write`
` .mode(SaveMode.Overwrite)`
` .insertInto(s"dwd.dwd_job_detail partition(dt='$dt')") `
` // 注意:上面的写法取决于 Spark 版本,更稳妥的方式是把 dt 加到 DataFrame 最后,然后 partitionBy("dt")`
` /* 稳妥的写法:`
` cleanedDf.withColumn("dt", lit(dt))`
` .write`
` .mode(SaveMode.Overwrite)`
` .format("hive")`
` .partitionBy("dt")`
` .saveAsTable("dwd.dwd_job_detail")`
` */`
` println("清洗完成!")`
` spark.stop()`
` }`
`}`
`
Step 4 DWS 层(汇总数据层)
设计目标:按业务主题聚合,减少 ADS 层计算量。
主要主题:城市主题、岗位分类主题。
create database if not exists dws;`
`-- 主题1:城市大数据岗位指标汇总表`
`CREATE TABLE dws.dws_city_job_summary (`
` city STRING,`
` job_count INT COMMENT '岗位数量',`
` avg_salary DOUBLE COMMENT '平均月薪',`
` company_count INT COMMENT '招聘公司数',`
` top_education STRING COMMENT '需求最多的学历'`
`)`
`PARTITIONED BY (dt STRING);`
`-- 数据聚合`
`INSERT OVERWRITE TABLE dws.dws_city_job_summary PARTITION (dt='2025-10-07')`
`SELECT`
` city,`
` COUNT(job_id) as job_count,`
` AVG(avg_salary) as avg_salary,`
` COUNT(DISTINCT company_name) as company_count,`
` -- 这里用一个简单的逻辑取Top1学历,实际可用窗口函数`
` '本科' as top_education `
`FROM dwd.dwd_job_detail`
`WHERE dt = '2025-10-07'`
`GROUP BY city;`
`-- 主题2:岗位分类汇总表(看哪类工作最赚钱)`
`CREATE TABLE dws.dws_category_job_summary (`
` job_category STRING,`
` avg_salary DOUBLE,`
` avg_experience DOUBLE,`
` job_count INT`
`)`
`PARTITIONED BY (dt STRING);`
`INSERT OVERWRITE TABLE dws.dws_category_job_summary PARTITION (dt='2025-10-07')`
`SELECT `
` job_category,`
` AVG(avg_salary) as avg_salary,`
` AVG(experience_min) as avg_experience,`
` COUNT(1) as job_count`
`FROM dwd.dwd_job_detail`
`WHERE dt = '2025-10-07'`
`GROUP BY job_category;`
`
Step5ADS 层(应用数据服务层)
设计目标:生成最终报表结果。
create database if not exists ads;`
`-- 指标1:全国大数据岗位高薪城市 Top10`
`CREATE TABLE ads.ads_top10_salary_city (`
` dt STRING,`
` rank INT,`
` city STRING,`
` avg_salary DECIMAL(10,2),`
` job_count INT`
`);`
`INSERT OVERWRITE TABLE ads.ads_top10_salary_city`
`SELECT`
` '2025-10-07' as dt,`
` row_number() OVER (ORDER BY avg_salary DESC) as rank,`
` city,`
` CAST(avg_salary AS DECIMAL(10,2)),`
` job_count`
`FROM dws.dws_city_job_summary`
`WHERE dt='2025-10-07' AND job_count > 10 -- 过滤掉样本太少的城市`
`LIMIT 10;`
`-- 指标2:学历的薪资回报率分析`
`CREATE TABLE ads.ads_education_salary_analysis (`
` dt STRING,`
` education STRING,`
` avg_salary DECIMAL(10,2),`
` job_share_rate DECIMAL(10,4) COMMENT '岗位占比'`
`);`
`INSERT OVERWRITE TABLE ads.ads_education_salary_analysis`
`SELECT`
` '2025-10-07' as dt,`
` education,`
` CAST(AVG(avg_salary) AS DECIMAL(10,2)),`
` CAST(COUNT(1) / SUM(COUNT(1)) OVER() AS DECIMAL(10,4)) as job_share_rate`
`FROM dwd.dwd_job_detail`
`WHERE dt='2025-10-07'`
`GROUP BY education;`
`
数据已经干净地躺在 DWD 表里了,接下来就可以回到 Hive 界面写 SQL 了。
-- DWS 层聚合`
`INSERT OVERWRITE TABLE dws.dws_city_stat PARTITION(dt='2025-10-07')`
`SELECT city, avg(min_salary) `
`FROM dwd.dwd_job_detail `
`WHERE dt='2025-10-07' `
`GROUP BY city;`
`
**四、**Scala 项目开发小贴士
依赖管理:
需要用 Maven 或 SBT 来管理依赖。记得引入 spark-core 和 spark-sql。
<dependency>`
` <groupId>org.apache.spark</groupId>`
` <artifactId>spark-sql_2.12</artifactId>`
` <version>3.x.x</version> <!-- 你的 Spark 版本 -->`
`</dependency>`
`
打包与运行:
Scala 代码不能像 Python 那样直接 python code.py 运行。
- 步骤 1: 在 IDEA 里编写代码。
- 步骤 2: 用 Maven/SBT 把项目打成 jar 包(例如 etl-1.0.jar)。
- 步骤 3: 把 jar 包上传到服务器。
- 步骤 4: 提交任务:
spark-submit \`
` --class com.yourproject.etl.JobDataEtl \`
` --master yarn \`
` ./etl-1.0.jar`
`
总结
- ODS: 存脏数据(JSON 串)。
- Spark (Scala): 读 ODS -> 解析 JSON (get_json_object) -> 清洗逻辑 -> 写 DWD。
- DWD: 存干净数据(Schema 明确)。
- DWS/ADS: 纯 SQL 分析。