☰
中华古诗词数据库chinese-poetry:从JSON到SQLite与向量检索实战
2026/10/2 0:19:58 网站建设 项目流程

简介:中华古诗词数据库(chinese-poetry)是一份面向开发者、数据爱好者与传统文化研究者的开源数据集,旨在解决古典文集获取门槛高、电子化程度低的问题,让诗词数据以结构化形式方便地融入各类项目。资源包共2000个文件,以1978个JSON数据文件为核心,涵盖唐诗、宋诗、宋词、元曲及作者信息等分卷内容,另附少量Markdown说明、Python脚本、JavaScript与文本文件,便于检索、解析与二次开发,压缩包约91.18MB。数据收录5.5万首唐诗、26万首宋诗、2.1万首宋词,涉及唐宋近1.4万位诗人与两宋1.5千位词人,规模完整、字段清晰。目前已有818人学习下载。借助该库,读者可快速搭建诗词检索、推荐、可视化或NLP训练项目,省去繁琐的采集与清洗环节,直接获得可用的结构化语料。

1. 中华古诗词数据库 chinese-poetry:把十万首诗词装进本地库的第一课

做中文 NLP 的人迟早会撞上一件事:模型能写代码、能答数学题,一让它对个下联、仿个七律,立刻露馅。原因不复杂,训练语料里古诗词的密度太低,格式、平仄、意象这些隐性规律根本没被喂进去。chinese-poetry 这个项目就是冲这个缺口来的——它把从先秦到近代的诗词曲赋整理成结构化 JSON,按朝代、作者、体裁分门别类,最全的版本收录诗词规模在三十万首量级,是中文圈里少有的、拿来就能用的古诗词语料库。

它解决的不是"我想读诗"这种需求,而是"我要拿诗词做检索、做微调、做对仗生成、做知识图谱"这类工程需求。适合谁?三类人:想给大模型加古文语感的算法工程师、要做诗词检索或推荐的后端、以及拿它当数据库课程设计素材的学生。这篇不聊风花雪月,只讲怎么把它落到你自己的机器上,从拉数据、建库、查询一路走到能扛住真实检索的索引设计,中间该踩的坑一个不落。

2. 先看清 chinese-poetry 的数据结构再动手

很多人一上来就git clone,然后对着满屏文件夹发懵。先花十分钟把目录逻辑理清,后面省下的时间不止十倍。这个项目的组织方式是按"体裁 + 朝代"双维度切分的,理解这一点,你才知道该加载哪些文件、跳过哪些。

2.1 目录分层与文件命名规律

仓库根目录下大致是这么几类内容:全唐诗、全宋词、全宋诗、五代诗词、蒙学、论语、曹操诗集、楚辞、纳兰性德、御定全唐五代诗等文件夹,每个文件夹里再按卷次或批次切成多个 JSON 文件,命名通常是poet.tang.0.json、poet.tang.1000.json、ci.song.0.json这种"体裁.朝代.起始序号.json"的格式。

单个 JSON 文件的结构非常朴素,就是一个数组,每个元素是一首诗:

[ { "author": "李世民", "paragraphs": [ "秦川雄帝宅,函谷壮皇居。", "绮殿千寻起,离宫百雉馀。" ], "title": "帝京篇十首 一", "id": "8b1a9953c4611296a827abf8c47804d7" } ]

字段只有四个:author(作者)、paragraphs(诗句数组,一句一行)、title(标题)、id(内容哈希)。宋词那边多一个rhythmic字段表示词牌名,这是词和诗最大的结构差异,建表时别漏。

提示:不同批次的文件字段可能略有出入,比如早期文件没有id,加载前先做一次字段归一化,否则入库时会因为 key 缺失直接报错。

2.2 为什么选 JSON 而不是直接上数据库

有人会问,既然最终要进数据库,为什么不直接提供 SQL dump?因为 JSON 是中间格式里最中立的:你用什么库都行,MySQL、PostgreSQL、SQLite、甚至向量数据库,都能从同一份 JSON 出发。项目本身不绑定任何存储引擎,这个设计对做数据库课程设计的人特别友好——你可以拿同一份数据在三种数据库里各建一遍,横向对比查询性能,这本身就是一篇能交差的报告。

从工程角度看,JSON 还有个好处是可增量加载。全量三十万首一次性读进内存大概几百 MB,普通开发机扛得住,但如果你只想先跑通流程,完全可以只加载全唐诗一个文件夹,几千个文件里挑前十个,几分钟就能验证链路通不通。

2.3 数据规模与加载前的心理预期

按最全的版本估算,全唐诗约五万七千首,全宋诗约二十五万首,全宋词约两万首,加上其他零散集子,总量在三十万到三十三万首之间。这个量级对数据库来说不算大,但对"逐文件读 JSON 再逐条 insert"这种朴素写法来说,是灾难级的慢——三十万次单条插入,SQLite 能跑十几分钟,MySQL 走网络更久。

所以加载策略必须批量。后面第 3 章会给出批量插入的具体写法,这里先记住一个数字:批量大小控制在 500 到 1000 条一批,太小了事务开销大,太大了单条 SQL 语句超长会触发max_allowed_packet限制。

3. 用 Python 把 JSON 灌进 SQLite 的最小可跑通方案

这一章给一套能直接抄的代码。选 SQLite 是因为它零配置、单文件、跨平台,最适合先跑通再迁移。等你验证完查询逻辑,再换 MySQL 或 PostgreSQL 只是改连接串的事。

3.1 环境准备与依赖安装

只需要 Python 3.8 以上和标准库,不需要额外装包。如果你打算后面接向量检索,再单独装sentence-transformers,但那是第 6 章的事。

# 拉取数据,浅克隆即可,历史提交对建库没用 git clone --depth 1 https://github.com/chinese-poetry/chinese-poetry.git cd chinese-poetry # 确认目录结构 ls -d */ | head -20

--depth 1是关键参数,完整仓库带历史提交体积会大好几倍,而我们只要最新数据。克隆完先ls看一眼,确认全唐诗、全宋词这些目录都在。

3.2 建表语句与字段设计

先设计表结构。核心表就一张poems,把诗和词统一存进去,用genre字段区分体裁:

CREATE TABLE IF NOT EXISTS poems ( id TEXT PRIMARY KEY, -- 内容哈希,天然去重 title TEXT NOT NULL, author TEXT, dynasty TEXT, -- 朝代,从目录名推断 genre TEXT, -- 诗 / 词 / 曲 rhythmic TEXT, -- 词牌名,诗为 NULL content TEXT NOT NULL, -- 诗句用换行拼接 char_count INTEGER -- 字数,便于按长度筛选 ); CREATE INDEX IF NOT EXISTS idx_author ON poems(author); CREATE INDEX IF NOT EXISTS idx_dynasty_genre ON poems(dynasty, genre);

id直接用 JSON 里的哈希做主键,天然去重,重复加载同一批文件不会产生脏数据。content把paragraphs数组用\n拼成一个字符串,查询时再按需切分。char_count是冗余字段,但按字数筛选(比如找五言、七言)时能省掉一次全表扫描。

3.3 批量入库脚本与参数说明

下面是完整的加载脚本,核心是executemany批量插入:

import json import os import sqlite3 import glob DB_PATH = "poetry.db" BATCH_SIZE = 800 # 每批插入条数,500-1000 之间较稳 def iter_poems(root): """遍历目录,产出归一化后的诗词字典""" # 目录名 -> (朝代, 体裁) 的映射,按需扩充 mapping = { "全唐诗": ("唐", "诗"), "全宋诗": ("宋", "诗"), "全宋词": ("宋", "词"), "五代诗词": ("五代", "诗"), } for folder, (dynasty, genre) in mapping.items(): pattern = os.path.join(root, folder, "**", "*.json") for path in glob.glob(pattern, recursive=True): try: with open(path, encoding="utf-8") as f: data = json.load(f) except (json.JSONDecodeError, UnicodeDecodeError): continue # 跳过损坏文件,不中断整体流程 if not isinstance(data, list): continue for item in data: paragraphs = item.get("paragraphs") or [] if not paragraphs: continue content = "\n".join(paragraphs) yield { "id": item.get("id") or str(hash(content)), "title": item.get("title", "无题"), "author": item.get("author", "佚名"), "dynasty": dynasty, "genre": genre, "rhythmic": item.get("rhythmic"), "content": content, "char_count": len(content.replace("\n", "")), } def load(root): conn = sqlite3.connect(DB_PATH) conn.execute("PRAGMA journal_mode=WAL") # 提升写入并发 cur = conn.cursor() batch, total = [], 0 sql = """INSERT OR IGNORE INTO poems (id,title,author,dynasty,genre,rhythmic,content,char_count) VALUES (:id,:title,:author,:dynasty,:genre,:rhythmic,:content,:char_count)""" for poem in iter_poems(root): batch.append(poem) if len(batch) >= BATCH_SIZE: cur.executemany(sql, batch) conn.commit() total += len(batch) batch.clear() print(f"已入库 {total} 首") if batch: cur.executemany(sql, batch) conn.commit() total += len(batch) conn.close() print(f"完成,共 {total} 首") if __name__ == "__main__": load(".")

几个参数值得单独说。BATCH_SIZE=800是实测下来 SQLite 比较舒服的值,再大内存占用上升但收益递减。PRAGMA journal_mode=WAL开启写前日志,批量写入时比默认的 rollback journal 快不少,代价是目录下会多出-wal和-shm文件,这是正常的。INSERT OR IGNORE配合主键哈希,重复运行脚本不会报错也不会产生重复行,这点对反复调试很友好。

iter_poems里用生成器而不是一次性list(),是为了控制内存——三十万首全展开成列表大概占几百 MB,生成器逐条产出,内存曲线是平的。异常处理里continue而不是raise,是因为仓库里确实存在个别编码异常的历史文件,为它们中断整个加载不值得。

3.4 验证入库结果的三条查询

加载完先别急着写业务,跑三条查询确认数据是对的:

-- 1. 总量与体裁分布 SELECT genre, COUNT(*) FROM poems GROUP BY genre; -- 2. 抽查作者作品数,李白应该排前列 SELECT author, COUNT(*) c FROM poems GROUP BY author ORDER BY c DESC LIMIT 10; -- 3. 按字数找五言绝句(20 字) SELECT title, author FROM poems WHERE char_count = 20 LIMIT 5;

第一条确认没漏加载,第二条验证作者字段解析正常,第三条验证char_count计算无误。三条都符合预期,说明链路通了。如果第一条里某个体裁数量为 0,八成是目录名映射写错了,回去核对mapping字典。

4. 检索场景下的索引与查询优化

数据进库只是开始,真正决定体验的是查询。诗词检索有两类高频需求:按作者/朝代精确筛,和按内容关键词模糊搜。这两类的优化手段完全不同,混着做会两头不讨好。

4.1 精确检索与组合索引

按作者查是最常见的。WHERE author = '李白'这种查询,单列索引就够。但实际业务里往往是"李白在唐代写的诗"这种组合条件,这时候单列索引会退化成全表扫描。正确做法是建组合索引,且把选择性高的列放前面:

-- 组合索引,author 选择性高于 dynasty CREATE INDEX idx_author_dynasty ON poems(author, dynasty); -- 命中组合索引的查询 SELECT title, content FROM poems WHERE author = '苏轼' AND dynasty = '宋';

判断哪个列选择性高,用SELECT COUNT(DISTINCT author), COUNT(DISTINCT dynasty) FROM poems一比就知道,author 的基数远大于 dynasty,所以放前面。这个顺序反了,索引基本白建。

4.2 内容模糊搜索的两种做法

WHERE content LIKE '%明月%'这种查询,索引帮不上忙,因为前置通配符会让 B 树索引失效。数据量小的时候还能忍,三十万首全表扫描一次大概几百毫秒,勉强能用。但如果你要做的是"搜明月出现过的所有诗句"这种高频功能,就得换方案。

轻量做法是用 SQLite 的 FTS5 全文索引:

CREATE VIRTUAL TABLE poems_fts USING fts5( title, author, content, content='poems', content_rowid='rowid' ); -- 从主表灌数据进 FTS 表 INSERT INTO poems_fts(rowid, title, author, content) SELECT rowid, title, author, content FROM poems; -- 查询,比 LIKE 快一到两个数量级 SELECT title, author FROM poems_fts WHERE poems_fts MATCH '明月' LIMIT 20;

FTS5 对中文默认按字切分,效果一般但能用。要更准就得上分词器,或者干脆把内容交给向量检索,那是第 6 章的内容。这里先记住:LIKE 适合低频、小结果集;FTS5 适合高频、需要排序的相关性搜索。

4.3 分页查询的深翻页陷阱

LIMIT 20 OFFSET 100000这种深翻页,数据库要先扫描并丢弃前十万行,越翻越慢。诗词场景里用户很少翻到很后面,但如果你做的是"随机推荐一首",用 OFFSET 就是灾难。

正确做法是用游标分页,记住上一页最后一条的主键:

-- 第一页 SELECT id, title FROM poems ORDER BY id LIMIT 20; -- 后续页,传入上一页最后的 id SELECT id, title FROM poems WHERE id > '上一页最后的id' ORDER BY id LIMIT 20;

这样每页查询都是索引范围扫描,翻到第一万页和第一页耗时一样。代价是不能跳页,但对诗词浏览这种场景,顺序翻页完全够用。

5. 加载与查询环节的避坑清单

这一章全是血泪经验,每条都对应一个真实会卡住你的现象。

5.1 现象:加载到一半报database is locked

原因:SQLite 默认同一时刻只允许一个写连接,如果你一边跑加载脚本一边开着 DB Browser 之类的工具,写锁会冲突。

解决:加载期间关掉所有图形化工具,或者改用 WAL 模式(前面脚本里已经开了)。如果还是锁,检查有没有残留的poetry.db-journal文件,删掉再重试。

5.2 现象:中文显示成乱码或问号

原因:读取文件时没指定编码,或者数据库连接没设 UTF-8。Windows 上尤其常见,默认编码可能是 GBK。

解决:open()一律显式写encoding="utf-8",SQLite 连接后执行PRAGMA encoding='UTF-8'。已经建好的库如果编码错了,只能删库重建,没有后悔药。

5.3 现象:char_count和实际字数对不上

原因:paragraphs里除了诗句,可能混入标点、空格,甚至个别文件里带 HTML 实体。直接len()会把标点也算进去。

解决:统计前先做清洗,re.sub(r'[^\u4e00-\u9fff]', '', content)只保留汉字再计数。这样五言绝句稳定是 20,七律稳定是 56,便于按体裁精确筛选。

5.4 现象:同一首诗在库里出现两次

原因:不同文件夹之间有内容重叠,比如全唐诗和御定全唐五代诗收录了同一首,但id字段一个有一个没有,导致哈希对不上。

解决:主键用内容哈希而不是文件里的id,即hashlib.md5(content.encode()).hexdigest()。这样只要内容一样,不管来自哪个文件都会被INSERT OR IGNORE挡掉。代价是计算哈希有开销,但三十万首也就几秒钟的事。

5.5 现象:查询WHERE author = '李白'返回 0 条

原因:数据里作者名可能带空格或异体字,比如"李白 "或"李白"用了不同的 Unicode 码位。

解决:入库时对author做strip()和 Unicode 归一化unicodedata.normalize('NFKC', name)。查询时也做同样处理,两边一致才能匹配上。这个坑在跨数据源合并时特别常见。

6. 把诗词库接进向量检索:一个能落地的进阶玩法

前面做的都是关键词检索,但诗词检索有个天然痛点:用户搜"思乡",希望返回的是"举头望明月"这种语义相关但字面不含"思乡"的句子。这就是向量检索的用武之地,也是这个数据库真正能拉开差距的玩法。

思路很直接:把每首诗的content用句向量模型编码成向量,存进支持向量检索的库,查询时把 query 也编码成向量,算余弦相似度取 Top-K。模型选text2vec-base-chinese或bge-small-zh这类中文小模型就够,诗词句子短,大模型是浪费。

from sentence_transformers import SentenceTransformer import sqlite3, numpy as np model = SentenceTransformer("shibing624/text2vec-base-chinese") conn = sqlite3.connect("poetry.db") rows = conn.execute("SELECT id, content FROM poems LIMIT 5000").fetchall() # 编码,normalize 后内积等价于余弦相似度 texts = [r[1].replace("\n", "") for r in rows] vecs = model.encode(texts, normalize_embeddings=True, batch_size=64) # 查询 q = model.encode(["思念故乡的月亮"], normalize_embeddings=True)[0] scores = vecs @ q top = np.argsort(-scores)[:5] for i in top: print(round(float(scores[i]), 3), texts[i][:30])

normalize_embeddings=True是关键参数,归一化之后矩阵乘法直接得到余弦相似度,省掉逐条计算的循环。batch_size=64是显存和速度的平衡点,太小了慢,太大了小显存机器会 OOM。先拿五千首试,跑通了再上全量,全量编码三十万首在单张消费级显卡上大概要一两个小时,建议夜里挂着跑。

向量存哪?数据量不大时直接存成.npy文件,查询时全量加载进内存算内积,三十万条 768 维向量约 900MB,内存够就无所谓。要更省就上 FAISS 建 IVF 索引,或者用支持向量字段的数据库。这里不展开,因为选型取决于你的部署环境,但核心链路就是上面这二十行。

最后说个我自己的习惯:每次改完加载脚本,我都会先只跑全唐诗一个目录,确认总量在五万七千上下、抽查三首内容无误,再放开全量。这个"小样本先验证"的习惯帮我省下过无数次删库重建的时间。诗词数据看着干净,实际坑都在编码和字段缺失这些细节里,慢一点反而快。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询