MiniProj之古诗词检索

A simple retrieval for Chinese Ancient Poems by python

Posted by jessychen on March 31, 2020

The most comprehensive database of Chinese poetry

包含 5.5 万首唐诗、26 万首宋诗、2.1 万首宋词和其他古典文集。诗人包括唐宋两朝近 1.4 万古诗人,和两宋时期 1.5 千古词人。此数据库通过 JSON 格式分发,可以让你很方便的开始你的项目。

Github上有不少宝藏项目,比如👆这个:最全中华古诗词库 - chinese-poetry。前几天基于这个宝库写了个检索的的小程序,这里做下开发过程的记录。实现了一个简单的demo, 见poem

  1. 技术分析
  2. 实现
  3. 演示
  4. 后记

技术分析

作为一名10后。。。。。。的老母亲,学校经常会留一些古诗词背诵的作业给孩子。可是,作为一名与高考脱节几十年的纯理工科生,好多诗我真是见都没见过(其实是还给老师了)。这时候,总是特别羡慕那些学富五车的家长,孩子提问时各种诗词文学典故信手拈来。不像我,只能巴巴的求助百度,有时候还出来一堆不靠谱的结果😭。不过好在知识不够,技术来凑,刚好发现这么个诗词宝库,怎能暴殄天物呢?

有了动手写一个诗词检索的想法,自然是要行动的。立刻把code下到本地,看下数据size。嗯,不是特别大,这样要是自己用用的话直接解析文件就可以了:

1
2
3
4
5
6
7
8
9
100K	./youmengying #幽梦影
 15M	./ci  #宋词
348K	./wudai  #五代
143M	./json   #全唐诗
172K	./sishuwujing  #四书五经
 72K	./lunyu   #论语
160K	./shijing  #诗经
1.0M	./mengxue   #蒙学
4.0M	./yuanqu    #元曲

再看下目录下的文件,根据数据大小,有的被切分成了多个文件,例如json,ci,有的占地不大的就只保留了一个,例如shijing,yuanqu。打开文件看看,都是标准的json格式,拿诗仙的看看,其他的也差不多,除了不同类型文件的key可能不同外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  {
        "author": "李白",
        "paragraphs": [
            "雲想衣裳花想容,春風拂檻露華濃。",
            "若非羣玉山頭見,會向瑤臺月下逢。"
        ],
        "tags": [
            "唐诗三百首",
            "女子",
            "乐府",
            "赞美",
            "近代曲辞"
        ],
        "title": "雜曲歌辭 清平調 一",
        "id": "b5f1094b-b9be-43cf-8e04-9b984642a630"
    }

发现没?全唐诗的目录下是繁体字,但其实并不是所有文件都是繁体,像宋词的数据作者已经转成简体了。至于为什么全唐诗保留的数据要是繁体,作者给出的解释是:

目前还没有计划做简体中文数据库。一对一繁简转换有很多工具可以做, 由于简体字的删减在某些诗词场景下通过一对一的转换是有问题的。 比如“发财”繁体是“發財”,而“理发”繁体应是“理髮”. 更多的人名不太容易避免。

检查完文件,再列一下这次的需求,很简单:

  1. 输入一个标题,或者作者,或者里面的一句话把对应的内容给检索出来
  2. 检索到多个匹配项时,优先返回全部匹配的,随后返回部分匹配的,并在查询时给定需要最多返回的条数
  3. 因为有时候我只想随便看看,有随机取篇内容返回的功能
  4. 为了对小盆友友好,把返回的格式转成简体

实现

这次的实现,依然选择了python,原因自然是因为方(tou)便(lan)。

确定接口

首先,要定义检索接口。根据需求,能够多维度的检索,但不同类型文件的json字段key不同,所以自然想到用dict作为检索条件的传递index_json,也方便以后的扩展。另外检索前最好能指定类型peom_type(是唐诗,还是宋词,还是诗经。。。),检索后能指定一个返回条目数count。当然,有随机看看这种设定的存在,输入参数自然不是必须的。这样,就确定了函数定义:

1
2
3
#index is a dict json str, like: '{"author":"李白", "title":"静夜思"}'
def get_poem(poem_type="", index_json="", count=1):
  ......

输入处理

接下来,是对输入做处理,包括:

根据诗词类型生成路径,没有指定类型的给随机确定一个

1
2
3
4
5
   if not poem_type:
        i = random.randint(1, len(poem_type_list))
        poem_type = poem_type_list(i)
    poem_type = poem_type.lower()
    poem_path = "%s/%s" % (poem_path_prefix, poem_type)

由于某些数据是繁体字(如全唐诗),在检索繁体数据时我们要将输入index_json转成繁体。这里用了个轻量级的繁简体转换库zhtools,只要两个python文件,就能实现简单的转换。并把输入的检索条件由str转成dict

1
2
3
4
5
6
7
8
    indexs = {}
    if poem_type in traditional_data and index_json:
        index_json = convert(index_json, "t")
    if index_json:
        indexs = json.loads(index_json)
        
def convert(text, tp="s"):
    return Converter('zh-han%s' % tp).convert(text)

数据预处理

像全唐诗这种由于数据量大而被分成多个json文件的,如果传了检索条件进来的话可以先做个简单的筛选来减少load的文件数目,毕竟如果遍历所有条目来查找的话,python的效率还是挺感人的。这里调用了grep来做简单的筛选。做完这步后,就能得到一个file_list,里面存的是python需要load的json文件列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    file_list = []
    if indexs and poem_type in grep_poems:
        for i in indexs:
            grep_cmd = ""
            if not i in ["content", "paragraphs"]:
                grep_cmd = "grep -rl \"\\\"%s\\\".*%s\" %s" % (i, indexs[i], poem_path)
            else:
                grep_cmd = "grep -rl \"%s\" %s" % (indexs[i], poem_path)
            res = subprocess.run(grep_cmd, shell=True, stdout=subprocess.PIPE).stdout.decode()
            file_list = res.strip().split("\n")
            break
    else:
        file_list = os.listdir(poem_path)
        file_list = [os.path.join(poem_path, f) for f in file_list]

检索

到了最重要的检索这一步,其实相当简单。就是遍历这把json文件load进来,然后挨个匹配。需要注意的是数据文件中某些字段的值是个list,如content, paragraghs之类,在查找的时候就不能粗暴的直接比较,而要遍历去看。同时,为了应对标题或内容只记得大概的场景,对每个条件还做了模糊比配。由于能力有限,这里的模糊匹配就是contains操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    poem_list = []
    poem_contain_list = []
    for f in file_list:
        if not f.endswith(".json"):
            continue
        poems = json.load(open(f,"r"))
        if not indexs:
            poem_list.extend(poems)
            continue
        for poem in poems:
            match = is_match(poem, indexs)
            if match == 2:
                poem_list.append(poem)
            elif match == 1:
                poem_contain_list.append(poem)
                
#retrun value: 0, no match; 1, contain match; 2, all match
def is_match(poem, indexs):
    match_level_final = 2
    for i in indexs:
        match_level = 0
        if type(poem[i]) is list:
            for p in poem[i]:
                current_level = is_match_str(indexs[i], p)
                match_level = current_level if match_level < current_level else match_level
        else:
            match_level = is_match_str(indexs[i], poem[i])
        if match_level == 0:
            match_level_final = 0
            break
        match_level_final = match_level if match_level < match_level_final else match_level_final
    return match_level_final
  
def is_match_str(query, original):
    if not query in original:
        return 0
    if query != original:
        return 1
    return 2

返回

检索完成后,会得到poem_listpoem_contain_list两个匹配的list。最后的工作就是根据指定的条数从这两个list里按优先级加随机取得需要的记录。然后看情况做个繁体到简体的转换,注意下编码的问题,就大功告成了!其中,方法convertAll可以将str,dict,和list对象中的汉字做繁简转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    if count <= len(poem_list):
        poem_list = random.sample(poem_list, count)
    else:
        count -= len(poem_list)
        if count <= len(poem_contain_list):
            poem_list.extend(random.sample(poem_contain_list, count))
        else:
            poem_list.extend(poem_contain_list)

    if poem_type in traditional_data:
        poem_list = convertAll(poem_list)
    return json.dumps(poem_list, ensure_ascii=False)
  
def convertAll(in_obj, tp="s"):
    in_type = type(in_obj)
    if in_type is str:
        return convert(in_obj, tp)
    out_obj = None
    if in_type is list:
        out_obj=[]
    elif in_type is dict:
        out_obj={}
    else:
        print("in obj type is not list or dict: %s" % in_type)
        return out_obj
    for obj in in_obj:
        if in_type is list:
            out_obj.append(convertAll(obj, tp))
        elif in_type is dict:
            out_obj[obj] = convertAll(in_obj[obj], tp)
    return out_obj

返回的out_obj里就是我们需要的内容了👏

演示

完成这么一项大工程当然少不了演(de)示(se)一番。先试试诗仙的静夜思吧:

python3 poem.py -t "tang" -i '{"title":"静夜思"}' -c 1

1
2
3
4
5
6
7
[{
	"author": "李白",
	"paragraphs": ["牀前看月光,疑是地上霜。", "举头望山月,低头思故乡。"],
	"tags": ["唐诗三百首", "新乐府辞", "思乡", "中秋", "一年级上册", "月亮", "五言绝句", "小学古诗"],
	"title": "静夜思",
	"id": "ca2c489a-e433-4c0f-8248-77d354f0665e"
}]

呃。。。怎么和想象的不太一样??赶紧网上去查查,才知道最早记载的宋代版本确实是上面这四句。然后经过广大劳动人民的多次改编,就成了现在课本里的这一种了😂。。。真是又长见识了。

再试一个宋词看看吧,来个熟悉的,东坡先生的水调歌头 (注意,宋词只有词牌名,没有标题,我们课本里的标题都是后人加上去的)

python3 poem.py -t "song" -i '{"author":"苏轼", "rhythmic":"水调歌头"}' -c 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[{
	"author": "苏轼",
	"paragraphs": ["落日绣帘卷,亭下水连空。", "知君为我,新作窗户湿青红。", "长记平山堂上,枕江南烟雨,渺渺没孤鸿。", "认得醉翁语,山色有无中。", "一千顷,都镜净,倒碧峰。", "忽然浪起,掀舞一叶白头翁。", "堪笑兰台公子,未解庄生天籁,刚道有雌雄。", "一点浩然气,千里快哉风。"],
	"rhythmic": "水调歌头"
}, {
	"author": "苏轼",
	"paragraphs": ["安石在东海,从事鬓惊秋。", "中年亲友难别,丝竹缓离愁。", "一旦功成名遂,准拟东还海道,扶病入西州。", "雅志困轩冕,遗恨寄沧洲。", "岁云暮,须早计,要褐裘。", "故乡归去千里,佳处辄迟留。", "我醉歌时君和,醉倒须君扶我,惟酒可忘忧。", "一任刘玄德,相对卧高楼。"],
	"rhythmic": "水调歌头"
}, {
	"author": "苏轼",
	"paragraphs": ["明月几时有,把酒问青天。", "不知天上宫阙,今夕是何年。", "我欲乘风归去,又恐琼楼玉宇,高处不胜寒。", "起舞弄清影,何似在人间。", "转朱阁,低绮户,照无眠。", "不应有恨,何事长向别时圆。", "人有悲欢离合,月有阴晴圆缺,此事古难全。", "但愿人长久,千里共婵娟。"],
	"rhythmic": "水调歌头",
	"tags": ["宋词三百首"]
}, {
	"author": "苏轼",
	"paragraphs": ["昵昵儿女语,灯火夜微明。", "恩冤尔汝来去,弹指泪和声。", "忽变轩昂勇士,一鼓填然作气,千里不留行。", "回首暮云远,飞絮搅青冥。", "众禽里,真彩凤,独不鸣。", "跻攀寸步千险,一落百寻轻。", "烦子指间风雨,置我肠中冰炭,起坐不能平。", "推手从归去,无泪与君倾。"],
	"rhythmic": "水调歌头"
}]

哇,原来苏轼写过这么多水调歌头呢。当然王菲唱过的那首最有名,选自宋词三百首

后记

其实,项目中用到的诗词文件只是这个库里的一部分内容,它还提供了诗词作者的简介,不同条目在各个搜索引擎中的热度,全唐诗的平仄读音,等等。。。诗词检索只是个最基本的使用,我们还能做的更多,例如:

  • 诗词填空考试,闯关,PK……各种小游戏,玩中掌握传统文化
  • 和文字转语音结合起来做个自动读诗程序,把每天的睡前故事改成睡前读诗,文化熏陶从小做起
  • 自动写诗AI,写作没有灵感怎么办?AI助手来帮你

没有做不到,只有想不到😜

当然,直接读文件的效率也是个问题。不过不用担心这个,热心网友也写了把文件导入数据库之类的工具,上github上直接拿来用就行。

只有后端的诗词检索用起来还是不方便的,回头还是要记个TODO把查询页面补上!

完整代码传送:poem


今天的内容就到这了,如果觉得对你有帮助的话,请关注我的微信号,让我们共同成长进步~


本文作者:Jessychen
版权声明:本博客所有文章除特别声明外,均采用CC-BY-NC-SA 4.0 Int'l许可协议
如需转载,烦请注明出处: