中华古诗词数据库chinese-poetry:从JSON到SQLite与向量检索实战
发布时间:2026/10/2 0:20:05来源:尧图网络
简介中华古诗词数据库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, recursiveTrue): try: with open(path, encodingutf-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_modeWAL) # 提升写入并发 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_SIZE800是实测下来 SQLite 比较舒服的值再大内存占用上升但收益递减。PRAGMA journal_modeWAL开启写前日志批量写入时比默认的 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, contentpoems, content_rowidrowid ); -- 从主表灌数据进 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()一律显式写encodingutf-8SQLite 连接后执行PRAGMA encodingUTF-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_embeddingsTrue, batch_size64) # 查询 q model.encode([思念故乡的月亮], normalize_embeddingsTrue)[0] scores vecs q top np.argsort(-scores)[:5] for i in top: print(round(float(scores[i]), 3), texts[i][:30])normalize_embeddingsTrue是关键参数归一化之后矩阵乘法直接得到余弦相似度省掉逐条计算的循环。batch_size64是显存和速度的平衡点太小了慢太大了小显存机器会 OOM。先拿五千首试跑通了再上全量全量编码三十万首在单张消费级显卡上大概要一两个小时建议夜里挂着跑。向量存哪数据量不大时直接存成.npy文件查询时全量加载进内存算内积三十万条 768 维向量约 900MB内存够就无所谓。要更省就上 FAISS 建 IVF 索引或者用支持向量字段的数据库。这里不展开因为选型取决于你的部署环境但核心链路就是上面这二十行。最后说个我自己的习惯每次改完加载脚本我都会先只跑全唐诗一个目录确认总量在五万七千上下、抽查三首内容无误再放开全量。这个小样本先验证的习惯帮我省下过无数次删库重建的时间。诗词数据看着干净实际坑都在编码和字段缺失这些细节里慢一点反而快。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网