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

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

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


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

復雜

上一篇文章里安利了這個非常驚艷的關于紅樓夢的可視化作品:InteractiveGraph/example1


有不少人喜歡,也有人說如此復雜的圖譜,反而會使人覺得頭大。其實我也有此感受,對于紅迷們來說,書中內容情節、人物關系都是很熟悉的,這樣的關系圖一點點看起來自然不會太費勁。

可整個作品還是蠻復雜的,即便人物、事件、地點、關系等以不同顏色區別開來并在節點上附有詳情介紹,且右上角亦有可交互的選項,但畢竟成百上千的節點和邊交織在一個網頁里,對于不熟悉紅樓夢的人來說,就更覺錯綜復雜了。

這里也想起之前接觸的一個知識圖譜API,其實同樣也不知道這些實體與關系,對于個人而言能有什么切入點、可以怎么利用起來。下圖展示了該知識圖譜關于鄧婕的所有信息。大家可自行更改最后的參數,就能看到其他所有實體的情況了,比如entity=胡歌等等。

兩個緣由

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

比如本項目里,究竟是如何從1600余頁、73萬余字的《紅樓夢》原著中提取出人物關系、情節事件的呢?想來應該不會人工手動實現的吧?如果能知曉實現的流程和技術,甚至有開源的代碼,那么其他人也就能輕松遷移到不同小說、不同文本領域上去,并實現同樣酷炫的關系圖譜了。


數據集

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


但數據集是別處提供的,并非從頭開始構建的。簡單搜索了下,目前只看到兩個疑似相關的項目: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,其實到底該如何在新的文本上構造可用的、靠譜的數據集依舊不得而知,后續會寫寫句法依存分析、信息抽取、事件圖譜等等的文章,敬請期待。(馬卡龍伏筆)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容