左手讀紅樓夢,右手寫 BUG,閒快活

想不出合適的標題,很喜歡關漢卿的這組元曲,就胡亂取了,順便安利下。html

適意行,安心坐,渴時飲飢時餐醉時歌,困來時就向莎茵臥。日月長,天地闊,閒快活!
舊酒投,新醅潑,老瓦盆邊笑呵呵,共山僧野叟閒吟和。他出一對雞,我出一個鵝,閒快活!
意馬收,心猿鎖,跳出紅塵惡風波,槐陰午夢誰驚破?離了利名場,鑽入安樂窩,閒快活!
南畝耕,東山臥,世態人情經歷多,閒將往事思量過。賢的是他,愚的是我,爭甚麼?
——元·關漢卿《四塊玉·閒適》node

本文代碼開源在:GitHub - DesertsX/gulius-projectspython

複雜

上一篇文章裏安利了這個很是驚豔的關於紅樓夢的可視化做品:InteractiveGraph/example1
git


有很多人喜歡,也有人說如此複雜的圖譜,反而會令人以爲頭大。其實我也有此感覺,對於紅迷們來講,書中內容情節、人物關係都是很熟悉的,這樣的關係圖一點點看起來天然不會太費勁。
github


可整個做品仍是蠻複雜的,即使人物、事件、地點、關係等以不一樣顏色區別開來並在節點上附有詳情介紹,且右上角亦有可交互的選項,但畢竟成百上千的節點和邊交織在一個網頁裏,對於不熟悉紅樓夢的人來講,就更覺錯綜複雜了。json

這裏也想起以前接觸的一個知識圖譜API,其實一樣也不知道這些實體與關係,對於我的而言能有什麼切入點、能夠怎麼利用起來。下圖展現了該知識圖譜關於鄧婕的全部信息。你們可自行更改最後的參數,就能看到其餘全部實體的狀況了,好比entity=胡歌等等。
api

兩個原因

言歸正傳,基於上文提到關係圖譜的複雜面貌的緣故,以及最近接觸了些依存句法分析、信息抽取、事件圖譜等知識(後續會寫寫這方面內容),於是也對實際項目中如何從非結構化的文本內容中抽取出結構化的數據很是感興趣。bash

好比本項目裏,到底是如何從1600餘頁、73萬餘字的《紅樓夢》原著中提取出人物關係、情節事件的呢?想來應該不會人工手動實現的吧?若是能知曉實現的流程和技術,甚至有開源的代碼,那麼其餘人也就能輕鬆遷移到不一樣小說、不一樣文本領域上去,並實現一樣酷炫的關係圖譜了。
app

數據集

幸運的是,這個項目代碼都是開源的,GitHub上介紹了詳細的實現流程。參見:InteractiveGraph/README_CN
python爬蟲


但數據集是別處提供的,並不是從頭開始構建的。簡單搜索了下,目前只看到兩個疑似相關的項目:GitHub - lzell/nickelGitHub - 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']
複製代碼

層級關係大體如此,categoriesdata同一級,節點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),娛樂會所,沒有嫩模。

相關文章
相關標籤/搜索