咱们好,我是小小明。 本文将带你用python看小说,文娱学习两不误。本文涉及的知识点有:

  1. 常规小说网站的爬取思路
  2. 根本的pandas数据整理
  3. lxml与xpath应用技巧
  4. 正则模式匹配
  5. Counter词频计算
  6. pyecharts数据可视化
  7. stylecloud词云图
  8. gensim.models.Word2Vec的运用
  9. scipy.cluster.hierarchy层次聚类

本文从传统匹配逻辑剖析过渡到机器学习的词向量,全方位进行文本剖析,值得学习,干货满满。

[toc]

金庸小说的收集

曾经金庸小说的网站有许多,但大部分现已无法拜访,但由于许多金庸迷的存在,新站也是源源不断呈现。我近期经过百度找到的一个还能够拜访的金庸小说网址是:aHR0cDovL2ppbnlvbmcxMjMuY29tLw==

Python探索金庸小说世界
image-20220807170748462

不过我现已准备好现已收集完结的数据,咱们能够直接下载数据,越过本章的内容。

数据源下载地址:gitcode.net/as604049322…

每部小说的创造日期

下面首要获取这15部作品的称号、创造年份和对应的链接。从开发者工具能够看到每行的a标签许多,咱们需要的节点的特征在于后续接近节点紧接着一个创造日期的字符串:

Python探索金庸小说世界
image-20220805095954183

那么咱们就能够经过遍历一切的a标签并判别其后续一个接近节点的内容是否契合日期格局,最终完好下载代码为:

import requests
from lxml import etree
import pandas as pd
import re
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
    "Accept-Language": "zh-CN,zh;q=0.9"
}
res = requests.get(base_url, headers=headers)
res.encoding = res.apparent_encoding
html = etree.HTML(res.text)
a_tags = html.xpath("//div[@class='jianjie']/p/a")
data = []
for a_tag in a_tags:
    m_obj = re.search("\((\d{4}(?:—\d{4})?年)\)", a_tag.tail)
    if m_obj:
        data.append((a_tag.text, m_obj.group(1), a_tag.attrib["href"]))
data = pd.DataFrame(data, columns=["称号", "创造时间", "网址"])

能够依照创造日期排序检查:

data.sort_values("创造时间", ignore_index=True, inplace=True)
data
称号 创造时间 网址
书剑恩仇录 1955年 /shujianenchoulu/
碧血剑 1956年 /bixuejian/
射雕英雄传 1957—1959年 /shediaoyingxiongzhuan/
神雕侠侣 1959—1961年 /shendiaoxialv/
雪山飞狐 1959年 /xueshanfeihu/
飞狐别传 1960—1961年 /feihuwaizhuan/
白马啸西风 1961年 /baimaxiaoxifeng/
倚天屠龙记 1961年 /yitiantulongji/
鸳鸯刀 1961年 /yuanyangdao/
天龙八部 1963—1966年 /tianlongbabu/
连城诀 1963年 /lianchengjue/
侠客行 1965年 /xiakexing/
笑傲江湖 1967年 /xiaoaojianghu/
鹿鼎记 1969—1972年 /ludingji/
越女剑 1970年 /yuenvjian/

章节页下载与次序校对

下面看看章节页节点的分布状况,以《雪山飞狐》为例:

Python探索金庸小说世界
image-20220807171622712

一起能够看到部分小说的节点呈现了倒序的状况,咱们需要在识别出倒序时将其正序,完好代码:

from urllib.parse import urljoin
def getTitleAndUrl(url):
    url = urljoin(base_url, url)
    data = []
    res = requests.get(url, headers=headers)
    res.encoding = res.apparent_encoding
    html = etree.HTML(res.text)
    reverse, last_num = False, None
    for i, a_tag in enumerate(html.xpath("//dl[@class='cat_box']/dd/a")):
        data.append([re.sub("\s+", " ", a_tag.text), a_tag.attrib["href"]])
        nums = re.findall("第(\d+)章", a_tag.text)
        if nums:
            if last_num and int(nums[0]) < last_num:
                reverse = True
            last_num = int(nums[0])
    # 次序校对并删去跋文之后的内容
    if reverse:
        data.reverse()
    return data

测验一下:

title2url = getTitleAndUrl(data.query("称号=='雪山飞狐'").网址.iat[0])
title2url
[['第01章', '/xueshanfeihu/488.html'],
 ['第02章', '/xueshanfeihu/489.html'],
 ['第03章', '/xueshanfeihu/490.html'],
 ['第04章', '/xueshanfeihu/491.html'],
 ['第05章', '/xueshanfeihu/492.html'],
 ['第06章', '/xueshanfeihu/493.html'],
 ['第07章', '/xueshanfeihu/494.html'],
 ['第08章', '/xueshanfeihu/495.html'],
 ['第09章', '/xueshanfeihu/496.html'],
 ['第10章', '/xueshanfeihu/497.html'],
 ['跋文', '/xueshanfeihu/498.html']]

能够看到章节现已顺利的正序排列。

每部小说的下载

小说每一章的详细页最终一行的数据咱们不需要:

Python探索金庸小说世界
image-20220807171724027

下载每章内容的代码:

def download_page_content(url):
    res = requests.get(url, headers=headers)
    res.encoding = res.apparent_encoding
    html = etree.HTML(res.text)
    content = "\n".join(html.xpath("//div[@class='entry']/p/text()")[:-1])
    return content

然后咱们就能够批量下载全部小说了:

import os
def download_one_novel(filename, url):
    "下载单部小说"
    title2url = getTitleAndUrl(url)
    print("创建文件:", filename)
    for title, url in title2url:
        with open(filename, "a", encoding="u8") as f:
            f.write(title)
            f.write("\n\n")
            print("下载:", title)
            content = download_page_content(url)
            f.write(content)
            f.write("\n\n")
os.makedirs("novels", exist_ok=True)
for row in data.itertuples():
    filename = f"novels/{row.称号}.txt"
    os.remove(filename)
    download_one_novel(filename, row.网址)

人物、武功和门派数据整理

为了更好剖析金庸小说,咱们还需要收集金庸小说的人物、武功和门派,个人并没有找到还能够拜访相关数据的网站,所以自行收集整理了相关数据:

Python探索金庸小说世界
image-20220805170001440

相关数据都以如下格局存储,例如金庸小说的人物:

小说1
人物1 人物2 ……
小说2
人物1 人物2 ……
小说3
人物1 人物2 ……

武功:

小说1
武功1 武功2 ……
小说2
武功1 武功2 ……
小说3
武功1 武功2 ……

数据源下载地址:gitcode.net/as604049322…

高频剖析

界说一个加载小说的办法:

def load_novel(novel):
    with open(f'novels/{novel}.txt', encoding="u8") as f:
        return f.read()

主角剖析

首要咱们加载人物数据:

with open('data/names.txt',encoding="utf-8") as f:
    data = [line.rstrip() for line in f]
novels = data[::2]
names = data[1::2]
novel_names = {k: v.split() for k, v in zip(novels, names)}
del novels, names, data

能够预览一下天龙八部中的人物:

print(",".join(novel_names['天龙八部'][:20]))
刀白凤,丁春秋,马夫人,马五德,小翠,于光豪,巴天石,不平道人,邓百川,风波恶,甘宝宝,公冶乾,木婉清,包不同,天狼子,太皇太后,王语嫣,乌老大,无崖子,云岛主

下面咱们寻找一下每部小说的主角,计算每个人物的进场次数,显然次数越多主角光环越强,下面咱们看看每部小说,呈现次数最多的前十个人物:

from collections import Counter
def find_main_charecters(novel, num=10, content=None):
    if content is None:
        content = load_novel(novel)
    count = Counter()
    for name in novel_names[novel]:
        count[name] = content.count(name)
    return count.most_common(num)
for novel in novel_names:
    print(novel, dict(find_main_charecters(novel, 10)))
书剑恩仇录 {'陈家洛': 2095, '张召重': 760, '徐天宏': 685, '霍青桐': 650, '余鱼同': 605, '文泰来': 601, '骆冰': 594, '周绮': 556, '李沅芷': 521, '陆菲青': 486}
碧血剑 {'袁承志': 3028, '何铁手': 306, '温青': 254, '阿九': 215, '洪胜海': 200, '焦宛儿': 197, '皇太极': 183, '崔秋山': 180, '穆人清': 171, '闵子华': 163}
射雕英雄传 {'郭靖': 5009, '黄蓉': 3650, '洪七公': 1041, '黄药师': 868, '周伯通': 654, '欧阳克': 611, '丘处机': 606, '梅超风': 480, '杨康': 439, '柯镇恶': 431}
神雕侠侣 {'杨过': 5991, '小龙女': 2133, '郭靖': 1431, '黄蓉': 1428, '李莫愁': 1016, '郭芙': 850, '郭襄': 778, '陆无双': 575, '周伯通': 555, '赵志敬': 482}
雪山飞狐 {'胡斐': 230, '曹云奇': 228, '宝树': 225, '苗若兰': 217, '胡一刀': 207, '苗人凤': 129, '刘元鹤': 107, '陶子安': 107, '田青文': 103, '范帮主': 83}
飞狐别传 {'胡斐': 2761, '程灵素': 765, '袁紫衣': 425, '苗人凤': 405, '马春花': 331, '福康安': 287, '赵半山': 287, '田归农': 227, '徐铮': 217, '商宝震': 217}
白马啸西风 {'李文秀': 441, '苏普': 270, '阿曼': 164, '苏鲁克': 147, '陈达海': 106, '车尔库': 99, '李三': 31, '丁同': 29, '霍元龙': 23, '桑斯': 22}
倚天屠龙记 {'张无忌': 4665, '赵敏': 1250, '谢逊': 1211, '张翠山': 1146, '周芷若': 825, '殷素素': 550, '杨逍': 514, '张三丰': 451, '灭绝师太': 431, '小昭': 346}
鸳鸯刀 {'萧中慧': 103, '袁冠南': 82, '卓天雄': 76, '周威信': 74, '林玉龙': 52, '任飞燕': 51, '萧半和': 48, '盖一鸣': 45, '逍遥子': 28, '常长风': 19}
天龙八部 {'段誉': 3372, '萧峰': 1786, '虚竹': 1636, '阿紫': 1150, '乔峰': 1131, '阿朱': 986, '慕容复': 925, '王语嫣': 859, '段正淳': 757, '木婉清': 734}
连城诀 {'狄云': 1433, '水笙': 439, '戚芳': 390, '丁典': 364, '万震山': 332, '万圭': 288, '花铁干': 256, '吴坎': 155, '血刀老祖': 144, '戚长发': 117}
侠客行 {'石破天': 1804, '石清': 611, '丁珰': 446, '白万剑': 446, '丁不四': 343, '谢烟客': 337, '闵柔': 327, '贝海石': 257, '丁不三': 217, '白自在': 199}
笑傲江湖 {'令狐冲': 5838, '岳不群': 1184, '林平之': 926, '岳灵珊': 919, '仪琳': 729, '田伯光': 708, '任我行': 525, '向问天': 513, '左冷禅': 473, '方证': 415}
鹿鼎记 {'韦小宝': 9731, '吴三桂': 949, '双儿': 691, '鳌拜': 479, '陈近南': 472, '方怡': 422, '茅十八': 400, '小桂子': 355, '施琅': 296, '吴应熊': 290}
越女剑 {'范蠡': 121, '阿青': 64, '勾践': 47, '薛烛': 29, '西施': 26, '文种': 23, '风胡子': 7}

上述成果用文本展现了每部小说的前5个主角,但是不行直观,下面我用pyecharts的树图展现一下:

from pyecharts import options as opts
from pyecharts.charts import Tree
data = []
for novel in novel_names:
    tmp = []
    data.append({"name": novel, "children": tmp})
    for name, count in find_main_charecters(novel, 5):
        tmp.append({"name": name, "value": count})
c = (
    TreeMap()
    .add("", data, levels=[
        opts.TreeMapLevelsOpts(),
        opts.TreeMapLevelsOpts(
            color_saturation=[0.3, 0.6],
            treemap_itemstyle_opts=opts.TreeMapItemStyleOpts(
                border_color_saturation=0.7, gap_width=5, border_width=10
            ),
            upper_label_opts=opts.LabelOpts(
                is_show=True, position='insideTopLeft', vertical_align='top'
            )
        ),
    ])
    .set_global_opts(title_opts=opts.TitleOpts(title="金庸小说主角"))
)
c.render_notebook()
Python探索金庸小说世界
image-20220805203818262

显然,《神雕侠侣》中的杨过和小龙女,《天龙八部》中的萧(乔)峰,段誉,虚竹,《射雕英雄传》的郭靖和黄蓉,《倚天屠龙记》的张无忌和赵敏 都是主角光环最强的人物。

武功剖析

运用上述相同的办法,剖析各种武功的呈现频次,首要加载武功数据:

with open('data/kungfu.txt', encoding="utf-8") as f:
    data = [line.rstrip() for line in f]
novels = data[::2]
kungfus = data[1::2]
novel_kungfus = {k: v.split() for k, v in zip(novels, kungfus)}
del novels, kungfus, data

界说计数办法:

def find_main_kungfus(novel, num=10, content=None):
    if content is None:
        content = load_novel(novel)
    count = Counter()
    for name in novel_kungfus[novel]:
        count[name] = content.count(name)
    return count.most_common(num)
for novel in novel_kungfus:
    print(novel, dict(find_main_kungfus(novel, 10)))
书剑恩仇录 {'芙蓉金针': 16, '柔云剑术': 15, '百花错拳': 13, '追魂夺命剑': 12, '三分剑术': 12, '八卦刀': 10, '铁琵琶手': 9, '无极玄功拳': 9, '甩手箭': 7, '黑沙掌': 7}
侠客行 {'雪山剑法': 46, '金乌刀法': 33, '碧针清掌': 8, '五行六合掌': 8, '梅花拳': 8, '罗汉伏魔神功': 3, '无妄神功': 1, '神倒鬼跌三连环': 1, '上清快剑': 1, '黑煞掌': 1}
倚天屠龙记 {'七伤拳': 98, '乾坤大移动': 93, '九阳真经': 46, '玄冥神掌': 43, '龙爪手': 24, '金刚伏魔圈': 21, '千蛛万毒手': 18, '幻阴指': 17, '寒冰绵掌': 16, '真武七截阵': 10}
天龙八部 {'六脉神剑': 148, '存亡符': 124, '凌波微步': 77, '化功大法': 52, '北冥神功': 36, '般若掌': 36, '火焰刀': 34, '小无相功': 28, '天山六阳掌': 25, '大金刚拳': 24}
射雕英雄传 {'九阴真经': 191, '铁掌': 169, '降龙十八掌': 92, '打狗棒法': 47, '蛤蟆功': 39, '空明拳': 25, '一阳指': 22, '先天功': 14, '双手互搏': 13, '杨家枪法': 13}
碧血剑 {'伏虎掌': 30, '混元功': 23, '两仪剑法': 21, '神行百变': 18, '蝎尾鞭': 12, '破玉拳': 9, '金蛇剑法': 5, '软红蛛索': 4, '混元掌': 4, '斩蛟拳': 4}
神雕侠侣 {'玉女素心剑法': 25, '黯然销魂掌': 19, '五毒神掌': 18, '龙象般若功': 12, '玉箫剑法': 10, '七星集会': 8, '美女拳法': 8, '天罗地网势': 7, '上天梯': 5, '三无三不手': 4}
笑傲江湖 {'辟邪剑法': 160, '独孤九剑': 80, '吸星大法': 67, '紫霞神功': 36, '易筋经': 33, '嵩山剑法': 33, '华山剑法': 30, '玉女剑十九式': 20, '恒山剑法': 20, '无双无对,宁氏一剑': 13}
连城诀 {'连城剑法': 29, '神照经': 23, '六合拳': 4}
雪山飞狐 {'胡家刀法': 8, '苗家剑法': 7, '龙爪捉拿手': 5, '追命毒龙锥': 2, '大捉拿手': 2, '飞天神行': 1}
飞狐别传 {'西岳华拳': 26, '八极拳': 20, '八仙剑法': 8, '四象步': 6, '燕青拳': 5, '赤尻连拳': 5, '一路华拳': 4, '金刚拳': 3, '毒砂掌': 3, '四门刀法': 2}
鸳鸯刀 {'夫妻刀法': 17, '呼延十八鞭': 6, '震天三十掌': 1}
鹿鼎记 {'化骨绵掌': 24, '拈花捉拿手': 12, '大慈大悲千叶手': 11, '沐家拳': 11, '八卦游龙掌': 10, '少林长拳': 7, '金刚护体神功': 5, '波罗蜜手': 4, '散花掌': 3, '金刚神掌': 2}

每部小说频次前5的武功可视化:

from pyecharts import options as opts
from pyecharts.charts import Tree
data = []
for novel in novel_kungfus:
    tmp = []
    data.append({"name": novel, "children": tmp})
    for name, count in find_main_kungfus(novel, 5):
        tmp.append({"name": name, "value": count})
c = (
    TreeMap()
    .add("", data, levels=[
        opts.TreeMapLevelsOpts(),
        opts.TreeMapLevelsOpts(
            color_saturation=[0.3, 0.6],
            treemap_itemstyle_opts=opts.TreeMapItemStyleOpts(
                border_color_saturation=0.7, gap_width=5, border_width=10
            ),
            upper_label_opts=opts.LabelOpts(
                is_show=True, position='insideTopLeft', vertical_align='top'
            )
        ),
    ])
    .set_global_opts(title_opts=opts.TitleOpts(title="金庸高频武功"))
)
c.render_notebook()
Python探索金庸小说世界
image-20220805203947382

门派剖析

加载数据并获取每部小说前10的门派:

with open('data/bangs.txt', encoding="utf-8") as f:
    data = [line.rstrip() for line in f]
novels = data[::2]
bangs = data[1::2]
novel_bangs = {k: v.split() for k, v in zip(novels, bangs) if k != "未知"}
del novels, bangs, data
def find_main_bangs(novel, num=10, content=None):
    if content is None:
        content = load_novel(novel)
    count = Counter()
    for name in novel_bangs[novel]:
        count[name] = content.count(name)
    return count.most_common(num)
for novel in novel_bangs:
    print(novel, dict(find_main_bangs(novel, 10)))
书剑恩仇录 {'红花会': 394, '言家拳': 7, '龙门帮': 7, '天山派': 5, '嵩阳派': 3, '南少林': 3}
侠客行 {'雪山派': 358, '长乐帮': 242, '侠客岛': 143, '金乌派': 48, '摩天崖': 38, '玄素庄': 37, '金刀寨': 25}
倚天屠龙记 {'明教': 738, '峨嵋派': 289, '天鹰教': 224, '昆仑派': 130, '龙门镖局': 85, '崆峒派': 83, '海沙派': 58, '巨鲸帮': 37, '神拳门': 20, '波斯明教': 16}
天龙八部 {'丐帮': 562, '星宿派': 203, '灵鹫宫': 157, '姑苏慕容': 150, '无量剑': 83, '逍遥派': 77, '大理段氏': 75, '青城派': 65, '蓬莱派': 23, '伏牛派': 8}
射雕英雄传 {'桃花岛': 289, '全真教': 99, '铁掌帮': 87, '仙霞派': 5}
白马啸西风 {'晋威镖局': 5}
碧血剑 {'仙都派': 51, '金龙帮': 47, '青竹帮': 45, '渤海派': 8, '永胜镖局': 6, '点苍派': 4, '飞虎寨': 4, '会友镖局': 2, '东支': 2, '千柳庄': 2}
神雕侠侣 {'绝情谷': 128, '古墓派': 87}
笑傲江湖 {'恒山派': 552, '华山派': 521, '嵩山派': 297, '五岳剑派': 281, '泰山派': 137, '衡山派': 102, '福威镖局': 102, '日月神教': 64, '武当派': 46, '金刀王家': 20}
连城诀 {'血刀门': 24}
雪山飞狐 {'平通镖局': 7, '饮马川山寨': 4, '青藏派': 2, '无极门': 2, '百会寺': 1}
飞狐别传 {'韦陀门': 49, '八卦门': 31, '天龙门': 24, '太极门': 22, '飞马镖局': 19, '五虎门': 16, '少林派': 15, '枫叶庄': 8, '镇远镖局': 2}
鸳鸯刀 {'威信镖局': 5}
鹿鼎记 {'天地会': 542, '神龙教': 161, '少林寺': 155, '清凉寺': 116, '王屋派': 38, '铁剑门': 12, '金顶门': 8, '武夷派': 3}

可视化:

from pyecharts import options as opts
from pyecharts.charts import Tree
data = []
for novel in novel_bangs:
    tmp = []
    data.append({"name": novel, "children": tmp})
    for name, count in find_main_bangs(novel, 5):
        tmp.append({"name": name, "value": count})
c = (
    TreeMap()
    .add("", data, levels=[
        opts.TreeMapLevelsOpts(),
        opts.TreeMapLevelsOpts(
            color_saturation=[0.3, 0.6],
            treemap_itemstyle_opts=opts.TreeMapItemStyleOpts(
                border_color_saturation=0.7, gap_width=5, border_width=10
            ),
            upper_label_opts=opts.LabelOpts(
                is_show=True, position='insideTopLeft', vertical_align='top'
            )
        ),
    ])
    .set_global_opts(title_opts=opts.TitleOpts(title="金庸高频门派"))
)
c.render_notebook()
Python探索金庸小说世界
image-20220805204907574

还能够测验一下树形图:

from pyecharts.charts import Tree
c = (
    Tree()
    .add("", [{"name": "门派", "children": data}], layout="radial")
)
c.render_notebook()
Python探索金庸小说世界
image-20220805205830062

归纳计算

下面咱们编写一个函数,输入一部小说名,能够输出其最高频的主角、武功和门派:

from pyecharts import options as opts
from pyecharts.charts import Bar
def show_top10(novel):
    content = load_novel(novel)
    charecters = find_main_charecters(novel, 10, content)[::-1]
    k, v = map(list, zip(*charecters))
    c = (
        Bar(init_opts=opts.InitOpts("720px", "320px"))
        .add_xaxis(k)
        .add_yaxis("", v)
        .reversal_axis()
        .set_series_opts(label_opts=opts.LabelOpts(position="right"))
        .set_global_opts(title_opts=opts.TitleOpts(title=f"{novel}主角"))
    )
    display(c.render_notebook())
    kungfus = find_main_kungfus(novel, 10, content)[::-1]
    k, v = map(list, zip(*kungfus))
    c = (
        Bar(init_opts=opts.InitOpts("720px", "320px"))
        .add_xaxis(k)
        .add_yaxis("", v)
        .reversal_axis()
        .set_series_opts(label_opts=opts.LabelOpts(position="right"))
        .set_global_opts(title_opts=opts.TitleOpts(title=f"{novel}功夫"))
    )
    display(c.render_notebook())
    bangs = find_main_bangs(novel, 10, content)[::-1]
    k, v = map(list, zip(*bangs))
    c = (
        Bar(init_opts=opts.InitOpts("720px", "320px"))
        .add_xaxis(k)
        .add_yaxis("", v)
        .reversal_axis()
        .set_series_opts(label_opts=opts.LabelOpts(position="right"))
        .set_global_opts(title_opts=opts.TitleOpts(title=f"{novel}门派"))
    )
    display(c.render_notebook())

例如检查天龙八部:

show_top10("天龙八部")
Python探索金庸小说世界
image-20220805221840724

词云图剖析

能够先添加一切的人物、武功和门派作为自界说词汇:

import jieba
for novel, names in novel_names.items():
    for name in names:
        jieba.add_word(name)
for novel, kungfus in novel_kungfus.items():
    for kungfu in kungfus:
        jieba.add_word(kungfu)
for novel, bangs in novel_bangs.items():
    for bang in bangs:
        jieba.add_word(bang)

文章整体词云检查

这儿咱们仅提取词长度不小于4的成语、俗语和短语进行剖析,以天龙八部这部小说为例:

from IPython.display import Image
import stylecloud
import jieba
import re
# 去除非中文字符
text = re.sub("[^一-龟]+", " ", load_novel("天龙八部"))
words = [word for word in jieba.cut(text) if len(word) >= 4]
stylecloud.gen_stylecloud(" ".join(words),
                          collocations=False,
                          font_path=r'C:\Windows\Fonts\msyhbd.ttc',
                          icon_name='fas fa-square',
                          output_name='tmp.png')
Image(filename='tmp.png')
Python探索金庸小说世界
image-20220805231205704

修正上述代码,检查《射雕英雄传》:

Python探索金庸小说世界
image-20220805231700498

神雕侠侣:

Python探索金庸小说世界
image-20220805232040279

主角相关剧情词云

咱们知道《神雕侠侣》这部小说最重要的主角是杨过和小龙女,咱们可能会对于杨过和小龙女之间所发生的故事很感爱好。如果经过程序快速了解呢?

咱们考虑把《神雕侠侣》这部小说每一段中呈现杨过及小龙女的段落进行jieba分词并制作词云。

相同咱们只看4个字以上的词:

data = []
for line in load_novel("神雕侠侣").splitlines():
    if "杨过" in line and "小龙女" in line:
        line = re.sub("[^一-龟]+", " ", line)
        data.extend(word for word in jieba.cut(line) if len(word) >= 4)
stylecloud.gen_stylecloud(" ".join(data),
                          collocations=False,
                          font_path=r'C:\Windows\Fonts\msyhbd.ttc',
                          icon_name='fas fa-square',
                          output_name='tmp.png')
Image(filename='tmp.png')
Python探索金庸小说世界
image-20220805234750851

这儿的每一个词都能联想到发生在杨过和小龙女背面的一个故事。

相同的思路看看郭靖和黄蓉:

data = []
for line in load_novel("射雕英雄传").splitlines():
    if "郭靖" in line and "黄蓉" in line:
        line = re.sub("[^一-龟]+", " ", line)
        data.extend(word for word in jieba.cut(line) if len(word) >= 4)
stylecloud.gen_stylecloud(" ".join(data),
                          collocations=False,
                          font_path=r'C:\Windows\Fonts\msyhbd.ttc',
                          icon_name='fas fa-square',
                          output_name='tmp.png')
Image(filename='tmp.png')
Python探索金庸小说世界
image-20220805235017735

最终咱们看看天龙八部的三兄弟相关的词云:

data = []
for line in load_novel("天龙八部").splitlines():
    if ("萧峰" in line or "乔峰" in line) and "段誉" in line and "虚竹" in line:
        line = re.sub("[^一-龟]+", " ", line)
        data.extend(word for word in jieba.cut(line) if len(word) >= 4)
stylecloud.gen_stylecloud(" ".join(data),
                          collocations=False,
                          font_path=r'C:\Windows\Fonts\msyhbd.ttc',
                          icon_name='fas fa-square',
                          output_name='tmp.png')
Image(filename='tmp.png')
Python探索金庸小说世界
image-20220806080552736

联系图剖析

人物联系剖析

金庸小说15部小说中估计呈现了1400个以上的人物,下面咱们将遍历小说的每一段,在一段中呈现的任意两个人物,都计数1。最终咱们取呈现频次最高的前200个联系对进行可视化。

完好代码如下:

from pyecharts import options as opts
from pyecharts.charts import Graph
import math
import itertools
count = Counter()
for novel in novel_names:
    names = novel_names[novel]
    re_rule = f"({'|'.join(names)})"
    for line in load_novel(novel).splitlines():
        names = list(set(re.findall(re_rule, line)))
        if names and len(names) >= 2:
            names.sort()
            for s, t in itertools.combinations(names, 2):
                count[(s, t)] += 1
count = count.most_common(200)
node_count, nodes, links = Counter(), [], []
for (n1, n2), v in count:
    node_count[n1] += 1
    node_count[n2] += 1
    links.append({"source": n1, "target": n2})
for node, count in node_count.items():
    nodes.append({"name": node, "symbolSize": int(math.log(count)*5)+5})
c = (
    Graph(init_opts=opts.InitOpts("1280px","960px"))
    .add("", nodes, links, repulsion=30)
)
c.render("tmp.html")

这次咱们生成了HTML文件是为了更便利的检查成果,前200个人物的联系状况如下:

Python探索金庸小说世界
image-20220806090626583

门派联系剖析

依照相同的办法剖析一切小说的门派联系:

from pyecharts import options as opts
from pyecharts.charts import Graph
import math
import itertools
count = Counter()
for novel in novel_bangs:
    bangs = novel_bangs[novel]
    re_rule = f"({'|'.join(bangs)})"
    for line in load_novel(novel).splitlines():
        names = list(set(re.findall(re_rule, line)))
        if names and len(names) >= 2:
            names.sort()
            for s, t in itertools.combinations(names, 2):
                count[(s, t)] += 1
count = count.most_common(200)
node_count, nodes, links = Counter(), [], []
for (n1, n2), v in count:
    node_count[n1] += 1
    node_count[n2] += 1
    links.append({"source": n1, "target": n2})
for node, count in node_count.items():
    nodes.append({"name": node, "symbolSize": int(math.log(count)*5)+5})
c = (
    Graph(init_opts=opts.InitOpts("1280px","960px"))
    .add("", nodes, links, repulsion=50)
)
c.render("tmp2.html")
Python探索金庸小说世界
image-20220806091631943

Word2Vec剖析

Word2Vec 是一款将词表征为实数值向量的高效工具,接下来,咱们将运用它来处理这些小说。

gensim 包提供了一个 Python 版的实现。

  • 源代码地址:github.com/RaRe-Techno…
  • 官方文档地址:radimrehurek.com/gensim/

之前我有运用gensim 包进行了相似文本的匹配,有爱好可查阅:《批量含糊匹配的三种办法》

Word2Vec练习模型

首要我要将一切小说的段落分词后添加到组织到一起(前面的程序能够重启):

import jieba
def load_novel(novel):
    with open(f'novels/{novel}.txt', encoding="u8") as f:
        return f.read()
with open('data/names.txt', encoding="utf-8") as f:
    data = f.read().splitlines()
    novels = data[::2]
    names = []
    for line in data[1::2]:
        names.extend(line.split())
with open('data/kungfu.txt', encoding="utf-8") as f:
    data = f.read().splitlines()
    kungfus = []
    for line in data[1::2]:
        kungfus.extend(line.split())
with open('data/bangs.txt', encoding="utf-8") as f:
    data = f.read().splitlines()
    bangs = []
    for line in data[1::2]:
        bangs.extend(line.split())
for name in names:
    jieba.add_word(name)
for kungfu in kungfus:
    jieba.add_word(kungfu)
for bang in bangs:
    jieba.add_word(bang)
# 去重
names = list(set(names))
kungfus = list(set(kungfus))
bangs = list(set(bangs))
sentences = []
for novel in novels:
    print(f"处理:{novel}")
    for line in load_novel(novel).splitlines():
        sentences.append(jieba.lcut(line))
处理:书剑恩仇录
处理:碧血剑
处理:射雕英雄传
处理:神雕侠侣
处理:雪山飞狐
处理:飞狐别传
处理:白马啸西风
处理:倚天屠龙记
处理:鸳鸯刀
处理:天龙八部
处理:连城诀
处理:侠客行
处理:笑傲江湖
处理:鹿鼎记
处理:越女剑

接下面咱们运用Word2Vec练习模型:

import gensim
model = gensim.models.Word2Vec(sentences)

我这边模型练习耗时15秒,若练习耗时较长能够把练习好的模型存到本地:

model.save("louis_cha.model")

以后能够直接从本地磁盘读取模型:

model = gensim.models.Word2Vec.load("louis_cha.model")

有了模型,咱们能够进行一些简略而风趣的测验。

留意:每次生成的模型有必定随机性,后续成果依据生成的模型而变化,并非完全一致。

相似人物、门派和武功

首要看与乔(萧)峰相似的人物:

model.wv.most_similar(positive=["乔峰", "萧峰"])
[('段正淳', 0.8006908893585205), ('张翠山', 0.8000873923301697), ('虚竹', 0.7957292795181274), ('赵敏', 0.7937390804290771), ('游坦之', 0.7803780436515808), ('石破天', 0.777414858341217), ('令狐冲', 0.7761642932891846), ('慕容复', 0.7629764676094055), ('贝海石', 0.7625609040260315), ('钟万仇', 0.7612598538398743)]

再看看与阿朱相似的人物:

model.wv.most_similar(positive=["阿朱", "蛛儿"])
[('殷素素', 0.8681862354278564), ('赵敏', 0.8558328747749329), ('木婉清', 0.8549383878707886), ('王语嫣', 0.8355365991592407), ('钟灵', 0.8338050842285156), ('小昭', 0.8316497206687927), ('阿紫', 0.8169034123420715), ('程灵素', 0.8153879642486572), ('周芷若', 0.8046135306358337), ('段誉', 0.8006759285926819)]

除了人物,咱们还能够看看门派:

model.wv.most_similar(positive=["丐帮"])
[('恒山派', 0.8266139626502991), ('门人', 0.8158190846443176), ('天地会', 0.8078100085258484), ('雪山派', 0.8041207194328308), ('魔教', 0.7935695648193359), ('嵩山派', 0.7908961772918701), ('峨嵋派', 0.7845258116722107), ('红花会', 0.7830792665481567), ('星宿派', 0.7826651930809021), ('长乐帮', 0.7759961485862732)]

还能够看看与降龙十八掌相似的武功秘籍:

model.wv.most_similar(positive=["降龙十八掌"])
[('空明拳', 0.9040402770042419), ('打狗棒法', 0.9009960293769836), ('太极拳', 0.8992120623588562), ('八卦掌', 0.8909589648246765), ('一阳指', 0.8891675472259521), ('七十二路', 0.8713394999504089), ('绝招', 0.8693119287490845), ('胡家刀法', 0.8578060865402222), ('六脉神剑', 0.8568121194839478), ('七伤拳', 0.8560649156570435)]

在 Word2Vec 的模型里,有过“中国-北京=法国-巴黎”的比如,咱们看看”段誉”和”段令郎”相似于乔峰和什么的联系呢?

def find_relationship(a, b, c):
    d, _ = model.wv.most_similar(positive=[b, c], negative=[a])[0]
    print(f"{a}-{b} 犹如 {c}-{d}")
find_relationship("段誉", "段令郎", "乔峰")
段誉-段令郎 犹如 乔峰-乔帮主

相似的还有:

# 情侣对
find_relationship("郭靖", "黄蓉", "杨过")
# 岳父女婿
find_relationship("令狐冲", "任我行", "郭靖")
# 非情侣
find_relationship("郭靖", "华筝", "杨过")
郭靖-黄蓉 犹如 杨过-小龙女
令狐冲-任我行 犹如 郭靖-黄药师
郭靖-华筝 犹如 杨过-郭芙

检查韦小宝相关的联系:

# 韦小宝
find_relationship("杨过", "小龙女", "韦小宝")
find_relationship("令狐冲", "盈盈", "韦小宝")
find_relationship("张无忌", "赵敏", "韦小宝")
find_relationship("郭靖", "黄蓉", "韦小宝")
杨过-小龙女 犹如 韦小宝-康熙
令狐冲-盈盈 犹如 韦小宝-方怡
张无忌-赵敏 犹如 韦小宝-阿紫
郭靖-黄蓉 犹如 韦小宝-丁珰

门派武功之间的联系:

find_relationship("郭靖", "降龙十八掌", "黄蓉")
find_relationship("武当", "张三丰", "少林")
find_relationship("任我行", "魔教", "令狐冲")
郭靖-降龙十八掌 犹如 黄蓉-打狗棒法
武当-张三丰 犹如 少林-玄慈
任我行-魔教 犹如 令狐冲-恒山派

聚类剖析

人物聚类剖析

之前咱们运用 Word2Vec 将每个词映射到了一个向量空间,因而,咱们能够使用这个向量表明的空间,对这些词进行聚类剖析。

首要取出一切人物对应的向量空间:

all_names = []
word_vectors = []
for name in names:
    if name in model.wv:
        all_names.append(name)
        word_vectors.append(model.wv[name])
all_names = np.array(all_names)
word_vectors = np.vstack(word_vectors)

聚类算法有许多,这儿咱们运用根本的Kmeans算法进行聚类,如果只分红3类,那么很明显地能够将世人分红主角,副角,跑龙套的三类:

from sklearn.cluster import KMeans
import pandas as pd
N = 3
labels = KMeans(N).fit(word_vectors).labels_
df = pd.DataFrame({"name": all_names, "label": labels})
for label, names in df.groupby("label").name:
    print(f"类别{label}{len(names)}个人物,前100个人物有:\n{','.join(names[:100])}\n")
类别0共103个人物,前100个人物有:
李秋水,向问天,马钰,顾金标,丁不四,耶律齐,谢烟客,陈正德,殷天正,洪凌波,灵智上人,闵柔,公孙止,完颜萍,梅超风,鸠摩智,冲虚,冯锡范,尹克西,陆冠英,王剑英,左冷禅,商老太,尹志平,徐铮,灭绝师太,风波恶,袁紫衣,殷梨亭,宋青书,阿九,韩小莹,乌老大,杨康,何铁手,范遥,朱聪,郝大通,周仲英,风际中,何太冲,张召重,一灯大师,田归农,尼摩星,霍都,潇湘子,梅剑和,南希仁,玄难,纪晓芙,韩宝驹,邓百川,裘千尺,朱子柳,宋远桥,渡难,俞岱岩,武三通,云中鹤,余沧海,花铁干,杨逍,段延庆,巴天石,东方不败,归辛树,梁子翁,赵志敬,韦一笑,赵半山,丘处机,武修文,侯通海,鲁有脚,石清,彭连虎,胖头陀,达尔巴,裘千仞,金花婆婆,金轮法王,木高峰,苗人凤,任我行,王处一,柯镇恶,樊一翁,黄药师,欧阳克,张三丰,曹云奇,沙通天,文泰来,白万剑,鹿杖客,陆菲青,班淑娴,商宝震,全金发
类别1共6个人物,前100个人物有:
渔人,汉子,少妇,胖子,大汉,农民
类别2共56个人物,前100个人物有:
张无忌,余鱼同,慕容复,木婉清,田伯光,郭襄,周伯通,陈家洛,乔峰,张翠山,丁珰,游坦之,岳不群,黄蓉,洪七公,岳灵珊,周芷若,马春花,杨过,阿紫,阿朱,赵敏,令狐冲,段正淳,水笙,石破天,徐天宏,程灵素,林平之,双儿,郭靖,袁承志,胡斐,陆无双,狄云,霍青桐,王语嫣,萧峰,李沅芷,骆冰,李莫愁,周绮,丁典,韦小宝,段誉,戚芳,小龙女,钟灵,殷素素,李文秀,谢逊,穆念慈,郭芙,方怡,仪琳,虚竹
类别3共236个人物,前100个人物有:
空智,章进,澄观,薛鹊,秃笔翁,曲非烟,田青文,郭啸天,陆大有,方证,阿碧,陶子安,吴三桂,钱老本,马行空,洪胜海,张勇,瑞大林,包不同,慕容景岳,康广陵,施琅,陆高轩,袁冠南,张康年,桃花仙,定逸,执法长老,范蠡,钟镇,陈达海,桃根仙,阿曼,李四,札木合,吴之荣,哈合台,传功长老,卓天雄,茅十八,风清扬,崔希敏,方生,王进宝,葛尔丹,常金鹏,秦红棉,薛慕华,侍剑,孙仲寿,范一飞,归二娘,孙不二,吴六奇,杨铁心,万震山,单正,玄寂,武敦儒,刘正风,西华子,樊纲,店伴,何足道,小昭,孙婆婆,苏普,谭婆,朱九真,耶律洪基,圆真,萧中慧,都大锦,司马林,叶二娘,安大娘,张三,杨成协,掌棒龙头,福康安,玉林,顾炎武,马超兴,殷离,莫声谷,郑萼,桃干仙,华筝,计无施,苏鲁克,费要多罗,苏荃,玄慈,卫璧,马光佐,常遇春,沐剑声,包惜弱,朱长龄,褚万里

咱们能够依据每个类其他人物数量的相对大小,判别该类其他人物是属于主角,副角仍是跑龙套。

下面咱们过滤掉众龙套人物之后,重新聚合成四类:

c = pd.Series(labels).mode().iat[0]
remain_names = all_names[labels != c]
remain_vectors = word_vectors[labels != c]
remain_label = KMeans(4).fit(remain_vectors).labels_
df = pd.DataFrame({"name": remain_names, "label": remain_label})
for label, names in df.groupby("label").name:
    print(f"类别{label}{len(names)}个人物,前100个人物有:\n{','.join(names[:100])}\n")
类别0共103个人物,前100个人物有:
李秋水,向问天,马钰,顾金标,丁不四,耶律齐,谢烟客,陈正德,殷天正,洪凌波,灵智上人,闵柔,公孙止,完颜萍,梅超风,鸠摩智,冲虚,冯锡范,尹克西,陆冠英,王剑英,左冷禅,商老太,尹志平,徐铮,灭绝师太,风波恶,袁紫衣,殷梨亭,宋青书,阿九,韩小莹,乌老大,杨康,何铁手,范遥,朱聪,郝大通,周仲英,风际中,何太冲,张召重,一灯大师,田归农,尼摩星,霍都,潇湘子,梅剑和,南希仁,玄难,纪晓芙,韩宝驹,邓百川,裘千尺,朱子柳,宋远桥,渡难,俞岱岩,武三通,云中鹤,余沧海,花铁干,杨逍,段延庆,巴天石,东方不败,归辛树,梁子翁,赵志敬,韦一笑,赵半山,丘处机,武修文,侯通海,鲁有脚,石清,彭连虎,胖头陀,达尔巴,裘千仞,金花婆婆,金轮法王,木高峰,苗人凤,任我行,王处一,柯镇恶,樊一翁,黄药师,欧阳克,张三丰,曹云奇,沙通天,文泰来,白万剑,鹿杖客,陆菲青,班淑娴,商宝震,全金发
类别1共6个人物,前100个人物有:
渔人,汉子,少妇,胖子,大汉,农民
类别2共56个人物,前100个人物有:
张无忌,余鱼同,慕容复,木婉清,田伯光,郭襄,周伯通,陈家洛,乔峰,张翠山,丁珰,游坦之,岳不群,黄蓉,洪七公,岳灵珊,周芷若,马春花,杨过,阿紫,阿朱,赵敏,令狐冲,段正淳,水笙,石破天,徐天宏,程灵素,林平之,双儿,郭靖,袁承志,胡斐,陆无双,狄云,霍青桐,王语嫣,萧峰,李沅芷,骆冰,李莫愁,周绮,丁典,韦小宝,段誉,戚芳,小龙女,钟灵,殷素素,李文秀,谢逊,穆念慈,郭芙,方怡,仪琳,虚竹
类别3共236个人物,前100个人物有:
空智,章进,澄观,薛鹊,秃笔翁,曲非烟,田青文,郭啸天,陆大有,方证,阿碧,陶子安,吴三桂,钱老本,马行空,洪胜海,张勇,瑞大林,包不同,慕容景岳,康广陵,施琅,陆高轩,袁冠南,张康年,桃花仙,定逸,执法长老,范蠡,钟镇,陈达海,桃根仙,阿曼,李四,札木合,吴之荣,哈合台,传功长老,卓天雄,茅十八,风清扬,崔希敏,方生,王进宝,葛尔丹,常金鹏,秦红棉,薛慕华,侍剑,孙仲寿,范一飞,归二娘,孙不二,吴六奇,杨铁心,万震山,单正,玄寂,武敦儒,刘正风,西华子,樊纲,店伴,何足道,小昭,孙婆婆,苏普,谭婆,朱九真,耶律洪基,圆真,萧中慧,都大锦,司马林,叶二娘,安大娘,张三,杨成协,掌棒龙头,福康安,玉林,顾炎武,马超兴,殷离,莫声谷,郑萼,桃干仙,华筝,计无施,苏鲁克,费要多罗,苏荃,玄慈,卫璧,马光佐,常遇春,沐剑声,包惜弱,朱长龄,褚万里

每次运行成果都不相同,咱们能够调整类别数量持续测验。从成果能够看到,反派更倾向于被聚合到一起,非正常名字的人物更倾向于被聚合在一起,主角更倾向于被聚合在一起。

人物层级聚类

现在咱们采用层级聚类的方式,检查人物间的层次联系,这儿相同龙套人物不再参加聚类。

层级聚类调用 scipy.cluster.hierarchy 中层级聚类的包,在此之前先解决matplotlib中文乱码问题:

import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

接下来调用代码为:

import scipy.cluster.hierarchy as sch
y = sch.linkage(remain_vectors, method="ward")
_, ax = plt.subplots(figsize=(10, 80))
z = sch.dendrogram(y, orientation='right')
idx = z['leaves']
ax.set_xticks([])
ax.set_yticklabels(remain_names[idx], fontdict={'fontsize': 12})
ax.set_frame_on(False)
plt.show()

然后咱们能够得到金庸小说世界的人物层次联系地图,成果较长仅展现一部分红果:

Python探索金庸小说世界
image-20220807002403925

当然一切小说混合发生的平行世界中,人物联系变得有些紊乱,读者有爱好能够拿单本小说作层次剖析,就能够得到较为准确的人物层次联系。

武功层级聚类

对各种武功作与人物层次聚类相同的操作:

all_names = []
word_vectors = []
for name in kungfus:
    if name in model.wv:
        all_names.append(name)
        word_vectors.append(model.wv[name])
all_names = np.array(all_names)
word_vectors = np.vstack(word_vectors)
Y = sch.linkage(word_vectors, method="ward")
_, ax = plt.subplots(figsize=(10, 40))
Z = sch.dendrogram(Y, orientation='right')
idx = Z['leaves']
ax.set_xticks([])
ax.set_yticklabels(all_names[idx], fontdict={'fontsize': 12})
ax.set_frame_on(False)
plt.show()

成果较长,仅展现部分红果:

Python探索金庸小说世界
image-20220807004049494

能够看到,比较少的黄色部分明显是主角比较厉害的武功,而绿色比较多的部分根本都是副角的武功。

门派层次聚类

最终咱们对门派进行层次聚类:

all_names = []
word_vectors = []
for name in bangs:
    if name in model.wv:
        all_names.append(name)
        word_vectors.append(model.wv[name])
all_names = np.array(all_names)
word_vectors = np.vstack(word_vectors)
Y = sch.linkage(word_vectors, method="ward")
_, ax = plt.subplots(figsize=(10, 25))
Z = sch.dendrogram(Y, orientation='right')
idx = Z['leaves']
ax.set_xticks([])
ax.set_yticklabels(all_names[idx], fontdict={'fontsize': 12})
ax.set_frame_on(False)
plt.show()
Python探索金庸小说世界
image-20220807004947892

比较少的这一类,根本都是在某几部小说中呈现的主要门派,而大多数门派都是打酱油的。

总结

本文从金庸小说数据的收集,到普通的频次剖析、剧情剖析、联系剖析,再到运用词向量空间剖析相似联系,最终运用scipy进行一切小说的各种层次聚类。

收成多多,干货满满。