使用Spark Graphx 探索你不知道的金庸武俠世界

GitHub地址:https://github.com/weijie-he/jinyong

一、緣起

2018年10月30日,金庸在香港逝世,享年94歲。

知道這個(gè)消息之后,我的情緒很低落,講臺(tái)上老師在講什么仿佛也聽(tīng)不見(jiàn)了,腦海中一直在回想著先生寫過(guò)的關(guān)于離別的句子。

程英道:“三妹,你瞧這些白云聚了又散,散了又聚,人生離合,亦復(fù)如斯。你又何必?zé)???她話雖如此說(shuō),卻也忍不住流下淚來(lái)。

卻聽(tīng)得楊過(guò)朗聲說(shuō)道:“今番良晤,豪興不淺,他日江湖相逢,再當(dāng)杯酒言歡。咱們就此別過(guò)?!?/p>

金庸先生告訴了我什么是“俠”。作為先生的忠實(shí)讀者,我覺(jué)得自己該做點(diǎn)什么來(lái)緬懷先生,以我自己的方式。

正好,我在學(xué)Spark,便想到了利用Spark Graphx 做金庸小說(shuō)人物關(guān)系分析圖。

二、需求分析

金庸先生給我們留下了什么呢?最著名的無(wú)非是“飛雪連天射白鹿,笑書(shū)神俠倚碧鴛”這14本小說(shuō)了。最容易想到的便是對(duì)這14本書(shū)做一張人物關(guān)系分析圖。但這一來(lái)人物太多,最后畫出的圖會(huì)很大;二來(lái)不同書(shū)之間的人物很多也沒(méi)什么關(guān)聯(lián),硬把他們放在同一張圖里并不妥當(dāng)。最終我決定只選取人物聯(lián)系最緊密的“射雕三部曲”(《射雕英雄傳》、《神雕俠侶》、《倚天屠龍記》)來(lái)進(jìn)行分析。

但是只分析人物又感覺(jué)略顯單薄。金庸小說(shuō)中還有一些其他的元素,比如如雷貫耳的稱號(hào)(東邪西毒)、高深莫測(cè)的武功(黯然銷魂掌)、神兵利器(倚天劍、屠龍刀)。我想把這些元素也加入到分析之中。

同時(shí)還要考慮怎么利用Spark Graphx 的圖計(jì)算功能,做一些有意義的分析。

最終確立了以下需求:

分析人物之間的親密度關(guān)系

找出“專屬昵稱”

(很多人物之間的交流并不會(huì)直呼其名,比如黃蓉會(huì)叫郭靖“靖哥哥”,我想找出類似的“專屬昵稱”)

探索小說(shuō)人物中“孤島群體”(即“小圈子”)

有沒(méi)有誰(shuí)經(jīng)常被某種武功/兵器揍

三、工作流程

3.1 獲取"射雕三部曲"小說(shuō)原文、人物名冊(cè)、稱號(hào)\武功\武器大全等初始數(shù)據(jù)

? ? 小說(shuō)原文很容易獲取,人物名冊(cè)、稱號(hào)\武功\武器大全 等也可以在網(wǎng)上搜到。

3.2 數(shù)據(jù)預(yù)處理,將數(shù)據(jù)轉(zhuǎn)換成Graphx需要的格式

? ? Graphx需要的是頂點(diǎn)集和邊集的信息。

? ? 在人物親密度圖中,我將人名、昵稱作為頂點(diǎn);在人物—武器關(guān)系圖中,我將人名、武器、武功作為頂點(diǎn)。

? 至于邊集信息,是這樣確定的:以原文中每一句話為單位。如果在這句話中,出現(xiàn)了兩個(gè)上述的“頂點(diǎn)”,則認(rèn)為他們產(chǎn)生了一次聯(lián)系。如果在這句話中,出現(xiàn)了三個(gè)“頂點(diǎn)”,則認(rèn)為他們兩兩之間都有一次聯(lián)系。以此類推。

處理完的結(jié)果保存在resources文件夾中。結(jié)果如下所示

?



?

3.3 使用Spark GraphX 生成圖

我想把聯(lián)系的次數(shù)作為邊的權(quán)重。首先就要統(tǒng)計(jì)同一個(gè)聯(lián)系出現(xiàn)的次數(shù)。這一步有點(diǎn)像WordCount,由于不想讓一些打醬油的人物出現(xiàn),所以還用了個(gè)filter函數(shù)過(guò)濾。

/**

* 統(tǒng)計(jì)關(guān)系出現(xiàn)的次數(shù)

* @param sc

* @param path:邊文件

* @param num:關(guān)系數(shù)量閾值

* @return

*/

defedgeCount(sc:SparkContext,path:String,num:Int)={

valtextFile=sc.textFile(path)

valcounts=textFile.map(word=>(word,1))

.reduceByKey(_+_).filter(_._2>num)

// ?? counts.collect().foreach(println)

counts

? }

使用頂點(diǎn)集和邊集構(gòu)建圖

/**

* 構(gòu)建圖

* @param sc

* @param path1:頂點(diǎn)文件

* @param path2:邊文件

* @param num:關(guān)系數(shù)量閾值

*/

defcreatGraph(sc:SparkContext,path1:String,path2:String,num:Int)={

valhero=sc.textFile(path1)

valcounts=edgeCount(sc,path2,num)

?

valverticesAll=hero.map{line=>

valfields=line.split('')

(fields(0).toLong,fields(1))

?? }

?

valedges=counts.map{line=>

valfields=line._1.split(" ")

Edge(fields(0).toLong,fields(1).toLong,line._2)//起始點(diǎn)ID必須為L(zhǎng)ong,最后一個(gè)是屬性,可以為任意類型

?? }

valgraph_tmp=Graph.fromEdges(edges,1L)

// ?? 經(jīng)過(guò)過(guò)濾后有些頂點(diǎn)是沒(méi)有邊,所以采用leftOuterJoin將這部分頂點(diǎn)去除

valvertices=graph_tmp.vertices.leftOuterJoin(verticesAll).map(x=>(x._1,x._2._2.getOrElse("")))

valgraph=Graph(vertices,edges)

?

graph

? }

至此,需求中的第一點(diǎn):人物親密度關(guān)系圖已經(jīng)生成。

類似的,我們更換一下頂點(diǎn)集和邊集,就可以生成人物——武器\武功的關(guān)系圖,從而找出有沒(méi)有誰(shuí)經(jīng)常被某種武功/兵器揍。

3.4 使用Spark GraphX 處理圖

可以通過(guò)找出度為1或2的點(diǎn),來(lái)尋找“專屬昵稱”。

/**

* 找出度為1或2的點(diǎn)

* @param g

* @tparam VD

* @tparam ED

* @return

*/

defminDegrees[VD,ED](g:GraphOps[VD,ED])={

// ?? g.degrees.filter(_._2<3).map(_._1).collect().mkString("\n")

g.degrees.filter(_._2<3).map(_._1).collect().map(a=>a.toInt)

? }

通過(guò)使用內(nèi)置函數(shù)connectedComponents()可以找到小說(shuō)人物中“孤島群體”(即“小圈子”)。

/**

* 使用連通組件找到孤島人群

* @param g

* @tparam VD

* @tparam ED

* @return

*/

defisolate[VD,ED](g:GraphOps[VD,ED])={

g.connectedComponents.vertices.map(_.swap).groupByKey().map(_._2).collect().mkString("\n")

? }

由于之前我們是每本書(shū)都生成一張圖,最后我們還需要把這幾張圖合并為一張圖。

思路就是先取得所有頂點(diǎn)信息,去除,再對(duì)這些頂點(diǎn)重新編號(hào)。再對(duì)這些新生成的點(diǎn)重新構(gòu)建邊。

/**

* 合并2張圖

* @param g1

* @param g2

* @return

*/

defmergeGraphs(g1:Graph[String,Int],g2:Graph[String,Int])={

valv=g1.vertices.map(_._2).union(g2.vertices.map(_._2)).distinct().zipWithIndex()

?

defedgeWithNewVid(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=>newEdge(x._2._1._1,x._2._2,x._2._1._2))

?? }

defreduceEdge(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))

? }

3.5 導(dǎo)出到Gephi

我們可以把圖像按照gexf格式輸出,然后在Gephi中打開(kāi),就可以進(jìn)行圖形化展示。

/**

* 輸出為gexf格式

* @param g:圖

* @tparam VD

* @tparam ED

* @return

*/

deftoGexf[VD,ED](g:Graph[VD,ED])={

"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+

"<gexf xmlns=\"http://www.gexf.net/1.2draft\" version=\"1.2\">\n"+

" <graph mode=\"static\" defaultedgetype=\"directed\">\n? "+

"<nodes>\n "+

g.vertices.map(v=>"? <node id=\""+v._1+"\" label=\""+v._2+"\" />\n").collect().mkString+

"</nodes>\n? "+

"<edges>\n"+

g.edges.map(e=>"? <edge source=\""+e.srcId+"\" target=\""+e.dstId+"\" weight=\""+e.attr+"\"/>\n").

collect().mkString+

"</edges>\n ? ? ?? </graph>\n ? ?? </gexf>"

?

? }

四、結(jié)果展示與分析

以下圖片的高清完整版可在output/pics中找到

4.1 人物親密度關(guān)系分析

可以看出郭靖和黃蓉的顏色是最深的(聯(lián)系是最緊密的)。這是因?yàn)樗麄冊(cè)凇渡涞瘛泛汀渡竦瘛分卸加泻芏鄳蚍荨!渡竦瘛分械哪信餍↓埮蜅钸^(guò)聯(lián)系也很緊密。相比之下《倚天》中的男女主張無(wú)忌和趙敏直接的線就淡的多了。一方面,這是因?yàn)橼w敏的出場(chǎng)時(shí)間太晚(全書(shū)40章,趙敏在第23章才出場(chǎng))。另一方面,張無(wú)忌優(yōu)柔寡斷,情感方面也一直在趙敏和周芷若之間猶豫不決,導(dǎo)致張無(wú)忌的情感線被周芷若分流了許多。

4.2 “專屬昵稱”分析

?

?

由于我只是篩選出了度為1和2的點(diǎn),但有些點(diǎn)是人名,而不是昵稱,不必看。

我原來(lái)以為“專屬昵稱”只出現(xiàn)在情侶之間,但發(fā)現(xiàn)有兩個(gè)例外。

洪七公——靖兒

這兩人情同父子。郭靖自幼喪父,洪七公也沒(méi)有子嗣。俗話說(shuō),“一日為師終身為父”,我覺(jué)得這兩個(gè)人不是父子,甚是父子。所以有這樣的“專屬昵稱”也不奇怪。

也許江南七怪也和郭靖情同父子,但可能是因?yàn)槌霈F(xiàn)的頻率不夠高,所以被過(guò)濾掉了,這張圖上并沒(méi)有出現(xiàn)。

陸無(wú)雙——傻蛋

全書(shū)只有陸無(wú)雙一人可以叫楊過(guò)”傻蛋“,因?yàn)楫?dāng)初楊過(guò)騙陸無(wú)雙自稱傻蛋。

那道姑道:“我?guī)讜r(shí)騙過(guò)你了?喂,小子,你叫甚么名字?”楊過(guò)道:“人人都叫我傻蛋,你不知道么?你叫甚么名字?”那道姑道:“傻蛋,你只叫我仙姑就得啦?!?/p>

摘錄了一下原文,發(fā)現(xiàn)短短幾句話,這道姑(陸無(wú)雙)就笑了2次,足見(jiàn)他們相處的多么愉快。過(guò)兒一生孤苦,和陸無(wú)雙在一起的日子也算是為數(shù)不多的快樂(lè)時(shí)光。我覺(jué)得他們倆很有成為情侶的可能,只可惜過(guò)兒心里已經(jīng)有了小龍女。最后他們倆結(jié)為了兄妹,也算是一段“有情人終成兄妹”的悲劇故事。

4.3 “孤島人群”分析

?

?

發(fā)現(xiàn)只有3個(gè)“孤島人群”(小團(tuán)體)。

簡(jiǎn)捷和薛公遠(yuǎn)是《倚天屠龍記》中被金花婆婆打傷,找胡青牛治病的人。和他們有交集的人確實(shí)很少。

李萍被段天德綁架,很長(zhǎng)一段時(shí)間內(nèi)只有他們兩個(gè)在一起,別人都不知道他們?nèi)チ四摹?/p>

術(shù)赤和察合臺(tái)是成吉思汗的兩個(gè)兒子。和他們有交集的人也很少。

這三本書(shū)中涉及到的人物,即使過(guò)濾完,也有將近200號(hào)人。如果在現(xiàn)實(shí)生活中,200人中應(yīng)該會(huì)有更多的小團(tuán)體,而且也不會(huì)全是2人組,可能有3~5人小團(tuán)體。

以下是我認(rèn)為可能的兩點(diǎn)原因:

小說(shuō)中,配角是為主角服務(wù)的,一般不會(huì)獨(dú)立于主線人物之外去寫小團(tuán)體

即便需要,寫2人也夠了,沒(méi)必要花筆墨寫

4.4 人物——武功\兵器分析

主要想看誰(shuí)經(jīng)常被哪種武功\兵器揍。

無(wú)忌——玄冥神掌

無(wú)忌小時(shí)候就因?yàn)橹辛诵ど裾撇铧c(diǎn)死掉,長(zhǎng)大后也經(jīng)常和玄冥二老斗。

郭靖——蛤蟆功

蛤蟆功可以說(shuō)是郭靖發(fā)明的,就是因?yàn)樗鄹牧恕毒抨幷娼?jīng)》,寫了本“九陰假經(jīng)”,才讓歐陽(yáng)鋒練成了蛤蟆功。后來(lái)也數(shù)次和歐陽(yáng)鋒的蛤蟆功交手?!渡竦瘛分行钸^(guò)也學(xué)了點(diǎn)蛤蟆功,被郭靖發(fā)現(xiàn)了,這又產(chǎn)生了一次交集。

楊過(guò)——金輪、浮塵

這是書(shū)中兩大反派金輪法王和李莫愁的武器。

五、后記

? 人人都知道金庸,可大多是通過(guò)影視作品,讀過(guò)原著的人少的可憐。做這個(gè)項(xiàng)目,在緬懷先生的同時(shí),也希望有更多的人能去讀一讀原著,體會(huì)一下先生筆下原汁原味的江湖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容