“滚滚长江东逝水,浪花淘尽英雄”。近来读《三国演义》,忽然想看看到底哪位英雄在书中提到的最多,于是就想用分词算法实现一下。
网上也确实有相关的案例,作为参考,自己又重写并优化了一遍。

思路

  • 下载《三国演义》txt文档
  • 使用jieba分词算法对文档进行分词处理
  • 将分词结果踢除停用词、标点符合、非人名等
  • 词频统计、并排序
  • 可视化展示

问题

按照上面的思路进行简单实施时,查看结果会发现几个问题

  • 名字
  • 三国人物有名、字、号等,还有其他的一些别称,如“相父”“曹阿瞒“刘皇叔”,要想办法统一成一个人
  • 词性
  • 比如“曰”、“大胜”等非人名的词不是我们需要统计的
  • 分词
  • 一些如“孔明曰”、“玄德问”、“操大怒”之类的词没有被分割开
  • 干扰词
  • 分词后会出现像“诸公”、“齐声”、“班师回”等明显,但是算法无法判断的非人名词语,干扰排序结果

所以对这些情况要进行特殊处理。

三国演义人物出场统计Python python三国演义人物 统计分析_词云

优化

  • 针对名、字、号的问题进行枚举判断。网上的案例中大多采用如 w.word == '玄德' 这种等号判断,但对于 “玄德遂”、“操乃”等分词不准确情况枚举不足。比如我们在文本里统计“操”字有2800多个,基本都是指代曹操,而用阿瞒、孟德、曹丞相等枚举等号判断时,最终统计的“曹操”词频只有1000多个,所以我采用的是包含判断,即 '操' in w.word'孟德' in w.word
  • 词性踢除。通过w.flag将nr(人名)外的其他词性踢除掉,这一步很多案例中是放在人物名字号的判断前,但是“玄德”会被当成x(未知词性),这样就会少算很多姓名,所以我放在了人物名字判断后,再踢除无关词性。
  • 对于词性为nr但是非人名的词,则需要在停用词的基础上,增加一张踢除干扰词的自定义文件

结果

经过以上处理后,基本排除了干扰,最后就是激动人心的时刻,那么谁才是罗贯中心中的南波万呢?
当当当当,那就是这位“可爱的奸雄”——曹操。

曹操:3014
刘备:2689
诸葛亮:2103
关羽:1047
吕布:868
周瑜:660
司马懿:643
袁绍:544
张飞:505
鲁肃:465
赵子龙:433
马超:358
孙权:351
董卓:342
魏延:322
姜维:302
黄忠:190
刘表:156
庞德:122
张辽:117

三国演义人物出场统计Python python三国演义人物 统计分析_自然语言处理_02

再来一张拉风的三国词云

三国演义人物出场统计Python python三国演义人物 统计分析_python_03


说明:该处理结果只对排名靠前的刘备、诸葛亮、曹操、关羽、张飞、吕布、司马懿等人物的分词结果进行了优化(见代码),其他诸如魏延、庞统、庞德等不影响第一排名的人物,名、字、号问题未进行详细处理。

附代码

import jieba.posseg as pseg
import jieba
import re
import matplotlib.pyplot as plt
import codecs

# 词云
import wordcloud
import imageio



# 定义主要人物的个数
keshihuaTop=10  # 可视化人物图人数
mainTop = 100  # 人物词云图人物数
peopleTop=10  # 人物关系图

# 获取图书数据
def get_book(file_path):
    fn = open(file_path,encoding='utf-8')
    stringdata =  fn.read()
    fn.close()
    return stringdata

# 文本处理
def bookdata_process(bookdata):
    pattern = re.compile(u'\t|\n|\.|-|:|;|\)|\(|\?|"')
    book_str = re.sub(pattern,'',bookdata)
    print('文本预处理完成')
    return book_str

# 获取停用词
def stop_word_list(file_path):
    stopwords = [line.strip() for line in open(file_path,'r',encoding='utf-8').readlines()]
    return stopwords

# 获取词频
count = {}
# 获取词频
def getWordTime(txt,stopWordList,wordFlagList):
    jieba.load_userdict("prepare/userword.txt")
    cutfinal = pseg.cut(txt)

    for w in cutfinal:
        if w.word == None or w.word in stopWordList:
            continue
        elif '刘玄德' in w.word or '玄德' in w.word or '刘豫州' in w.word or'备' in w.word or '大耳贼' in w.word \
                or '先主' in w.word or '刘皇叔' in w.word or '皇叔' in w.word or '大耳' in w.word or '玄德公' in w.word \
                or '汉中王'in w.word or '刘备' in w.word:
            real_word = '刘备'
        elif '孔明' in w.word or '卧龙' in w.word or '卧龙先生' in w.word or'亮' in w.word or '武侯' in w.word \
                or '武乡侯' in w.word or '诸葛丞相'in w.word or '相父' in w.word or '诸葛孔明' in w.word or '诸葛亮' in w.word:
            real_word = '诸葛亮'
        elif '曹孟德' in w.word or '曹公' in w.word or '曹贼' in w.word or'操' in w.word or '曹丞相'  in w.word \
                or '曹操'in w.word or '魏公'in w.word or '魏王' in w.word or '阿瞒'in w.word or '曹阿瞒' in w.word \
                or '孟德' in w.word or '操军' in w.word:
            real_word = '曹操'
        elif '关云长' in w.word or '云长' in w.word or '关二爷' in w.word or'关公' in w.word or '关将军' in w.word \
                or '美髯公'in w.word or '汉寿亭侯' in w.word or '关云' in w.word or '关某' in w.word:
            real_word = '关羽'
        elif '赵云' in w.word or '子龙' in w.word or '常山' in w.word or '赵将军' in w.word:
            real_word = '赵子龙'
        elif '张翼德' in w.word or '三弟' in w.word or '翼德' in w.word or '张翼' in w.word:
            real_word = '张飞'
        elif '吕奉先' in w.word or '奉先' in w.word or '布' in w.word or '吕将军' in w.word or '三姓家奴' in w.word:
            real_word = '吕布'
        elif '卓' in w.word or '仲颖' in w.word or '董老贼' in w.word:
            real_word = '董卓'
        elif '瑜' in w.word or '公瑾' in w.word or '周郎' in w.word:
            real_word = '周瑜'
        elif '仲达' in w.word or '司马仲达' in w.word or '懿' in w.word:
            real_word = '司马懿'
        elif '刘景升' in w.word or '景升' in w.word:
            real_word = '刘表'
        elif '超' in w.word or '孟起' in w.word or '马孟起' in w.word:
            real_word = '马超'
        elif '阿斗' in w.word:
            real_word = '刘禅'
        elif '仲谋' in w.word or '吴王' in w.word or '吴主孙权' in w.word:
            real_word = '孙权'
        elif '袁本初' in w.word or '本初' in w.word or '绍' in w.word:
            real_word = '袁绍'
        elif '肃' in w.word or '子敬' in w.word or '子敬' in w.word:
            real_word = '鲁肃'
        elif '伯约' in w.word:
            real_word = '姜维'
        elif '瓒' in w.word:
            real_word = '公孙瓒'
        elif (len(wordFlagList)>0 and w.flag not in wordFlagList):
            # print(w.word + w.flag)
            continue
        else:
            real_word = w.word
        count[real_word] = count.get(real_word,0) + 1


# 分词生成人物词频(写入文档)
def writeWordResult(items,sinkPath,topN):
    with codecs.open(sinkPath, "w", "utf-8") as f:
        if len(items) < topN:
            topN = len(items)
        for i in range(topN):
            word, count = items[i]
            f.write("{}:{}\n".format(word, count))

# 生成词云
def creat_wordcloud(excludes):
    bg_pic = imageio.imread('prepare/sanguo.jpg')
    wc = wordcloud.WordCloud(font_path='prepare/simhei.ttf',
                             background_color='white',
                             width=1000, height=800,
                             stopwords=excludes,# 设置停用词
                             max_words=500,
                             mask=bg_pic  # mask参数设置词云形状
                             )
    # 从单词和频率创建词云
    wc.generate_from_frequencies(count)
    # 保存图片
    wc.to_file('output/三国演义词云_人名.png')

    #  显示词云图片
    plt.imshow(wc)
    plt.axis('off')
    plt.show()


if __name__ == '__main__':

    excludes = stop_word_list("prepare/exclude.txt")
    stopwordlist = stop_word_list("prepare/tingyong.txt") + excludes
    bookdata = get_book("prepare/sgyy.txt")
    txt = bookdata_process(bookdata)
    # 设置词性--nr:人名
    # 玄德会被识别为未知词性(x)
    # 亮、操会被识别为动词(v)
    # 云会被识别为地名(ns)
    wordFlagList = {'nr'}
    getWordTime(txt,stopwordlist,wordFlagList)
    items = list(count.items())
    # 进行降序排列 根据词语出现的次数进行从大到小排序
    items.sort(key=lambda x: x[1], reverse=True)
    sink_path = "output/sanguo_word_count.txt"
    writeWordResult(items,sink_path,300)
    creat_wordcloud(excludes)