sparkcore-sparksql数据清洗

数据格式

原格式

日期时间种类监测站1数据监测站…数据
StringIntStringDoubleDouble

数据清洗

PM2.5表、O3表…

时间监测站数据
String(“yyyy-MM-dd-HH“)StringDouble

这样会出现大量数据冗余但是去掉了空值,并且以时间和监测站为主键更加容易操作。

源数据表节选:

datehourtype1001A1002A
201501021AQI11785
201501021PM2.58834
201501021PM2.5_24h4854

思路

先把第一行的表头提取出来,影响操作数据。

除去第一行表头,获取所有数据,对每行数据进行切分,把type作为RDD的主键,再以date+hour作为key,其余数据作为value拼接成map,map为RDD的值。为了区分map中的key和value,date和hour使用",“链接,多个value使用”-"链接。

既(PM2.5,(20150102,1,88-34))

使用filter只取某一个type的数据,然后去掉key,既(20150102,1,88-34)

对RDD进行行转列,对后排的数据进行切分,切分后格式为(20150102,1,Array[88,34])

然后再进行模式匹配,对切分后的数组进行拆分,既行转列(20150102,1,88),(20150102,1,34)

在表头中取出所有的监测站代码组成一个字符串数组

将每一列的数据和监测站代码一一对应。(20150102,1,1001A,88)

输出至文件中。

这个垃圾数据我日,大体上没有问题,具体的文件中全是报错,一定要把所有情况考虑清楚。

有的文件中最后几行没有任何数据,比如22点以后不进行检测的。这样你如果直接去拉去line(3),就会因为没有数据导致数组溢出。

有的文件表头不全,你明明切出来是1563个监测站,但是第1563个监测站就会报数组溢出,挺离谱的。

有的文件甚至连检测的污染物类型都没有,你一取line(2)数组溢出我是没想到的,日了狗了。

数据结构优化

日期监测站0时数据1时数据…时数据
String(“yyyy-MM-dd“)StringDoubleDoubleDouble

减少了数据冗余,有效的减少存储量。因为一天一共就24个小时,合并成列也不算多。源数据两千八百多个列就挺尼玛离谱。

这样需要先进行行转列,把表头的监测站代码放进数据中,再列转行,把时间调整到表头,比较麻烦,但是既解决了空值问题,又减少了数据冗余。

既然第一步已经将数据格式化了,就可以存入hive进行操作了。

数据上传

先使用rz命令将数据上传到集群中,再上传至hdfs

hadoop fs -put air2015-2018/ /air/input

数据操作

package com.digitalchina

import java.io.IOException
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object AirQualityTest {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf()
      .setAppName("AirQuality")
    val sc = new SparkContext(sparkConf)
      val readRDD = sc.textFile(args(0))
        val airRDD = readRDD.map(
        line => {
          //分割字符串
          val lines = line.split(",")
          //先判断到底是否有数据
          if(lines.length>3) {
            //以日期,时间为key
            val key = lines(0) + "," + lines(1)
            //剩下的所有数据为value,循环组成一个map
            var value = lines(3)
            for (i <- 4 to lines.length - 1) {
              value = value + "-" + lines(i)
            }
            //以污染物为主键,map为值组成RDD
            (lines(2), (key, value))
          }else{
            //如果没有数据,就直接给三个空字符串,后面会去掉空字符串,键值不要给污染物类型line(2)
            //有的数据没有污染物的类型,尼玛离谱line(2)报了个数组溢出,表结构都没有为什么不丢弃啊?
            ("", ("", ""))
          }
        })
      collectRDD(airRDD,"PM2.5",args(1))
      collectRDD(airRDD,"PM10",args(1))
      collectRDD(airRDD,"SO2",args(1))
      collectRDD(airRDD,"NO2",args(1))
      collectRDD(airRDD,"O3",args(1))
      collectRDD(airRDD,"CO",args(1))
      sc.stop()
  }

  def collectRDD(airRDD: RDD[(String, (String, String))],pollution:String,output:String) {
    //取第一行表头(type,(date,hour,1001A,1002A,1003A))
      val headRDD = airRDD.first()
    //生成监测站代码数组(1001A,1002A,1003A,1004A,1005A,1006A)
    val stations = headRDD._2._2.split("-")
    //去除表头只保留数据
    val valRDD = airRDD.filter(row => row != headRDD)
    //例如(PM2.5,(20150102,1,88,34,82,100,126,85))
    val PM25RDD = valRDD.filter(x=>{
      //只取pollution的数据
      x._1.equals(pollution)
    }).map(
      //进行行转列
      x => {
        //对后排的多列数据进行切分
        (x._2._1,x._2._2.split("-"))
      }
    ).map(
      x=>{
        var i= -1
        x._2.map(
          //把后排数据格式化,行转列拆分
          //(20150102,1,88)
          y=>
          {
            i=i+1
              //判定i是否会溢出,有的数据没有表头,数据长度比表头长度要长,造成数组溢出
            if(i<=stations.length-1) {
              //取监测站代码,拼接,以时间+监测站为key,数值为value
              (x._1, stations(i), y)
            }else {
              //如果有冗余字段找不到表头,就舍弃
              (x._1,"","")
          }
          }
        )
      }
      //最后再去除空值,完美
    ).flatMap(x=>x).filter(x=>x._3!="")
    PM25RDD.map(
      x=>{
        //最后去掉括号(date,time,station,value)->date,time,station,value
        x._1+","+x._2+","+x._3
      }
      //先将文件写入临时目录
    ).repartition(1).saveAsTextFile(output+"/"+pollution)
    println("统计数据"+pollution+"成功")
    }
}

注意这里打的jar包,就是target下面的那个几kb的jar,如果把你的依赖包全部导入以后,不仅jar变成了几百MB,而且会报错,因为saprk集群的环境会优先在你的jar依赖中寻找包。

java.lang.ClassNotFoundException: com.digitalchina.AirQualityTest.calss
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at org.apache.spark.util.Utils$.classForName(Utils.scala:177)
	at org.apache.spark.deploy.SparkSubmit$.org$apache$spark$deploy$SparkSubmit$$runMain(SparkSubmit.scala:688)
	at org.apache.spark.deploy.SparkSubmit$.doRunMain$1(SparkSubmit.scala:181)
	at org.apache.spark.deploy.SparkSubmit$.submit(SparkSubmit.scala:206)
	at org.apache.spark.deploy.SparkSubmit$.main(SparkSubmit.scala:121)
	at org.apache.spark.deploy.SparkSubmit.main(SparkSubmit.scala)
spark-submit --master yarn --class com.digitalchina.AirQualityTest airquality-1.0-SNAPSHOT.jar /air/input/*/* /air/output

注意这里的参数,第一个参数是数据的原路径,第二个参数是输出路径,spark运行是默认hdfs路径,如果–master local 默认本地路径。

还要注意集群中scala的版本,一定要和开发包的scala版本一致。

java.io.IOException: unexpected exception type
java.lang.NoSuchMethodError: scala.runtime.ObjectRef.create(Ljava/lang/Object;)Lscala/runtime/Object

以上两种错误都是scala编译版本和spark集群的scala版本不一致导致的,注意这里是spark中集成的scala,而不是你安装的scala。CDH默认安装的是spark1.6.0,建议重新安装spark2.x,spark2和spark1.6可以共存不冲突。pom中更改了scala版本后,在package前记得先运行一下,防止编译器从之前的缓存中拿修改前的配置打包。

spark-shell启动客户端,可以从日志信息中看到集成scala版本是2.10.5,把pom文件和global lib里的scala版本换成2.10.5即可。

运行过程

在这里插入图片描述

数据导入

package sparkSQL

import org.apache.spark.sql.SparkSession
import org.apache.spark.{SparkConf, sql}

object AirQualityHive {
  def main(args: Array[String]): Unit = {
    System.setProperty("HADOOP_USER_NAME", "root")
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("sparkSQLS")
    val spark = new sql.SparkSession.Builder()
      .enableHiveSupport()
      .config(sparkConf)
      .getOrCreate()
//        loadHive("pm25",spark)
        loadHive("pm10",spark)
        loadHive("so2",spark)
        loadHive("no2",spark)
        loadHive("o3",spark)
        loadHive("co",spark)
    spark.stop()
  }

  def loadHive(pollution:String,spark:SparkSession): Unit = {
    /**
     * 参数1:污染物名称
     * 参数2:sparkSession
     */

    //TODO 建表

    spark.sql("CREATE TABLE IF NOT EXISTS`"+pollution+
      """val`(
        |  `date` string,
        |  `hour` string,
        |  `stationid` string,
        |  `value` double)
        |row format delimited fields terminated by ','
        |""".stripMargin)
    var a=""
    if (pollution=="pm25"){
      a="PM2.5"
    }else {
      a=pollution.toUpperCase
    }
    spark.sql("load data inpath 'hdfs://cdh01:8020/air/output/"+a+"/part-00000' into table "+pollution+"val")
    var s = "CREATE TABLE IF NOT EXISTS "+pollution+""" as
                    |select date,stationid,
                    |sum(case when hour=0 then value end) as 0h,
                    |sum(case when hour=1 then value end) as 1h,
                    |sum(case when hour=2 then value end) as 2h,
                    |sum(case when hour=3 then value end) as 3h,
                    |sum(case when hour=4 then value end) as 4h,
                    |sum(case when hour=5 then value end) as 5h,
                    |sum(case when hour=6 then value end) as 6h,
                    |sum(case when hour=7 then value end) as 7h,
                    |sum(case when hour=8 then value end) as 8h,
                    |sum(case when hour=9 then value end) as 9h,
                    |sum(case when hour=10 then value end) as 10h,
                    |sum(case when hour=11 then value end) as 11h,
                    |sum(case when hour=12 then value end) as 12h,
                    |sum(case when hour=13 then value end) as 13h,
                    |sum(case when hour=14 then value end) as 14h,
                    |sum(case when hour=15 then value end) as 15h,
                    |sum(case when hour=16 then value end) as 16h,
                    |sum(case when hour=17 then value end) as 17h,
                    |sum(case when hour=18 then value end) as 18h,
                    |sum(case when hour=19 then value end) as 19h,
                    |sum(case when hour=20 then value end) as 20h,
                    |sum(case when hour=21 then value end) as 21h,
                    |sum(case when hour=22 then value end) as 22h,
                    |sum(case when hour=23 then value end) as 23h
                    |from
                    |""".stripMargin+pollution+"val group by date,stationid"
    spark.sql(s)
  }
}

这个主要看hql语句就行了,有时候idea里报错我就直接复制到hive里执行,问题不大。正常来讲是可以直接运行的。

结果数据

结果数据展示

datestation0h1h2h3h4h5h6h7h8h9h
201501021001ANULL88.094.082.047.034.018.09.010.07.0
201501021002ANULL34.015.013.010.08.010.014.010.016.0
201501021003ANULL82.081.068.036.016.08.05.04.03.0

在这里插入图片描述

数据导出

使用sqoop组件,将hive中计算后的数据导入到mysql中。

首先在mysql中创库建表这里注意表结构和hive中的一定要保持一致,以co表为例,其他表改改名就行了。

CREATE TABLE `co` (
  `date` varchar(255) DEFAULT NULL,
  `stationid` varchar(255) DEFAULT NULL,
  `0h` double(10,3) DEFAULT NULL,
  `1h` double(10,3) DEFAULT NULL,
  `2h` double(10,3) DEFAULT NULL,
  `3h` double(10,3) DEFAULT NULL,
  `4h` double(10,3) DEFAULT NULL,
  `5h` double(10,3) DEFAULT NULL,
  `6h` double(10,3) DEFAULT NULL,
  `7h` double(10,3) DEFAULT NULL,
  `8h` double(10,3) DEFAULT NULL,
  `9h` double(10,3) DEFAULT NULL,
  `10h` double(10,3) DEFAULT NULL,
  `11h` double(10,3) DEFAULT NULL,
  `12h` double(10,3) DEFAULT NULL,
  `13h` double(10,3) DEFAULT NULL,
  `14h` double(10,3) DEFAULT NULL,
  `15h` double(10,3) DEFAULT NULL,
  `16h` double(10,3) DEFAULT NULL,
  `17h` double(10,3) DEFAULT NULL,
  `18h` double(10,3) DEFAULT NULL,
  `19h` double(10,3) DEFAULT NULL,
  `20h` double(10,3) DEFAULT NULL,
  `21h` double(10,3) DEFAULT NULL,
  `22h` double(10,3) DEFAULT NULL,
  `23h` double(10,3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后再linux中使用sqoop的shell命令将数据导出。

sqoop export \
--connect jdbc:mysql://cdh00:3306/airquality \
--username root \
--password root \
--table co \
--export-dir hdfs://cdh01:8020/user/hive/warehouse/co \
--input-fields-terminated-by '\001' \
--input-null-string '\\N' \
--input-null-non-string '\\N'

前面的数据库链接啥的就不说了,看一下–input-fields-terminated-by ‘\001’,这里是指hive的数据的分隔符,如果你在建hive表时没有设置分隔符的话默认是按照"\001"分割的,在notepad++中查看是SOH特殊字符。上面建表时我忘了更改分隔符row format delimited fields terminated by ‘,’,分隔符是默认的’\001’,所以这一条参数不加也不会报错。

一定要注意最后两行,如果不加最后两行会报错。

20/08/27 15:03:05 ERROR tool.ExportTool: Error during export: 
Export job failed!
	at org.apache.sqoop.mapreduce.ExportJobBase.runExport(ExportJobBase.java:439)
	at org.apache.sqoop.manager.SqlManager.exportTable(SqlManager.java:931)
	at org.apache.sqoop.tool.ExportTool.exportTable(ExportTool.java:80)
	at org.apache.sqoop.tool.ExportTool.run(ExportTool.java:99)
	at org.apache.sqoop.Sqoop.run(Sqoop.java:147)
	at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:70)
	at org.apache.sqoop.Sqoop.runSqoop(Sqoop.java:183)
	at org.apache.sqoop.Sqoop.runTool(Sqoop.java:234)
	at org.apache.sqoop.Sqoop.runTool(Sqoop.java:243)
	at org.apache.sqoop.Sqoop.main(Sqoop.java:252)

这里的错误就是经典的hive和mysql的表结构不对应,sqoop把你的数据往mysql中塞的时候格式不符报错了,他就会返回这个错误。处理方式首先就应该检查两个表结构是否一致,在检查后我发现两个表结构是完全一致的,也有可能是上面说的hive的分隔符弄错了,检查后也没有问题,hive中确实使用用默认的’\001’作为分隔符,那为什么会报错嘞?

除了hive表结构之外,我们再检查一下hive的数据,在hdfs/user/hive/warehouse/co中,可以在linux中hadoop fs -cat /user/hive/warehouse/co查看,为了方便我这里选择在windows中下载查看。

201501122002A\N2.105

可以看到分隔符确实是"\001",但是double数据中有一个突兀的"\N"。因为hive中的null值默认是按照字符串"\N"存储的,所以在获取数据时出现字符串"\N",而"\N"是字符串格式不符合double类型导致报错。这里有两种处理方法,一种是将表结构统一改成varchar类型,但是这样"\N"就按照字符串存入了,不方便处理,所以加上后两条,在sqoop遇到"\N"后按照null进行插入处理。

在这里插入图片描述

表结构转换后就很方便操作了。如果感觉有空值难以接受的话,可以用coval表进行操作。

posted @ 2020-08-27 16:01  鹤望兰号  阅读(565)  评论(0)    收藏  举报