简介:这份资源是 chinese-poetry 项目的完整本地化压缩包,面向从事中文文本处理、数据挖掘与诗词应用开发的学习者和工程师。由于原仓库在 GitHub 上 clone 或下载 zip 速度较慢,作者委托朋友下载后整理分享,方便需要离线使用诗词语料的人直接获取。压缩包共 1371 个文件,约 84.85MB,其中 1339 个 json 文件是核心语料,涵盖唐诗、宋词、元曲及作者信息等结构化数据;另有少量 md 说明文档、png 与 jpeg 图片、yml 配置、py 脚本、js 脚本、db 数据库文件等,兼顾数据读取与二次开发。目前已有 1083 人学习下载。拿到后可直接用于构建诗词检索、推荐、知识图谱或 NLP 训练数据集,省去网络下载与格式转换的麻烦,目录按朝代与体裁组织,便于按需筛选和批量处理。
1. 诗词数据工程的第一块砖:为什么我建议你从这份 JSON 压缩包开始
如果你做过诗词类应用,大概率经历过这个场景:想做一个飞花令小工具,或者给 App 加一个「每日一诗」模块,结果卡在数据源上。爬某诗词网站吧,反爬策略三天两头变;找现成数据库吧,字段残缺、繁简混杂、作者朝代对不上。折腾两周,代码一行没写,全耗在洗数据上了。这份「中国诗词大全 json 版」压缩包解决的正是这个前置问题——它把诗词数据以结构化 JSON 的形式打包好,拿来就能解析、就能入库、就能直接喂给前端。适合谁?做诗词类小程序、古文学习工具、AI 对对联 Demo、NLP 文本分析练手项目的开发者。你不需要再关心数据从哪来,只需要关心拿它做什么。
2. 数据结构拆解:一份 JSON 里到底装了什么
2.1 文件组织方式与字段设计
拿到压缩包解压后,常见的目录组织是按体裁或朝代分文件,比如poet.tang.json、poet.song.json、shijing.json、lunyu.json这类命名。每个文件是一个 JSON 数组,数组里每个对象代表一首诗词。字段设计通常长这样:
{ "title": "静夜思", "author": "李白", "paragraphs": [ "床前明月光,疑是地上霜。", "举头望明月,低头思故乡。" ], "strains": [], "tags": ["思乡", "五言绝句"] }title是标题,author是作者名,paragraphs是正文数组——注意它是数组不是字符串,因为绝句和律诗需要按联拆分,前端渲染时逐行显示更自然。strains一般用于词牌名或曲牌名,诗类数据里通常是空数组。tags是分类标签,不是所有版本都有,但如果有,对做筛选和推荐很有用。
这个字段设计的好处是扁平、无嵌套、无外键依赖。你不需要先查作者表再查诗词表,一首诗的所有信息都在一个对象里。坏处也明显:作者信息重复存储,如果做作者维度的统计,需要自己去重。常见做法是导入时建两张表,一张poets一张poems,用作者名做关联。
2.2 数据规模与体裁覆盖
从「大全」这个定位来看,这类压缩包通常覆盖唐诗、宋词、诗经、论语、楚辞、纳兰性德词等主要体裁。唐诗宋词是重头,各自几万首的量级。诗经 305 篇、论语 20 篇这类经典是完整收录。文件大小通常在几十 MB 级别,解压后纯文本 JSON 可能到 100MB 上下,取决于是否包含注释和译文。
这里有个容易翻车的地方:不同体裁的文件字段可能不完全一致。比如诗经的paragraphs可能按「章」拆分,而唐诗按「联」拆分。你在写解析器的时候不能假设所有文件结构完全统一,得做字段兼容。我一般会先写一个探测脚本,遍历所有 JSON 文件,统计每个文件的字段集合和类型,确认没有意外字段再写正式导入逻辑。
import json import os from collections import defaultdict def probe_schema(data_dir): """遍历目录下所有 JSON 文件,统计字段出现情况""" schema_map = defaultdict(set) for fname in os.listdir(data_dir): if not fname.endswith('.json'): continue fpath = os.path.join(data_dir, fname) with open(fpath, 'r', encoding='utf-8') as f: data = json.load(f) # 只取前 50 条做样本,避免全量加载 for item in data[:50]: for key in item.keys(): schema_map[fname].add(key) return schema_map # 输出每个文件的字段集合,快速发现结构差异 result = probe_schema('./chinese-poetry') for fname, keys in result.items(): print(f"{fname}: {sorted(keys)}")这段脚本的作用是「摸清家底」。data[:50]取前 50 条做样本,避免几万条全加载拖慢速度。defaultdict(set)保证同一个文件里重复字段只记一次。跑完之后你会得到一张表,哪个文件有哪些字段一目了然。如果发现某个文件多了notes或translation字段,你就知道导入时要不要单独处理。
2.3 繁简与异体字问题
这是诗词数据里最容易被低估的坑。同一个字在不同版本里可能写法不同,比如「床前明月光」的「床」在某些版本里是「牀」,「举头望明月」的「举」可能是「擧」。如果你的应用面向普通用户,需要统一转成简体;如果面向学术场景,反而要保留原字形。
常见做法是在导入层做一次规范化,但保留原始字段。比如加一个paragraphs_simplified字段,用 OpenCC 或类似库做转换,原始paragraphs不动。这样前端默认展示简体,需要时还能切回原文。转换时注意多音字和通假字,机器转换不可能 100% 准确,所以原始数据一定要留着,这是你的后悔药。
3. 从 JSON 到数据库:导入流程与索引设计
3.1 建表与批量插入
JSON 文件适合传输和阅读,但真要拿来做查询、分页、全文检索,还是得进数据库。SQLite 适合本地工具和小型应用,MySQL 或 PostgreSQL 适合服务端。下面以 SQLite 为例,给一个完整的导入脚本。
import json import sqlite3 import os def init_db(db_path): conn = sqlite3.connect(db_path) cur = conn.cursor() cur.execute(''' CREATE TABLE IF NOT EXISTS poems ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT, content TEXT NOT NULL, genre TEXT, tags TEXT, source_file TEXT ) ''') # 为作者和标题建索引,加速按作者查询和标题搜索 cur.execute('CREATE INDEX IF NOT EXISTS idx_author ON poems(author)') cur.execute('CREATE INDEX IF NOT EXISTS idx_title ON poems(title)') conn.commit() return conn def import_json(conn, data_dir): cur = conn.cursor() total = 0 for fname in os.listdir(data_dir): if not fname.endswith('.json'): continue # 从文件名推断体裁,比如 poet.tang.json -> tang genre = fname.replace('poet.', '').replace('.json', '') fpath = os.path.join(data_dir, fname) with open(fpath, 'r', encoding='utf-8') as f: data = json.load(f) batch = [] for item in data: title = item.get('title', '') author = item.get('author', '') paragraphs = item.get('paragraphs', []) content = '\n'.join(paragraphs) # 按行拼接,保留换行 tags = ','.join(item.get('tags', [])) batch.append((title, author, content, genre, tags, fname)) # 批量插入,比逐条插入快一个数量级 cur.executemany( 'INSERT INTO poems (title, author, content, genre, tags, source_file) VALUES (?,?,?,?,?,?)', batch ) total += len(batch) conn.commit() print(f"{fname}: 导入 {len(batch)} 条") print(f"总计导入 {total} 条") return conn conn = init_db('./poems.db') import_json(conn, './chinese-poetry') conn.close()几个关键点说明。content字段用'\n'.join(paragraphs)把数组拼成字符串,换行符保留,前端展示时按\n切分即可。genre从文件名推断,这样你不需要额外维护一张体裁表。executemany是批量插入,比循环单条execute快很多,几万条数据几秒钟就能跑完。索引建在author和title上,因为这两个字段最常出现在查询条件里。
3.2 全文检索方案
如果你要做「搜索包含某个字的诗句」这种功能,LIKE '%字%'在几万条数据上还能撑住,但到几十万条就明显慢了。SQLite 可以用 FTS5 扩展建虚拟表,MySQL 可以用全文索引,PostgreSQL 有tsvector。以 SQLite FTS5 为例:
-- 创建 FTS5 虚拟表,content 字段参与全文索引 CREATE VIRTUAL TABLE poems_fts USING fts5( title, author, content, content='poems', content_rowid='id' ); -- 把已有数据灌入 FTS 表 INSERT INTO poems_fts (rowid, title, author, content) SELECT id, title, author, content FROM poems; -- 查询包含「明月」的诗句,按相关度排序 SELECT p.title, p.author, p.content FROM poems_fts f JOIN poems p ON p.id = f.rowid WHERE poems_fts MATCH '明月' ORDER BY rank LIMIT 20;content='poems'表示 FTS 表不单独存一份数据,而是引用poems表,节省空间。MATCH是 FTS5 的查询语法,比LIKE快得多,而且支持分词和相关度排序。注意中文分词需要额外配置,SQLite 自带的分词器对中文支持一般,如果搜索效果不理想,可以考虑用 jieba 先分词再存入,或者换用支持中文分词的搜索引擎。
3.3 分页与随机推荐
诗词类应用经常需要「随机来一首」的功能。用ORDER BY RANDOM() LIMIT 1在几万条数据上没问题,但数据量大了会全表扫描。更好的做法是先用COUNT(*)拿到总数,然后随机一个 offset:
import random import sqlite3 def random_poem(conn): cur = conn.cursor() cur.execute('SELECT COUNT(*) FROM poems') total = cur.fetchone()[0] offset = random.randint(0, total - 1) cur.execute('SELECT title, author, content FROM poems LIMIT 1 OFFSET ?', (offset,)) return cur.fetchone()OFFSET在数据量极大时也有性能问题,但几十万条以内可以接受。如果追求极致,可以预生成一个随机 ID 列表,或者用WHERE id >= ?的方式。分页查询同理,LIMIT ? OFFSET ?是标准做法,前端传页码和每页条数,后端算 offset。
4. 避坑与排查:导入诗词数据时最容易翻车的几个点
4.1 编码问题导致乱码
现象:导入后数据库里出现明月这种乱码,或者直接报UnicodeDecodeError。
原因:JSON 文件本身是 UTF-8,但 Python 在某些系统上默认用 GBK 打开文件。另外有些数据源在生成 JSON 时用了ensure_ascii=True,中文被转义成\uXXXX,虽然不影响解析,但直接看文件内容会懵。
解决:打开文件时强制指定encoding='utf-8'。如果遇到\uXXXX转义,json.load会自动还原,不用手动处理。如果文件带 BOM 头,用encoding='utf-8-sig'。
4.2 字段缺失导致 KeyError
现象:导入脚本跑到某一条突然报KeyError: 'author'。
原因:不是所有诗词都有作者,比如《诗经》大部分篇目作者不详,某些版本里author字段直接不存在,而不是空字符串。
解决:统一用item.get('author', '')取值,给默认值。同理tags、strains这些字段也可能缺失。写解析器时永远假设字段可能不存在,这是处理第三方数据的铁律。
4.3 重复数据与唯一性冲突
现象:同一个文件导入两次,数据库里出现两份一模一样的数据。
原因:没有做去重。JSON 文件本身可能包含重复条目,或者你重复执行了导入脚本。
解决:建表时加唯一约束,比如UNIQUE(title, author, content),插入时用INSERT OR IGNORE。或者在导入前先清空表。我一般会在导入脚本开头加一个--reset参数,默认不删数据,需要重置时手动指定。
4.4 大文件内存溢出
现象:导入某个特别大的 JSON 文件时,程序被系统杀掉,或者报MemoryError。
原因:json.load一次性把整个文件读进内存,如果文件几百 MB,内存不够就崩了。
解决:用ijson库做流式解析,逐条读取而不是全量加载。或者提前把大文件拆成小文件。对于几十 MB 的文件,现代机器一般扛得住,但如果你在容器里跑,内存限制可能只有 512MB,那就得注意了。
4.5 繁简转换过度
现象:把「千里江陵一日还」转成了「千里江陵一日还」,看起来没问题,但把「髮」转成了「发」,而原文其实是「頭髮」的「髮」,转换后变成「头发」,意思对了但字形丢了。
原因:繁简转换不是一一对应的,一个字可能对应多个简体字,机器转换会丢信息。
解决:原始字段永远保留,转换结果单独存一个字段。前端默认展示转换后的,但提供「查看原文」的开关。做学术或严肃场景时,直接展示原文,不做转换。
5. 二次开发:从数据包到可上线的诗词应用
5.1 基于标签的推荐与筛选
数据里的tags字段是二次开发的金矿。你可以基于标签做「同类推荐」——用户在看一首「思乡」主题的诗,侧边栏推荐其他「思乡」标签的诗。实现上就是一句 SQL:
SELECT title, author, content FROM poems WHERE tags LIKE '%思乡%' AND id != ? ORDER BY RANDOM() LIMIT 5;注意LIKE '%思乡%'在 tags 字段上做模糊匹配,如果 tags 是逗号分隔的,更严谨的做法是用FIND_IN_SET(MySQL)或者把 tags 拆成关联表。数据量不大时模糊匹配够用,数据量大了再考虑优化。
5.2 飞花令功能的实现思路
飞花令的核心逻辑是「给定一个字,找出所有包含这个字的诗句」。用 FTS5 的MATCH查询就能实现:
def feihualing(conn, keyword, limit=10): cur = conn.cursor() cur.execute(''' SELECT p.title, p.author, p.content FROM poems_fts f JOIN poems p ON p.id = f.rowid WHERE poems_fts MATCH ? ORDER BY rank LIMIT ? ''', (keyword, limit)) return cur.fetchall()MATCH的参数直接传关键字,FTS5 会做分词和匹配。如果要做「诗句接龙」,那就需要更复杂的逻辑——提取每句的最后一个字,然后找以这个字开头的诗句。这需要在导入时额外建一个「首字索引」和「尾字索引」,属于进阶改造。
5.3 数据校验与质量监控
上线前建议跑一遍数据质量检查,确认没有空标题、空内容、作者字段异常等问题。下面这个脚本可以快速统计异常数据:
def quality_check(conn): cur = conn.cursor() checks = { '空标题': "SELECT COUNT(*) FROM poems WHERE title = '' OR title IS NULL", '空内容': "SELECT COUNT(*) FROM poems WHERE content = '' OR content IS NULL", '空作者': "SELECT COUNT(*) FROM poems WHERE author = '' OR author IS NULL", '内容过短': "SELECT COUNT(*) FROM poems WHERE LENGTH(content) < 10", } for name, sql in checks.items(): cur.execute(sql) count = cur.fetchone()[0] print(f"{name}: {count} 条")跑完心里有数,哪些数据需要清洗、哪些可以忽略。空作者在诗经里很正常,不用管;空内容就是脏数据,得删。
5.4 一个我踩过的坑
早期做诗词应用时,我直接把paragraphs数组用str()转成字符串存进数据库,结果前端拿到的是['床前明月光,疑是地上霜。', '举头望明月,低头思故乡。']这种带方括号和引号的字符串,展示出来极其难看。后来改成'\n'.join()才正常。从那以后我每次处理数组字段都强制走一遍「拼接还是序列化」的判断,再也不敢随手str()了。希望帮到你。
本文还有配套的精品资源,点击获取