《Shell语言调用SparkSQL抽取业务DB数据到hadoop集群》讲述了如何将业务库的数据etl到hadoop集群ods层,在hadoop集群上经过spark,hive控件处理dwd层,dm层以及app层后,很多需要还是需要将集群的数据再分发到集群外,比如数据导成excel,csv,数据回写到mysql,sql server等等,也找了很多大数据工具,感觉都不是很灵活,于是乎就自己用python写一些常用的,第一次这么全面的写ETL工具,真香!
windows或者shell环境下,包有点大,可能有网络不稳定的话就多试几次,总会成功的,如果还不成功,再百度下办法。
pip install pyspark
首先第一步是利用pyspark去数据仓库Hive里面把数据取出来,存在dataframe上,注意 ding.main(n)是有异常的话钉钉报警,可以参考: 调度Job报错或异常触发钉钉报警(Python 3.x版)。
#-*-coding:utf-8-*-
from pyspark.sql import HiveContext,SparkSession
from pyspark.sql.functions import col
import os
import sys
from ding_talk_warning_report_py.main import ding_talk_with_agency as ding
#传入的参数是hive的取数脚本,确保脚本准确
def exec_hive(hivesql):
try:
#初始化,如果要执行hive sql,需要在初始化中加入enableHiveSupport()
spark = SparkSession.builder.master("yarn").appName("DistributeData").enableHiveSupport().getOrCreate()
hive_context = HiveContext(spark)#初始化pyspark的环境变量
spark_df = hive_context.sql(hivesql)#执行Hive sql结果赋值给spark dataframe
return spark_df #返回一个spark的dataframe,dataframe可以理解为一张表
except Exception as e:
print(e)
n = [0,94]
ding.main(n) #报错的话钉钉报警,可以参考:调度Job报错或异常触发钉钉报警(Python 3.x版)
raise e //异常外抛,系统爆错出来
以下是将Hive的抽取结果写入excel或者csv,实际调用的时候可以写一个type参数来确定是调用导入excel的export_excel还是导入csv的export_csv,注意 ding.main(n)是有异常的话钉钉报警,可以参考: 调度Job报错或异常触发钉钉报警(Python 3.x版)。
import init_spark
import os
import sys
import pandas as pd
from ding_talk_warning_report_py.main import ding_talk_with_agency as ding
#参数1 hive_sql:hive上的取数脚本
#参数2 excel_path:excel的输出路径,如果不写,默认输出当前路径
#参数3 excel_name:excel的文件名字
#参数4 sheet:excel的sheet名字
#参数5 header_list:excel的表头,允许为空,即不需要表头
def export_excel(hive_sql,excel_path,excel_name,sheet,header_list):
try:
#调用init_spark
spark_df = init_spark.exec_hive(hive_sql)
#转成padas dataframe,如果表头为空,则无表头处理
if(not header_list):
padas_df = pd.DataFrame(spark_df.collect())
#if(header_list or isinstance(header_list,list)):
else:
padas_df = pd.DataFrame(spark_df.collect(),columns=header_list)
#获取当前目录
cur_path = os.path.dirname(__file__)
#设置Excel文件名
path = '%s/%s'%(excel_path,excel_name)#如果excel输出路径为空,则默认输出到当前路径,否则输出到指定路径。
padas_df.to_excel(path,sheet_name=sheet,encoding='utf-8',index=False, header=header_list)#将数据写入excel
except Exception as e:
print(e)
n = [0,95]
ding.main(n)
raise e //异常外抛,系统爆错出来
#参数1 hive_sql:hive上的取数脚本
#参数2 csv_path:csv的输出路径,如果不写,默认输出当前路径
#参数3 csv_name:csv的文件名字
#参数4 sep_name:csv的分隔符
#参数5 header_list:csv的表头,允许为空,即不需要表头
#参数5 compression_name='infer',不压缩
def export_csv(hivesql,csv_path,csv_name,sep_name,header_list,compression_name='infer'):
try:
#调用init_spark
spark_df = init_spark.exec_hive(hivesql)
#转成padas dataframe,如果表头为空,则无表头处理
if(not header_list):
print("have no header")
padas_df = pd.DataFrame(spark_df.collect())
#if(header_list or isinstance(header_list,list)):
else:
print("have header")
padas_df = pd.DataFrame(spark_df.collect(),columns=header_list)
#设置csv的输出路径和文件名,如果不写,默认输出当前路径
path = '%s/%s'%(csv_path,csv_name)
#写入csv
padas_df.to_csv(path,encoding='utf-8',index=False, header=header_list,sep=sep_name,compression=compression_name)
#padas_df.to_csv(path,encoding='utf-8',index=False, header=True,compression=compression_name)
except Exception as e:
print(e)
n = [0,96]
ding.main(n)
raise e //异常外抛,系统爆错出来
往往生成excel或者csv后,都需要以邮件附件的形式发送,具体python代码如下。
#-*-coding:utf-8-*-
import os
import configparser
import sys
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.header import Header
from ding_talk_warning_report_py.main import ding_talk_with_agency as ding
from email.header import Header
# 发送邮件
#参数
#mail_msg 邮件正文内容
#mail_subject 邮件主题
#mail_to 收件人
#mail_cc 抄送人
#mail_bcc 密送人
#is_attachment='N' 是否附件,默认为否
#attch_path='' 附件路径
#attch_name='' 附件名称
def send_email(mail_msg,mail_subject,mail_to,mail_cc,mail_bcc,is_attachment='N',attch_path='',attch_name=''):
config = configparser.ConfigParser()
#获取当前目录
file_path = os.path.dirname(__file__)
#设置config文件的路径,这里是从配置文件中读取邮箱服务器的信息。
ini_path = "%s/conf/myconfig.ini"%file_path
config.read(ini_path)
mail_host = config['mail_config']["mail_host"] #邮箱服务器IP
mail_port = int(config['mail_config']["mail_port"]) #邮箱服务器端口
mail_user = config['mail_config']["mail_user"] #邮箱服务器登录用户名
mail_passwd = config['mail_config']["mail_passwd"] #邮箱服务器登录密码
sender = config['mail_config']["sender"] #邮箱发件人
try:
# 三个参数:第一个为文本内容,第二个 plain 设置文本格式,第三个 utf-8 设置编码
message = MIMEMultipart()
message['From'] = Header("dw_bi", 'utf-8') #如果要匿名发送,则可以用Header匿名发件人
message['To'] = mail_to # 收件人邮箱昵称、收件人邮箱账号
message['Cc'] = mail_cc #邮件抄送人
message['Bcc'] = mail_bcc #邮件暗送人
message['Subject'] = mail_subject #邮件主题
#邮件正文内容
message.attach(MIMEText(mail_msg,'html','utf-8'))
if is_attachment == 'Y':
# 添加附件(附件为Excel或者csv格式的文本)
excel_path = '%s/%s'%(attch_path,attch_name)
att = MIMEApplication(open(excel_path, 'rb').read(),'utf-8')
att["Content-Type"] = 'application/octet-stream'
att.add_header('Content-Disposition', 'attachment', filename=attch_name)
message.attach(att)
server = smtplib.SMTP(mail_host,mail_port) #发件人邮箱中的SMTP服务器,端口
server.login(mail_user,mail_passwd) #发件人邮箱用户和密码
# 第三个参数,msg 是字符串,表示邮件。我们知道邮件一般由标题,发信人,收件人,邮件内容,附件等构成,发送邮件的时候,要注意 msg 的格式。这个格式就是 smtp 协议中定义的格式
server.sendmail(sender,message['To'].split(",") + message['Cc'].split(",")+ message['Bcc'].split(","),message.as_string())
except smtplib.SMTPException as e:
print(e)
print("Error: 无法发送邮件")
n = [0,20]
ding.main(n)
raise e //异常外抛,系统爆错出来
主函数调用,为了规范,我把邮件组和这次的行为做成mysql配置表cfg_email,邮件组配置表如下:
CREATE TABLE `cfg_email` (
`id` bigint(20) NOT NULL AUTO_INCREMENT comment '自增id',
`email_name` varchar(100) CHARACTER SET utf8 NOT NULL comment '邮件名字',
`email_owner` varchar(100) CHARACTER SET utf8 NOT NULL comment '邮件主人名字',
`group_id` int(11) NOT NULL comment '邮件所属组id,可以是一个部门,也可以是一个系列的人',
`group_name` varchar(100) CHARACTER SET utf8 NOT NULL comment '邮件所属组名字,可以是一个部门,也可以是一个系列的人',
`is_enable` int(11) DEFAULT NULL comment '是否有效',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`create_user` varchar(100) CHARACTER SET utf8 NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_user` varchar(100) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `mail_key` (`email_name`,`group_id`,`is_enable`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
此次整体的配置表cfg_app_pushdown_file_to_mail如下,主要涉及文件的类型,参数以及邮件的参数
CREATE TABLE `cfg_app_pushdown_file_to_mail` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`data_set_id` int(11) NOT NULL COMMENT '同一个数据集',
`sequence_id` int(11) NOT NULL COMMENT '同一个数据集执行顺序',
`sql_file` varchar(100) CHARACTER SET utf8 NOT NULL COMMENT '输入的sql文件名',
`header_list` varchar(2000) CHARACTER SET utf8 NOT NULL COMMENT '输出文件的表头,不需要的话写空格',
`file_type` varchar(20) CHARACTER SET utf8 NOT NULL COMMENT '输出文件类型,写csv或excel',
`excel_sheet` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '如果输出文件为excel,则写sheet名,否则可为null',
`csv_seq` varchar(20) CHARACTER SET utf8 DEFAULT NULL COMMENT '如果是csv文件,csv文件的列分隔符',
`file_path` varchar(200) CHARACTER SET utf8 DEFAULT NULL COMMENT '输出数据文件路径,如果不填,则默认输出执行程序的当前路径',
`file_name` varchar(200) CHARACTER SET utf8 NOT NULL COMMENT '输出文件名,不需要要带文件后缀',
`is_send_mail` varchar(5) CHARACTER SET utf8 NOT NULL COMMENT '是否发送邮件,1是,0或其他否',
`mail_to_groupid` int(11) DEFAULT NULL COMMENT '邮件收件组id,组id参考cfg_email表',
`mail_cc_groupid` int(11) DEFAULT NULL COMMENT '邮件抄送人组id,组id参考cfg_email表',
`mail_bcc_groupid` int(11) DEFAULT NULL COMMENT '邮件暗送组id,组id参考cfg_email表',
`mail_subject` varchar(100) CHARACTER SET utf8 DEFAULT NULL COMMENT '邮件主题',
`is_enable` int(11) NOT NULL COMMENT '是否有效',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_user` varchar(100) CHARACTER SET utf8 NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_user` varchar(100) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
对于这个mysql的认证配置信息,我们可以写在当前目录下的conf folder下的myconfig.ini下,即 “./conf/myconfig.ini”里面,其中mail_config是邮箱服务器的配置,mysql_dw_config是存储以上两张配置表的信息,内容如下:
[mail_config]
mail_host = 10.256.25.109
mail_port = 45
mail_user = dw_bi
mail_passwd = 123456zxc
sender = dw_bi@iloveyou,cn
[mysql_dw_config]
host = 10.256.49.38
port = 3306
user = dw_user
passwd = 123iloveyou
db = dw_config
charset = utf8
以上都准备好了后,才是真的万事具备了,开始准备python主函数的调用,具体如下
#-*-coding:utf-8-*-
import requests
import configparser
import json
import pymysql
from sqlalchemy import create_engine
import pandas as pd
from email.mime.text import MIMEText
import init_spark
from pyspark.sql import HiveContext,SparkSession
import os
import send_mail
import sys
import export_file
from ding_talk_warning_report_py.main import ding_talk_with_agency as ding
#获取mysql配置的登录连接信息,
def getcon():
config = configparser.ConfigParser()
file_path = os.path.dirname(__file__)
ini_path = "%s/conf/myconfig.ini"%file_path
config.read(ini_path)
config_host = config['mysql_dw_config']["host"]
config_port = int(config['mysql_dw_config']["port"])
config_user = config['mysql_dw_config']["user"]
config_passwd = config['mysql_dw_config']["passwd"]
config_db = config['mysql_dw_config']["db"]
config_charset = config['mysql_dw_config']["charset"]
## print(config_host+'\t'+config_port+'\t'+config_user+'\t'+config_passwd+'\t'+config_db+'\t'+config_charset)
try:
print(1)
conn = pymysql.Connect(host=config_host, port=config_port, database=config_db, user=config_user,password=config_passwd, charset=config_charset)
return conn
except Exception as e:
print(e)
raise e //异常外抛,系统爆错出来
def main(argv):
event_day=argv[1] #指定时间日期,用于给生成的文件名加后缀
myid= int(argv[2]) #指定id来选择配置表cfg_app_pushdown_file_to_mail的哪一条记录
contends=argv[3] #传入的sql脚本,因为sql脚本可能带参数,所以这个sql是shell处理过的完全能在hive环境跑的通的脚本
print("sql text:"+contends)
dw_config=getcon() #通过getcon获取连接
#将配置表cfg_app_pushdown_file_to_mail和邮件配置表组合cfg_email,得到所有的调用参数
sqlcmd = """select
cfm.id
,cfm.data_set_id
, cfm.sequence_id
, ifnull(cfm.sql_file,'') as sql_file
, ifnull(cfm.header_list,'') header_list
, cfm.file_type
, ifnull(cfm.excel_sheet,'') excel_sheet
, ifnull(cfm.csv_seq,'') csv_seq
, ifnull(cfm.file_path,'') file_path
, ifnull(cfm.file_name,'') file_name
, cfm.is_send_mail
, ifnull(GROUP_CONCAT(DISTINCT cmt.email_name SEPARATOR ','),'') mail_to
, ifnull(GROUP_CONCAT(DISTINCT cmcc.email_name SEPARATOR ','),'') mail_cc
, ifnull(GROUP_CONCAT(DISTINCT cmbcc.email_name SEPARATOR ','),'') mail_bcc
, ifnull(cfm.mail_subject,'') mail_subject
, cfm.is_enable
, cfm.create_time
, cfm.create_user
, cfm.update_time
, cfm.update_user
from cfg_app_pushdown_file_to_mail cfm
left join cfg_email cmt
on cfm.mail_to_groupid=cmt.group_id
and cmt.is_enable=1
left join cfg_email cmcc
on cfm.mail_cc_groupid=cmcc.group_id
and cmcc.is_enable=1
left join cfg_email cmbcc
on cfm.mail_bcc_groupid=cmbcc.group_id
and cmbcc.is_enable=1
where cfm.id=%d
group by cfm.id,cfm.data_set_id, cfm.sequence_id, cfm.sql_file, cfm.header_list , cfm.file_type, cfm.excel_sheet"""
sql1 = sqlcmd % (myid)
cfg_dataframe=pd.read_sql(sql1,dw_config) #将所有的参数放在dataframe里面
##print(cfg_dataframe)
if cfg_dataframe.empty:
print("no file to report")
else:
sql_file=cfg_dataframe['sql_file'].values[0]
print("sql_file:"+sql_file)
header_list=cfg_dataframe['header_list'].values[0]
header_list=header_list.split(",")
print("header_list:")
print(header_list)
file_type=cfg_dataframe['file_type'].values[0]
print("file_type:"+file_type)
excel_sheet=cfg_dataframe['excel_sheet'].values[0]
print("excel_sheet:"+excel_sheet)
csv_seq=cfg_dataframe['csv_seq'].values[0]
print("csv_seq:"+csv_seq)
file_path=cfg_dataframe['file_path'].values[0]
print("file_path:"+file_path)
file_name=cfg_dataframe['file_name'].values[0]
print("file_name:"+file_name)
is_send_mail=cfg_dataframe['is_send_mail'].values[0]
print("is_send_mail:"+is_send_mail)
mail_to=cfg_dataframe['mail_to'].values[0]
print("mail_to:"+mail_to)
mail_cc=cfg_dataframe['mail_cc'].values[0]
print("mail_cc:"+mail_cc)
mail_bcc=cfg_dataframe['mail_bcc'].values[0]
print("mail_bcc:"+mail_bcc)
mail_subject=cfg_dataframe['mail_subject'].values[0]
print("mail_subject:"+mail_subject)
#获取sql文件的内容
#with open(sql_file) as file_object:
# contends = file_object.read()
hive_sql = contends
print("hive sql text:"+hive_sql)
#生成一个list作为列名
#header_list=['学号','姓名','性别','年龄'] #参数1
#header_list=""
if header_list=="":
header_list=False
print("header_list:%s"%header_list)
file_name = file_name+event_day #参数3
#获取当前目录
cur_path = os.path.dirname(__file__)
#如果文件路径为空,则默认选当前文件夹,否则选指定的file_path
if file_path=="":
my_file_path = cur_path
else:
my_file_path=file_path
#如果文件类型选excel
if file_type=='excel': #参数5
file_name=file_name+".xlsx" #文件名加xlsx后缀
my_sheet=excel_sheet #参数4 #excel的sheet名
#调用export_file.py中的export_excel函数,生成excel
export_file.export_excel(hive_sql=hive_sql,excel_path=my_file_path,excel_name=file_name,sheet=my_sheet,header_list=header_list)
print("the excel has create on %s"%file_name)
#如果文件类型选csv
if file_type=='csv':
file_name=file_name+".csv" #文件名加csv后缀
my_sep_name=csv_seq #参数6 csv分隔符
my_compression_name='infer' #选择不压缩
# export_csv(hivesql,csv_path,csv_name,sep_name,compression_name='infer',header_list=[])
#调用 export_file.export_csv来生成csv export_file.export_csv(hivesql=hive_sql,csv_path=my_file_path,csv_name=file_name,sep_name=my_sep_name,header_list=header_list,compression_name=my_compression_name)
print("the csv has create on %s" % file_name)
#如果要发邮件附件
if is_send_mail=="1": #参数8
print("send mail start!")
mail_msg = '''
Dear all:
  附件中是'%s',请查收!
  此邮件为系统自动发送,请勿直接回复!如有任何疑问,可联系数仓相关人员:
  数据仓库组 [email protected];
'''%file_name
#message = MIMEText(mail_msg, 'html', 'utf-8')
#调用send_mail.py文件中的send_email函数,发邮件
send_mail.send_email(mail_msg=mail_msg,mail_subject=mail_subject,mail_to=mail_to,mail_cc=mail_cc,mail_bcc=mail_bcc,is_attachment='Y',attch_path=my_file_path,attch_name=file_name)
print("send mail successfully!")
if __name__ == "__main__":
main(sys.argv)
#main函数有三个参数
# argv[1] 指定时间日期,用于给生成的文件名加后缀
# int(argv[2]) 指定id来选择配置表cfg_app_pushdown_file_to_mail的哪一条记录
# argv[3] #传入的sql脚本,因为sql脚本可能带参数,所以这个sql是shell处理过的完全能在hive环境跑的通的脚本
主函数好了,但是不至于每天都让人去调用对吧,所以选择大家最喜欢的shell语言完成调度,这样就完成了自动化的部署,之前提到过,还有个重要的取数脚本要处理,而且取数脚本还及其可能是带动态化参数的,所以方便起见,我们把它放在当前的一个sql文件里面例子如下students.sql:
-- ['学号','姓名','性别','年龄']
select
sno,
sname,
ssex,
sage
from students
where ssex='m'
and event_day=%s
自动化的shell语言脚本call_job.sh如下:
#!/usr/bin/env bash
mydate=$1 #指定的日期
myid=$2 #指定配置表cfg_app_pushdown_file_to_mail 的id
my_sql_file_argv=$3 #指定sql取数脚本的文件名
mydate=$(date -d"1 day ago ${mydate}" +%Y%m%d) #T-1的天数统计
event_week=$(date -d ${
mydate} +%V) #计算所属的周
event_week=$((10#$event_week)) #convert enentweek to Int type
event_hour=00
cur_dir=`pwd`
echo ${mydate}
echo ${event_week}
echo ${event_hour}
#解析文件内容和替换参数
my_sql_file=`cat ${
my_sql_file_argv} | tr "\n" " " | tr "\r" " "`
my_sql_file=`printf "${my_sql_file}" "${mydate}"`
echo "mydate:${mydate}"
echo "myid:${myid}"
echo "my_sql_file:${my_sql_file}"
#source_path1 需要改动,检测原始数据是否存在
source_path1=/hive/warehouse/app/students/event_week=${event_week}/event_day=${mydate}/event_hour=${event_hour}/_SUCCESS
#判断依赖的Source_path路径下文件是否都存在
function testfile()
{
local total_success_flag=0
for((i=1;i<=1;i++)); #需要修改
do
mysource_path=`eval echo '$'"source_path$i"`
hadoop fs -test -e $mysource_path
hdfs_flag=$?
((total_success_flag+=hdfs_flag))
done
echo $total_success_flag
}
#统计依赖的source文件是否ready的个数
total_success_result=$(testfile)
echo "total not exists files: $total_success_result"
n=0
while (($total_success_result != 0))
do
#如果文件不存在,睡一分钟
sleep 1m
((n++))
#尝试60次,即一小时,再没有结果就认为报错超时
if [ $n -gt 60 ]; then
echo "等待1个小时,系统数据还没加载到hdfs"
cd ${cur_dir}/../src/ding_talk_warning_report_py/main/
python3 ding_talk_with_agency.py 99
exit 7
fi
total_success_result=$(testfile)
echo "total not exists files: $total_success_result"
done
#利用pyspark调用主函数app_pushdown_file_to_mail.py
spark-submit --master yarn --deploy-mode client --executor-memory 2G --executor-cores 1 --num-executors 4 --queue etl ./../src/app_pushdown_file_to_mail.py ${mydate} ${myid} "${my_sql_file}"
my_flag=$? #检测上一句是否执行成功
echo "status:$my_flag"
if [ $my_flag -eq 0 ];then
echo "send to monitor_report as a excel attach file has successed"
else #不成功则钉钉报警
echo "send to monitor_report as a excel attach file has failed,deal with it hurry up!"
cd ${cur_dir}/../src/ding_talk_warning_report_py/main/
python3 ding_talk_with_agency.py 93
exit 7
fi
毕竟是第一次写etl小工具,写的还是有点粗糙的,还有很大的改善空间吧,但是感觉用起来还蛮舒服的,根据自己的要求随心所欲,同时也进行的配置表进阶,让以后这些Job都能在配置表上统一化管理,略能得瑟一下吧,从此之后什么什么ETL工具?渣渣!(手动滑稽),尽情期待Hadoop集群数据分发——pyspark导出Hive集群数据及python写入mysql或sql server 。