这个月遇到了一个需求,上游业务同事需要把某个系统下某个数据源下的所有数据给存储到数仓里,一开始感觉需求很简单,上游给我合成一个表,我在数仓就建表接数据就好了,真是个清闲活,于是开开心心的搞,但是数据源调查和业务沟通的时候发现一个非常严重的问题:
这个数据源下有600多个文件,表名没有任何规律,这些文件多的有800多行,少的只有1行2行,最恶心的是,每个表的字段数不是一样的,比如第一个表字段数可能是10个,第二个表有100个字段,真实的表最多的字段不超过300个,并且每个字段的含义(表头)也不一样
后续每天会有100个左右最新的任务数据每天同步,可能是新增也可能是修改
这就算了,让我感到崩溃的是,数据源所有的文件,业务合并不成一张表?!我在想那岂不是我得一个表一个表手工建?那么多不同的字段怎么处理?后期维护怎么办?数据质量怎么把控?
心里快崩溃了,600多个表,就是用AI写sql脚本,一个星期都有可能搞不完,而且还要求准确性还有后续能够快速查询和开发,光表名我都记不住,都还很重要
数仓的设计出来处理结构化 的数据的,但是这种超多个文件的数据源,显然超出我人工处理的范围内,表的字段都不固定,含义也不固定,这不典型的非结构化数据吗?难不成啥也不干半个月就在工位上库库建表了?后续再有新表我再新建?
经过头发掉了10086根后,先分析下背景:
1.虽然数据不同,好在都是csv文件,至少文件格式统一
2.虽然每个表字段数不同,但是表的字段最长的数量不会超过300
3.表头虽然不断变化,每个表都不一样,但是本质上表头也是一条数据,应该可以获取
取得了一部分的思路去解决:
1.业务合并不成一张表给我,我怎么去读取每个文件呢?spark.read可以读取多个文件
2。如何做到只更新部分数据?而保留历史数据?还要新增任务数据?
分区表就可以完成新建和更新,对分区里的数据insert overwrite就行
理论成立,实践开始:
拿到每天传文件的路径后,使用Python Spark 读取文件,用到Spark.read.csv方法的时候,以为可以按照行路径下列最多的表给我建一个大的表,其他的表都会插进去,完美解决,后面发现一个大问题
Spark.read在读取某个路径下 "/*.csv"(全部的csv文件的时候)他是随机读第一个文件来建表的!!!!并且和文件名和首行、首列都没有关系,完全随机读
这就产生一个贼恐怖的问题,比如这600多个表里,read文件的时候,读取第一个表的时候,
假设第一个表只有有10列,第二个表有100列,那spark在read就只会读取第二个文件的前10列,
后面90列直接丢掉,最终df转换成视图的时候,这个视图里只有这个10列的表了
关键后面每天给我的任务数据还不固定,要是第二天同步的表里就2行,我最长300列的表的数据,最后不只有2列了?
重新整理了思路后,发现我可以通过Python,我把所有的文件合并在一起,不够300列的字段我就补到300列出来,这样read不管读取哪个表都可以拿到300列全部表的全部数据了
思路就是,
1.先读取全部文件夹下的文件名,之后将列名放到循环中,让read循环去读取并把改造好的表新建到另外一个地方,
2.再通过read读取这个路径下的全部统一完格式 的文件就完美解决了,
另外需要让业务同事在每个新加的表上新加一个字段,用于区分表头这个数据,并且把表头插入表的head=‘False’就可以按照ID分区建表了
于是就有了以下的代码:
import re
from pyspark.sql.functions import col
from pyspark.sql import SparkSession
from pyspark.sql.functions import lit, col
from pyspark.sql.types import StructType, StructField, StringType, NullType
## Synapse管道的record_day传参,可以忽略,就是一个日期20220202
record_day: str = DatePartition
record_day=record_day.replace('/','')
##1.天才第一步,雀氏牛逼酷!
# 加载数据源路径下所有文件的文件名,并放到一个集合中
ods_tables = {}
for sys_fs in mssparkutils.fs.ls(f'旧的文件路径/{record_day}/'):
for table in mssparkutils.fs.ls(sys_fs.path):
find_t = re.findall( '(.*)',table.name)
if find_t:
ods_tables.setdefault(find_t[0][0], set())
ods_tables[find_t[0][0]].add(table.name)
print(ods_tables)
#第二步,使用一个循环遍历ods_tables的键和值
for key, filenames in ods_tables.items():
# 对于每个键(可能是某种分类或前缀),遍历其对应的文件名集合
for filename in filenames:
# 这里你可以对每一个文件,{filename}(即表名或CSV文件名)进行任何需要的操作
# 例如,打印出来
print(f"处理表名: {filename}")
service_url=f'旧的文件路径/{record_day}/{filename}'
#第三步,这里写写读取read函数就好,记住不要表头作为第一行
df = spark.read.load(service_url , format='csv', header=False)
#第四步,获取文件后,准备给文件补充至300列,不够数量的置为空
# 获取当前DataFrame的字段数量
current_cols = len(df.columns)
# 补充至300个空列
# 需要添加的列数
num_cols_to_add = 300 - current_cols
# 添加空列
for i in range(num_cols_to_add):
new_col_name = f"empty_{i+1}"
df = df.withColumn(new_col_name, lit("Null"))
#第五步,文件写到新的路径下
# 设置保存的CSV文件路径
output_path = f'新的文件路径/{record_day}/{filename}'
# 保存Spark表为CSV文件
pandas_df = df.toPandas()
pandas_df.to_csv(output_path, index=False)
print(f"({filename}已经完成")
# pandas_df.to_excel(output_path, index=False)
# pandas_df.to_json(output_path,)
之后新路径下的所有文件就格式统一了,这样spark.read.csv()在随机读取第一个文件的时候,
不管读取到哪个文件,都是最大的列数,就不会出现文件丢失的情况了,因为都是300列,所有的数据也都可以通过循环结束后的一个read,读取到全部的文件,之后构建视图建表,数据就乖乖进数仓啦!!
df = spark.read.load(service_url , format='csv', header=False)
结束,撒花!