接到一個有趣的作業,就是分析豆瓣用戶關注的小組,通過小組標簽給這個用戶畫像。
任務主要有這幾部分:
1.通過爬取的數據,利用Spark Graphx對這些數據構圖
2.將這個圖進行可視化
3.對用戶進行畫像分析,找出他的興趣標簽
環境搭建
首先需要搭建Spark,如果需要yarn進行可視化管理的話還需要安裝Hadoop,這里我安裝的是Hadoop2.7.4+Spark2.2.0
CentOS7安裝Hadoop2.7.4
1.安裝JDK1.8
將原有的OpenJDK卸載,并下載rpm包進行安裝,將JAVA_HOME、PATH等環境變量配置好,檢驗JAVA是否安裝成功。
2.安裝Hadoop2.7.4
配置免密登錄
ssh-keygen -t dsa -P '' -f ~/.ssh/id_dsa
cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys
新建文件夾/usr/hadoop,并在該目錄下再新建四個文件夾
/usr/hadoop/hdfs/data
/usr/hadoop/hdfs/name
/usr/hadoop/hdfs/namesecondary
/usr/hadoop/tmp
下載Hadoop2.7.4,并將其放置在/usr/hadoop/目錄下,解壓
設置環境變量,并使環境變量生效,source /etc/profile
JAVA_HOME=/usr/java/jdk1.8.0_144/
JRE_HOME=/usr/java/jdk1.8.0_144/jre/
SCALA_HOME=/usr/lib/scala
HADOOP_HOME=/usr/hadoop/hadoop-2.7.4
SPARK_HOME=/usr/spark-2.2.0-bin-hadoop2.7
PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin:$SCALA_HOME/bin:$SPARK_HOME/bin:$HADOOP_HOME/bin
CLASSPATH=$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar
export JAVA_HOME
export JRE_HOME
export PATH
export CLASSPATH
export SCALA_HOME
export HADOOP_HOME
export SPARK_HOME
進入$HADOOP_HOME/etc/hadoop目錄,配置 hadoop-env.sh等。涉及的配置文件如下:
hadoop-2.7.4/etc/hadoop/hadoop-env.sh
hadoop-2.7.4/etc/hadoop/yarn-env.sh
hadoop-2.7.4/etc/hadoop/core-site.xml
hadoop-2.7.4/etc/hadoop/hdfs-site.xml
hadoop-2.7.4/etc/hadoop/mapred-site.xml
hadoop-2.7.4/etc/hadoop/yarn-site.xml
(注意:有的文件只有template,需要改名,例如mv mapred-site.xml.template mapred-site.xml)
配置hadoop-env.sh
# The java implementation to use.
#export JAVA_HOME=${JAVA_HOME}
export JAVA_HOME=/usr/java/jdk1.8.0_144
配置yarn-env.sh
#export JAVA_HOME
export JAVA_HOME=/usr/java/jdk1.8.0_144
配置core-site.xml
添加如下配置:
description最好不要用中文
<configuration>
<property>
<name>fs.default.name</name>
<value>hdfs://localhost:9000</value>
<description>HDFS的URI,文件系統://namenode標識:端口號</description>
</property>
<property>
<name>hadoop.tmp.dir</name>
<value>/usr/hadoop/hdfs/tmp</value>
<description>namenode上本地的hadoop臨時文件夾 </description>
</property>
</configuration>
配置hdfs-site.xml
添加如下配置
<configuration>
<!—hdfs-site.xml-->
<property>
<name>dfs.name.dir</name>
<value>/usr/hadoop/hdfs/name</value>
<description>namenode上存儲hdfs名字空間元數據 </description>
</property>
<property>
<name>dfs.data.dir</name>
<value>/usr/hadoop/hdfs/data</value>
<description>datanode上數據塊的物理存儲位置</description>
</property>
<property>
<name>dfs.replication</name>
<value>1</value>
<description>副本個數,配置默認是3,應小于datanode機器數量</description>
</property>
</configuration>
配置mapred-site.xml
添加如下配置:
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configuration>
配置yarn-site.xml
添加如下配置:
<configuration>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<property>
<name>yarn.resourcemanager.webapp.address</name>
<value>${yarn.resourcemanager.hostname}:8999</value>
</property>
</configuration>
(注意:這里將yarn的管理端口改為8999,訪問管理頁面時也需要用該端口訪問)
Hadoop啟動
1)格式化namenode
$ bin/hdfs namenode –format
當多次格式化時,遇到個選擇,選擇no,如果選擇yes,將會導致namenode和datanode中/usr/hadoop/hdfs/data/current/VERSION、/usr/hadoop/hdfs/name/current/VERSION中CclusterID 不一致,從而發生sbin/start-all.sh啟動時,有的DataNode進程啟動不起來(jps查看),遇到這樣情況,將name/current下的VERSION中的clusterID復制到data/current下的VERSION中,覆蓋掉原來的clusterID,讓兩個保持一致,然后重啟,啟動后執行jps,查看進程,參考(https://my.oschina.net/u/189445/blog/509385)
2)啟動NameNode 和 DataNode 守護進程
$ sbin/start-dfs.sh
3)啟動ResourceManager 和 NodeManager 守護進程
$ sbin/start-yarn.sh
或者直接$sbin/start-all.sh 將上述所有進程啟動。
啟動驗證
1)執行jps命令,有如下進程,說明Hadoop正常啟動
# jps
54679 NameNode
54774 DataNode
15741 Jps
55214 NodeManager
55118 ResourceManager
54965 SecondaryNameNode
在瀏覽器中輸入:http://HadoopMaster的IP:8999/ 即可看到YARN的ResourceManager的界面。注意:默認端口是8088,這里我設置了yarn.resourcemanager.webapp.address為:${yarn.resourcemanager.hostname}:8999。
或輸入http://HadoopMaster的IP:50070/查看NameNode狀態
Spark安裝
下載spark-2.2.0-bin-hadoop2.7,并進行解壓,配置SPARK_HOME環境變量,運行spark-shell,查看spark是否能夠正常啟動。
至此,生產環境搭建完畢!
開發環境
折騰了兩天,寫代碼運行調試,最麻煩的環節還屬運行調試。調試都是通過maven將程序打成jar包,然后上傳到裝有Hadoop、Spark的服務器(用一個虛擬機來模擬)上在沙盒里進行運行,執行效率之慢可想而知。有沒有什么更為便捷的辦法,寫完代碼,右鍵直接執行呢,答案是有的。
Win7 64位+IDEA開發Spark應用
下載編譯好的Hadoop bin目錄文件夾(其中包含winutils.exe、hadoop.dll等文件)
設置環境變量HADOOP_HOME,在Path變量中增加一條,%HADOOP_HOME%/bin
下載Hadoop對應版本編譯好的Spark文件
設置環境變量SPARK_HOME,在Path變量中增加一條,%SPARK_HOME%/bin
cmd彈出窗口中測試安裝是否成功
(這個版本可能會報Hive錯誤,可以忽略)
IDEA配置
在運行某個Scala應用時,需要增加一條配置參數,
-Dspark.master=local[2]
如若開發時依然提示找不到Hadoop目錄,可以在代碼中增加一條屬性配置
System.setProperty("hadoop.home.dir", "D:\\hadoop-2.7.4\\")
正題
首先,看一下數據結構,有兩個數據集,一個是用戶數據機另一個是小組數據集,這些數據集都是從Mongodb中導出而來。
用戶(persons.json)
{"_id":{"$oid":"59f3de6b0b6e9a0b9ca7bf4e"},"name":"person1","no":"168812667","group1":"HZhome","group2":"145219","group3":"276209","group4":"hzhouse","group5":"467221"}
...
小組(groups.json)
{"_id":{"$oid":"59f3de5f0b6e9a0b9ca7bf49"},"name":"杭州租房","no":"HZhome","tag1":"杭州","tag2":"租房","tag3":"合租","tag4":"求租","tag5":"杭州租房"}
...
由實例數據可以看出,persons.json每行記錄存有用戶信息,同時還包括該用戶加入的組號(groupx)。而groups.json中記錄小組的信息。這兩個數據集通過groupsno進行關聯(注意:groupno并非是數字字符串)
其次,對數據進行處理
因為每行都是一條json格式的記錄,可以利用fastjson對記錄進行解析,因此pom.xml文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dhl</groupId>
<artifactId>DoubanGraphx</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<encoding>UTF-8</encoding>
<scala.tools.version>2.11</scala.tools.version>
<scala.version>2.11.8</scala.version>
<spark.version>2.2.0</spark.version>
</properties>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_${scala.tools.version}</artifactId>
<version>${spark.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_${scala.tools.version}</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-graphx_${scala.tools.version}</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.32</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<version>2.15.2</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
對persons.json進行解析,構建用戶和小組之間的關系,同時graphx需要Long類型字段作為其VertexID,這里我們通過利用導出數據的oid字段進行運算獲得(作為Mongodb表中的rowid,該字段應該具備唯一性)
case class Person(poidhex: VertexId, oid: String, name: String, no: String, groupno: String, vertextype: String)
def parsePerson(str: String): List[Person] = {
var result = List[Person]()
val json = JSON.parseObject(str)
val oidjson = json.getJSONObject("_id")
val oid = oidjson.getString("$oid")
val oidhex = new BigInteger(oid, 16).longValue()
val name = json.getString("name")
val no = json.getString("no")
val groups = new ListBuffer[String]
val jsonset = json.keySet().iterator()
while (jsonset.hasNext() == true) {
val strkey = jsonset.next()
if (strkey.length() > 4 && strkey.substring(0, 5).compareTo("group") == 0) {
result .::=(Person(oidhex, oid, name, no, json.getString(strkey),"p"))
}
}
result
}
同樣,對groups.json進行處理
case class Group(goidhex: VertexId, oid: String, name: String, groupno: String, tags: List[String], vertextype: String)
def parseGroup(str: String): Group = {
val json = JSON.parseObject(str)
val oidjson = json.getJSONObject("_id")
val oid = oidjson.getString("$oid")
val oidhex = new BigInteger(oid, 16).longValue()
val name = json.getString("name")
val groupno = json.getString("no")
var tags = List[String]()
val jsonset = json.keySet().iterator()
while (jsonset.hasNext() == true) {
val strkey = jsonset.next()
if (strkey.length() > 3 && strkey.substring(0, 3).compareTo("tag") == 0) {
tags .::=(json.getString(strkey))
}
}
Group(oidhex, oid, name, groupno, tags, "g")
}
審查數據時候發現groups.json中存在no相同的記錄,為此需要進行去重
System.setProperty("hadoop.home.dir", "D:\\hadoop-2.7.4\\")
val conf = new SparkConf().setAppName("Douban User Relationship")
val sc = new SparkContext(conf)
val sqlContext = new org.apache.spark.sql.SQLContext(sc)
import sqlContext.implicits._
val personsData = sc.textFile("C:\\Users\\daihl\\Desktop\\persons2.json")
val groupsData = sc.textFile("C:\\Users\\daihl\\Desktop\\groups2.json")
val personsRDD: RDD[Person] = personsData.flatMap(parsePerson).cache()
val groupsRDD: RDD[Group] = groupsData.map(parseGroup).cache()
//將RDD轉為DataFrame
val personsdf = sqlContext.createDataFrame(personsRDD)
val groupsdf = sqlContext.createDataFrame(groupsRDD)
//根據groupno進行去重
val groupsds = groupsdf.dropDuplicates("groupno")
再通過groupno字段,將兩個數據集進行連接,并生成graphx的邊
val relation = personsdf.join(groupsds, personsdf("groupno") === groupsds("groupno"))
val edges = EdgeRDD.fromEdges(relation.rdd.map(row => Edge(row.getAs[Long]("poidhex"), row.getAs[Long]("goidhex"), ())))
再將person和group進行合并,作為圖中的節點
由于數據集的合并需要相同的schema,所以需要對person和group進行schema轉變
val newNames=Seq("oidhex", "oid", "name","no","vertextype")
val personsselect = personsdf.select("poidhex","oid", "name","no","vertextype").dropDuplicates("no").toDF(newNames:_*)
val groupsselect = groupsds.select("goidhex","oid", "name","groupno","vertextype").toDF(newNames:_*)
最終構建圖
val vertexnode: RDD[(VertexId, (String, String, String))] = personsselect.union(groupsselect).rdd.map(row => (new BigInteger(row(1).toString, 16).longValue(), (row(2).toString, row(3)toString, row(4)toString)))
val defaultvertexnode = ("null", "null", "null")
val graph =Graph(vertexnode,edges,defaultvertexnode)
graphx圖的可視化
最簡單的可以利用GraphStream進行可視化(linkuriou.js也值得研究)
//創建原始可視化對象
val graphStream:SingleGraph = new SingleGraph("GraphStream")
// 設置graphStream全局屬性. Set up the visual attributes for graph visualization
// 加載頂點到可視化圖對象中
for ((id,(name:String, no:String, vertextype:String)) <- graph.vertices.collect()) {
val node = graphStream.addNode(id.toString).asInstanceOf[SingleNode]
node.addAttribute("ui.label",id +"\n"+name)
}
//加載邊到可視化圖對象中
for (Edge(x, y, defaultvertexnode) <- graph.edges.collect()) {
val edge = graphStream.addEdge(x.toString ++ y.toString,
x.toString, y.toString,
true).
asInstanceOf[AbstractEdge]
}
//顯示
graphStream.display()
總結
1.對Spark、Spark Graphx有了初步的了解和認識
2.對RDD、DataFrame、DataSet的操作的理解還需要深入
接下來工作
1.嘗試利用GraphFrames進行構圖
2.嘗試利用linkuriou.js進行圖的可視化
3.對用戶進行畫像分析,找出他的興趣標簽