系列文章目录
上一篇:【Python Onramp】3. Python的文本分析(1)jieba分词:第三方库和基本面向对象编程
下一篇:【Python Onramp】5.利用Pyecharts进行可视化:综合应用
本文目录
项目描述
文本是程序之间的重要介质,文本处理也是编程的重要问题。我们在上一节的基础上,对红楼梦做更多的研究。
github仓库见Honour-Van:CS50 TextAnalyser和TextAnalyser_co
这里我们仍以红楼梦作为研究样例,对文本进行词频统计分析: 项目中你将:
- 分章节统计主要人物的词频变化
- 统计人物共现关系
- 绘制简单的pyecharts图
分章节统计红楼梦六大主要人物出现次数(包括贾宝玉、贾母、林黛玉、王熙凤、王夫人、薛宝钗),使用pyecharts绘制成折线图。
分析人物共现关系(同时出现在同一个自然段中就认为是一次共现),绘制四大家族的共现关系pyecharts图。四大家族的标注通过手动完成。
语法总览
语法点1:基于面向对象进行功能扩展
对于一个封闭的对象来说,为了提供对外的新功能,只需要向其中添加新的函数即可。详见实现部分。
语法点2:正则表达式
廖雪峰的教程:https://www.liaoxuefeng.com/wiki/1016959663602400/1017639890281664
Runoob教程:https://www.runoob.com/python3/python3-reg-expressions.html
由于这个文本的章节标题行具有一定的规则,我们可以试图用一定的方式来描述这种规律。
用来描述一类字符串的模式的表达式就是正则表达式,良好编制的正则表达式可以很好地进行字串匹配,是很多字符串解析工具的基础。
这个文本的匹配,可以使用如下的正则表达式:
import re
title_pattern = re.compile(r'.* 第.*?回 .*\n')
.
表示任意字符*
表示任意个数?
表示非贪婪匹配
判断时使用:
if title_pattern.match(line):
pass
语法点3:pyecharts折线图
Echarts是一个非常优秀的可视化框架。
PyEcharts作为其在Python端的封装是一个很好的尝试。将具有强大的处理能力的Python和可视化工具直接对接起来,具有非常良好的生态。
官网:https://pyecharts.org/#/
上面提供了大量的使用示例。
我们这一节要使用的折线图示例可以在如下的链接中找到:
https://gallery.pyecharts.org/#/Line/README
具体实现
step1:分章节
基于正则表达式,我们分行对原文本进行分析,如果遇到标题行,就重新开一个文件保存。
其余的语法点在之前的博客中都已经讲解过,代码如下:
import re
title_pattern = re.compile(r'.* 第.*?回 .*\n')
cnt = 1
fout = open(f'./data/{cnt}.txt', 'w', encoding='utf-8')
with open('./红楼梦.txt', 'r', encoding='utf-8') as f:
for line in f.readlines():
if title_pattern.match(line):
fout.close()
fout = open(f'./data/{cnt}.txt', 'w', encoding='utf-8')
cnt += 1
line = line.strip()
print(line)
if len(line):
print(line, file=fout)
step2:面向对象功能扩展:查词典
我们在之前的文本分析器中已经实现了人物词频统计的功能。
出于之前的用程序设计解决问题的考虑,这里要让机器自动地提取出对应人物的频率。
似乎可以从保存的文件中读出来的,但这样产生了多余的I/O操作;我们要想办法直接从从对象中取出对应的人物出现频率。但问题在于,对象具有对外的低耦合特征,这也是面向对象编程所保证的,所以我们要给原有的对象类添加新的接口。
于是我们做了如下的查字典接口
def look_up(self, lookup_table=[]):
return [self.word_dict.get(x, 0) for x in lookup_table]
只要输入一个待查人物的词频,即可获得了对应的词频结果
将之前分好章节的文本依次读入进行查找,代码如下:
from analyser import TextAnalyser
lut = ['贾宝玉','贾母','林黛玉','王熙凤','王夫人','薛宝钗']
data = []
for i in range(1, 121):
analyser = TextAnalyser(f'./data/{i}.txt')
analyser.start(show=False)
# analyser.get_result(f'./out/{i}.csv',num=100,show=False)
data.append(analyser.look_up(lut))
step3:pyecharts绘图
对于PyEcharts来说,通常只是一个粘贴代码的工作。
这一节的可视化可以作为一个多折线图的模板。
import pyecharts.options as opts
from pyecharts.charts import Line
from pyecharts.globals import ThemeType
xdata = [f'第{x}回' for x in range(1, 121)]
(
Line(init_opts=opts.InitOpts(width='1700px',
height='600px',
page_title="红楼梦人物统计",
theme=ThemeType.SHINE))
.set_global_opts(
tooltip_opts=opts.TooltipOpts(is_show=True),
xaxis_opts=opts.AxisOpts(type_="category"),
yaxis_opts=opts.AxisOpts(
type_="value",
axistick_opts=opts.AxisTickOpts(is_show=True),
splitline_opts=opts.SplitLineOpts(is_show=True),
),
toolbox_opts=opts.ToolboxOpts(is_show=True),
title_opts=opts.TitleOpts(title="红楼梦人物统计",subtitle="分章节"),
datazoom_opts=opts.DataZoomOpts()
)
.add_xaxis(xaxis_data=xdata)
.add_yaxis(
series_name="贾宝玉",
y_axis=[x[0] for x in data],
symbol="emptyCircle",
is_symbol_show=True,
is_smooth=True,
label_opts=opts.LabelOpts(is_show=False),
)
.add_yaxis(
series_name="贾母",
y_axis=[x[1] for x in data],
symbol="emptyCircle",
is_symbol_show=True,
is_smooth=True,
label_opts=opts.LabelOpts(is_show=False),
)
.add_yaxis(
series_name="林黛玉",
y_axis=[x[2] for x in data],
symbol="emptyCircle",
is_symbol_show=True,
is_smooth=True,
label_opts=opts.LabelOpts(is_show=False),
)
.add_yaxis(
series_name="王熙凤",
y_axis=[x[3] for x in data],
symbol="emptyCircle",
is_symbol_show=True,
is_smooth=True,
label_opts=opts.LabelOpts(is_show=False),
)
.add_yaxis(
series_name="王夫人",
y_axis=[x[4] for x in data],
symbol="emptyCircle",
is_symbol_show=True,
is_smooth=True,
label_opts=opts.LabelOpts(is_show=False),
)
.add_yaxis(
series_name="薛宝钗",
y_axis=[x[5] for x in data],
symbol="emptyCircle",
is_symbol_show=True,
is_smooth=True,
label_opts=opts.LabelOpts(is_show=False),
itemstyle_opts=opts.ItemStyleOpts(color='purple')
)
.render("chapter.html")
)
人物共现分析
分段进行人物枚举,统计一个文本内两个人物(节点)共同出现的次数。
总的思想是,分段切分,在每一段中进行人物查找,然后将该段内的所有人物节点建立一次联系。最终统计两两节点之间发生的连接数。越多关系越紧密。
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@file:find_cooccur.py
@author: Honour-Van: fhn037@126.com
@date:2021/04/29 13:58:49
@description: 练习题:统计《红楼梦》中的人物共现情况
@version: 1.0: 改换文本内容进行测试
1.1: 根据上一个版本中得到的结果,进行分析并添加同义词和忽略词。
'''
import jieba
import jieba.posseg as pseg
import json
##--- 第0步:准备工作,重要变量的声明
# 输入文件
txt_file_name = './assets/红楼梦.txt'
# 输出文件
node_file_name = './out/红楼梦-人物节点.csv'
link_file_name = './out/红楼梦-人物连接.csv'
# 运行时,经常出现目标文件被打开,导致写文件失败
# 可以提前测试打开,这样可以很大程度避免问题,但更好的方式是用异常处理机制
test = open(node_file_name, 'w')
test.close()
test = open(link_file_name, 'w')
test.close()
# 打开文件,读入文字
txt_file = open(txt_file_name, 'r', encoding='utf-8')
line_list = txt_file.readlines()
txt_file.close()
#print(line_list) # 测试点
# 加载用户字典
jieba.load_userdict('./assets/userdict.txt')
##--- 第1步:生成基础数据(一个列表,一个字典)
line_name_list = [] # 每个段落出现的人物列表
name_cnt_dict = {} # 统计人物出现次数
# !!设置忽略词的列表和同义词的字典
ignore_list = []
ignore_config = "assets/ignore_list.txt"
with open(ignore_config, 'r', encoding='utf-8') as f:
ignore_list = f.readline().strip()
syno_dict = {}
syno_config = 'assets/syno_dict.json'
with open(syno_config, 'r', encoding='utf-8') as f:
syno_dict = json.load(f)
# print(ignore_list)
# print(syno_dict)
print('正在分段统计……')
print('已处理词数:')
progress = 0 # 用于计算进度条
for line in line_list: # 逐个段落循环处理
word_gen = pseg.cut(line) # peseg.cut返回分词结果,“生成器”类型
line_name_list.append([])
for one in word_gen:
word = one.word
flag = one.flag
if len(word) == 1: # 跳过单字词
continue
if word in ignore_list: # 跳过标记忽略的人名
continue
# 对指代同一人物的名词进行合并
if word in syno_dict:
word = syno_dict[word]
if flag == 'nr':
line_name_list[-1].append(word)
if word in name_cnt_dict.keys():
name_cnt_dict[word] = name_cnt_dict[word] + 1
else:
name_cnt_dict[word] = 1
# 因为词性分析耗时很长,所以需要打印进度条,以免用户误以为死机了
progress = progress + 1
progress_quo = int(progress/1000)
progress_mod = progress % 1000 # 取模,即做除法得到的余数
if progress_mod == 0: # 每逢整千的数,打印一次进度
#print('---已处理词数(千):' + str(progress_quo))
print('\r' + '-'*(progress_quo//10) + '> '\
+ str(progress_quo) + 'k', end='')
# 循环结束点
print()
print('基础数据处理完成')
#print(line_name_list) # 测试点
#print('-'*20)
#print(name_cnt_dict) # 测试点
##--- 第2步:用字典统计人名“共现”数量(relation_dict)
relation_dict = {}
# 只统计出现次数达到限制数的人名
name_cnt_limit = 100
for line_name in line_name_list:
for name1 in line_name:
# 判断该人物name1是否在字典中
if name1 in relation_dict.keys():
pass # 如果已经在字典中,继续后面的统计工作
elif name_cnt_dict[name1] >= name_cnt_limit: # 只统计出现较多的人物
relation_dict[name1] = {} # 添加到字典
#print('add ' + name1) # 测试点
else: # 跳过出现次数较少的人物
continue
# 统计name1与本段的所有人名(除了name1自身)的共现数量
for name2 in line_name:
if name2 == name1 or name_cnt_dict[name2] < name_cnt_limit:
# 不统计name1自身;不统计出现较少的人物
continue
if name2 in relation_dict[name1].keys():
relation_dict[name1][name2] = relation_dict[name1][name2] + 1
else:
relation_dict[name1][name2] = 1
print('共现统计完成,仅统计出现次数达到' + str(name_cnt_limit) + '及以上的人物')
##--- 第3步:输出统计结果
#for k,v in relation_dict.items(): # 测试点
# print(k, ':', v)
# 字典转成列表,按出现次数排序
item_list = list(name_cnt_dict.items())
item_list.sort(key=lambda x:x[1],reverse=True)
## 导出节点文件
node_file = open(node_file_name, 'w')
# 节点文件,格式:Name,Weight -> 人名,出现次数
node_file.write('Name,Weight\n')
node_cnt = 0 # 累计写入文件的节点数量
for name,cnt in item_list:
if cnt >= name_cnt_limit: # 只输出出现较多的人物
node_file.write(name + ',' + str(cnt) + '\n')
node_cnt = node_cnt + 1
node_file.close()
print('人物数量:' + str(node_cnt))
print('已写入文件:' + node_file_name)
## 导出连接文件
# 共现数可以看做是连接的权重,只导出权重达到限制数的连接
link_cnt_limit = 10
print('只导出数量达到' + str(link_cnt_limit) + '及以上的连接')
link_file = open(link_file_name, 'w')
# 连接文件,格式:Source,Target,Weight -> 人名1,人名2,共现数量
link_file.write('Source,Target,Weight\n')
link_cnt = 0 # 累计写入文件的连接数量
for name1,link_dict in relation_dict.items():
for name2,link in link_dict.items():
if link >= link_cnt_limit: # 只输出权重较大的连接
link_file.write(name1 + ',' + name2 + ',' + str(link) + '\n')
link_cnt = link_cnt + 1
link_file.close()
print('连接数量:' + str(link_cnt))
print('已写入文件:' + link_file_name)
关系图绘制如下:
# -*- encoding: utf-8 -*-
'''
@file:draw_relationship.py
@author: Honour-Van: fhn037@126.com
@date:2021/04/29 15:36:02
@description: 用第三方库pyecharts绘制关系图(Graph),带分类节点
使用前面程序生成的红楼梦人物关系数据
对先前得到的数据关系数据进行手工标注,为人物节点添加上家族信息
@version:1.0
'''
from pyecharts import options as opts
from pyecharts.charts import Graph
from pyecharts.globals import ThemeType
from math import exp, sqrt
# --- 第0步:准备工作
# 输入文件
node_file_name = './assets/红楼梦-人物节点-分类.csv' # 手工增加国别分类
link_file_name = './out/红楼梦-人物连接.csv'
# 输出文件
out_file_name = './out/关系图-分类-红楼人物.html'
# --- 第1步:从文件读入节点和连接信息
node_file = open(node_file_name, 'r')
node_line_list = node_file.readlines()
node_file.close()
del node_line_list[0] # 删除标题行
link_file = open(link_file_name, 'r')
link_line_list = link_file.readlines()
link_file.close()
del link_line_list[0] # 删除标题行
# --- 第2步:解析读入的信息,存入列表
# 类别列表,用于给节点分成不同系列,会自动用不同颜色表示
categories = [{}, {'name': '贾'}, {'name': '史'}, {
'name': '王'}, {'name': '薛'}, {'name': '其他'}]
node_in_graph = []
for one_line in node_line_list:
one_line = one_line.strip('\n')
one_line_list = one_line.split(',')
# print(one_line_list) # 测试点
node_in_graph.append(opts.GraphNode(
name=one_line_list[0],
value=int(one_line_list[1]),
symbol_size=sqrt(int(one_line_list[1])/8), # 手动调整节点的尺寸
category=int(one_line_list[2]))) # 类别,例如categories[2]=='史'
# print('-'*20) # 测试点
link_in_graph = []
for one_line in link_line_list:
one_line = one_line.strip('\n')
one_line_list = one_line.split(',')
# print(one_line_list) # 测试点
link_in_graph.append(opts.GraphLink(
source=one_line_list[0],
target=one_line_list[1],
value=int(one_line_list[2])))
# --- 第3步:画图
c = Graph(init_opts=opts.InitOpts(
theme=ThemeType.CHALK,
page_title='红楼梦人物关系'
))
c.add("",
node_in_graph,
link_in_graph,
edge_length=[30, 70],
repulsion=5000,
categories=categories,
gravity=0.7,
linestyle_opts=opts.LineStyleOpts(curve=0.2), # 增加连线弧度
layout="force", # "force"-力引导布局,"circular"-环形布局
)
c.set_global_opts(title_opts=opts.TitleOpts(
title="红楼梦人物关系",
subtitle="按四大家族分类"))
c.render(out_file_name)
try:
import os
os.system("explorer .\out\关系图-分类-红楼人物.html")
except:
pass
总结
一个思想:面向对象具有良好的扩展性
几个要点:
- 正则表达式
- 共现分析方法
- Pyecharts的简单绘制