基于Spark的数据处理 --图数据库的数据处理与转换(一)

1 篇文章 0 订阅
1 篇文章 0 订阅

这里介绍一下Spark对大数据处理的一些过程和使用心得。举一个之前工作中的实际开发的例子:有这样一个需求场景,在公安行业中有各式各样的数据,我们这里挑选旅馆数据作为例子,假设数据结构如下:

身份证号(sfzh)    姓名(xm)    旅馆代码(lgdm)    房号(fh)    入住时间(rzsj)    离店时间(ldsj)


现在需要将这些数据进行转换成csv文件并导入到neo4j图形数据库中,neo4j需要的csv数据主要有两种类型,一种是节点数据一种是边数据。节点数据比较好理解,即所有人的信息,这里主要以身份证号作为唯一标识,csv中的格式如下:

idcard,name
111111111111111111,张三


边的数据相对有点复杂,主要结构为:身份证号1,身份证号2,入住时间1,入住时间2,离店时间1,离店时间2,旅馆代码,房号。csv文件格式如下:

idcard1,idcard2,rzsj1,rzsj2,ldsj1,ldsj2,lgdm,fh
111111111111111111,111111111111111112,20150101213412,20150101213832,20150102092612,20150102092615,111111,201

一条边数据其实就表示某两个人在某一时间点某一旅馆的KF记录,这些记录导入到neo4j以后,通过一个身份证号就可以查询这个人的所有与之KF的人的关系图了,是不是有点小兴奋? 大笑


现在来详细分析一下如何用spark将数据库中的单一数据转换成neo4j的所需要的节点和边的数据。


首先我们需要将数据库的数据抽成文本文件上传到hdfs上,有很多工具可以实现这点,比如kettle可以将数据库表抽成txt文件然后在上传至hdfs,或者用scoop直接导入hive也行。我这里是将数据库中的字段数据以tab键(\t)分隔生成txt文件然后上传至hdfs。然后就可以用spark进行数据处理了。


先将数据处理并导出节点数据的csv,这个相对比较简单,就是按身份证号和姓名distinct。本来这样做就可以了,但是考虑到数据的不准确性,比如身份号唯一但是姓名不是唯一的也就是说一个身份证号可能有两个名字,于是根据身份证号和姓名distinct出来可能会有两个一样的身份证号,这样就会产生重复的节点数据了。于是还是需要用reduce来操作:

object LgPersonNode {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("spark://IP:7077").setAppName("LgPersonNode")
    val sc = new SparkContext(conf)
    val lgdataRdd = sc.textFile("hdfs://HOST/user/test/lg16.txt")

    val lgdataMapRdd = lgdataRdd.map(_.split('\t')).filter(x => x.length > 6).filter(_ (6).length == 18).map(x => (x(6), x(1)))
    val rdd2 = lgdataMapRdd.reduceByKey((x, y) => x).map(x => x._1 + "," + x._2)

    rdd2.saveAsTextFile("hdfs://HOST/user/test/person16")
  }
}


这里split以后的x(6)是身份证号字段,x(1)是姓名字段,其中要过滤一下身份证号码不是18位的数据,然后将身份证号和姓名作为keyValue来map一下。然后reduceByKey也就是by身份证号,然后随机取一个姓名作为和身份证唯一对应的姓名(如果有多个姓名的话),然后再将身份证号和姓名以逗号分隔map一下输出到hdfs。


输出到hdfs后,这里的person16是个文件夹,不是一个文件哦。里面是很多reduce生成的part文件,我们需要把这些part文件全部合并并导出到本地文件,执行如下操作:

先删除person16里面的_SUCCESS文件: hadoop fs -rm /user/test/_SUCCESS

然后将所有part文件合并导出到本地csv文件:hadoop fs -getmerge /user/test/person16/ ./person16.csv

导出后就是一个身份证-姓名的节点数据文件了。由于neo4j导入csv文件需要一个列头,那么需要在csv文件第一行插入列头:sed -i '1i\idcard,name' person16.csv

至此就生成了完整的节点数据csv文件了。

接下来的难点来了,边数据的生成是这次数据处理中最复杂的操作。首先如何判定两个人开过一个F?这里我们暂定设立这么一个规则:同一天之内,两个身份证号在同一个旅馆代码下同一个房号下有记录的,并且:A身份证的离店时间大于B身份证的入住时间且B的离店时间大于A的入住时间的,就判定两人为一条KF关系数据,也就是一条边数据,边的两头的节点是两人的身份证号。那么我们可以根据 旅馆代码-房号-年月日 来作为key来进行mapReduce操作,在map的时候可以这么操作:

.map(x => (x(45) + "-" + x(10) + "-" + x(11).substring(0, 8) /*lgdm-fh-rzsj_date*/ ,
  (x(6) /*sfzh*/ , x(11) /*rzsj*/ , x(12) /*ldsj*/ , x(45) /*lgdm*/ , x(10) /*fh*/ )))


key为lgdm-fh-rzsj_date,value为其他一些属性的元组。接下来的难点就是如何将map后的这些数据处理成两两关系的边数据。这里可以先把数据简化一下,设计一个简单的数据原型:

111111,1
111112,1
111113,1
111114,2
111112,2
111115,3
111116,3
111117,4
111118,5
111119,5
111113,5


假设第一列为身份证数据,第二列为旅馆房号代码数据,那么这批数据如果生成边数据的话应该如下:

111111,111112,1
111111,111113,1
111112,111113,1
111114,111112,2
111115,111116,3
111118,111119,5
111118,111113,5
111119,111113,5


其中同一旅馆代码下的两两身份证都会生成一条数据,而 111117,4 这条记录只有一个身份证记录所以不会生成边数据。这里还有个问题,就是图数据库中关系是有方向的,但是对于旅馆数据来说关系是不区分方向的,那么1111111,111112这个关系和111112,111111这个关系其实是一种关系,但是在这里却有可能产生两条不同方向的边记录。为了解决这个问题,可以拿身份证的hashcode来比一下大小,大的在前小的在后,那么两个身份证之间就只有一个方向的边记录了。


那么我们来看一下如何通过旅馆记录来生成如上的两两关系的边记录:


首先一样按旅馆房号代码进行map:rdd1.map(_.split(',')).map(x => (x(1), x(0)))
然后对其groupByKey一下之后将会得到一个(key,List)的rdd,然后直接取它的List来map:.map(x => x._2.map(y => (x._1, y))) 将会得到一个List的map:

List((4,111117))
List((2,111114),(2,111112))
List((5,111118),(5,111119),(5,111113))
List((3,111115),(3,111116))
List((1,111111),(1,111112),(1,111113))


对每一个List内部的数据进行两两的取值操作,在flatMap一下就得到了边数据的rdd了,完整代码如下:

object RefSample {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("spark://IP:7077").setAppName("RefSample")
    val sc = new SparkContext(conf)
    val rdd1 = sc.textFile("hdfs://HOST/user/test/ttt3.txt")
    val rdd2 = rdd1.map(_.split(',')).map(x => (x(1), x(0))).groupByKey().map(x => x._2.map(y => (x._1, y)))
    val rdd3 = rdd2.flatMap(x => {
      if (x.size < 2)
        List("")
      else {
        val res = new ListBuffer[String]
        val list = x.toList
        for (i <- 0 until list.size - 1) {
          makeRelationsSub(i, list, x.size, res)
        }
        if (res.isEmpty)
          List("")
        else
          res
      }
    }).map(_.split(',')).filter(x => x.length != 0 && x(0) != "")
    rdd3.collect.foreach(x => println(x(0) + "," + x(1) + "," + x(2)))
  }

  def makeRelationsSub(i: Int, list: List[(String, String)], len: Int, res: ListBuffer[String]): Unit = {
    for (j <- i until len - 1) {
      var id1 = list(i)._2
      var id2 = list(j + 1)._2
      if (id1 != id2) {
        if (id1.hashCode < id2.hashCode) {
          val tmp = id1
          id1 = id2
          id2 = tmp
        }
        res.append(id1 + "," + id2 + "," + list(j)._1)
      }
    }
  }
}


这段代码的输出为:

111114,111112,2
111119,111118,5
111118,111113,5
111119,111113,5
111116,111115,3
111112,111111,1
111113,111111,1
111113,111112,1


可以看到输出了完整的两两的边数据记录。

替换为正式的数据,完整代码如下:

object LgRelation {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("spark://IP:7077").setAppName("LgRelation")
    val sc = new SparkContext(conf)
    val lgdataRdd = sc.textFile("hdfs://HOST/user/test/lg16.txt")

    val lgdataMapRdd = lgdataRdd.map(_.split('\t')).filter(x => x.length == 46).filter(_ (6 /*zjhm*/).length == 18)
      .filter(_ (11 /*rzsj*/).length /* == 14*/ >= 8).filter(_ (10 /*hf*/) != "").filter(_ (45 /*lgdm*/) != "")
      .map(x => (x(45) + "-" + x(10) + "-" + x(11).substring(0, 8) /*lgdm-fh-rzsj_date*/ ,
        (x(6) /*sfzh*/ , x(11) /*rzsj*/ , x(12) /*ldsj*/ , x(45) /*lgdm*/ , x(10) /*fh*/ )))

    val rdd2 = lgdataMapRdd.groupByKey().map(x => x._2)

    val rdd3 = rdd2.flatMap(x => {
      if (x.size < 2)
        List("")
      else {
        val res = new ListBuffer[String]
        val list = x.toList
        for (i <- 0 until list.size - 1) {
          makeRelationsSub(i, list, x.size, res)
        }

        if (res.isEmpty)
          List("")
        else
          res
      }
    }).map(_.split(',')).filter(x => x.length != 0 && x(0) != "")
      .map(x => x(0) + "," + x(1) + "," + x(2) + "," + x(3) + "," + x(4)
        + "," + x(5) + "," + x(6) + "," + x(7))

    rdd3.saveAsTextFile("hdfs://HOST/user/test/rel16")
  }


  def makeRelationsSub(i: Int, list: List[(String, String, String, String, String)], len: Int, res: ListBuffer[String]): Unit = {
    for (j <- i until len - 1) {
      var item1 = list(i)
      var item2 = list(j + 1)
      val id1 = item1._1
      val id2 = item2._1
      if (id1 != id2) {
        if (id1.hashCode < id2.hashCode) {
          val tmp = item1
          item1 = item2
          item2 = tmp
        }

        //A的离店时间大于B的入住时间,B的离店时间大于A的入住时间,同时满足
        if (rzldCheck(item1._3, item2._2) && rzldCheck(item2._3, item1._2))
          res.append(item1._1 + "," + item2._1 + "," + item1._2 + "," + item2._2 + "," + item1._3 + ","
            + item2._3 + "," + item1._4 + "," + item1._5)
      }
    }
  }

  def rzldCheck(ldsj: String, rzsj: String): Boolean = {
    try {
      if (ldsj.length != 14 || rzsj.length != 14) return true

      val ldsjInt = new java.lang.Long(ldsj)
      val rzsjInt = new java.lang.Long(rzsj)
      if (ldsjInt < rzsjInt) //如果离店时间小于入住时间判断为不是同住
        false
      else
        true
    } catch {
      case ex: Exception => true
    }
  }

}


生成完后再进行一轮上面的数据合并导出操作就生成了完整的边数据的csv文件了。


下一篇继续介绍一下如何将这两个csv文件导入到neo4j数据库。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值