简介《汉字简体繁体参照表》是一份面向开发者、数据分析师及语言处理学习者的实用数据集收录约4792条简体与繁体汉字映射关系可支撑多语言软件简繁切换、文本预处理、数据清洗与学术研究等场景。资源包共4个文件压缩包仅162KB提供SQL、JSON、CSV、XLSX四种格式SQL适合入库查询JSON适合接口交换CSV便于pandas/Excel批量处理XLSX可直观浏览编辑。内容按简体、繁体及关联字段组织字段清晰无需额外清洗即可直接导入现有项目。已有673人学习下载对需要快速构建简繁转换工具或开展汉字数据研究的用户而言是一份轻量且高性价比的参考资料。1. 汉字简体繁体参照表数据库为什么中文检索、输入法和 NLP 都绕不开它一提到“汉字简体繁体参照表数据库”很多人第一反应是“不就是一张字表嘛找两个字段一映射就完了”。但真做起来会发现它同时是字符集问题、多对多映射问题、词级消歧问题和数据库设计问题的交汇点。你搜“头发”数据库里存的是“頭髮”检索召回不匹配做输入法词库迁移简体转繁体后出现“後台”“裏面”这种半吊子结果做网页简繁切换生僻字直接变成问号。这张表解决的就是这一类文本规范化问题适合后端开发者、NLP 工程师、搜索团队和做中文内容运营的同事使用。本文从表结构设计讲起一直落到装载命令、查询逻辑和踩坑记录照着做就能搭出一套可维护的简繁映射数据服务。2. 表结构设计把一简对多繁装进两张表的最小模型2.1 字符集、排序规则和主键为什么不能拍脑袋建表第一件事不是写字段而是定字符集。简体繁体对照表里全是 CJK 字符其中包含康熙部首、生僻字和扩展 B 区以后的汉字这些字符在 MySQL 里必须用 utf8mb4 才能完整存储。很多老项目库表用的是 utf8它是 utf8mb3 的别名一个字符最多 3 字节遇到“”这类码位在 20000 之后的字就直接变问号。SQLite 没这个问题因为它的文本存储不按 MySQL 的字符集模型走但你在 MySQL 里做增量同步时DDL 里不写清楚 charset 就是给自己埋雷。排序规则我推荐 utf8mb4_bin不要用 utf8mb4_unicode_ci。原因很实际unicode_ci 会把一些字符按大小写、重音规则视为等价在唯一索引上可能把两个不同的字误判成同一个bin 按码点精确比较对映射表这种场景是最稳妥的。你可能会问那主键用码点数字还是用字符本身我建议加一个自增 id 做主键码点单独作为业务唯一键的一部分。原因是“发”对应“發”和“髮”这种一对多关系一行装不下字符自身做唯一键会直接挡住这种合理数据。码点列留着用来做范围筛选、去重统计和和其他字表做 JOIN非常顺手。CREATE TABLE cjk_char_map ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, code_point INT UNSIGNED NOT NULL COMMENT 简体字 Unicode 码点, simplified VARCHAR(8) NOT NULL COMMENT 简体字, traditional VARCHAR(8) NOT NULL COMMENT 繁体/异体字, direction ENUM(s2t,t2s) NOT NULL COMMENT 映射方向, region VARCHAR(16) NOT NULL DEFAULT ALL COMMENT 适用范围逗号分隔ALL/TW/HK/MO, source VARCHAR(32) NOT NULL DEFAULT COMMENT 来源标识, verified TINYINT NOT NULL DEFAULT 0 COMMENT 1人工审核, PRIMARY KEY (id), UNIQUE KEY uk_pair (code_point, traditional, direction, region), KEY idx_simplified (simplified), KEY idx_traditional (traditional) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin;这段 DDL 里唯一索引是核心。一条映射一行同一个简体字可以出现多行每行对应一个不同的繁体目标用 (code_point, traditional, direction, region) 保证不重复。查询时按 simplified 或 traditional 走索引否则几万行数据全表扫描也能用但加上以后批量校验明显更快。code_point 列用 INT UNSIGNED 是为了和 Unicode 码点范围对齐不要用 INT 带符号否则排序和边界判断容易糊涂。2.2 一简对多繁为什么必须多行存放不少从零开始的团队会把“发”的繁体直接写成“發髮”塞进一个字段查询时再 split。这种设计短期能用但一进词表关联就崩你不知道当前上下文该取哪个拆分逻辑散落在业务代码里最后每个调用方各写一套。正确做法是多行存放。以“发”为例它在“发展”里对应“發”在“头发”里对应“髮”。两条数据分别入库语义各自独立后续用词表消歧时按短语匹配到具体某一行。INSERT INTO cjk_char_map (code_point, simplified, traditional, direction, region, source, verified) VALUES (21457, 发, 發, s2t, ALL, unicode, 1), (21457, 发, 髮, s2t, ALL, phrase:头发, 1);这种插入方式有一个隐含要求source 字段要能追溯。第一条来自 Unicode 官方映射第二条来自词表“头发”的拆分将来发现“髮”还有别的语义时可以按 source 批量排查。不要小看这个字段简繁数据整理一次往往跨几个月没有来源标记后期根本不敢改数据。2.3 词表才是主战场phrase_map 的字段与索引单字映射撑不起真实场景。中文简繁转换的难点从来不在字而在词两岸三地对同一个词有不同说法“数据库”在台湾叫“資料庫”“软件”叫“軟體”“打印”叫“列印”。这些差异用单字映射硬拼出来的是“數據庫”“軟件”这种混合体看着像繁体其实不是目标地区的惯用表达。所以要单独建一张词表。CREATE TABLE cjk_phrase_map ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, phrase VARCHAR(64) NOT NULL COMMENT 简体词或短语, target VARCHAR(64) NOT NULL COMMENT 对应繁体词或短语, direction ENUM(s2t,t2s) NOT NULL DEFAULT s2t, region VARCHAR(16) NOT NULL DEFAULT ALL COMMENT 逗号分隔ALL/TW/HK/MO, source VARCHAR(32) NOT NULL DEFAULT , verified TINYINT NOT NULL DEFAULT 0, freq INT NOT NULL DEFAULT 0 COMMENT 词频越大越优先, PRIMARY KEY (id), UNIQUE KEY uk_phrase (phrase, target, direction, region), KEY idx_phrase (phrase) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin;词表里我特别留了 freq 和 verified 两个字段。freq 用来在字词冲突时做兜底排序verified 用来标记人工审核过的记录。实际使用时词表命中优先于字表但如果同一个词在不同来源里打架freq 高或者 verified1 的优先。region 字段建议用逗号分隔的代码串查询时用 FIND_IN_SET 判断虽然不够第三范式但比拆成关联表省事很多维护 CSV 时也更直观。3. 数据清洗从原始码表到可导入 TSV 的四个步骤3.1 数据来源和授权是第一个坑简繁映射的原始数据常见来源有几个Unicode 官方 Unihan 数据库里的 kTraditionalVariant 和 kSimplifiedVariant 字段这是单字映射的基础各类开源简繁转换词库里的字词对比如成熟的中文简繁转换项目里附带的词典文件加上你自己积累的术语表。这里有一件事必须做检查授权。有的词库允许商用要求保留版权声明有的只允许个人使用。把数据导入数据库之前先在项目文档里记录每批数据的来源和授权方式不然团队大了以后没人说得清这批映射能不能用于商业产品。我不建议直接抓网页上的转换结果来拼数据集因为网页转换本身就可能带错且无法追溯来源。更实际的做法是以 Unihan 单字映射为骨架以开源词库和人工术语表为血肉每一步都记录在 source 字段里。3.2 清洗脚本去重、码点校验和方向标记原始数据通常长这样简繁两个字符加一个方向标识可能是 CSV、TSV 或者 Unihan 的导出格式。我的习惯是统一转成 CSV再用 Python 做一遍清洗。清洗内容包括去掉空行、NFKC 归一化全角半角、校验码点范围、按映射对去重、统计一简对多繁的行数。这段脚本看起来简单但它决定了导入数据库后能不能省心。import csv import unicodedata from collections import defaultdict def is_cjk_char(ch): cp ord(ch) return ( (0x4E00 cp 0x9FFF) or (0x3400 cp 0x4DBF) or (0xF900 cp 0xFAFF) or (0x20000 cp 0x2FA1F) ) def clean_mapping(in_path, out_path): seen set() stats defaultdict(int) with open(in_path, encodingutf-8-sig) as fin, \ open(out_path, w, encodingutf-8, newline) as fout: writer csv.writer(fout) writer.writerow([code_point, simplified, traditional, direction, region, source]) for row in csv.reader(fin): if not row or len(row) 3: stats[skip_empty] 1 continue simp unicodedata.normalize(NFKC, row[0]).strip() trad unicodedata.normalize(NFKC, row[1]).strip() direction row[2].strip() if direction not in (s2t, t2s): stats[skip_direction] 1 continue if len(simp) ! 1 or len(trad) ! 1: stats[skip_non_char] 1 continue if not is_cjk_char(ord(simp)) or not is_cjk_char(ord(trad)): stats[skip_non_cjk] 1 continue key (ord(simp), trad, direction) if key in seen: stats[skip_duplicate] 1 continue seen.add(key) writer.writerow([ord(simp), simp, trad, direction, ALL, raw]) print(stats)清洗里的几个细节值得说。NFKC 归一化只用于把全角数字、字母、空格统一成半角它不等于简繁转换别试图用 normalize 解决“發/髮”这种问题。码点范围校验用于拦截标点、拉丁字母和假名混入字表否则后面做 JOIN 时会把非常用字符也带上。去重必须基于“码点目标字方向”做完整组合只按码点去重会漏掉一简对多繁的合法数据。输出里的 region 先统一写成 ALL后续人工审校时再按地区拆分。清洗完以后把统计结果打出来看一眼skip_duplicate 特别高说明多个来源数据高度重合正常skip_non_cjk 特别高说明原始文件里有大量非汉字内容多半是格式解析错了。3.3 词汇级差异处理一张表装不下所有繁体习惯字级映射清洗完词级映射才是真正花时间的地方。拿“台”举例对应繁体至少有“台、臺、檯、颱”分别用于“台地、台灣、檯燈、颱風”。再比如“里”对应“里、裡、裏”台湾用“裡”多香港“裏”也不少见。这些差异不处理做出来的转换总有一边看着别扭。我的做法是把词表按 region 拆行维护形成“一个词条、多个地区版本”的格局。人工审校阶段中文编辑拿到的 CSV 直接就是字段齐全的表格在 Excel 里筛选、批注、修改改完导回。很多团队没有数据库可视化后台这套“Excel 打开 CSV 审校、再导入数据库”的流程反而是最顺手的维护方式。简体繁体台湾繁体香港说明信息資訊資訊两岸通用数据库資料庫資料庫计算机领域里裡裏地区差异着著着台湾多用著表格里“着/著”是典型例子。台湾繁体里“看着”常写作“看著”香港繁体则更多保留“看着”。如果你的产品只面向台湾用户词表里直接放“看着→看著”面向香港用户就得保留“看着→看着”或者按香港惯用说法单独映射。region 字段在这里起过滤作用查询时按目标地区过滤才能输出用户习惯的文本。4. 装载与查询SQLite / MySQL 的建库导入和词表优先转换器4.1 SQLite 单文件方案Linux 服务器上最省事的词库包如果你的简繁参照表要交付给下游多个团队使用SQLite 是性价比最高的载体。一个 .db 文件拷走就能用不需要单独部署数据库服务Python、Java、Go 都有原生驱动。我一般会在 Linux 服务器上把清洗好的 CSV 直接灌进 SQLite压缩以后作为离线词库包分发。import sqlite3 import csv conn sqlite3.connect(cjk_map.db) cur conn.cursor() cur.executescript( CREATE TABLE IF NOT EXISTS cjk_char_map ( code_point INTEGER NOT NULL, simplified TEXT NOT NULL, traditional TEXT NOT NULL, direction TEXT NOT NULL, region TEXT NOT NULL DEFAULT ALL, source TEXT NOT NULL DEFAULT , PRIMARY KEY (code_point, traditional, direction, region) ); CREATE INDEX IF NOT EXISTS idx_simplified ON cjk_char_map(simplified); CREATE INDEX IF NOT EXISTS idx_traditional ON cjk_char_map(traditional); ) with open(cleaned_char_map.csv, encodingutf-8) as f: reader csv.DictReader(f) rows [ (int(r[code_point]), r[simplified], r[traditional], r[direction], r[region], r[source]) for r in reader ] cur.executemany( INSERT OR REPLACE INTO cjk_char_map VALUES (?, ?, ?, ?, ?, ?), rows, ) conn.commit() conn.execute(VACUUM)这里用 INSERT OR REPLACE 而不是普通 INSERT是为了容忍清洗阶段漏网的重复主键。代价是后插入的数据会覆盖先插入的所以导入前务必确认 CSV 里的行顺序符合你的优先级预期。executemany 批量提交比逐条 execute 快一个数量级几万行数据毫秒级完成最后 VACUUM 是回收 SQLite 删除旧页后的空间别省略。SQLite 的建表语句不用写 ENGINE 和 CHARSET它默认就是完整的 Unicode 文本存储。唯一要注意的是 Python 自带的 sqlite3 模块在并发写场景下会有锁竞争但简繁词库这种读多写少的离线包完全没必要上并发写。4.2 MySQL 方案LOAD DATA 和 staging 表生产环境要支持在线更新、多人协作时还是得放到 MySQL 这类服务端数据库里。MySQL 装载大批量数据最推荐的是 LOAD DATA LOCAL INFILE比逐条 INSERT 快得多。常用的命令就三条建库、导数、校验。mysql -u root -p -e CREATE DATABASE IF NOT EXISTS cjk DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; mysql --local-infile1 -u root -p cjk \ -e LOAD DATA LOCAL INFILE cleaned_char_map.csv INTO TABLE cjk_char_map \ FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY \ \ IGNORE 1 LINES (code_point, simplified, traditional, direction, region, source); mysql -u root -p cjk -e SELECT direction, COUNT(*) FROM cjk_char_map GROUP BY direction;LOAD DATA 的参数要按实际文件调整CSV 用逗号分隔就写 FIELDS TERMINATED BY ,带引号的字段加 OPTIONALLY ENCLOSED BY 第一行是表头就写 IGNORE 1 LINES。--local-infile1 是客户端选项MySQL 服务端默认配置可能禁用它如果报错就去 my.cnf 里确认 local_infile 参数。注意 CSV 如果是 Excel 导出的大概率带 UTF-8 BOMLOAD DATA 会把 BOM 当成第一个字段的一部分所以清洗阶段最好统一输出无 BOM 的 UTF-8 文件。我特别建议先把数据导入 staging 表而不是正式表。正式表里可能已经有线上数据LOAD DATA 一旦撞上唯一键就中断。先导入 cjk_char_map_staging跑几条校验 SQL确认行数和重复情况没问题再 RENAME 或者 INSERT INTO ... SELECT 合并。这样整个过程其实就是一次标准的数据库增删改查建表是 DDL导入是写校验是查修正是改回滚是删每一步都能用 SQL 对账。4.3 工程落地一个词表优先的简繁转换器数据入库以后最终要落到查询逻辑。最简单粗暴的方案是逐个字符查 cjk_char_map但前面说过这样会把“头发”转成“頭發”。正确做法是词表优先在 cjk_phrase_map 里做最长匹配匹配不到的字再走单字表。下面这个 Python 函数是我常用的最小实现。def convert_line(text, phrase_map, char_map): result [] i 0 max_len max(len(p) for p in phrase_map) if phrase_map else 1 while i len(text): matched False for length in range(min(max_len, len(text) - i), 1, -1): seg text[i:i length] if seg in phrase_map: result.append(phrase_map[seg]) i length matched True break if matched: continue ch text[i] result.append(char_map.get(ch, ch)) i 1 return .join(result) phrase_map {头发: 頭髮, 发展: 發展} char_map {发: 發, 展: 展} print(convert_line(头发的发展, phrase_map, char_map)) # 输出頭髮的發展这里最关键的是贪心最长匹配每到一个位置先从最长的短语开始尝试命中就整段消费没命中才降级到单字。这样“头发”会整体映射成“頭髮”不会拆成“頭”和“發”。phrase_map 和 char_map 在进入函数前先从数据库加载成 dict构造一次后续行级转换都是内存操作。如果每秒要处理上万行文本建议再把它改成 AC 自动机版本但绝大多数内部工具用这个版本就够。查询 SQL 侧也有配套思路。需要批量校验一段文本时可以先把文本分词后 JOIN cjk_phrase_map 和 cjk_char_map需要导出全量映射时直接查视图。不要把转换逻辑写成一长串 SQL 函数调试一次会非常痛苦。把转换器放在应用层数据库只负责提供干净的映射数据这是团队协作时最不容易吵架的边界。5. 避坑要点简繁数据库最常见的 5 个翻车现场5.1 生僻字入库变问号数据直接毁了现象从 Unihan 导出的扩展 B 区汉字导入 MySQL 后变成“?”或者一串乱码读取出来也不是原来的字。原因表字符集是 utf8 而不是 utf8mb4或者建表时字符集对了但 mysql 客户端的连接字符集没指定导致写入时被转码还有种常见情况是终端本身显示不了生僻字让人误以为库里坏了。解决DDL 里统一写 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin命令行连接加上 --default-character-setutf8mb4Java 连接串里加 characterEncodingutf8。导入完成后立刻执行 SELECT SIMPLIFIED, HEX(SIMPLIFIED) 抽查几个扩展区的字别等业务方反馈才发现。5.2 头发变成頭發一简对多繁只取了第一条现象转换结果里“头发”变成“頭發”“里面”变成“裏面”甚至“里面”原样保留整体看起来不伦不类。原因应用层只查了 cjk_char_map按码点直接取第一条映射或者虽然查了词表但词表里根本没有“头发”这条记录程序静默降级到字表。解决数据模型上字表和词表必须并存字符映射只作为兜底应用层按每个字符去查找时永远不要盲目取 LIMIT 1要带上 region 和 freq 条件。词表维护要覆盖高频词特别是“发、后、里、面、干、台”这类多对一繁字的高频搭配。5.3 简繁双向互转不幂等干部变成幹部又变成干部现象一段文本先转繁体再转简体结果和原文不一致更隐蔽的是“乾隆”被转成“干隆”“乾杯”变成“干杯”或反过来。原因t2s 不是 s2t 的逆运算。“乾”在“乾隆、乾坤”里读 qián不简化在“干燥、干杯”里才对应简体的“干”。字表只能表达字符层面的一一映射无法处理专有名词。解决t2s 方向必须独立维护严禁用 s2t 表反向推导。t2s 词表里把“乾隆、乾坤、乾宅”这类词条锁死让它们整体保留“乾”字。发布前对专名表做一遍全量转换测试确保没有回流。5.4 一套繁体走天下香港台湾用户都来投诉现象面向香港用户的产品输出“裡”而不是“裏”面向台湾用户的转换结果出现“著/着”混用用户一看就知道不是本地习惯。原因region 字段形同虚设或者清洗时全部写成了 ALL导致无法按地区过滤。解决词表和字表都按地区拆行。台湾常用“資訊、裡、著”香港常用“資訊/資料、裏、着”。查询时用 FIND_IN_SET(TW, region) 过滤。如果你的产品同时服务港澳台把 region 当必传参数缺省值不要设成 ALL强制调用方想清楚目标地区。5.5 LOAD DATA 导入到一半撞唯一键前功尽弃现象MySQL 导入几万行数据时中途报 Duplicate entry表里只剩前半段数据后半段没进去。原因多个来源数据合并时存在同一映射对重复出现唯一键不是 code_point 而是完整映射组合也有可能是 CSV 文件第一行表头没被 IGNORE被当成数据插进去了。解决先导入 staging 表跑下面这条 SQL 检查重复确认干净以后再合并到正式表。SELECT code_point, traditional, direction, region, COUNT(*) FROM cjk_char_map_staging GROUP BY code_point, traditional, direction, region HAVING COUNT(*) 1;这条语句能一次找出所有重复冲突。清洗脚本里要保留 source 字段重复数据的出现原因通常靠它追查。LOAD DATA 之前也可以先用 wc -l 统计文件行数、用 head 看前几行快速判断表头是否正常。不要直接对线上正式表跑 LOAD DATAstaging 表是后悔药。6. 覆盖率与增量维护发布前跑一条 SQL把词表变成后悔药6.1 用一条 SQL 对版本做体检简繁数据库不是一次建完就结束的东西新词、新术语、用户反馈都会要求持续加数据。每次发布前我会先跑一条总量 SQL把当前版本的字符数和词条数记下来归档。这个数字不说明覆盖是否完整但它能帮你看出版本间是否有异常波动——比如词表行数突然少了一半多半是清洗脚本误删了数据。SELECT (SELECT COUNT(DISTINCT code_point) FROM cjk_char_map WHERE directions2t) AS s2t_chars, (SELECT COUNT(DISTINCT code_point) FROM cjk_char_map WHERE directiont2s) AS t2s_chars, (SELECT COUNT(*) FROM cjk_phrase_map) AS phrase_count;再配合一张常用字表做缺口检查。把 GB2312 里常用的 3500 个汉字导入一张 common_chars 表LEFT JOIN 到 s2t 映射上查出来没匹配到的字符就是明明常用却漏了映射的字。CREATE TABLE common_chars (ch VARCHAR(8) PRIMARY KEY) ENGINEInnoDB DEFAULT CHARSETutf8mb4; SELECT c.ch FROM common_chars c LEFT JOIN cjk_char_map m ON c.ch m.simplified AND m.direction s2t WHERE m.id IS NULL;空结果不代表万事大吉但有结果一定代表有缺口。很多生僻字默认可以原样保留不转常用字却必须全覆盖这条 SQL 就是守门员。6.2 增量维护要改 CSV不要直接改线上库词表新增词条时我养成了固定习惯先改 CSV跑一遍清洗再导入 staging最后才合入线上。反过来直接对 MySQL 执行一句 UPDATE短期看着省事长期会让数据源和线上库产生漂移下次全量重建时新词全丢。增量插入可以复用一条 UPSERTMySQL 里等价于“存在就更新不存在就插入”INSERT INTO cjk_phrase_map (phrase, target, direction, region, source, verified, freq) VALUES (云服务, 雲端服務, s2t, TW, 术语表, 1, 10) ON DUPLICATE KEY UPDATE target VALUES(target), source VALUES(source), verified VALUES(verified);复用这条语句的前提是唯一键包含 phrase、target、direction、region否则重复数据会悄悄插进去。词频字段 freq 建议人工给个初始值后续从线上日志统计真实词频再回填它会直接影响“字词冲突时优先选哪个”的兜底逻辑。我之前维护词库时最常翻的车就是图省事直接改线上库后来强制自己先改 CSV 再导入配合上面两条校验 SQL批量事故基本消失。简繁参照表的价值不在一张静态字表而在于不断吸收真实语料里的词级用法把维护流程变得可回滚这套数据库才能真正长期用下去。希望帮到你。本文还有配套的精品资源点击获取