☰
数据清洗与数据库设计:从纯文本到可查询的唐诗数据集
2026/10/9 12:23:13 网站建设 项目流程

简介:面向需要中文古典诗歌语料开展练习与开发的数据分析爱好者、数据库初学者及Web前端人员,这份“唐诗三百首”结构化数据集可统一用于SQL查询、JSON解析、CSV表格处理和Excel筛选,帮助用户快速搭建文本处理、数据入库或接口演示的小型实验。压缩包整体约141KB,共4个文件,json、csv、sql、xlsx各1个,分别适配程序解析、数据交换、数据库导入和电子表格场景,覆盖主流数据格式且便于按需取用。这种多格式设计便于从不同工具切入同一份数据,省去格式转换环节。数据量为320条,记录以统一格式存放,适合批量检索、统计和二次加工;SQL脚本可直接导入MySQL等数据库,JSON便于前端或接口测试,CSV与Excel则适合快速浏览和编辑。资源已有1452人学习下载,体积紧凑、上手门槛低,是入门中文文本分析和数据库操作的实用样本集。

1. 数据库-唐诗三百首数据集:不只是三百首诗的文本堆

拿到“数据库-唐诗三百首数据集”这个标题时,我原以为就是从网上搬一份唐诗三百首的文本,导入数据库就完事。真正动手才发现,网上流传的版本十个里有八个是给读者读的,不是给程序用的:标题带注释、正文混着诗序、作者名字繁简体混写、标点全角半角交替出现。直接导入数据库,你以为拿到的是三百条干净记录,实际拿到的是三百首诗的“毛坯房”。这篇笔记围绕“数据库-唐诗三百首数据集”讲清楚一件事:如何把一份纯文本诗集,变成一套能查、能算、能支撑上层应用的结构化数据。适合正在做中文语料库、诗词类应用或者文本检索课程的从业者,读完可以直接照着复现。

2. 数据集从哪来:三份公开语料的取舍与清洗基准

2.1 语料对比:三份来源各自的脾气

构建数据库-唐诗三百首数据集的第一步不是建表,而是选语料。常见做法是同时拉取三份公开来源做交叉比对:一份是某古籍站点整理的简体横排本,优点是断句干净,缺点是加了大量注释和赏析文字;一份是某开源诗歌库导出的 JSON,字段齐全但作者名和题目常有OCR残留;还有一份是繁体竖排点校本,准确率高但需要用工具做繁简转换。

我一般会以繁体点校本为底本,因为它的文字错误率最低。简体横排本次之,JSON 版只用来补漏。三份语料放在同一个工作目录下,文件名前缀区分来源。

来源优点主要问题用途
某点校本字词准确、诗序完整繁体、无结构化字段底本
某简体横排本站点断句规范混入赏析文字交叉校对
某开源诗歌库自带字段、有作者索引标题常带“其一”“并序”等噪声补漏和元数据参考

选定底本之后要做的第一件事是统一编码。所有文件强制转成 UTF-8,去掉 BOM。我见过最典型的翻车现场是:某 JSON 文件头部带 BOM,Python 读取后第一首诗的题目变成了\ufeff鹿柴,查询时永远查不到。转换用一条命令就能解决:

sed -i '1s/^\xEF\xBB\xBF//' source_poem.json iconv -f GBK -t UTF-8 source_poem.txt > source_poem_utf8.txt

第一条命令删除 UTF-8 BOM,第二条命令把 GBK 编码的文本转成 UTF-8。如果源文件本身就是 UTF-8,第二条命令会报错,这时可以先用file命令确认编码格式。常见坑是直接把 GBK 文件设置为 UTF-8 读取,Python 会抛出UnicodeDecodeError,立刻中断整个清洗流程。

2.2 清洗流水线:用脚本把文本切成结构化字段

拿到干净的文本之后,下一步是把纯文本切分成“题目 + 作者 + 正文”三要素。唐诗三百首的常见排版格式是“题目 / 作者 / 正文”,正文内每两句一行,绝句四句、律诗八句。切分逻辑我用正则主匹配,辅以人工抽查规则。

import re import json raw_text = open("tangpoem_clean.txt", encoding="utf-8").read() blocks = re.split(r"\n(?=[\u4e00-\u9fa5]{2,12}$)", raw_text) records = [] for block in blocks: lines = block.strip().split("\n") if len(lines) < 3: continue title = lines[0].strip() author = lines[1].strip() body = "".join(lines[2:]).replace(" ", "") if len(body) < 20: continue records.append({ "title": title, "author": author, "body": body, "length": len(body) }) with open("poems_parsed.json", "w", encoding="utf-8") as f: json.dump(records, f, ensure_ascii=False, indent=2)

这段脚本的关键正则\n(?=[\u4e00-\u9fa5]{2,12}$)匹配“下一个换行后是 2 到 12 个汉字且之后就是行尾”的位置,以此作为新诗的分隔点。实际使用中这个正则会漏掉两类边界情况:一是作者名超过三字,如“王昌龄”“李商隐”本身是三个字,但个别冷门作者名是四字;二是诗前带有序号的版本,如“《渭城曲》并序”。这两个问题会在避坑章节细讲。

2.3 校验关卡:标题全不全、作者对不对、字数量得准不准

清洗输出的 JSON 先别急着入库,先跑几个校验函数。我每次都会做的三个校验是:全表标题是否为空、作者名是否落在预期集合内、正文按七言五言的字数是否符合格律常识。

parsed = json.load(open("poems_parsed.json", encoding="utf-8")) titles = [p.get("title") for p in parsed] authors = set(p.get("author") for p in parsed) assert all(t and len(t) > 1 for t in titles), "存在空标题" assert "李白" in authors and "杜甫" in authors, "作者集合异常" for p in parsed: body = p["body"] if len(body) % 5 == 0: p["genre"] = "五言" elif len(body) % 7 == 0: p["genre"] = "七言" else: p["genre"] = "杂言"

校验逻辑并不复杂,但能第一时间发现切分错误。如果某首诗被切成正文只有十几个字,通常说明题目被当成了正文的一部分,需要手动检查。还有一种情况是注释行没删干净,正文里混入“(一作……)”之类的夹注,这类内容会影响后续的全文检索结果。校验通过后再进入正式的数据库结构设计阶段。

3. 表结构设计与入库:从文本到可查询的三张表

3.1 表设计:作品表、作者表、句子表各司其职

数据库-唐诗三百首数据集的核心在于结构设计。只建一张大宽表存全部文本确实能跑,但查询“某个作者的所有五言绝句”就会变成正则扫描,性能完全不可控。我把数据拆成三张表:作者表存作者基本信息,作品表存诗歌元数据,句子表存每一联的诗句。作者和作品是一对多,作品和句子是一对多。

CREATE TABLE authors ( author_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE ); CREATE TABLE poems ( poem_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author_id INTEGER NOT NULL REFERENCES authors(author_id), genre TEXT DEFAULT '杂言', body TEXT NOT NULL, source_url TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE poem_lines ( line_id INTEGER PRIMARY KEY AUTOINCREMENT, poem_id INTEGER NOT NULL REFERENCES poems(poem_id), line_order INTEGER NOT NULL, content TEXT NOT NULL );

作品表里的body字段保留整首诗全文,用于全文检索和展示;句子表用line_order记录顺序。为什么不只存body?因为后续做韵脚分析、诗句接龙、按句检索时,句子表可以直接用LIKE查询单句而不必整诗扫描。三张表各自独立维护,后续增加注释表、赏析表都不需要改动核心结构。

3.2 入库脚本:幂等导入与事务控制

表结构建好之后写导入脚本。导入逻辑要有幂等性:同一份 JSON 跑两次不会产生重复记录。常见做法是以title + author_id作为业务唯一键,插入前先查一遍。

import sqlite3 import json conn = sqlite3.connect("tangpoem.db") cur = conn.cursor() poems = json.load(open("poems_parsed.json", encoding="utf-8")) for p in poems: cur.execute( "INSERT OR IGNORE INTO authors(name) VALUES(?)", (p["author"],) ) cur.execute("SELECT author_id FROM authors WHERE name = ?", (p["author"],)) author_id = cur.fetchone()[0] cur.execute( "SELECT poem_id FROM poems WHERE title = ? AND author_id = ?", (p["title"], author_id) ) if cur.fetchone(): continue cur.execute( "INSERT INTO poems(title, author_id, genre, body) VALUES(?,?,?,?)", (p["title"], author_id, p["genre"], p["body"]) ) poem_id = cur.lastrowid lines = [l.strip() for l in p["body"] if l.strip()] for order, line in enumerate(lines, start=1): cur.execute( "INSERT INTO poem_lines(poem_id, line_order, content) VALUES(?,?,?)", (poem_id, order, line) ) conn.commit() conn.close()

这段脚本里有几个参数值得注意:INSERT OR IGNORE依赖authors.name的唯一约束,重复作者不会报错而是直接跳过;poem_id用lastrowid获取,确保外键关联正确;enumerate(..., start=1)保证句子顺序从 1 开始,方便后续按顺序拼接。整个循环包在一个事务里,commit 放在最后,入库三百首左右的数据量毫秒级完成。

3.3 字段扩展:注释、译文、赏析怎么存

唐诗三百首的常见版本都带有注释或赏析,这些内容建议不要塞进poems.body。我会单独建一张annotations表,用poem_id关联,每条注释按annotation_type区分“注音”“释义”“赏析”“译文”。

CREATE TABLE annotations ( annotation_id INTEGER PRIMARY KEY AUTOINCREMENT, poem_id INTEGER NOT NULL REFERENCES poems(poem_id), annotation_type TEXT NOT NULL, content TEXT NOT NULL );

查询一首含注释的诗时,用一次 JOIN 拉出所有注解,按类型归类渲染。这个设计的好处是把“原文”和“解读”隔离开,全文检索时不会因为检索词命中了赏析文字而带出整首诗。很多现成的语料库把赏析混在诗正文后面,检索“明月”匹配到赏析里的“明月寄相思”,导致相关度排序完全失真。分表存是唯一的干净出路。

4. 查询与参数调整:让数据集真正回答你的问题

4.1 高频查询:按作者、按主题、按句式检索的 SQL

表结构和数据都到位后,最常见的三个查询是:查某个作者的全部诗作、查含有某个意象词的诗句、查所有五言绝句。这三个查询覆盖了绝大多数唐诗类应用的基础功能。

-- 查询某作者的全部作品 SELECT p.title, p.genre, p.body FROM poems p JOIN authors a ON a.author_id = p.author_id WHERE a.name = '王维'; -- 查询包含"明月"的诗句及所属诗题 SELECT pl.content, p.title, a.name FROM poem_lines pl JOIN poems p ON p.poem_id = pl.poem_id JOIN authors a ON a.author_id = p.author_id WHERE pl.content LIKE '%明月%'; -- 统计各类型诗作数量 SELECT genre, COUNT(*) AS cnt FROM poems GROUP BY genre ORDER BY cnt DESC;

第一个查询依赖authors.name的唯一索引,第二条查询只扫描句子表,第三条查询是聚合操作。在三百首规模下这些查询都在毫秒级,但要注意第二条的LIKE '%明月%'无法利用索引,数据量放大到三万首时就会明显变慢,后面会讲怎么优化。

4.2 索引与字符集参数:怎么设才不会慢到翻车

数据库-唐诗三百首数据集如果只在本地用,索引不必太激进,但两张外键必须建索引。poems.author_id和poem_lines.poem_id是 JOIN 的桥梁,不建索引会让 JOIN 变成双重循环。

CREATE INDEX idx_poems_author ON poems(author_id); CREATE INDEX idx_lines_poem ON poem_lines(poem_id); CREATE INDEX idx_lines_content ON poem_lines(content);

第三条索引比较特殊。MySQL 里对VARCHAR字段加索引时可以用前缀长度:CREATE INDEX idx_lines_content ON poem_lines(content(20)),只索引前二十个字符。这样做是因为LIKE '%明月%'用不上索引,但如果查询固定在“从句首开始的某几个字”,前缀索引就能命中。SQLite 不支持前缀索引参数,但支持COLLATE NOCASE,适合英文或拼音字段,中文场景用默认BINARY即可。

字符集参数是另一个必调项。MySQL 建库时统一使用utf8mb4,排序规则用utf8mb4_unicode_ci。如果库是latin1,中文写入直接变乱码;如果排序规则选utf8mb4_general_ci,对中文来说差异不大,但涉及生僻字时unicode_ci更稳妥。SQLite 没有单独的字符集参数,存储的就是 UTF-8 字节流,但 Python 连接时务必设置sqlite3.connect("tangpoem.db")后执行一次PRAGMA encoding;确认库确实是 UTF-8。

4.3 模糊匹配与正则:扩展检索的边界

LIKE是模糊查询的主力,但有两个限制:中文字符后匹配无法利用索引、多个关键字组合需要嵌套OR。我一般会再把 SQLite 的正则函数注册进来,用于“按韵脚检索”这类复杂查询。

SELECT content FROM poem_lines WHERE content GLOB '*[月雪]';

GLOB是 SQLite 自带的类正则语法,*匹配任意字符,[月雪]匹配方括号内任一单字。这条语句能查出所有以“月”或“雪”收尾的诗句,是初级的韵脚检索。如果要更严格的押韵判断,需要先把韵脚字转成拼音再比较韵母,这一步放到进阶章节演示。这里先记住:GLOB是大小写敏感的,中文不受影响,但如果库里混了英文字符就要小心。

5. 避坑指南:唐诗数据集落地中的五个典型问题

5.1 问题一:题目带着“并序”混入正文,导致切分错位

现象:某首诗的body字段里有几十个字的诗序,整首诗被识别成“杂言”,字数和格律全部错乱。

原因:唐诗里“并序”是常见结构,清洗正则把它当成了下一首诗的标题,或者把序文当成了正文的一部分。

解决:在清洗时单独提取“并序”和“并记”两种情况。常见做法是先按“并序”切分,把序文丢弃或单独存到注释表,再执行常规切分。我在清洗脚本里加了re.sub(r"并序.*", "", block, flags=re.S)之后,杂言诗数量立刻降了三分之一。

5.2 问题二:繁简体混排导致查询缺一整个字

现象:检索“云”只能查到简体数据,繁体版里的“雲”全部漏掉,统计结果比预期少几首。

原因:三份语料来源混杂,繁体点校本没有统一的繁简转换就直接入库了。

解决:清洗阶段统一做繁简转换。Python 里用opencc库,配置文件选择t2s(繁体转简体),转换后再执行去重。注意opencc会把“乾隆”等专名里的“乾”转成“干”,需要做一次专名词表回刷。另一个办法是在查询层做繁简双写匹配,但干扰排序逻辑,不如入库前就统一。

5.3 问题三:同一首诗在不同版本里标题不同,入库重复

现象:抽查时发现某首诗出现两条记录,标题分别是“相思”和“江上赠李龟年”,作者一样,正文完全相同。

原因:不同版本采用了不同的标题体系,有的用泛称,有的用原题,业务唯一键title + author_id失效。

解决:把唯一键改成title + author_id + body前20字三重判定。更稳妥的是导入脚本里对body做 MD5,用body_md5作为唯一键的一部分。我后来在表里加了一列body_hash TEXT UNIQUE,重复入库的问题彻底根治。

5.4 问题四:字符串长度参数设错导致正文被截断

现象:查询出来的某首律诗只有前四句,后四句凭空消失。

原因:建表时body字段写成VARCHAR(100),五言律诗正文 40 字加上标点超过 100 字符时被数据库自动截断。SQLite 没这个限制,但 MySQL 默认严格模式会直接报错,非严格模式下就静默截断。

解决:body字段统一用TEXT,不要用VARCHAR设置长度。只有像title这样明确短小的字段才用VARCHAR(100)。这个修改虽然看起来基础,但在多人协作时经常有人为了“节省空间”把TEXT改回VARCHAR(200),恰好够用又恰好不够。

5.5 问题五:诗句拆分时把联句断错,影响对仗分析

现象:某一联“两个黄鹂鸣翠柳,一行白鹭上青天”被拆成两行存入句子表,line_order顺序正确,但做对仗分析时发现第二句经常对不上。

原因:唐诗原版排版往往“两句一行”,清洗脚本按\n拆行后,poem_lines里存的是“联”而不是“句”。后续做平仄分析需要的是单句,不是联。

解决:入库后做一次二次切分,用,和。作为句子边界,把每条记录拆成单句。我写了一个后处理脚本遍历poem_lines,如果content里同时包含逗号和句号就再次切分并重置line_order。这一步做完之后,平仄、对仗、韵脚分析的准确性才真正可靠。

6. 进阶用法:从静态库到可交互的唐诗知识底座

数据集入库只是第一步,真正有价值的是在此基础上做查询服务。我习惯把tangpoem.db扔到一个轻量级 HTTP 服务后面,用 Python 内置的sqlite3和http.server搭一个只读接口,支持按作者、按诗句、按格律三个路由。三百首的体量完全不需要上重型数据库,一个文件数据库加几十行代码就是一套可交互的查询后端。

from http.server import BaseHTTPRequestHandler, HTTPServer import sqlite3 import urllib.parse class PoemHandler(BaseHTTPRequestHandler): def do_GET(self): query = urllib.parse.urlparse(self.path) params = urllib.parse.parse_qs(query.query) conn = sqlite3.connect("tangpoem.db") conn.row_factory = sqlite3.Row if query.path == "/by_author": sql = "SELECT title, genre, body FROM poems JOIN authors USING(author_id) WHERE authors.name = ?" rows = conn.execute(sql, (params.get("name", [""])[0],)).fetchall() elif query.path == "/by_line": sql = "SELECT content, title FROM poem_lines JOIN poems USING(poem_id) WHERE content LIKE ?" rows = conn.execute(sql, (f"%{params.get('q', [''])[0]}%",)).fetchall() else: rows = [] data = [dict(r) for r in rows] body = json.dumps(data, ensure_ascii=False).encode("utf-8") self.send_response(200) self.send_header("Content-Type", "application/json; charset=utf-8") self.end_headers() self.wfile.write(body) conn.close()

这个接口的价值不在于性能,而在于把数据集变成独立服务,前端、爬虫、分析脚本都可以统一走 HTTP 取数,不必每个人都去读 SQLite 文件。接口只暴露只读查询,数据库文件依旧保持只读属性,避免并发写入破坏数据。

另一个值得做的小工具是“诗句接龙查询”:输入上句末尾的字,查出所有以同韵母开头的下句。实现上用poem_lines表加一个韵脚字段,先在入库时用拼音库给每句末字标注韵母,然后WHERE rhyme = ?直接索引。三百首规模下,这个功能跑起来毫无压力,但体验却能从“查得到”变成“玩得动”。

做这套流程下来我最大的一个教训是:数据集项目最耗时间的永远是清洗环节,而不是数据库设计。第一次做的时候花了大半天调正则、对繁简,建表和写查询总共才用一个小时。如今我养成的习惯是拿到任何公开语料,第一件事先跑唯一性校验和空值检查,确认没有重复项、没有空字段再谈别的。数据干净了,后面的所有环节都顺。希望这篇笔记能帮你少走我踩过的这些坑,把更多时间留给真正有价值的功能和内容分析上。

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

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

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

立即咨询