用Python解析古籍《史记》(一)

文本数据挖掘,分析,信息可视化等学科的作用是通过对文本数据的整理和研究,从各种角度全方位探索文本中的信息,从中提取出有价值的东西,特别是有些隐藏的,不易被普通阅读方式发现的信息模式。

当我们面对海量的文本大数据,无法通过普通的逐字阅读的方式来探索时,这些计算机信息技术和方法就可辅助人们完成曾经不可能完成的任务。

众所周知,这种方法已经被人们大量用于各个领域,工商业,科研,医疗,教育,等等,它的用处就不必多言。

今天,我也想用这种方法来探索一下古籍,先选一本比较有名的古书《史记》来做一下实验。

古人说:“工欲善其事,必先利其器”。技术工具的选用上可以有很多种,对我来说,Python系的工具是我目前最熟悉的,而且它除了支持所有基本的数据科学功能以外,还方便用来对文本进行进行人工智能处理,所以我把它当作首选利器,以下是我的实验计划:

  1. 工作台的搭建,文本数据的获取。
  2. 文本预处理,去标点,空行,分割,融合,数据结构化,等等。
  3. 对文本进行总览分析,信息可视化。
  4. 探索文本各方面的细节,信息可视化。

首先,工作台的搭建,我选用云工作台,上面自带有网盘和Python Jupyter Notebook虚拟运行环境,这样比较方便我切换电脑,无论在哪台电脑上,只要有浏览器,我就可以登录到云工作台上,进行研究,保存数据,不需要重新搭建一番工作台。

当然,个人有各自的偏好,也可以在本地搭建工作台。工具库我选择比较通用的Python数据科学三件套,Pandas用于结构化数据处理,Numpy用于矩阵运算, Matplotlib用于信息可视化制图,这三样是必备的,当然还有其他的工具包,等使用到的时候才来介绍一下。

然后我从网上找到《史记》文本的,用任意文本编辑器打开,如下。

先大体看看文本结构,这是一个单文本的《史记》文件,包括十二本纪,十表,八书,三十世家,七十列传,一共有一百三十卷内容。

全文字数有505056字,没有进行文本校对,所以,这不一定是准确的字数,因为网上的文本资料可能是不全的或者有错漏的。但还是非常感谢那些免费发放电子文本的人和机构,让我们有大量的古籍资料唾手可得。

为了便于分析,我打算先进行文本分割,把它分成130份单独的文本。

对于古文的文本分割,我目前所知有几种方法,一是根据关键字,比如“史记卷第xxx”这样的字眼来作为分隔符,把整个文本读入程序中的字符串,再用分割函数,从选定的分隔符入手,自动分割成不同的子字符串,再把每一个子字符串重新存储为单独的文本,就达到了基本能的分割效果。

但并不是每一个原始文本都自带有这样合适的关键字,在这种情况下,还可以手动加入自定义的分隔符,然后用同样的原理来分割。

对于不讲究章节目录的原始文本,还可以进行等量分割,按长度或者文件大小进行等量分割。

我用的是手动加入分隔符的方法,在每一卷后面加入一个自定义的分割符来处理,如下:

然后,我用Python写了一个程序,如下,这不一定是最好,最优雅的程序,但目前够用就行:

def 用分隔符分割(path, fileName, bianhao):
    os.chdir(path)
    with open(fileName+'.htm','r', encoding='utf-8') as file:
        content = file.read()
        if content.find('') == -1:
            print("Skip")
            return
        duanluo = content.split('')
        if os.path.exists(fileName):
            shutil.rmtree(fileName)
        if not os.path.exists(fileName):
            os.makedirs(fileName)
        os.chdir(path+fileName)
        index = 1
        print(len(duanluo))
        for duan in duanluo:
            duanhao = 数字转汉字(index)
            if bianhao == 3:
                prefix = str('{:03}'.format(index))+'-'
            elif bianhao == 2:
                prefix = str('{:02}'.format(index))+'-'
            with open(prefix+fileName+'卷'+duanhao+".htm", 'w', encoding='utf-8') as juan:
                juan.write(duan)
                index += 1
        os.chdir(path)

这个函数需要用到一个子函数,来进行数字转汉字,以便于给子文件命名为史记卷一,史记卷二...,的汉字序列,如下:

def 数字转汉字(num):
    num_dict = {'1':'一', '2':'二', '3':'三', '4':'四', '5':'五', '6':'六', '7':'七', '8':'八', '9':'九', '0':'零', }
    index_dict = {1:'', 2:'十', 3:'百', 4:'千', 5:'万', 6:'十', 7:'百', 8:'千', 9:'亿'}
    nums = list(f'{num}')
    nums_index = [x for x in range(1, len(nums)+1)][-1::-1]
    str = ''
    for index, item in enumerate(nums):
        str = "".join((str, num_dict[item], index_dict[nums_index[index]]))
    str = re.sub("零[十百千零]*", "零", str)
    str = re.sub("零万", "万", str)
    str = re.sub("亿万", "亿零", str)
    str = re.sub("零零", "零", str)
    str = re.sub("零b" , "", str)
    return str

在传入文本参数执行以上函数后,便得到了以下的分割结果,我的文本是以.htm结尾的,这不重要,这种格式是方便我的古籍网站(http://xinyige.cool)的需要,各人可以根据自己的情况而改变。

我在分割文本时,特意在文件名上加了序号001,002等,不然文本排列的顺序会乱,因为操作系统不会按照汉字的卷一,卷二的顺序来排列,所以在文件名上加上数字序号,操作系统才会识别出文本的顺序来显示。

然后再用一段代码把文本去除掉标点和空行,以及非汉字字符(多是乱码),再把所有子文件转化为一个Pandas数据结构表,以方便结构化处理数据,为了增加代码的中文可读性,我用中文写下了以下代码。

def 去除标点(文句):
  标点集 = ',。;?‘’:“”!,.;:""''…`○''!()/·•、《》,()'
  无标点文句 = [x for x in 文句 if x not in 标点集]
  无标点文句 = ''.join(无标点文句)
  return 无标点文句
def 去除空行(文句):
  无空行文句 = [x for x in 文句 if x.split()]
  无空行文句 = ''.join(无空行文句)
  return 无空行文句
def 去除非汉字字符(文句):
  字符集 = 'abcdefghijklmnopqrstuvwxyzàèíùúāīǒ△々◎'
  汉字字符 = [x for x in 文句 if x not in 字符集]
  汉字字符 = ''.join(汉字字符)
  return 汉字字符
文档= "/GujiCool/GujiLib/心一阁藏/史藏/正史/史记/"
os.chdir(文档)
文表 = {}
卷目 = []
卷文 = []
卷字数 = []
全文字数 = 0
for 路径, _, 文本 in os.walk(文档):
    文本.sort()
    for 卷 in 文本:
      卷目.append(卷.split('-')[1].split('.')[0])
      with open(卷) as file:
        文句 = 文具.去除标点(file.read())
        文句 = 文具.去除空行(文句)
        文句 = 文具.去除非汉字字符(文句)
        字数 = len(文句)
        全文字数 += 字数
        卷文.append(文句)
        卷字数.append(字数)
文表 = {
    '卷目': 卷目,
    '卷文': 卷文,
    '卷字数': 卷字数
}
史记文表 = pd.DataFrame(文表)

再用了另一段代码加入预先手工提取出来的卷名(自动提取太麻烦,就直接从文本的目录里拷贝出来,用代码插入Pandas数据表)

史记文表.insert(loc=1, column='卷名', value=卷名)
史记文表.head()

处理的结果如下(只显示前5行),这样就形成了一个便于进一步分析的结构化数据,为什么选择使用Pandas的数据结构呢?因为学过数据科学的都知道,这是一个很强大的数据分析工具,后面大家会看到一些效果。

到此,数据预处理便初步完成了,接下来可以对这样的结构化数据进行一些总览性的探索,以下代码可以用来展示《史记》中每一卷的字数对比图。

tu = 史记文表.plot(x='卷名', y='卷字数', kind='barh', figsize=(16, 50), color='#5fba34')
for p in tu.patches:
  tu.annotate(p.get_width(),(p.get_x()+p.get_width(), p.get_y()), size=15)
tu.legend(fancybox=True,shadow=True,fontsize=15)
plt.xlabel('卷字数', fontsize=20)
plt.ylabel('卷名', fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.tight_layout()
plt.show()
tu.figure.savefig("/GujiCool/文表/史记文表.png")

可以看出,在一百三十多卷《史记》文本中,它们各自的篇幅差别还是挺大的,篇幅最长的是《秦始皇本纪》,还有几篇字数上万的篇章如下:

而字数最少的篇章只有几十个,上百个字,应该是原始的文本资料不全。

查看一下原始数据,果然,这是《史记》十表的内容,原始文件把表的内容省略了,估计是因为原始文本是OCR识别得出的结果,可能因为那时的OCR技术识别不出古籍影印图片中的这部分内容。

我特意查了一下《史记》的古籍影音资料,表是确实存在的,只是文本资料省略去了。看来,古籍数字化的工作还任重道远啊。不过我们比起古人来说,这样已经好太多了。许多古人一辈子可能都看不到一本完整的书。

接下来我想做一下文字的统计,看整部《史记》中用到的单字的频率分布,看那些单字用得最多,通过这样的分布,可以惊鸿一瞥,大概了解一下整部《史记》的文字面貌。

当然,除了单字,最好是看词汇分布,但是古文词汇分词是一个很困难的事情,不像单字分词那么容易,即使是最先进的人工智能模型,也不能够好好解决这个问题。所以,古文的词汇分词我留到下一篇文章再来讨论,那是人工智能自然语言处理科目中一个很基础,很重要的话题。

下面代码用来把史记Pandas文表中的卷文融合在一起,装到一个字符串中,由此可以得到全文的所有单字字符串。

史记全文= ''.join(史记文表['卷文'].tolist())
print("全文字数:",len(史记全文))
print("前50个字:", 史记全文[:50])

运行结果如下:

再来看看无重复的单字与全文单字的对比情况,可见无重复的单字只占很少的比例,中文只需要用少量的单字排列组合就可以书写很多内容。

#有多少无重复的单字?
无重复单字 = set(史记全文)
print("无重复单字的数量: ", len(无重复单字))
print("无重复单字与全文单字的比例: ", "{:.00%}".format(len(无重复单字)/len(史记全文)))
print("前30个无重复的单字: ",sorted(无重复单字)[:30])

再来去除一些停词,也就是助词以及一些常用但可以忽略的词语,如,之乎者也之类的词。可以看出停词占的比例有13%左右。

# 除去停词(助词以及一些常用的但可以忽略的词)
停词 = ['之','乎','者','也','哉','矣','而','不','其','于','曰','以','有','故', '则']
无停词史记全文 = [ t for t in 史记全文 if t not in 停词 ]
print(无停词史记全文[:10])
print("史记全文字数: ", len(史记全文))
print("去停词史记全文字数: ", len(无停词史记全文))
print("无停词文占全文的比例: ", "{:.00%}".format(len(无停词史记全文)/len(史记全文)))

然后可以通过nltk工具包来查看一下《史记》全文单字的频率分布,看哪些字出现的频率最高,来看看排名前十的单字都是些什么(要查看更多的内容,也随时可以查,但是这里篇幅有限,就不一一列出了)。我们可以看出出现最多的是“王”字,有8000多次。

#查看词频分布
史记词频分布 = FreqDist(无停词史记全文)
首十个字 = FreqDist(dict(史记词频分布.most_common(10)))
plt.figure(figsize=(25, 10))
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.tight_layout()
首十个字.plot()

对于陌生的文本,这些单字或词汇,甚至重复句子的出现频率等,这些信息可以有助于我们了解文本的大体内容是什么,还有作者的行文风格,等等,从而得出一些初步的评估。

我记得有人通过这样的方式,来研究《红楼梦》前八十回和后续的四十回章节是否是同一个作者写的,研究发现,似乎两者的行文风格,用词确实存在着一些隐藏的差别,所以有人估计确实不是同一个人写的。以后我有空也来做一篇《红楼梦》的文本分析,看看有什么发现。

对于词频的分布,除了图表的展示,还有一种特别的可视化方式,叫做词云,我们来做一个史记全文的词云看看是什么效果:

# 词云分布
plt.figure(figsize=(25, 10))
wc_fd = WordCloud(background_color="white", font_path='/content/gdrive/My Drive/GujiCool/GujiLib/msyh.ttf')
wc_fd.generate_from_frequencies(史记词频分布)
plt.imshow(wc_fd, interpolation='bilinear')
plt.axis("off")
plt.show()

可以看出来,这种频率分布的效果更加显著了,字面越大代表它出现的频率越多,这样更加能够认识到全文的风貌了。

今天就到这里吧,这篇主要集中在全文的单字分析上,下一篇尝试进行词汇分析,以及对单个章节进行更详细的细节分析,了解一下不同章节各自的具体情况。

喜欢的话,请关注与点赞哦!

展开阅读全文

页面更新:2024-05-30

标签:史记   汉字   文句   空行   单字   标点   古籍   字数   文本   数据   全文

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top