想不出合适的标题,很喜欢关汉卿的这组元曲,就胡乱取了,顺便安利下。
适意行,安心坐,渴时饮饥时餐醉时歌,困来时就向莎茵卧。日月长,天地阔,闲快活!旧酒投,新醅泼,老瓦盆边笑呵呵,共山僧野叟闲吟和。他出一对鸡,我出一个鹅,闲快活!意马收,心猿锁,跳出红尘恶风波,槐阴午梦谁惊破?离了利名场,钻入安乐窝,闲快活!南亩耕,东山卧,世态人情经历多,闲将往事思量过。贤的是他,愚的是我,争甚么?——元·关汉卿《四块玉·闲适》
本文代码开源在:GitHub-DesertsX/gulius-projects
复杂
文章里安利了这个非常惊艳的关于红楼梦的可视化作品:InteractiveGraph/example1。
有不少人喜欢,也有人说如此复杂的图谱,反而会使人觉得头大。其实我也有此感受,对于红迷们来说,书中内容情节、人物关系都是很熟悉的,这样的关系图一点点看起来自然不会太费劲。
可整个作品还是蛮复杂的,即便人物、事件、地点、关系等以不同颜色区别开来并在节点上附有详情介绍,且右上角亦有可交互的选项,但毕竟成百上千的节点和边交织在一个网页里,对于不熟悉红楼梦的人来说,就更觉错综复杂了。这里也想起之前接触的一个知识图谱API,其实同样也不知道这些实体与关系,对于个人而言能有什么切入点、可以怎么利用起来。下图展示了该知识图谱关于邓婕的所有信息。大家可自行更改最后的参数,就能看到其他所有实体的情况了,比如entity=*等等。
两个缘由
言归正传,基于上文提到关系图谱的复杂面貌的缘故,以及最近接触了些依存句法分析、信息抽取、事件图谱等知识(后续会写写这方面内容),因而也对实际项目中如何从非结构化的文本内容中抽取出结构化的数据非常感兴趣。
比如本项目里,究竟是如何从余页、73万余字的《红楼梦》原著中提取出人物关系、情节事件的呢?想来应该不会人工手动实现的吧?如果能知晓实现的流程和技术,甚至有开源的代码,那么其他人也就能轻松迁移到不同小说、不同文本领域上去,并实现同样酷炫的关系图谱了。
数据集
幸运的是,这个项目代码都是开源的,GitHub上介绍了详细的实现流程。参见:InteractiveGraph/README_CN。
但数据集是别处提供的,并非从头开始构建的。简单搜索了下,目前只看到两个疑似相关的项目:GitHub-lzell/nickel、GitHub-iainbeeston/nickel,有待后续进一步验证。honglou.jsonhonglou.json数据集来自于中国古典名著《红楼梦》(又名《石头记》,wikipedia/Dream_of_the_Red_Chamber)。在这部小说中贾宝玉、林黛玉、薛宝钗是主要人物。这个数据集中定义了超过个实体,其中包括书中的人物,地点和时间,以及超过个这些实体之间的连接。nickel
github提供了数据集。此数据集中或有纰漏,但是对于一个图数据项目的示例来说已经足够好了。虽然遇到了些阻碍,但所幸数据集还在,不如直接去分析统计下里面的人物、地点、事件和关系,在辅助理解复杂的关系图谱的同时,看看能否逆向的获取些构建数据集的灵感启示。
准备数据
红楼梦数据集在此文件里dist/examples/honglou.json。点击raw后,全选复制新页面里的所有数据,并粘贴到本地文件中,文件名取为InteractiveGraph_HongLouMeng.json。
删除下面无用的代码,方可后续读取json数据时不出错。最后记得保存成utf-8编码格式。
translator:{nodes:function(node){//setdescriptionif(node.description===undefined){vardescription=palign=center;if(node.image!==undefined){description+=imgsrc=+node.image+width=/br;}description+=b+node.label+/b+[+node.id+];description+=/p;if(node.info!==undefined){description+=palign=left+node.info+/p;}else{if(node.title!==undefined)description+=palign=left+node.title+/p;}node.description=description;}},},简单展示下数据格式,其实和GitHub上的差不多:
{categories:{person:人物,event:事件,location:地点},data:{nodes:[{label:共读西厢,value:2,id:,categories:[event],info:宝玉到沁芳桥边桃花底下看《西厢记》,正准备将落花送进池中,黛玉说她早已准备了一个花冢,正来葬花。黛玉发现《西厢记》,宝玉借书中词句,向黛玉表白。黛玉觉得冒犯了自己尊严,引起口角,宝玉赔礼讨饶,黛玉也借《西厢记》词句,嘲笑了宝玉。于是两人收拾落花,葬到花冢里去。},......],edges:[{id:,label:位于,from:,to:},...]读取数据
以上,完成了数据准备过程,接下来可以开始在jupyternotebook里进行分析挖掘。
importjsonimportcodecswithcodecs.open(InteractiveGraph_HongLouMeng.json,r,encoding=utf-8)asjson_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=[]fornum,nodeinenumerate(nodes):ifnode[categories][0]==event:event_nodes.append(node)print(len(event_nodes))字典元素组成的列表直接用pandas转成表格格式:
importpandasaspddf=pd.DataFrame(event_nodes)df.head()其中label就是事件名称,info是内容简介,value貌似是觉得节点大小的,未做细究,本次均不做探索。
将事件全部提取出来:events=df[label].values.tolist()events存成列表格式,方便后续处理,注意,所有事件并非按照小说里情节发展的顺序排列的,所以看起来会较为混乱:
[共读西厢,林如海捐馆扬州城,海棠诗社,紫鹃试玉,魇魔姊弟,羞笼红麝串,麒麟伏双星,纳鸳鸯,撵晴雯,偷娶尤二姐,软语救贾琏,大闹学堂,拐卖巧姐,乱判葫芦案,*设相思局,情赠茜香罗,勇救薛蟠,倪二轻财尚义,神游太虚幻境,借剑杀人,平儿失镯,平儿行权,司棋被捉,巧结梅花络,亲尝莲叶羹,宝玉挨打,大闹厨房,香菱学诗,凤姐托孤,旺儿妇霸成亲,弄权铁槛寺,智能偷情,勾引薛蝌,贾*借钱,探春远嫁,刘姥姥一进荣国府,黛玉葬花,宝钗扑蝶,金钏投井,大观园试才,秦可卿淫丧天香楼,迎春误嫁中山狼,金玉良缘,王熙凤协理宁国府,元妃省亲,甄士隐梦幻识通灵,晴雯撕扇,凤姐泼醋,探春理家,湘云醉眠芍药裀,尤三姐殉情,抄检大观园,黛玉焚稿,黛玉之死,晴雯补裘,元宵丢英莲,冷子兴演说荣国府,木石前盟,贤袭人娇嗔箴宝玉]拿到这些事件后下一步该怎么办?让我们再明确下本文的目的之一,即看看能否逆向找出数据构造的规则与逻辑。那么自然而然的就有一个问题:这些事件都是如何从原著中抽取出来或者总结出来的呢?
作为中国古典四大名著之首的《红楼梦》,有余页、73万余字(人民文学出版社版本),涉及的人物和事件繁多,若是单纯靠人工去总结,显然并不可取,而且也无法迁移到其他文本上去。当然,《红楼梦》本身广受读者喜爱,历来研究的人也多,且妇孺皆知、耳熟能详,网上现成的人物名单、事件罗列,想来或多或少都是有的,此处暂且不表。
考虑到《红楼梦》本身是章回体小说,各章回的名字高度总结概括了本章的内容,一个合理的猜想就是从章回中直接抽取出事件内容。那么就来看看这59条数据里有多少是完全和章回名重合的呢?
获取章节名
首先从《红楼梦》小说章节目录网站获取各章回名称,简单写个爬虫就行。
importrequestsfromlxmlimportetreeurl=