项目背景介绍
四大名著,又称四大小说,是汉语文学中不可多得的作品。这四部著作历久不衰,其中的故事、场景,已经深深地影响了国人的思想观念、价值取向。四部著作都有很高的艺术水平,细致的刻画和所蕴含的思想都为历代读者所称道。
本次将以小说HLM为例,介绍中文文本的统计分析和文本发掘等方面的基本知识。
数据准备
关于小说内容,读者可自行搜集。
项目流程
1.读取小说内容
# 获取HLM文本数据 with open('HLM.txt','r',encoding='utf-8')as f: text = f.read()
2.统计词频
import jieba # 分词并统计词频 def wordFreq(text,topn): words = jieba.lcut(text.strip()) # 对文本进行分词操作 counts = {} for word in words: # 统计每个词出现的频率,存放在字典counts中 if len(word) == 1: # 如果该词的长度为1,则跳过,不参与统计。 continue counts[word] = counts.get(word,0) + 1 items = list(counts.items()) items.sort(key=lambda x:x[1],reverse=True) # 按照词频进行排序 f = open('HLM_词频.txt','w',encoding='utf-8') for i in range(topn): # topn表示要取的词的个数,将频率最高的topn个词及其频率数存放在文件中 word,count = items[i] f.writelines("{}\t{}\n".format(word,count)) f.close() wordFreq(text,20) # 这里我们提取出频率最高的前20个词
出现频率最高的词语是“宝玉”,出现了3772次,宝玉当仁不让的是《HLM》的一号主角。后面紧跟的高频词是“什么”,“一个”,这些也都是无意义的词语,需要我们进一步去除。
3.去除停用词
这里我们只需要加载停用词库,统计词频的时候加上判断即可。
# 分词并统计词频 def wordFreq(text,topn): words = jieba.lcut(text.strip()) # 对文本进行分词操作 # 加载停用词库 stopwords = [line.strip() for line in open('停用词库.txt','r',encoding='utf-8').readlines()] counts = {} for word in words: # 统计每个词出现的频率,存放在字典counts中 if len(word) == 1: # 如果该词的长度为1,则跳过,不参与统计。 continue elif word not in stopwords: # 如果该词不在停用词列表stopwords中,才参与统计 counts[word] = counts.get(word,0) + 1 items = list(counts.items()) items.sort(key=lambda x:x[1],reverse=True) # 按照词频进行排序 f = open('HLM_词频.txt','w',encoding='utf-8') for i in range(topn): # topn表示要取的词的个数,将频率最高的topn个词及其频率数存放在文件中 word,count = items[i] f.writelines("{}\t{}\n".format(word,count)) f.close()
观察输出结果,不难发现“凤姐”和“凤姐儿”应该是一个人物,包括后面的“黛玉”和“林黛玉”,“林妹妹”也是一个人,“宝玉”以及“宝二爷”也应该一起统计。
所以我们还需要处理小说中同一人物,不同称呼的情况。对上面的代码进行改进。
# 分词并统计词频 def wordFreq(text,topn): words = jieba.lcut(text.strip()) # 对文本进行分词操作 # 加载停用词库 stopwords = [line.strip() for line in open('停用词库.txt','r',encoding='utf-8').readlines()] counts = {} for word in words: # 统计每个词出现的频率,存放在字典counts中 if len(word) == 1: # 如果该词的长度为1,则跳过,不参与统计。 continue elif word not in stopwords: # 如果该词不在停用词列表stopwords中,才参与统计 if word == '凤姐儿': word = '凤姐' elif word == '林黛玉' or word == '林妹妹': word = '黛玉' elif word == '二爷': word = '宝玉' counts[word] = counts.get(word,0) + 1 items = list(counts.items()) items.sort(key=lambda x:x[1],reverse=True) # 按照词频进行排序 f = open('HLM_词频.txt','w',encoding='utf-8') for i in range(topn): # topn表示要取的词的个数,将频率最高的topn个词及其频率数存放在文件中 word,count = items[i] f.writelines("{}\t{}\n".format(word,count)) f.close()
4.绘制词云图
这里我们调用获取词频函数,获取TOP500,最后绘制词云图
# 绘制词云 import matplotlib.pyplot as plt import wordcloud import imageio wordFreq(text,500) # 获取TOP500的词频 word_cloud_text = open('HLM_词频.txt','r',encoding='utf-8').read() bg_pic = imageio.imread('star.jpg') # 读入形状图片 wc = wordcloud.WordCloud(font_path=r'C:\Windows\Fonts\simhei.ttf', background_color='white', width=1000, max_words=200, mask=bg_pic, # mask参数设置词云形状 height=860, margin=2 ).generate(word_cloud_text) wc.to_file('HLMcloud_star.png') # 保存图片
5.章回处理
导入Python中的正则表达式(re模块),通过正则匹配,找到文中所有“第**回”形式的字符。“\u4e00”和“\u9fa5”是unicode编码,并且正好是中文编码的开始和结束的两个值,所以这个正则表达式可以用来判断字符串中是否包含中文。
# 章回处理 import re chapter = re.findall('第[\u4e00-\u9fa5]+回',text) lst_chapter = [] for x in chapter: # 去除重复的章节 if x not in lst_chapter and len(x)<=5: lst_chapter.append(x) print(lst_chapter)
获取每一回起始和结束数据
lst_start_chapterIndex = [] for x in lst_chapter: # 找出每一回在原文中的起始位置 lst_start_chapterIndex.append(text.index(x)) lst_end_chapterIndex = lst_start_chapterIndex[1:]+[len(text)] # 找出每一回在原文中的结束位置,本回的结束位置就是下一回的起始位置。最后一回的结束位置就是全文的结束。zip将每一回的起始和结束位置拼成一个元组,存放在lst_chapterindex列表中。 lst_chapterIndex = list(zip(lst_start_chapterIndex,lst_end_chapterIndex)) print(lst_chapterIndex)
5.1HLM之“刘姥姥三进荣国府”
统计在每一回中刘姥姥的出场次数并用折线图可视化。
# 统计刘姥姥出现的次数 cnt_liulaolao = [] for i in range(120): start = lst_chapterIndex[i][0] end = lst_chapterIndex[i][1] cnt_liulaolao.append(text[start:end].count('刘姥姥')) plt.rcParams['font.sans-serif'] = ['SimHei'] #解决中文显示 plt.rcParams['axes.unicode_minus'] = False #解决符号无法显示 plt.figure(figsize=(15,6)) plt.plot(range(120),cnt_liulaolao,label='刘姥姥出场次数') plt.title('《HLM》——刘姥姥三进荣国府',fontdict={'fontsize':14}) plt.xlabel('章节数',fontdict={'fontsize':14}) plt.ylabel('出现次数',fontdict={'fontsize':14}) plt.legend() plt.show()
5.2HLM之“哭说笑闹总关情”
我们尝试分析一下在小说的每一回里,“笑”和“喜”,“哭”与“悲”分别出现的次数,能不能通过这样一个小小的切入点,看一看贾府的兴衰与荣辱变化。统计每一回中“笑”和“喜”,“哭”与“悲”的出现次数。 并用折线图可视化。
# 统计情绪次数 cnt_laugh = [] cnt_cry = [] for i in range(120): start = lst_chapterIndex[i][0] end = lst_chapterIndex[i][1] cnt_laugh.append(text[start:end].count('笑')+text[start:end].count('喜')) cnt_cry.append(text[start:end].count('哭')+text[start:end].count('悲')) # 可视化 plt.figure(figsize=(15,6)) plt.plot(range(120),cnt_laugh,label='笑和喜') plt.plot(range(120),cnt_cry,label='哭和悲') plt.title('《HLM》120回悲喜变化图',fontdict={'fontsize':14}) plt.xlabel('章节数',fontdict={'fontsize':14}) plt.ylabel('出现次数',fontdict={'fontsize':14}) plt.legend() plt.show()
5.3HLM之平均段落数与字数
统计每一章段落数和字数并可视化。
cnt_chap = [] # 存放每一回的段落数 cnt_word = [] # 存放每一回的字符总数 for i in range(120): start = lst_chapterIndex[i][0] end = lst_chapterIndex[i][1] cnt_chap.append(text[start:end].count('\n')) cnt_word.append(len(text[start:end])) # 可视化 plt.figure(figsize=(15,8)) plt.scatter(cnt_chap,cnt_word) for i in range(120): plt.text(cnt_chap[i]-2,cnt_word[i]+100,lst_chapter[i],size=7) plt.xlabel('章节段数',fontdict={'fontsize':14}) plt.ylabel('章节字数',fontdict={'fontsize':14}) plt.title('《HLM》120回',fontdict={'fontsize':14}) plt.show()
5.4HLM之人物社交关系网络
提取小说中的人名
分析人物关系,首先我们需要将书中涉及到的人物姓名提取出来。我们利用在之前生成的词频文件,抽取排名靠前的二十四个人物,研究他们之间的社交关系情况。简便起见,将这些人物名称存放在列表Names中。
Names=['宝玉','凤姐','贾母','黛玉','王夫人','老太太','袭人','贾琏','平儿','宝钗','薛姨妈','探春','鸳鸯','贾政','晴雯','湘云','刘姥姥','邢夫人','贾珍','紫鹃','香菱','尤氏','薛蟠','贾赦']
基于共现关系获取人物关系权重
一般我们认为,在一篇文章中的同一段出现的两个人物之间,一定具有某种关联,因此我们将每一段中的人物角色抽取出来,然后以段落为单位,统计两个角色同时出现的共现次数,并把结果存在一个二维矩阵之中。
Names=['宝玉','凤姐','贾母','黛玉','王夫人','老太太','袭人','贾琏','平儿','宝钗','薛姨妈','探春','鸳鸯','贾政','晴雯','湘云','刘姥姥','邢夫人','贾珍','紫鹃','香菱','尤氏','薛蟠','贾赦'] relations = {} lst_para = text.split('\n') # 按段落划分,假设在同一段落中出现的人物具有共现关系 for t in lst_para: for name1 in Names: if name1 in t: for name2 in Names: if name2 in t and name1 != name2 and (name2,name1) not in relations: relations[(name1,name2)] = relations.get((name1,name2),0)+1 print(relations.items())
接下来对权重值做归一化操作,找到共现次数的最大值,然后将所有的共现次数映射到0-1之间。
maxRela = max([v for k,v in relations.items()]) relations = {k:v/maxRela for k,v in relations.items()} print(relations)
绘制人物社交网络图
使用networkx库,绘制人物社交网络图。
import networkx as nx plt.figure(figsize=(12,12)) G= nx.Graph() # 根据relations的数据向G中添加边 for k,v in relations.items(): G.add_edge(k[0],k[1],weight=v) elarge = [(u,v)for (u,v,d) in G.edges(data=True) if d['weight']>0.6] emidle = [(u,v)for (u,v,d) in G.edges(data=True) if (d['weight']>0.3)&(d['weight']<=0.6)] esmall = [(u,v)for (u,v,d) in G.edges(data=True) if d['weight']<=0.3] # 设置图形布局 pos = nx.spring_layout(G) # 设置节点样式 nx.draw_networkx_nodes(G,pos,alpha=0.8,node_size=800) nx.draw_networkx_edges(G,pos,edgelist=elarge,width=2.5,alpha=0.9,edge_color='g') nx.draw_networkx_edges(G,pos,edgelist=emidle,width=1.5,alpha=0.6,edge_color='r') nx.draw_networkx_edges(G,pos,edgelist=esmall,width=1,alpha=0.4,edge_color='b',style='dashed') nx.draw_networkx_labels(G,pos,font_size=12) plt.axis('off') plt.title('《HLM》主要人物社交关系网络图') plt.show()
emmm,这个风格好像有点不太好看,我们换个风格。
plt.figure(figsize=(8,6)) pos = nx.circular_layout(G) # 设置节点样式 nx.draw_networkx_nodes(G,pos,alpha=0.6,node_size=800) nx.draw_networkx_edges(G,pos,edgelist=elarge,width=2.5,alpha=0.9,edge_color='g') nx.draw_networkx_edges(G,pos,edgelist=emidle,width=1.5,alpha=0.6,edge_color='r') nx.draw_networkx_edges(G,pos,edgelist=esmall,width=1,alpha=0.2,edge_color='b',style='dashed') nx.draw_networkx_labels(G,pos,font_size=12) plt.axis('off') plt.title('《HLM》主要人物社交关系网络图') plt.show()
绿色权值最大,代表关系越深;红色权值处于中间,关系一般;蓝色权值最低,关系较弱。
从关系图中,我们可以看出贾宝玉社交关系又多又深,不愧是小说中的主角,社交广泛。
好了,以上就是本次分享的项目案例,如果对读者有所帮助,那就留下你的三连吧。