左手讀紅樓夢,右手寫BUG,閒快活
想不出合適的標題,很喜歡關漢卿的這組元曲,就胡亂取了,順便安利下。
適意行,安心坐,渴時飲飢時餐醉時歌,困來時就向莎茵臥。日月長,天地闊,閒快活!
舊酒投,新醅潑,老瓦盆邊笑呵呵,共山僧野叟閒吟和。他出一對雞,我出一個鵝,閒快活!
意馬收,心猿鎖,跳出紅塵惡風波,槐陰午夢誰驚破?離了利名場,鑽入安樂窩,閒快活!
南畝耕,東山臥,世態人情經歷多,閒將往事思量過。賢的是他,愚的是我,爭甚麼?
——元·關漢卿《四塊玉·閒適》
本文程式碼開源在: GitHub - DesertsX/gulius-projects
複雜
上一篇文章裡安利了這個非常驚豔的關於紅樓夢的視覺化作品: InteractiveGraph/example1 。
有不少人喜歡,也有人說如此複雜的圖譜,反而會使人覺得頭大。其實我也有此感受,對於紅迷們來說,書中內容情節、人物關係都是很熟悉的,這樣的關係圖一點點看起來自然不會太費勁。
可整個作品還是蠻複雜的,即便人物、事件、地點、關係等以不同顏色區別開來並在節點上附有詳情介紹,且右上角亦有可互動的選項,但畢竟成百上千的節點和邊交織在一個網頁裡,對於不熟悉紅樓夢的人來說,就更覺錯綜複雜了。
這裡也想起之前接觸的一個知識圖譜API,其實同樣也不知道這些實體與關係,對於個人而言能有什麼切入點、可以怎麼利用起來。下圖展示了該知識圖譜關於鄧婕的所有信息。大家可自行更改最後的引數,就能看到其他所有實體的情況了,比如 entity=胡歌
等等。
兩個緣由
言歸正傳,基於上文提到關係圖譜的複雜面貌的緣故,以及最近接觸了些依存句法分析、資訊抽取、事件圖譜等知識(後續會寫寫這方面內容),因而也對實際專案中如何從非結構化的文字內容中抽取出結構化的資料非常感興趣。
比如本專案裡,究竟是如何從1600餘頁、73萬餘字的《紅樓夢》原著中提取出人物關係、情節事件的呢?想來應該不會人工手動實現的吧?如果能知曉實現的流程和技術,甚至有開源的程式碼,那麼其他人也就能輕鬆遷移到不同小說、不同文字領域上去,並實現同樣酷炫的關係圖譜了。
資料集
幸運的是,這個專案程式碼都是開源的,GitHub上介紹了詳細的實現流程。參見: InteractiveGraph/README_CN 。
但資料集是別處提供的,並非從頭開始構建的。簡單搜尋了下,目前只看到兩個疑似相關的專案: GitHub - lzell/nickel 、 GitHub - iainbeeston/nickel ,有待後續進一步驗證。
honglou.json
honglou.json資料集來自於中國古典名著《紅樓夢》(又名《石頭記》, wikipedia / Dream_of_the_Red_Chamber) 。 在這部小說中賈寶玉、林黛玉、薛寶釵是主要人物。這個資料集中定義了超過300個實體,其中包括書中的人物,地點和時間,以及超過500個這些實體之間的連線。
nickel2008@github 提供了資料集。此資料集中或有紕漏,但是對於一個圖資料專案的示例來說已經足夠好了。
雖然遇到了些阻礙,但所幸資料集還在,不如直接去分析統計下里面的人物、地點、事件和關係,在輔助理解複雜的關係圖譜的同時,看看能否逆向的獲取些構建資料集的靈感啟示。
準備資料
紅樓夢資料集在此檔案裡 dist/examples/honglou.json
。點選 raw
後,全選複製新頁面裡的所有資料,並貼上到本地檔案中,檔名取為 InteractiveGraph_HongLouMeng.json
。
刪除下面無用的程式碼,方可後續讀取json資料時不出錯。最後記得儲存成 utf-8
編碼格式。
"translator": { "nodes": function (node) { //set description if (node.description === undefined) { var description = "<p align=center>"; if (node.image !== undefined) { description += "<img src='" + node.image + "' width=150/><br>"; } description += "<b>" + node.label + "</b>" + "[" + node.id + "]"; description += "</p>"; if (node.info !== undefined) { description += "<p align=left>" + node.info + "</p>"; } else { if (node.title !== undefined) description += "<p align=left>" + node.title + "</p>"; } node.description = description; } }, }, 複製程式碼
簡單展示下資料格式,其實和GitHub上的差不多:
{ "categories": { "person": "人物", "event": "事件", "location": "地點" }, "data": { "nodes": [{ "label": "共讀西廂", "value": 2, "id": 3779, "categories": [ "event" ], "info": "寶玉到沁芳橋邊桃花底下看《西廂記》,正準備將落花送進池中,黛玉說她早已準備了一個花冢,正來葬花。黛玉發現《西廂記》,寶玉借書中詞句,向黛玉表白。黛玉覺得冒犯了自己尊嚴,引起口角,寶玉賠禮討饒,黛玉也借《西廂記》詞句,嘲笑了寶玉。於是兩人收拾落花,葬到花冢裡去。" }, ...... ], "edges": [{ "id": 3776, "label": "位於", "from": 3838, "to": 3851 }, ... ] 複製程式碼
讀取資料
以上,完成了資料準備過程,接下來可以開始在 jupyter notebook
裡進行分析挖掘。
import json import codecs with codecs.open('InteractiveGraph_HongLouMeng.json', 'r',encoding='utf-8') as json_str: json_dict = json.load(json_str) print(json_dict.keys()) print(json_dict["categories"].keys()) print(json_dict["categories"]) nodes = json_dict['data']['nodes'] edges = json_dict['data']['edges'] 複製程式碼
層級關係大致如此, categories
和 data
同一級,節點 nodes
和邊 edges
同一級,並且歸屬於 data
,也是本次要統計分析的所有資料, categories
指明三種節點資料型別,即: 'person': '人物', 'event': '事件', 'location': '地點
。
dict_keys(['categories', 'data']) dict_keys(['person', 'event', 'location']) dict_keys(['nodes', 'edges']) {'person': '人物', 'event': '事件', 'location': '地點'} 複製程式碼
紅樓多少事
首先來看看資料中都包含了哪些紅樓夢中的事件,直接篩選出型別為 event
的節點,共拿到59條資料。
event_nodes = [] for num, node in enumerate(nodes): if node['categories'][0] == 'event': event_nodes.append(node) print(len(event_nodes)) 複製程式碼
字典元素組成的列表直接用 pandas
轉成表格格式:
import pandas as pd df = pd.DataFrame(event_nodes) df.head() 複製程式碼
其中 label
就是事件名稱, info
是內容簡介, value
貌似是覺得節點大小的,未做細究,本次均不做探索。
將事件全部提取出來:
events = df['label'].values.tolist() events 複製程式碼
存成列表格式,方便後續處理,注意,所有事件並非按照小說裡情節發展的順序排列的,所以看起來會較為混亂:
['共讀西廂','林如海捐館揚州城','海棠詩社','紫鵑試玉', '魘魔姊弟','羞籠紅麝串','麒麟伏雙星','納鴛鴦', '攆晴雯','偷娶尤二姐','軟語救賈璉','大鬧學堂', '拐賣巧姐','亂判葫蘆案','毒設相思局','情贈茜香羅', '勇救薛蟠','倪二輕財尚義','神遊太虛幻境','借劍殺人', '平兒失鐲','平兒行權','司棋被捉','巧結梅花絡', '親嘗蓮葉羹','寶玉捱打','大鬧廚房','香菱學詩', '鳳姐託孤','旺兒婦霸成親','弄權鐵檻寺','智慧偷情', '勾引薛蝌','賈政借錢','探春遠嫁','劉姥姥一進榮國府', '黛玉葬花','寶釵撲蝶','金釧投井','大觀園試才', '秦可卿淫喪天香樓','迎春誤嫁中山狼','金玉良緣','王熙鳳協理寧國府', '元妃省親','甄士隱夢幻識通靈','晴雯撕扇','鳳姐潑醋', '探春理家','湘雲醉眠芍藥裀','尤三姐殉情','抄檢大觀園', '黛玉焚稿','黛玉之死','晴雯補裘','元宵丟英蓮', '冷子興演說榮國府','木石前盟','賢襲人嬌嗔箴寶玉'] 複製程式碼
拿到這些事件後下一步該怎麼辦?讓我們再明確下本文的目的之一,即看看能否逆向找出資料構造的規則與邏輯。那麼自然而然的就有一個問題:這些事件都是如何從原著中抽取出來或者總結出來的呢?
作為中國古典四大名著之首的《紅樓夢》,有1600餘頁、73萬餘字(人民文學出版社版本),涉及的人物和事件繁多,若是單純靠人工去總結,顯然並不可取,而且也無法遷移到其他文字上去。當然,《紅樓夢》本身廣受讀者喜愛,歷來研究的人也多,且婦孺皆知、耳熟能詳,網上現成的人物名單、事件羅列,想來或多或少都是有的,此處暫且不表。
考慮到《紅樓夢》本身是章回體小說,各章回的名字高度總結概括了本章的內容,一個合理的猜想就是從章回中直接抽取出事件內容。那麼就來看看這59條資料裡有多少是完全和章回名重合的呢?
獲取章節名
首先從《紅樓夢》小說章節目錄網站獲取各章回名稱,簡單寫個爬蟲就行。
import requests from lxml import etree url = 'https://www.555zw.com/book/39/39480/' r = requests.get(url) r.encoding = r.apparent_encoding selector = etree.HTML(r.content) contents = selector.xpath('//tr//a/@title') print(len(contents)) contents 複製程式碼
注意需要設定編碼格式,否則會亂碼。展示部分資料
120 ['第一回 甄士隱夢幻識通靈 賈雨村風塵懷閨秀', '第二回 賈夫人仙逝揚州城 冷子興演說榮國府', '第三回 賈雨村夤緣復舊職 林黛玉拋父進京都', '第四回 薄命女偏逢薄命郎 葫蘆僧亂判葫蘆案', '第五回 遊幻境指迷十二釵 飲仙醪曲演紅樓夢', '第六回 賈寶玉初試雲雨情 劉姥姥一進榮國府', ...] 複製程式碼
經過一些簡單處理後(具體可見程式碼: GitHub - DesertsX/gulius-projects ,本文略過),拿到章回與事件對應關係
chapter_df = pd.DataFrame({"chapter":chapters, "title":contents}) def is_event(title): for event in event_chaps: if event in title: return event return '' chapter_df['title2event'] = chapter_df['title'].apply(is_event) chapter_df.head(10) 複製程式碼
title2event
列可以看成能直接從章回名中提前出事件名。
接著將 title2event
列非空的所有行都標上顏色,由於在整個表格裡只標出特定的行的程式碼寫不出來(太菜),只能將非空的行選出來後再設定顏色。
chapter_df[chapter_df.title2event != ''] .style.set_properties(**{'background-color': '#ccff99', 'color': '#B452CD'}) 複製程式碼
因為很少看到有人像在 excel
一樣,用不同顏色顯示 jupyter notebook
裡的表格資料,於是搜了下,還真有實現的方式:pandas-docs/style。
由上圖可知,共有18條(18/59=30%)事件是一字不差包含在章回名裡的。不過感覺非紅迷的朋友,可能不熟悉這些事件到底是什麼情節(是這樣嗎?)
非章節名的事件
接著看看其他41條事件,這裡按人物角色和小說情節出現的前後順序進行簡單整理,比較耳熟能詳的有: '木石前盟', '金玉良緣', '共讀西廂', '寶釵撲蝶','黛玉葬花','晴雯撕扇', '湘雲醉眠芍藥裀', '香菱學詩'
等等。
'元宵丟英蓮', '木石前盟', '金玉良緣', '麒麟伏雙星', '神遊太虛幻境','秦可卿淫喪天香樓', '倪二輕財尚義', '智慧偷情', '旺兒婦霸成親', '大鬧學堂', '寶玉捱打', '元妃省親', '共讀西廂', '寶釵撲蝶', '海棠詩社', '湘雲醉眠芍藥裀', '香菱學詩', '魘魔姊弟', '金釧投井', '紫鵑試玉', '大鬧廚房', '司棋被捉', '晴雯撕扇', '晴雯補裘', '攆晴雯', '平兒失鐲', '鳳姐託孤', '拐賣巧姐', '探春理家', '探春遠嫁', '黛玉葬花', '黛玉之死', '納鴛鴦', '偷娶尤二姐', '尤三姐殉情', '賈政借錢', '勇救薛蟠', '勾引薛蝌',} 複製程式碼
其中, '寶釵撲蝶'和'黛玉葬花'
均對應 第二十七回 滴翠亭楊妃戲彩蝶 埋香冢飛燕泣殘紅
。可見還是可以轉換成從章節名裡提取事件的。
以上就是對資料集中事件這一維度的分析,藉助章回名和耳熟能詳的橋段,可以拿到大多數事件。而有了事件後,如何提取事件中涉及的主要人物,這又是需要解決的,並且如何對其他不含章回名的、不那麼熟悉的文字進行實體關係抽取、事件圖譜構建等等都是需要進一步研究的。
location 地點
接下來,看看 location
地點資料。格式如下:
{ "label": "太虛幻境", "value": 1, "id": 3860, "categories": [ "location" ], "info": "太虛幻境,《紅樓夢》中的女兒仙境,警幻仙子司主。它位於離恨天之上、灌愁海之中的放春山遣香洞,以夢境的形式向甄士隱、賈寶玉二位有緣人顯現。" }, 複製程式碼
程式碼很簡單,和上面 event
事件差不多:
loc_nodes = [] for num, node in enumerate(nodes): if node['categories'][0] == 'location': loc_nodes.append(node) print(len(loc_nodes)) loc_df = pd.DataFrame(loc_nodes) loc_df.head(10) 複製程式碼
本資料集給出的地點不算多,僅26條,主要是城市、賈府、大觀園、各主要人物的住處等等。這部分可以用命名實體識別、或手動建立地點詞典、或網上找現成的彙總等,應該能比較方便的實現,所以不展開了。至於人物與地點關係的抽取,同樣不清楚有什麼自動化的方式可以實現嘛?
['榮國府', '寧國府', '大觀園', '太虛幻境', '蘇州', '京郊', '揚州', '金陵', '京城', '胡州', '大同府', '閶門', '應天府', '怡紅院', '瀟湘館', '蘅蕪苑', '秋爽齋', '暖香塢', '綴錦樓', '稻香村', '鳳藻宮','櫳翠庵', '梨香院', '玄真觀','葫蘆廟', '南海'] 複製程式碼
看到這些熟悉地名,也是想起自己曾去過北京和上海青浦南北兩處大觀園,網上盜張圖,懷念一下:
person 人物
再來看看 person
人物資料詳情。格式如下:
{ "label": "林黛玉", "value": 21, "image": "./images/photo/林黛玉.jpg", "id": 4037, "categories": [ "person" ], "info": "金陵十二釵之冠(與寶釵並列)。林如海與賈敏之女,寶玉的姑表妹,寄居榮國府 。她生性孤傲,多愁善感,才思敏捷。她與寶玉真心相愛,是寶玉反抗封建禮教的同盟,是自由戀愛的堅定追求者。" }, 複製程式碼
轉成表格格式:
person_nodes = [] for num, node in enumerate(nodes): if node['categories'][0] == 'person': person = node['label'] person_nodes.append(node) print(len(person_nodes)) person_df = pd.DataFrame(person_nodes) person_df.head(10) 複製程式碼
共242條人物資料,其中有112人附帶了1987版《紅樓夢》電視劇的角色劇照,照片統一存放在: dist\examples\images\photo
。
陳曉旭版的林黛玉瞭解一下:
用 百年百圖の中國(1900-1999):另類python爬蟲和PIL拼圖 一文裡的程式碼將所有圖片拼到一起看看。裡面混入了一個奇怪的東西(黑白的那張)。
另外,尤三姐的照片搞錯成了尤二姐,於是有兩張尤二姐的,即第四行倒數第三四張(一位“紅迷”的自我修養,後面還發現了其他BUG,稍後再談)。
edges 邊
最後再來看看人物與人物、人物與地點、人物與事件的關係。資料格式:
"edges": [{ "id": 3776, "label": "位於", "from": 3838, "to": 3851 }, { "id": 3777, "label": "位於", "from": 3839, "to": 3851 }, 複製程式碼
轉成表格形式:
edges_df = pd.DataFrame(edges) edges_df.head() 複製程式碼
共25類694條資料。
['參與', '僕人', '居住地', '父親', '原籍', '母親', '丈夫', '妻子', '哥哥', '交好', '位於', '同宗', '姐姐', '私通', '老師', '姬妾', '喜歡', '跟班', '乾孃', '奶媽', '知己', '陪房', '前世', '連宗', '有恩'] 複製程式碼
用 pyecharts
繪製各類關係及其數量的柱形圖。
最近python交友娛樂會所群(QQ:613176398)裡看到很多人都也在用這個庫,不過我又想重新用ECharts來“美顏”圖表了,以往整理過的程式碼和示例可見: 圖表太醜怎麼破,ECharts神器帶你飛! 。這裡也用一下,顏值碾壓。
在這些關係中,首先看到了“私通”二字,那麼就來看下都是誰和誰私通吧。寫成函式方便複用。這裡 edges
只包含相關節點的 id
,需要從 person
裡拿到對應的人物名稱。
def word2id(word): df = edges_df[edges_df.label== word] from_id = df['from'].values.tolist() to_id = df['to'].values.tolist() return from_id, to_id def id2label(ids): tables = [] for ID in ids: tables.append(person_df[person_df['id']==ID]) labels = pd.concat(tables)['label'].values.tolist() return labels def get_relation(from_id,to_id): for from_label, to_label in zip(id2label(from_id), id2label(to_id)): print(from_label, '--> {} -->'.format(word), to_label) word = "私通" from_id,to_id = word2id(word) get_relation(from_id,to_id) 複製程式碼
以下就是私通名單!《紅樓夢》裡蠻出名的一句話是焦大說的: “爬灰的爬灰,養小叔子的養小叔子” ,不明真相的吃瓜群眾可以自行搜尋。
賈薔 --> 私通 --> 齡官 賈珍 --> 私通 --> 秦可卿 賈璉 --> 私通 --> 多姑娘 薛蟠 --> 私通 --> 寶蟾 王熙鳳 --> 私通 --> 賈蓉 秦可卿 --> 私通 --> 賈薔 司棋 --> 私通 --> 潘又安 寶蟾 --> 私通 --> 薛蟠 尤三姐 --> 私通 --> 賈珍 鮑二家的 --> 私通 --> 賈璉 智慧兒 --> 私通 --> 秦鍾 萬兒 --> 私通 --> 茗煙 複製程式碼
其中,賈璉也就是王熙鳳鳳姐的丈夫,分別和多姑娘、鮑二家的有私情。這裡不得不開個車,其實《紅樓夢》裡也有幾個黃段子的,下面兩則均出自 第二十一回 《賢襲人嬌嗔箴寶玉 俏平兒軟語救賈璉》 :
賈璉見她嬌俏動情,便摟著求歡,被平兒奪手跑了,急的賈璉彎著腰恨道:“死促狹小瀅婦!一定浪上人的火來,他又跑了。”平兒在窗外笑道:“我浪我的,誰叫你動火了?難道圖你受用一回,叫他知道了,又不待見我。”
下面這個更好笑,因為新版紅樓夢電視劇把這部分拍成了拔火罐,也是佩服導演的“神來之筆”,為18歲以下青少年的心理健康出了一份力。可見: 為什麼網上對於舊版《紅樓夢》的評價比新版《紅樓夢》好那麼多,舊版紅樓是否被過度神話?
那個賈璉,只離了鳳姐便要尋事,獨寢了兩夜,便十分難熬,便暫將小廝們內有清俊的選來出火。
言歸正傳,本以為這裡出現了個BUG: 秦可卿 --> 私通 --> 賈薔
應該是 秦可卿 --> 私通 --> 賈珍
,但一搜真有這些猜想,也就隨它去吧。
另外在原著裡秦可卿,乳名兼美,暗含兼有釵黛之美的意思,在寶玉夢遊太虛幻境時,寫到“其鮮豔嫵媚,有似乎寶釵,風流嫋娜,則又如黛玉”。也是金陵十二釵中最先去世的女子。
再來看看其他關係:“喜歡”
林黛玉 --> 喜歡 --> 賈寶玉 薛寶釵 --> 喜歡 --> 賈寶玉 妙玉 --> 喜歡 --> 賈寶玉 秦可卿 --> 喜歡 --> 賈寶玉 彩雲 --> 喜歡 --> 賈環 尤三姐 --> 喜歡 --> 柳湘蓮 藕官 --> 喜歡 --> 菂官 彩霞 --> 喜歡 --> 賈環 齡官 --> 喜歡 --> 賈薔 複製程式碼
“知己”
林黛玉 --> 知己 --> 紫鵑 妙玉 --> 知己 --> 邢岫煙 史湘雲 --> 知己 --> 林黛玉 複製程式碼
“交好”
賈寶玉 --> 交好 --> 秦鍾 賈寶玉 --> 交好 --> 柳湘蓮 賈寶玉 --> 交好 --> 蔣玉菡 賈寶玉 --> 交好 --> 北靜王 賈蓉 --> 交好 --> 賈璉 賈薔 --> 交好 --> 秦鍾 秦鍾 --> 交好 --> 香憐 薛蟠 --> 交好 --> 柳湘蓮 薛蟠 --> 交好 --> 馮紫英 薛蟠 --> 交好 --> 金榮 柳湘蓮 --> 交好 --> 秦鍾 賈雨村 --> 交好 --> 冷子興 蔣玉菡 --> 交好 --> 北靜王 賈芸 --> 交好 --> 賈薔 賈菌 --> 交好 --> 賈藍 賴尚榮 --> 交好 --> 柳湘蓮 癩頭和尚 --> 交好 --> 跛足道人 晴雯--> 交好 --> 麝月 襲人 --> 交好 --> 平兒 小紅 --> 交好 --> 墜兒 瑞珠 --> 交好 --> 寶珠 柳嫂子 --> 交好 --> 芳官 馬道婆 --> 交好 --> 趙姨娘 複製程式碼
感覺挺多和自己想的不一樣的。但也懶得管了。逃……
小結
以上算是“簡單”完成了對該資料集的探索和分析,程式碼開源在: GitHub - DesertsX/gulius-projects ,其實到底該如何在新的文字上構造可用的、靠譜的資料集依舊不得而知,後續會寫寫句法依存分析、資訊抽取、事件圖譜等等的文章,敬請期待。(馬卡龍伏筆)
歡迎關注公眾號:牛衣古柳(ID:Deserts-X)。Python交友娛樂會所群(QQ群 613176398),娛樂會所,沒有嫩模。