31851条成语数据库落库指南:从zip解压、SQLite清洗到FTS5检索
简介中华成语数据库收录31851条成语每条均含拼音与释义大多数补充出处和例句适合语文教学、词汇研究、词典编撰及成语爱好者使用可作查询检索与统计分析的基础数据源。压缩包共3个文件分别以CSV表格、SQL脚本和TXT纯文本三种格式存储便于直接导入数据库、Excel或文本工具查看处理整体约6.63MB轻量易用。目前已有1060人学习下载。数据库不仅提供可批量查询的成语条目还能通过SQL关联出处文献、示例语境等字段支持按典籍、关键字或使用频率做深层挖掘纯文本格式则方便快速浏览或二次整理。无论入门学习者还是专业研究者都能借助这套结构化数据快速搭建自己的成语知识库用于教学课件、语料分析、文化科普或应用程序开发等场景。1. 31851个成语打包成数据库这份zip值不值得你花时间做中文NLP、成语问答、小学语文工具或者古汉语语料库的人大概率都被同一件事卡住过网上号称几十万的“中华成语数据库”真要用的时候却凑不齐一份带拼音、带解释、带出处、带例子的干净数据。这份打包成zip的成语数据库总量31851条每条都含成语和拼音大部分还带解释、出处和例子正好把最常见的四类字段失血一次补齐。它的价值不在数量多而在字段相对全、格式统一省掉自己从百科页面手工扒数据的功夫。适合三类人做nlp数据集清洗的、做语文教育类产品的、以及单纯想本地搭一个成语查询库的开发者。2. 拆包摸底先搞清zip里的数据长什么样再谈落库刚拿到一个zip包直接双击解压是最容易翻车的动作。中文zip包在Windows和Linux之间流转文件名编码经常错乱解压出来全是“锟斤拷”或者根本解不开。所以第一步不是解压而是先判断这个zip包到底用什么编码写的。2.1 用Python探查zip内部结构避免解压乱码zip格式本身不强制规定文件名编码Windows自带的压缩工具默认用GBKLinux下的zip默认用UTF-8macOS某些图形工具又会用别的编码。拿Python的zipfile模块读的时候文件名字符串是按cp437解码的中文几乎必然变成乱码。常见做法是把乱码字符串还原成字节再尝试常用中文编码import zipfile archive_path chengyu_31851.zip with zipfile.ZipFile(archive_path) as zf: for info in zf.infolist(): raw info.filename.encode(cp437) for enc in (utf-8, gbk, big5): try: name raw.decode(enc) print(f编码[{enc}] 文件名: {name}) break except UnicodeDecodeError: continue # 同时打印文件大小和压缩方式判断是不是空壳 print(f 大小: {info.file_size} 压缩后: {info.compress_size} 方式: {info.compress_type})这段代码做的事很朴素先按cp437把zipfile里面的文件名编码还原成原始字节再用三个常见编码依次尝试。utf-8排在前面因为现在新打包的数据绝大多数是utf-8如果是十年前的老资源gbk命中的概率更高。输出里的compress_type也有用0代表存储不压缩8代表deflate压缩看到大量0要警惕zip是不是被二次处理过。提示如果三个编码都解不出来别急着下结论可能是zip注释或flag位被篡改。后面专门讲zip伪加密的问题。确认编码后写一个解压脚本把探出来的编码映射直接用于写出文件import zipfile from pathlib import Path archive_path chengyu_31851.zip out_dir Path(chengyu_data) out_dir.mkdir(exist_okTrue) with zipfile.ZipFile(archive_path) as zf: for info in zf.infolist(): if info.is_dir(): continue raw info.filename.encode(cp437) enc utf-8 for candidate in (utf-8, gbk, big5): try: raw.decode(candidate) enc candidate break except UnicodeDecodeError: continue safe_name raw.decode(enc) target out_dir / Path(safe_name) target.parent.mkdir(parentsTrue, exist_okTrue) target.write_bytes(zf.read(info)) print(解压完成:, [p.name for p in out_dir.iterdir()])参数说明这个脚本把文件名编码探查和解压合并成一步target用Path处理子目录。一个容易忽略的细节是info.is_dir()判断zip里有些垃圾条目内容为空但带斜杠结尾不跳过会多出一个0字节文件。解压完成后最好把目录列表打印出来对一下数量级zip里有多少个文件、多少个文件夹做到心里有数。2.2 数据体检总数、缺字段、重复率一眼看完解压后常见格式是csv、json或者excel。不管是哪种第一件事是体检。我一般写一个短脚本把三条硬指标跑出来总数是否等于31851、缺解释/缺出处/缺例子的分别有多少、成语本身有没有重复。import csv from collections import Counter path chengyu_data/chengyu.csv # 先用utf-8-sig兼容Excel导出的BOM头 with open(path, encodingutf-8-sig) as f: reader csv.DictReader(f) fieldnames reader.fieldnames rows [row for row in reader] print(字段名:, fieldnames) print(记录数:, len(rows)) def missing_count(rows, field): count 0 for r in rows: val (r.get(field) or ).strip() if not val: count 1 return count for f in fieldnames: mc missing_count(rows, f) print(f缺[{f}]的条数: {mc}) words [r.get(成语, ).strip() for r in rows] print(去空后成语数:, len([w for w in words if w])) dupe_counter Counter(w for w in words if w) dupes {w: c for w, c in dupe_counter.items() if c 1} print(重复成语:, len(dupes), 最严重的:, dupes.most_common(3))这段体检脚本里我特意用strip()去掉首尾空格因为不少csv里成语前后带着\r或者全角空格不清理会直接造成统计偏差。缺字段的统计口径也值得说明空字符串和只有空格的字段都算缺失但“出处”这个字段特殊有的数据源会用“不详”“佚名”这类占位符脚本统计不出来需要肉眼抽样看几行原始数据再决定要不要把占位符也算缺失。记录数和预期对不上是常事常见原因是csv里混着分隔符转义错误、多行字段或者数据源本身把“成语”和“熟语”混在一起。先把这三条指标跑出来再看是否值得继续投入清洗。3. 把成语数据落进SQLite建表、去重与增删改查全流程数据体检过关后就该考虑存到哪。csv文件不适合多条件查询一次按拼音过滤、按字数统计就要全表扫描。常见做法是落进SQLite单文件、无服务、Python内置支持31851条成语连一个页都占不满查询响应在毫秒级。3.1 表结构唯一索引和字段类型一次定清楚CREATE TABLE IF NOT EXISTS chengyu ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT NOT NULL, pinyin TEXT, explanation TEXT, source TEXT, example TEXT ); CREATE UNIQUE INDEX IF NOT EXISTS idx_chengyu_word ON chengyu(word);建表的关键不是字段多而是约束。word加TEXT NOT NULL确保不会插入空成语唯一索引idx_chengyu_word是第二道防线即使导入脚本忘了去重数据库层面也能拦下重复。pinyin不建索引是有意为之——拼音字段后续更适合做全文检索而不是等值匹配建普通B-tree索引收益不大。example字段可能有很长的文本SQLite对文本长度没有硬限制但要注意默认的SQLITE_MAX_LENGTH是10亿字节正常语料远达不到。3.2 批量导入executemany加事务避免逐行commit导入时的经典错误是一个insert就commit一次31851条跑半天。正确做法是预编译SQL用executemany成批写入最后统一commitimport sqlite3 conn sqlite3.connect(chengyu.db) cur conn.cursor() cur.execute(DROP TABLE IF EXISTS chengyu_import) cur.execute( CREATE TABLE chengyu_import ( word TEXT, pinyin TEXT, explanation TEXT, source TEXT, example TEXT ) ) # 预编译插入语句executemany一次提交一批 insert_sql INSERT INTO chengyu_import VALUES (?, ?, ?, ?, ?) data [ (r.get(成语, ).strip(), r.get(拼音, ).strip(), r.get(解释, ).strip(), (r.get(出处, ) or ).strip(), (r.get(例子, ) or ).strip()) for r in rows ] cur.executemany(insert_sql, data) conn.commit() print(导入行数:, cur.execute(SELECT COUNT(*) FROM chengyu_import).fetchone()[0])这里我把数据先导进一张无约束的chengyu_import表而不是直接进正式表目的就是把“清洗”和“入库”解耦。executemany的作用是复用同一个SQL语句模板Python端把数据分批传给SQLite避免了循环里逐条execute的开销。commit放在最后一次性提交SQLite默认是自动提交模式不显式commit的话31851条插入会在事务边界频繁刷盘速度会慢一到两个数量级。3.3 去重与补写从import表洗进正式表导入只是第一步真正麻烦的是去重。有些成语在数据源里出现多次可能只是例子字段不同或者出处写法不一样。我的处理方式是从import表按成语分组优先选例子最完整的那条写入正式表cur.execute(DELETE FROM chengyu) cur.execute( INSERT INTO chengyu (word, pinyin, explanation, source, example) SELECT word, pinyin, explanation, source, example FROM chengyu_import GROUP BY word HAVING MAX(length(example)) ) conn.commit() cur.execute(SELECT COUNT(*), COUNT(DISTINCT word) FROM chengyu) total, distinct_words cur.fetchone() print(f正式表总数: {total}, 去重后: {distinct_words})这段SQL里有两处值得解释。第一GROUP BY word把同名的成语聚成一组SQLite允许SELECT里的非聚合列直接出现此时取的是该组“第一个遇到的行”配合HAVING MAX(length(example))实际上选的是例子文本最长的那一行。第二DELETE FROM chengyu放在导入前保证重复执行脚本不会累积数据。正式表总数和去重后数字如果对不上说明依然有脏数据混进来。注意SQLite的GROUP BY与HAVING组合在不同版本下取“哪一行”的行为略有差异。如果发现选出来的不是自己想要的把上面的SQL改成子查询先按word和length(example)排序再分组结果更可控。3.4 增删改查四条基本操作落库后的日常操作无非增删改查这里给一套可以直接套用的模板-- 查按成语精确匹配 SELECT * FROM chengyu WHERE word 画蛇添足; -- 查按拼音前缀匹配 SELECT word, pinyin FROM chengyu WHERE pinyin LIKE hua% LIMIT 20; -- 改修正错别字 UPDATE chengyu SET word 画蛇添足 WHERE word 画蛇填足; -- 删清除空解释的记录 DELETE FROM chengyu WHERE explanation IS NULL OR trim(explanation) ;LIKE hua%这种写法在word有索引时走不了索引属于全表扫描。31851条数据量小扫描也就几毫秒不用纠结。但如果后续数据涨到百万级就应该改用pinyin字段的单独索引或者直接上FTS5全文检索这一步在进阶章节展开。4. 数据清洗实战拼音规范、出处缺失与例子质量的三个硬骨头落库只是开始真正的体力活是清洗。标题里说了“每个成语都包括拼音解释大多数还包括出处和例子”这句话翻译成人话就是出处和例子的缺失率不会低而且格式一定不统一。我从实际处理经验出发把最常见的三个硬骨头单独拎出来讲。4.1 拼音声调规范数字声调、符号声调、无空格大杂烩成语数据的拼音字段常看到三种风格混在一起“hàn yǔ”“han4 yu3”“hanyu”。如果要做发音接口、按拼音排序、或生成拼音首字母索引混用格式会直接让下游逻辑崩掉。我的习惯是统一转成带数字声调的格式再用一个函数来回切换import re PINYIN_TONE_MAP { ā: a1, á: a2, ǎ: a3, à: a4, ē: e1, é: e2, ě: e3, è: e4, ī: i1, í: i2, ǐ: i3, ì: i4, ō: o1, ó: o2, ǒ: o3, ò: o4, ū: u1, ú: u2, ǔ: u3, ù: u4, ü: v0, ǖ: v1, ǘ: v2, ǚ: v3, ǜ: v4, } def normalize_pinyin(text: str) - str: if not text: return text # 先处理拼音输入法式的带调字母 s for ch in text: s PINYIN_TONE_MAP.get(ch, ch) # 把字母和数字之间的多余空格收掉统一用小写 s re.sub(r\s, , s) s s.lower() return s examples [hàn yǔ, han4 yu3, Hanyu, lì zhì] for e in examples: print(f{e} - {normalize_pinyin(e)})normalize_pinyin做的事不复杂带调字母映射成字母加数字去掉所有空格统一小写。这样“hàn yǔ”“han4 yu3”“Hanyu”都会变成同一种格式。注意ü的映射我把它映射成v0作为声调占位因为标准拼音输入法里ü用v代替这个细节能避免后续排序和比较时出现多余的u。真正生产级的处理还得靠pypinyin这类库但那个库依赖词库处理成语这种固定文本反而容易把多音字搞错我的经验是先用自己的规则清洗一遍再人工抽样检查。4.2 出处缺失与“佚名”占位符的区别对待标题说“大多数还包括出处和例子”意味着出处字段不会全。处理缺失的方式直接影响数据可用性。我的原则是保留空值不做伪补全。市面上有些数据集直接把出处填成“不详”这在展示场景可以接受但在检索排序、去重合并场景里“不详”和空字符串是两个完全不同的值会让后续统计多出很多工作量。import re def normalize_source(source: str) - str: s (source or ).strip() if not s: return # 统一全角标点去掉多余空格 s s.replace(, ,).replace(, :) s re.sub(r\s, , s) # 把不详佚名这类占位符归一成空 if s in (不详, 佚名, 无, 未知): return return s # 应用示例 for src in [《史记·项羽本纪》, 不详, 佚名, 出处 : 庄子 ]: print(f{src!r} - {normalize_source(src)!r})这段函数解决的是“占位符要不要当缺失处理”的问题。保存时建议把归一的结果写回单独的字段不要让原字段直接丢数据。我之前吃过一次亏把占位符直接清掉后后来又想做一轮数据补全反而失去了线索。保留原始值加一个clean_source字段想回退随时能回退这就是后悔药的正确吃法。4.3 例子质量判断过滤“造句模板”和重复片段例子的质量问题比缺失更难处理。有的例子是从百科复制的文言原文有的是小学生造句还有就是把成语拆开重组的占位句比如“他画蛇添足地添了一笔”。判断例子是否可用我一般看三个信号长度、标点、与成语的相关性。def example_score(example: str, word: str) - float: s (example or ).strip() if len(s) 6: return 0.0 # 信号1包含成语本身相关性保底 in_word 1.0 if word in s else 0.0 # 信号2以句号感叹号结尾的完整句加分 has_end 1.0 if any(p in s[-1] for p in 。) else 0.0 # 信号3中文标点数量标点越多越像自然句子 punct_count sum(1 for ch in s if ch in 。、) return 0.4 * in_word 0.3 * has_end 0.3 * min(punct_count / 3, 1.0) # 对整表打分低于0.5的标为可疑 flagged [r for r in rows if example_score(r.get(例子, ), r.get(成语, )) 0.5] print(可疑例子数量:, len(flagged))example_score的权重是我自己调出来的经验值。包含成语本身这条权重最高因为一个连成语都不会出现的例子很可能张冠李戴。带结尾标点表示句子完整而不是从中间截断。标点数量体现句子的自然程度纯流水账或纯名词罗列的得分会很低。这套评分不完美但能把31851条里最可疑的几百条捞出来人工复核而不是靠肉眼从头看到尾。5. 避坑排查zip伪加密、编码错乱、数量对不上与乱码的四个现场数据类zip项目最常见的问题集中在解压和落库两个环节。下面四条都是我实际处理类似数据包时踩过的坑按踩坑频率排序。5.1 zip文件显示需要密码但资源描述说没加密现象解压工具弹窗要求输入密码输入什么都是“密码错误”。原因zip的加密标志位被人为篡改。zip格式里每个文件头有个flag位bit 0置1表示“加密”。有些人在打包后用十六进制编辑器把flag改了或者用“zip伪加密”方式隐藏内容造成假锁。文件本身可能根本没加密也可能真的加密了。解决先用Python检查ZipInfo.flag_bits看加密位是否真的置位import zipfile with zipfile.ZipFile(chengyu_31851.zip) as zf: for info in zf.infolist(): print(f{info.filename}: flag_bits{info.flag_bits:#06x})如果是0x0001的变形伪装加密部分解压工具会拦。常见做法是换Python的zipfile去读zipfile对伪加密的处理比图形工具宽松很多情况下能直接读出内容。真加密就只能改用思路获取数据这个就不展开了。提示伪加密和真加密从flag_bits上很难100%区分还要配合文件的具体表现判断。5.2 csv里汉字全变成了“锟斤拷”现象用Excel打开csv中文列全是乱码叠加。原因csv文件是UTF-8编码Excel默认按ANSI/GBK打开。这跟数据本身无关是打开方式问题。解决不要用Excel直接双击csv。先落地成SQLite或者用文本编辑器指定编码打开。Python读取时用utf-8-sig兼容带BOM的UTF-8文件前文的体检脚本已经演示过。落到数据库以后用命令行查一遍确认中文正常再考虑导出Excel。5.3 总数不是31851多了或少了现象统计出来31351、32150和标题宣称的不一致。原因csv里有多行字段串行或者记录中有换行符导致DictReader断行还有一种常见状况是数据源把“成语接龙”的重复内容也算进去去重之后掉到30000出头。解决多出来的可能不是记录而是字段内换行。先用csv阅读器的严格模式校验格式再用唯一索引排掉重复。少了的排查思路是先看去重数量标题里的31851如果是去重前总量去重后变少是正常的。我自己的项目中就遇到过“31851是含重复条数的总行数去重后只有30102”的情况这个数字只能作为参考口径别当验收硬指标。5.4 SQLite查出来是乱码但csv里正常现象csv在编辑器里显示正常导入SQLite后中文乱码。原因导入时sqlite3连接没有开utf-8或者从Python传数据时用了错误的编码打开文件。解决连接和文件打开两侧统一用utf-8。Python的sqlite3模块默认处理的就是Unicode字符串问题通常出在open(path, encoding...)那一步。检查代码里是不是漏了encoding参数或者用了中文文件路径但没转成Path类型。踩过这个坑之后我的习惯是导入完成后立即跑一条SELECT抽查三十条而不是等到查询阶段才发现。6. 进阶用FTS5把成语库变成可检索的本地小工具数据清洗入库后下一步就是把死数据用起来。SQLite内置的FTS5全文检索比LIKE好用一个数量级。6.1 建立FTS5虚拟表和增量同步CREATE VIRTUAL TABLE IF NOT EXISTS chengyu_fts USING fts5( word, pinyin, explanation, source, example, contentchengyu, content_rowidid ); INSERT INTO chengyu_fts(chengyu_fts) VALUES(rebuild);contentchengyu声明这里用的是外部内容表content_rowid对应chengyu表的id。rebuild命令把现有数据全量灌进去后续只对增量数据单独insert即可。6.2 支持拼音检索和模糊查询的客户端import sqlite3 conn sqlite3.connect(chengyu.db) cur conn.cursor() def search(keyword: str, limit: int 10): q fSELECT word, pinyin, substr(explanation, 1, 30) AS brief FROM chengyu_fts WHERE chengyu_fts MATCH ? LIMIT {limit} return cur.execute(q, (f{keyword} OR {keyword}*,)).fetchall() print(search(画蛇添足)) print(search(多此一举))MATCH查询支持前缀匹配和NEAR邻近操作符比如查“画蛇添足 NEAR 多此一举”可以找到同时出现两个成语的段落这是LIKE做不到的。FTS5默认分词器对中文是一字一字切分对2到3个字的词匹配效果要实测必要时换用unicode61分词器或者自定义分词这个就要按数据量级再调了。我的习惯是把这套检索脚本包装成一个命令行小工具配合成语接龙、拼音首字母速查和出处统计几个子命令平常读书想查一个词条随时能调出来。做到这个程度这份31851条的数据才算真正变成自己的资产。希望这篇笔记里的建表、清洗和避坑经验能帮你在同样的数据上少走几趟弯路。本文还有配套的精品资源点击获取