GitHub地址:https://github.com/weijie-he/jinyong
2018年10月30日,金庸在香港逝世,享年94歲。
知道這個消息之後,我的情緒很低落,講臺上老師在講什麼彷彿也聽不見了,腦海中一直在回想著先生寫過的關於離別的句子。
程英道:「三妹,你瞧這些白雲聚了又散,散了又聚,人生離合,亦復如斯。你又何必煩惱?」 她話雖如此說,卻也忍不住流下淚來。
卻聽得楊過朗聲說道:「今番良晤,豪興不淺,他日江湖相逢,再當杯酒言歡。咱們就此別過。」
金庸先生告訴了我什麼是「俠」。作為先生的忠實讀者,我覺得自己該做點什麼來緬懷先生,以我自己的方式。
正好,我在學Spark,便想到了利用Spark GraphX 做金庸小說人物關係分析圖。
金庸先生給我們留下了什麼呢?最著名的無非是「飛雪連天射白鹿,笑書神俠倚碧鴛」這14本小說了。最容易想到的便是對這14本書做一張人物關係分析圖。但這一來人物太多,最後畫出的圖會很大;二來不同書之間的人物很多也沒什麼關聯,硬把他們放在同一張圖裡並不妥當。最終我決定只選取人物聯繫最緊密的「射鵰三部曲」(《射鵰英雄傳》、《神鵰俠侶》、《倚天屠龍記》)來進行分析。
但是隻分析人物又感覺略顯單薄。金庸小說中還有一些其他的元素,比如如雷貫耳的稱號(東邪西毒)、高深莫測的武功(黯然銷魂掌)、神兵利器(倚天劍、屠龍刀)。我想把這些元素也加入到分析之中。
同時還要考慮怎麼利用Spark GraphX 的圖計算功能,做一些有意義的分析。
最終確立了以下需求:
? 小說原文很容易獲取,人物名冊、稱號武功武器大全 等也可以在網上搜到。
? GraphX需要的是頂點集和邊集的信息。
? 在人物親密度圖中,我將人名、暱稱作為頂點;在人物—武器關係圖中,我將人名、武器、武功作為頂點。
至於邊集信息,是這樣確定的:以原文中每一句話為單位。如果在這句話中,出現了兩個上述的「頂點」,則認為他們產生了一次聯繫。如果在這句話中,出現了三個「頂點」,則認為他們兩兩之間都有一次聯繫。以此類推。
? 處理完的結果保存在resources文件夾中。結果如下所示
我想把聯繫的次數作為邊的權重。首先就要統計同一個聯繫出現的次數。這一步有點像WordCount,由於不想讓一些打醬油的人物出現,所以還用了個filter函數過濾。
/** * 統計關係出現的次數 * @param sc * @param path:邊文件 * @param num:關係數量閾值 * @return */ def edgeCount(sc:SparkContext,path:String,num:Int) ={ val textFile = sc.textFile(path) val counts = textFile.map(word => (word, 1)) .reduceByKey(_ + _).filter(_._2>num) // counts.collect().foreach(println) counts }
使用頂點集和邊集構建圖
/** * 構建圖 * @param sc * @param path1:頂點文件 * @param path2:邊文件 * @param num:關係數量閾值 */ def creatGraph(sc:SparkContext,path1:String,path2:String,num:Int) ={ val hero = sc.textFile(path1) val counts = edgeCount(sc,path2,num)
val verticesAll = hero.map { line => val fields = line.split( ) (fields(0).toLong, fields(1)) }
val edges = counts.map { line => val fields = line._1.split(" ") Edge(fields(0).toLong, fields(1).toLong, line._2)//起始點ID必須為Long,最後一個是屬性,可以為任意類型 } val graph_tmp = Graph.fromEdges(edges,1L) // 經過過濾後有些頂點是沒有邊,所以採用leftOuterJoin將這部分頂點去除 val vertices = graph_tmp.vertices.leftOuterJoin(verticesAll).map(x=>(x._1,x._2._2.getOrElse(""))) val graph = Graph(vertices,edges)
graph }
至此,需求中的第一點:人物親密度關係圖已經生成。
類似的,我們更換一下頂點集和邊集,就可以生成人物——武器武功的關係圖,從而找出有沒有誰經常被某種武功/兵器揍。
可以通過找出度為1或2的點,來尋找「專屬暱稱」。
/** * 找出度為1或2的點 * @param g * @tparam VD * @tparam ED * @return */ def minDegrees[VD,ED](g:GraphOps[VD,ED])={ // g.degrees.filter(_._2<3).map(_._1).collect().mkString(" ") g.degrees.filter(_._2<3).map(_._1).collect().map(a =>a.toInt) }
通過使用內置函數connectedComponents()可以找到小說人物中「孤島羣體」(即「小圈子」)。
/** * 使用連通組件找到孤島人羣 * @param g * @tparam VD * @tparam ED * @return */ def isolate[VD,ED](g:GraphOps[VD,ED]) ={ g.connectedComponents.vertices.map(_.swap).groupByKey().map(_._2).collect().mkString(" ") }
由於之前我們是每本書都生成一張圖,最後我們還需要把這幾張圖合併為一張圖。
思路就是先取得所有頂點信息,去除,再對這些頂點重新編號。再對這些新生成的點重新構建邊。
/** * 合併2張圖 * @param g1 * @param g2 * @return */ def mergeGraphs(g1:Graph[String,Int],g2:Graph[String,Int]) ={ val v = g1.vertices.map(_._2).union(g2.vertices.map(_._2)).distinct().zipWithIndex()
def edgeWithNewVid(g:Graph[String,Int]) ={ g.triplets.map(et=>(et.srcAttr,(et.attr,et.dstAttr))) .join(v) .map(x => (x._2._1._2,(x._2._2,x._2._1._1))) .join(v) .map(x=> new Edge(x._2._1._1,x._2._2,x._2._1._2)) } def reduceEdge(g3:Graph[String,Int],g4:Graph[String,Int])={ edgeWithNewVid(g3).union(edgeWithNewVid(g4)). map(e=>((e.dstId,e.srcId),e.attr)). reduceByKey(_+_). map(e=>Edge(e._1._1,e._1._2,e._2)) } Graph(v.map(_.swap),reduceEdge(g1,g2)) }
我們可以把圖像按照gexf格式輸出,然後在Gephi中打開,就可以進行圖形化展示。
/** * 輸出為gexf格式 * @param g:圖 * @tparam VD * @tparam ED * @return */ def toGexf[VD,ED](g:Graph[VD,ED]) ={ "<?xml version="1.0" encoding="UTF-8"?> " + "<gexf xmlns="http://www.gexf.net/1.2draft" version="1.2"> " + " <graph mode="static" defaultedgetype="directed"> " + "<nodes> " + g.vertices.map(v => " <node id=""+v._1+"" label=""+v._2+"" /> ").collect().mkString+ "</nodes> "+ "<edges> "+ g.edges.map(e => " <edge source=""+e.srcId+"" target=""+e.dstId+"" weight=""+e.attr+""/> "). collect().mkString+ "</edges> </graph> </gexf>"
}
以下圖片的高清完整版可在output/pics中找到
可以看出郭靖和黃蓉的顏色是最深的(聯繫是最緊密的)。這是因為他們在《射鵰》和《神鵰》中都有很多戲份。《神鵰》中的男女主小龍女和楊過聯繫也很緊密。相比之下《倚天》中的男女主張無忌和趙敏直接的線就淡的多了。一方面,這是因為趙敏的出場時間太晚(全書40章,趙敏在第23章纔出場)。另一方面,張無忌優柔寡斷,情感方面也一直在趙敏和周芷若之間猶豫不決,導致張無忌的情感線被周芷若分流了許多。
由於我只是篩選出了度為1和2的點,但有些點是人名,而不是暱稱,不必看。
我原來以為「專屬暱稱」只出現在情侶之間,但發現有兩個例外。
這兩人情同父子。郭靖自幼喪父,洪七公也沒有子嗣。俗話說,「一日為師終身為父」,我覺得這兩個人不是父子,甚是父子。所以有這樣的「專屬暱稱」也不奇怪。
也許江南七怪也和郭靖情同父子,但可能是因為出現的頻率不夠高,所以被過濾掉了,這張圖上並沒有出現。
全書只有陸無雙一人可以叫楊過」傻蛋「,因為當初楊過騙陸無雙自稱傻蛋。
那道姑笑道:「我幾時騙過你了?喂,小子,你叫甚麼名字?」楊過道:「人人都叫我傻蛋,你不知道麼?你叫甚麼名字?」那道姑笑道:「傻蛋,你只叫我仙姑就得啦。」
? 摘錄了一下原文,發現短短几句話,這道姑(陸無雙)就笑了2次,足見他們相處的多麼愉快。過兒一生孤苦,和陸無雙在一起的日子也算是為數不多的快樂時光。我覺得他們倆很有成為情侶的可能,只可惜過兒心裡已經有了小龍女。最後他們倆結為了兄妹,也算是一段「有情人終成兄妹」的悲劇故事。
發現只有3個「孤島人羣」(小團體)。
簡捷和薛公遠是《倚天屠龍記》中被金花婆婆打傷,找胡青牛治病的人。和他們有交集的人確實很少。
李萍被段天德綁架,很長一段時間內只有他們兩個在一起,別人都不知道他們去了哪。
朮赤和察合臺是成吉思汗的兩個兒子。和他們有交集的人也很少。
這三本書中涉及到的人物,即使過濾完,也有將近200號人。如果在現實生活中,200人中應該會有更多的小團體,而且也不會全是2人組,可能有3~5人小團體。
以下是我認為可能的兩點原因:
主要想看誰經常被哪種武功兵器揍。
無忌小時候就因為中了玄冥神掌差點死掉,長大後也經常和玄冥二老斗。
蛤蟆功可以說是郭靖發明的,就是因為他篡改了《九陰真經》,寫了本「九陰假經」,才讓歐陽鋒練成了蛤蟆功。後來也數次和歐陽鋒的蛤蟆功交手。《神鵰》中小楊過也學了點蛤蟆功,被郭靖發現了,這又產生了一次交集。
這是書中兩大反派金輪法王和李莫愁的武器。
人人都知道金庸,可大多是通過影視作品,讀過原著的人少的可憐。做這個項目,在緬懷先生的同時,也希望有更多的人能去讀一讀原著,體會一下先生筆下原汁原味的江湖。
推薦閱讀: