【Python Onramp】4. Python文本分析(2)分章节统计、人物共现和pyecharts可视化

系列文章目录

【Python Onramp】 0. 卷首语

上一篇:【Python Onramp】3. Python的文本分析(1)jieba分词:第三方库和基本面向对象编程
下一篇:【Python Onramp】5.利用Pyecharts进行可视化:综合应用

项目描述

文本是程序之间的重要介质,文本处理也是编程的重要问题。我们在上一节的基础上,对红楼梦做更多的研究。
github仓库见Honour-Van:CS50 TextAnalyserTextAnalyser_co

这里我们仍以红楼梦作为研究样例,对文本进行词频统计分析: 项目中你将:

  1. 分章节统计主要人物的词频变化
  2. 统计人物共现关系
  3. 绘制简单的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

总结

一个思想:面向对象具有良好的扩展性
几个要点:

  1. 正则表达式
  2. 共现分析方法
  3. Pyecharts的简单绘制
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值