前言:中国古代的文人在吟诗作词时常化用前人的好词佳句,对研究中国古代文学的人来说,可能知道诗人A收到著名诗人B的影响很大,但若把A所有作品中化用B的字段全找出来,不知要来回比对多久。然而没有这一数据,就无法进行后续的分析。皓首穷经,仅仅能研究透几人而已——究其原因,不是水平不够,而是时间有限,机械式的检索工作耗费了学者太多时间精力,需要用技术解放。
1 任务
说人话:将目标诗人A的诗集与参考诗人B的诗集进行比对,得到A诗集中化用B诗集的部分。
技术表述:检测两字符串中的重复子串,其中当连续重复字符长度超过e(化用标准)时,认为是重复子串。
2 步骤
2.1 数据获取
首先从各种古籍数据库获取两人的诗集。由研究者检索、下载得到。将文本转为统一的编码格式,如utf-8,便于计算机处理。
2.2 数据规整
- 去标点:一般是没有标点的,如果有,全删除,以减少计算量
- 加分隔:绝大多数文本诗的标题独占一行,并且和正文区分明显,如前有空格,或后有作者等等。为了后续处理能够区分标题与正文,在标题后加入分隔符,如空格。
- 去换行:绝大多数文本诗的标题独占一行,诗正文或不换行或一句或两句一行。把正文中的换行全删掉。
- 去除数字、无关字符(如卷XX)、对形如“XXn首…其一…其二…”系列中的各标题进行完善
规整过后的诗集变为类似csv的形式。本文中案例的格式是
题目 正文,如
春晓 春眠不觉晓处处闻啼鸟夜来风雨声花落知多少
数据规整的具体实现因原始数据而异,(多用正则表达式)所以没什么代码好贴的。不过恰恰由于原始数据的文本太多样化,还是免不了人工。这一步是最花时间的。
2.3 文本读取
使用Python(不推荐3以下的版本。对于中文处理太不友好,各种转码搞晕)的字典来存储诗集。
这里就用到了2.2中的分隔符,使用split
函数来将每首诗的题目作为字典的key
,内容作为value
。
注意其参数1,只分割一次,避免正文中也出现分隔符导致分成多段无法匹配key,value两个参数的情况。
#从文件中读入诗集的函数
def ReadPoem(fname):
d = {}
i = 1
f = open(fname+'.txt',encoding='utf-8')
for l in f.readlines():
try:
k,v = l.split(' ',1)
except Exception as e:
#输出格式有误的诗题,以便整改源数据
print(l)
d[str(i)+k.strip()] = v.strip()
i += 1
#print(i)
f.close()
print(fname+"诗集解析完毕!共有 %d 首诗" % len(d))
return d
一个诗集文件动辄十几万字,仅靠2.2的数据规整难免有所疏漏,因此这里使用了异常来进一步检查不规整的地方。并且用i给每个诗词编了号,功能有3:
- 方便后续结果的浏览
- 辅助检查读入过程有没有遗漏
- 防止同名诗解析出同样的key导致字典的value被重写
使用ReadPoem()
函数,就将A、B的诗集分别读入了字典d1
, d2
中。
2.4 循环比对
这部分是连续重复字段检索的核心算法。无奈本人Python水平太低,写完之后觉得完全是c的风格,惭愧,凑合看吧。。。
- 四层循环正文遍历:A的每首诗pa→B的每首诗pb→A诗的每个字wa→B诗的每个字wb,对所有正文进行遍历;
- 向下窥探重复子串:如果
wa==wb
,则需要比对下一个wa和下一个wb是否也相等。这样直到重复字段rec_c
结束为止; - 化用标准满足判断:如果
len(rec_c)
>用户指定的化用标准重复子串长度e,则rec_c需要记录下来; - 跳过已测重复子串:如果检测出了符合标准的连续重复字段,则pa中的这段不需要再检测,所以循环的索引需要往后跳
len(rec_c)-1
。因为索引改变,所以相关的循环不能用for ... in ...
; - 组装题目和化用串:
rec_c
是单段重复子串,两首诗之间可能会有多个重复子串,使用rec
存储两首诗比对出的各rec_c
,将两首诗的题目pa,pb和重复内容rec
组装起来。
则核心代码如下(部分变量名和上文不同):
#########################循环比对######################
e = 3 #化用标准
num = 0 #进度
l_r = []#结果列表
#遍历A的诗集
for p1 in d1:
#遍历B的诗集
for p2 in d2:
rec = ''
i = 0
#遍历A诗的正文
while i < len(d1[p1]):
j = 0
#遍历B诗的正文
while j < len(d2[p2]):
rec_c = ''#两首诗中所有的重复子串
if d1[p1][i] == d2[p2][j]:#匹配到了1个相同的字
rec_c += d1[p1][i]
#往下窥探直到没有连续相同的字
for k in range(1,min(len(d1[p1])-i,len(d2[p2])-j)):
#记录重复子串
if d1[p1][i+k] == d2[p2][j+k]:
rec_c += d1[p1][i+k]
#不再有连续相同的字,中断循环
else:
break
#跳过已检测出的重复子串
i += (len(rec_c)-1)
j += (len(rec_c)-1)
#重复子串长度>=e,将重复子串加入rec,以顿号相连
if len(rec_c) >= e:
rec = rec + rec_c + '、'
j += 1
i += 1
#记录化用情况
if len(rec)>0:
l_r.append(p1+ ' ------ ' + p2 + ':' + rec[:-1] + '\r\n')
#显示进度
num += 1
print("%d / %d 已完成" % (num,len(d1)))
代码中的记录化用情况
部分对结果进行了组装,得到形如
A某首诗的标题 ------ B某首诗的标题:重复子串1、重复子串2、...
形式的结果表达。
最后再用sort
结合lambda
表达式对结果排序,将重复串长的放前边。
sort(l_r, lambda x:len(rec.split(':',1)[1]), reverse=True)
将结果列表l_r
写入文件(注意加上参数encoding='utf-8'
,不然win默认gbk
可能无法编码解码),就得到了诗词化用分析的结果。
将上述代码封装一下,使用一个列表存储各诗人的名字,从而实现批量比对。这时候自己就可以喝个咖啡看个电影健个身咯~
3 讨论
不论是算法还是代码应该都有很大的改进空间。使用本文中的程序分析11万字和13万字的两个文本,笔记本i7单核用了大概70±10min左右(具体数值忘了记录)。虽然时间不算短,但这个体量的古文献如果靠学者自己比对,那就得“髮稀目涩光陰短,衣帶漸寬人憔悴 ”了!
虽然没有使用大数据和NLP相关的技术(中文+古文+诗词,这也很难用NLP搞吧),智能性有待提高,但这种程度的自动化就已经大大简化了研究者的工作,并且在古代文人诗词化用检索这个方向上具有一定的通用性。理工科的技术引入文学研究领域也是很有必要的呢。