《今生今世》是渣男胡兰成所写的一部自传体小说。今天我们就来分析一下在他所写的自传中的人物关系图谱,分析一下胡兰成到底和多少女人有关系。
代码奉上
# -*- coding: utf-8 -*- #@author: Yuhao Zhang #2018.5.30 import jieba import codecs from collections import defaultdict from pandas import DataFrame import pandas as pd #路径设置。 #文档介绍 #jsjs.txt是胡兰成写的小说《今生今世》。 #person.txt是一个语料库,里面放了很多该小说的角色名称。 #synonymous_dict.txt是角色的别名 #其他两个文件是存放程序计算所得各个人物关系之间的边的权重 TEXT_PATH = 'jsjs.txt' DICT_PATH = 'person.txt' SYNONYMOUS_DICT_PATH = 'synonymous_dict.txt' SAVE_NODE_PATH = 'node.csv' SAVE_EDGE_PATH = 'edge.csv' #类的初始化 class RelationshipView: def __init__(self, text_path, dict_path, synonymous_dict_path): self._text_path = text_path self._dict_path = dict_path self._synonymous_dict_path = synonymous_dict_path self._person_counter = defaultdict(int) self._person_per_paragraph = [] self._relationships = {} self._synonymous_dict = {} def generate(self): self.count_person() self.calc_relationship() self.save_node_and_edge() def synonymous_names(self): with codecs.open(self._synonymous_dict_path, 'r', 'utf-8') as f: lines = f.read().split('\n') for l in lines: self._synonymous_dict[l.split(' ')[0]] = l.split(' ')[1] return self._synonymous_dict def get_clean_paragraphs(self): new_paragraphs = [] last_paragraphs = [] with codecs.open(self._text_path, 'r', 'utf-8') as f: paragraphs = f.read().split('\r\n') paragraphs = paragraphs[0].split('\u3000') for i in range(len(paragraphs)): if paragraphs[i] != '': new_paragraphs.append(paragraphs[i]) for i in range(len(new_paragraphs)): new_paragraphs[i] = new_paragraphs[i].replace('\n', '') new_paragraphs[i] = new_paragraphs[i].replace(' ', '') last_paragraphs.append(new_paragraphs[i]) return last_paragraphs def count_person(self): paragraphs = self.get_clean_paragraphs() synonymous = self.synonymous_names() print('start process node') with codecs.open(self._dict_path, 'r', 'utf-8') as f: name_list = f.read().split(' 10 nr\n') for p in paragraphs: jieba.load_userdict(self._dict_path) poss = jieba.cut(p) self._person_per_paragraph.append([]) for w in poss: if w not in name_list: continue if synonymous.get(w): w = synonymous[w] self._person_per_paragraph[-1].append(w) if self._person_counter.get(w) is None: self._relationships[w] = {} self._person_counter[w] += 1 return self._person_counter def calc_relationship(self): print("start to process edge") for p in self._person_per_paragraph: for name1 in p: for name2 in p: if name1 == name2: continue if self._relationships[name1].get(name2) is None: self._relationships[name1][name2] = 1 else: self._relationships[name1][name2] += 1 return self._relationships def save_node_and_edge(self): excel = [] for name, times in self._person_counter.items(): excel.append([]) excel[-1].append(name) excel[-1].append(name) excel[-1].append(str(times)) data = DataFrame(excel, columns=['Id', 'Label', 'Weight']) data.to_csv('node.csv', encoding='gbk') excel = [] for name, edges in self._relationships.items(): for v, w in edges.items(): if w > 3: excel.append([]) excel[-1].append(name) excel[-1].append(v) excel[-1].append(str(w)) data = DataFrame(excel, columns=['Source', 'Target', 'Weight']) data.to_csv('edge.csv', encoding='gbk') print('save file successful!') if __name__ == '__main__': v = RelationshipView(TEXT_PATH, DICT_PATH, SYNONYMOUS_DICT_PATH) v.generate()
先将代码全部贴出来,大家复制粘贴即可运行,接下来我再慢慢得一行一行仔细讲解代码逻辑。
先把程序运行效果图贴出来。
基础关系图谱
恋人关系图谱
设计思想
整个程序的实现过程是这样的。
首先,我们预先准备好语料库。(里面含有小说主人公姓名以及别名,这样做的目的降低了程序的难度,不然还要涉及到知识图谱的实体识别。有关知识图谱的东西我最近在看,以后会写这方面博客)
然后我们将这篇将近三十万字的小说按段落分开,对每一段进行单独分析,对两个实体之间的边的权重进行计算。具体的说,它的权重是如何计算的呢?比如第一段我们结合语料库发现里面有三个胡兰成,一个张爱玲,一个周佛海。那么我们就给胡兰成和张爱玲之间的边权重更新为3,张爱玲和周佛海之间更新为1,胡兰成和周佛海之间更新为3。然后将每个段落的每两个点之间的权重加和,最后写入excel表中保存为csv格式。这是为了方便使用Gephi可视化。
当然同时我们也进行了节点大小的计算,其实就是单纯计算这个实体名字在文中出现的次数。比如胡兰成出现了5752次,那么它在可视化图中的大小你可以理解为5752个单位这么大。
node.csv
edge.csv
代码详解
我只讲我认为最难懂的部分,一些我认为简单的如果你不懂可以私信评论我都可以,我一般会很快回复。
我认为最难懂的就是count_person()函数中的一部分,当然这一部分也可以说是精华。
for p in paragraphs: jieba.load_userdict(self._dict_path) poss = jieba.cut(p) self._person_per_paragraph.append([]) for w in poss: if w not in name_list: continue if synonymous.get(w): w = synonymous[w] self._person_per_paragraph[-1].append(w) if self._person_counter.get(w) is None: self._relationships[w] = {} self._person_counter[w] += 1 return self._person_counter
首先在一个将小说每段作为一个字符串元素的列表paragraphs中循环,也就是说p是小说中的每一段。
然后在加载了语料库之后,对该段进行结巴分词,并将该段分词结果存在poss列表中。如果语料库中的实体在poss中可以找的到,那么我们就从存储别名字典中依据这个实体来找他的唯一名字(因为有可能是别名,我们要统一换成真正的姓名进行加权)。
get()是字典这个类型所有的属性,在该段代码中体现为,判断该字典中有无含有以w为key的元素。
大家也许会疑惑,为何self._person_counter是一个字典呢?这源于程序一开头的一行代码。
self._person_counter = defaultdict(int)
想要理解这个defaultdict的作用,将我下面贴出来的这几行代码敲进去试试就知道了。它其实起到一个给字典的值设置一个默认值的快捷方式。
from collections import defaultdict person = defaultdict(int) test_list = ['胡兰成', '张爱玲', 'Bob', 'Bob', 'Nick', '胡兰成'] for p in test_list: person[p] += 1 print(person)
可视化
可视化我是用到了Gephi这个软件。
PS下载这个软件需要用到java组件,需要下载一个jre,听过来人一句劝告,千万不要下载最新的那个10的那个版本。
然后就是Gephi简单的使用教程。放几张网图,大家学习一下。
新建工程
导入数据
选择样式
修改字体
调整布局
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句