天天看點

左手讀紅樓夢,右手寫 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個這些實體之間的連接配接。

[email protected] 提供了資料集。此資料集中或有纰漏,但是對于一個圖資料項目的示例來說已經足夠好了。

雖然遇到了些阻礙,但所幸資料集還在,不如直接去分析統計下裡面的人物、地點、事件和關系,在輔助了解複雜的關系圖譜的同時,看看能否逆向的擷取些建構資料集的靈感啟示。

準備資料

紅樓夢資料集在此檔案裡

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),娛樂會所,沒有嫩模。