☰
从零构建3500常用汉字字典:JSON数据清洗与Python调用实战
2026/10/22 18:28:50 网站建设 项目流程

简介:3500个常用汉字代码调用字典,由作者hj960511整理发布,是一份用于随机取名、名称生成的实用数据资源,面向需要批量构造人名、企业名或网名昵称的开发者、文案创作者与业余爱好者。字典覆盖常用汉字表,既能独立作为基础字库,也能通过配套的Python脚本read.py快速读取、随机组合,适合快速搭建各类名称生成器。资源压缩包内共3个文件,包含txt格式的汉字字典、Python调用脚本及使用说明文档,整体仅10KB,结构小巧,txt中的数据易于查看和替换,方便二次修改。目前已有601人学习,验证了其在取名类小工具场景中的参考价值。读者不仅可以直接运行脚本生成人名、企业名、网名昵称,还可调整拼接规则,定制字数、风格或行业维度,拓展出更多个性化生成玩法,节省从零整理字库的时间。

1. 3500个常用汉字的调用字典:先想明白它解决什么问题

有一阵子我在做输入法关联词的候选排序,翻资料时发现一个有意思的现象:国标字符集里收了6763个汉字,但日常语料里高频覆盖的常用字却只有3500个。这个3500不是玄学,它是从公开的常用字表口径来的,一级字2500个加上二级字1000个,覆盖一般书刊超过99%的用字。市面上现成的字符集数据要么太大、带上各种生僻字,要么字段混乱,不如自己整理一份干净的、能直接加载成dict的常用汉字代码调用字典来得顺手。

这份字典不是什么大工程,本质上就是把3500个汉字对应的读音、笔画数、常用级别、Unicode码位整理成结构化数据,用JSON落地,加载到内存里变成一个可查询的映射。它能解决三件事:一是给随机取字、人名生成、验证码这类需求提供一个受控字库;二是给汉字排序、按拼音索引这类功能提供一个统一数据源;三是在做OCR后处理、输入法候选字排序时,有一个可预期的回退表。适合想在Python、JavaScript或者数据库项目里快速接入汉字能力的开发者。

下文不写“怎么找到现成下载包”,而是写完整构建链路:数据结构怎么选、字表怎么清洗校验、常见调用场景怎么写、哪些坑会反复踩,最后给一个能直接抄的进阶用法。整个过程跟着做一遍,你手上也会有一份自己能维护、能解释每一行数据的字典。

2. 数据结构选型:为什么JSON加dict是性价比最高的答案

2.1 先想清楚调用方是谁:dict、JSON与SQLite的边界

一份数据文件怎么组织,取决于你在哪一端调用它。3500条记录这个体量有个特点:放进SQLite属于给自己加负担,每次还得建连接跑SQL;放进CSV又缺了类型信息,遇到多音字要存数组时只能自己约定分隔符;最省事的还是JSON。我一般按“数据是只读还是可写”来选型,这份字典整理完之后基本不再改,属于典型的只读资源,JSON在这种场景下有两个明显优势:一是可读性好,出现缺失字时直接打开文件就能肉眼排查;二是所有主流语言都内置了解析能力,不需要额外引入依赖。

只有一种情况我会考虑上SQLite:当同一份字表要被多个系统共享,且每个系统都要做增删改时,数据库的事务和权限控制才有意义。单机单应用场景下,JSON加启动时加载,代码量最少,出错面最小。前端场景要单独说一句,浏览器里没有文件系统,JSON不能直接import成同步数据,常见做法是把字典作为静态资源发布,通过网络请求加载。好在3500条全量也就百KB量级,压缩后更小,一次异步加载、之后都命中缓存,比拆成几百个小模块更省事。如果你在Node侧做工具脚本,那就和Python一样直接用fs读取,不用额外设计。

2.2 字段设计:一个能覆盖常用场景的最小字典模板

字段设计决定了这份字典能不能复用。我最终落地的结构是每个字一个对象,包含字符本身、Unicode码位、读音数组、笔画数、级别和频次位。下面是最小模板:

{ "中": { "unicode": "U+4E2D", "pinyin": ["zhong1", "zhong4"], "stroke": 4, "level": "1", "freq": 1 }, "国": { "unicode": "U+56FD", "pinyin": ["guo2"], "stroke": 8, "level": "1", "freq": 2 } }

字段说明:unicode是码位,用于排序和排障;pinyin是数组而不是字符串,后面会讲为什么多音字必须这样存;stroke是笔画数,给排序用;level标记一级还是二级字;freq是频次位,用于需要“常用优先”的场景,比如随机取字时希望优先取到常见字,而不是冷僻字打头。这个结构有个好处:下游不管做人名生成、候选字排序还是笔画排序,都不需要再查第二份数据。

2.3 键用汉字还是Unicode:两种方案的取舍

整份JSON变成一个dict时,最直观的做法是用汉字本身作为键,调用代码写起来很舒服。但这里藏着一个不易察觉的问题:JSON对象的键在序列化时是有序的,可一旦你用文本编辑器动过文件、或者换了一种语言重新dump,键的顺序就可能变化,下游如果依赖“字典里第几个字”这种逻辑,就会悄悄出错。所以我建议,不管最终键选什么,导出时统一按Unicode码位排序,让文件内容保持稳定。

另一个方案是用Unicode码位作键,值里保留char字段。好处是键在任何编码环境下都不会有歧义,坏处是调用侧要写chr()来回转换,可读性差一点。我最终采用的折中方案是:对外接口用汉字作键,落地结构里保留unicode字段,导出时统一按码位排序。这样排障时打开文件是按码位顺序的,代码里又不需要转义,两头都占住了。

3. 从零构建3500字字典:数据来源、清洗与校验脚本

3.1 确定字表口径:一级2500与二级1000为什么容易混

先说明白3500这个数字怎么来的。公开常用字表把常用字分成两级:一级字2500个,是日常阅读书写最高频的部分;二级字1000个,是次高频、在一般出版物里也常出现但频率稍低的字。很多人在整理时把这两个级别混在一个文件里,或者干脆把国标字符集的6763个字全塞进来,数字对不上,后面所有基于“3500”的统计逻辑就全歪了。我的做法是:以公开常用字表为准,清洗后做两次断言——第一断言总数必须是3500,第二断言前2500个必须落在频次最高的那一级。

实际操作中还有一个容易翻车的点:网上流传的字表文件质量参差不齐,有的混进了繁体字,有的把异体字和简体字重复收录,有的文件里还混着全角空格甚至标点符号。所以拿到字表后的第一件事不是转格式,而是做数据体检。判断字表是否干净的标准很简单:每一行一个汉字;字符必须落在Unicode的CJK统一表意文字区段;去除不可见字符后不能有任何控制符。任何一行满足不了这三条,先修源文件,不要急着往下走。

3.2 清洗与去重:从原始字表到结构化JSON的Python脚本

下面这份脚本是我反复用的一套清洗流程,把原始字表文件读进来,过滤、去重、排序,最后导出JSON。源文件格式不限,只要每行一个字符即可。

import json # 1. 从源文件读入字表,每行一个字 with open("raw_chars.txt", "r", encoding="utf-8") as f: lines = f.read().splitlines() # 2. 只保留CJK统一表意文字区段内的单字 def is_cjk(ch: str) -> bool: cp = ord(ch) return 0x4E00 <= cp <= 0x9FFF cleaned = [] for line in lines: token = line.strip() if len(token) != 1: # 跳过空行和多字符行 continue if not is_cjk(token): # 过滤标点、字母、控制符 continue if token not in cleaned: # 按出现顺序去重 cleaned.append(token) # 3. 按Unicode码位排序,保证输出顺序绝对稳定 cleaned.sort(key=lambda ch: ord(ch)) # 4. 数量校验:数量不对必须立刻暴露,不能带病运行 assert len(cleaned) == 3500, f"字表数量不对: {len(cleaned)}" # 5. 组装成字典并导出JSON HANZI_DICT = {} for idx, ch in enumerate(cleaned): HANZI_DICT[ch] = { "unicode": f"U+{ord(ch):04X}", "pinyin": [], # 后续用注音数据填充 "stroke": 0, # 后续用笔画数据填充 "level": "1" if idx < 2500 else "2", "freq": idx + 1 } with open("hanzi_3500.json", "w", encoding="utf-8") as f: json.dump(HANZI_DICT, f, ensure_ascii=False, indent=2)

这个脚本里几个参数值得说明。is_cjk的码位区间取的是0x4E00到0x9FFF,这是CJK统一表意文字的基本区段,能把绝大多数繁体、异体字排除在外;如果你要更严格,再叠加一个“目标字表里存在才保留”的白名单判断。len(cleaned) == 3500这个断言是关键,数量不对就抛异常,而不是打印一行警告继续跑。json.dump里ensure_ascii=False让中文直接可读,indent=2方便打开文件排查,生产环境可以去掉缩进减小体积。level字段按排序后的序号切分,前2500个归为一级,剩下1000个归为二级,这个逻辑固定在脚本里,后面手改文件也不会破坏分级。

3.3 校验环节:字数核对和生僻字抽查方法

脚本跑完不等于数据可信,还要做三个确认。第一,抽检高频字“一、中、国、人、我”必须都在字典里,直接用in判断;第二,抽查二级字,比如“抒、彰、殉”这类,确认它们存在且level字段是"2";第三,把JSON的前30个字符和后30个字符打印出来肉眼扫一遍,确认顺序符合码位序而不是随机序。这三个检查做完,数据才算进入可用状态。

import json with open("hanzi_3500.json", "r", encoding="utf-8") as f: data = json.load(f) # 检查一:高频字必须存在 for ch in ["一", "中", "国", "人", "我"]: assert ch in data, f"缺高频字: {ch}" # 检查二:抽查二级字 for ch in ["抒", "彰", "殉"]: assert ch in data, f"缺二级字: {ch}" assert data[ch]["level"] == "2", f"级别错误: {ch}" # 检查三:打印首尾各5字,肉眼确认序 chars = list(data.keys()) print("开头:", chars[:5]) print("结尾:", chars[-5:])

校验代码本身的逻辑并不复杂,真正有价值的是这三个检查覆盖了三个不同维度的风险:高频字缺失说明来源字表不完整;二级字级别错误说明分级逻辑写错;首尾字符顺序异常说明排序步骤被绕过。我在实际使用中把这套检查固化成一个独立的validate.py,每次拿到新字表或手动改过JSON后都跑一遍,确认无误再进版本管理。

4. 在项目里调用这份字典:Python与TypeScript两种落地方式

4.1 Python场景:加载字典并按拼音取候选字

数据进内存之后,所有调用都变成字典查询。Python侧最常见的需求是按拼音前缀取候选字,比如输入“zhong”,希望拿到“中、众、终、钟”这一组字。最简单的实现是直接遍历全表:

import json with open("hanzi_3500.json", "r", encoding="utf-8") as f: HANZI_DATA = json.load(f) def find_by_pinyin_prefix(prefix: str, limit: int = 10): result = [] for ch, info in HANZI_DATA.items(): for p in info["pinyin"]: if p.startswith(prefix): result.append((ch, info["pinyin"])) break if len(result) >= limit: break return result print(find_by_pinyin_prefix("zhong", limit=5))

这里走的是全表遍历,3500条数据在Python里遍历一次是毫秒级,完全不需要建索引。pinyin字段是数组,所以内层循环用startswith匹配时会把多音字的每个读音都试一遍。注意break的层级:内层break是找到一个读音命中就停止这个字继续遍历读音,外层break是凑够limit就不再往下跑了。如果你后续要做真正的拼音前缀补全,下一章的进阶部分会给前缀树方案,那个才是正确做法,这里先理解数据流。

4.2 前端场景:TypeScript类型定义与按需加载

前端环境没有文件系统,JSON需要通过网络加载。给TypeScript项目接入时,第一步是定义类型,第二步是写一个异步加载函数,把JSON对象转成原生Map。Map相比普通对象有两个优势:查询时间稳定,且不会受到原型链上同名属性的干扰。

interface HanziInfo { unicode: string; pinyin: string[]; stroke: number; level: string; freq: number; } async function loadHanziMap(url: string): Promise<Map<string, HanziInfo>> { const res = await fetch(url); const data = (await res.json()) as Record<string, HanziInfo>; return new Map(Object.entries(data)); } const hanziMap = await loadHanziMap("/assets/hanzi_3500.json"); const info = hanziMap.get("中"); console.log(info?.pinyin);

Object.entries(data)把JSON对象转成[key, value]对的数组,再传给Map构造函数,这样查询复杂度是O(1)。加载时机上,我建议在应用启动时做一次,后续路由切换不重复请求。如果担心首屏体积,可以让字典文件走浏览器缓存或放到单独chunk里按需加载,但不要让每个业务组件各自请求一遍同一个JSON,那会造成重复解析。

4.3 把“按拼音取字”封装成通用查询函数

把逻辑收拢成一个函数能避免业务代码里到处写遍历。我一般会设计一个带pinyin、limit、sort_by三个参数的查询接口,sort_by支持freq和stroke两个选项,cover住大多数场景:

def query_hanzi( pinyin: str = "", limit: int = 10, sort_by: str = "freq", ): items = [] for ch, info in HANZI_DATA.items(): if pinyin and not any( p.startswith(pinyin) for p in info["pinyin"] ): continue items.append((ch, info)) if sort_by == "stroke": items.sort(key=lambda x: x[1]["stroke"]) elif sort_by == "freq": items.sort(key=lambda x: x[1]["freq"]) return [ch for ch, _ in items[:limit]] # 调用示例:查"yi"开头的读音,按笔画排序,取前8个 print(query_hanzi(pinyin="yi", limit=8, sort_by="stroke"))

any(p.startswith(pinyin) for p in info["pinyin"])这种写法在多音字场景下很重要,只要任意一个读音命中就计入结果。sort_by默认走freq,这样候选字顺序稳定且符合直觉;改成stroke后同一组字的排列顺序会从结构上变化,方便做笔画排序需求。这个函数直接暴露给业务层,字典内部的存储结构调整不影响调用方。

5. 避坑指南:3500字字典接入项目后最常见的5个坑

5.1 坑一:字序怎么总和对不上

现象:本地跑排序结果正常,发到测试环境后候选字的顺序变了,同一个“zhong”查出来的字序和预期不一致。原因:JSON文件在编辑过程中被不同工具重排过键序,或者运行时用的字典文件经过了渠道二次打包,键的遍历顺序发生了改变。解决:导出JSON时固定按Unicode码位排序,并将排序逻辑留在清洗脚本里,不依赖编辑器手动整理;同时打印字典首尾字符做抽查,确认加载的文件确实是预期的那份。

5.2 坑二:多音字把排序带偏

现象:“重”字有“zhong4”和“chong2”两个读音,做拼音排序时它被排到“c”开头的组里,产品经理想按“重量”的读音排,两边对不上。原因:字典里只保留了一个默认读音,排序逻辑取的就是这个唯一值,于是不同人对“正确读音”的理解产生了分歧。解决:pinyin字段改成数组存全量读音,排序时明确约定取数组第0个作为sort_key,注释里写明“这里是默认读音约定,不代表唯一读音”。这样数据是全的,行为是可解释的。

5.3 坑三:文件编码在GBK与UTF-8之间反复横跳

现象:同一个JSON在某些环境能加载,在另一些环境报UnicodeDecodeError,打开文件看到开头有个像“锘”一样的乱码字符。原因:源字表可能是GBK编码,导出时误用了带BOM的UTF-8,或者相反,读取时没做编码匹配。解决:统一起源文件读取用encoding="utf-8",导出JSON用encoding="utf-8"且不加BOM;如果确实要处理Windows工具产生的BOM文件,读取时用utf-8-sig,清洗后重新导出干净格式。

5.4 坑四:拿3500当汉字全集,生僻字直接翻车

现象:用户在输入框里输入“龘”,系统查字典返回空,接口报错或直接拦截了这个字符。原因:3500字表本身就是常用字口径,不是汉字全集,Unicode里的CJK扩展区还有大量生僻字没被收录。解决:设计回退策略,字典查不到时保留原字透传返回,不拦截不报错;同时把字典的覆盖范围写成注释或文档,明确告知下游“仅覆盖常用字,生僻字走透传”。这一条在验证码、OCR后处理这类场景尤其重要,拦截生僻字等于让用户无法输入合法内容。

5.5 坑五:加载性能偶发卡顿,罪魁是重复构造

现象:页面切到汉字查询页时偶尔卡一下,接口响应时间波动大,但单次查询本身很快。原因:每次进入页面都重新加载JSON并重新构造Map或dict,没有复用;更隐蔽的情况是有人把所有拼音组合提前展开成独立索引,内存里多存好几份拷贝。解决:模块级单例缓存,加载一次后全局共享;拼音索引采用懒构建,第一次查询时才生成,后续查询复用。3500条数据的加载解析成本本可以忽略,重复构造才是性能问题的来源。

6. 进阶用法:把3500字典变成查询和排序的通用基础设施

6.1 用字典做笔画排序:一套通用的sort_key写法

有了stroke字段,任何字符数组都能按笔画数排序。我一般封装成一个小函数,未收录字符给一个极大值兜底,避免排序中途崩溃:

def sort_by_stroke(chars: list[str]) -> list[str]: return sorted( chars, key=lambda ch: HANZI_DATA.get(ch, {}).get("stroke", 999) )

这个写法把“查字典、取笔画、缺失兜底”合在了一行里,下游排序时不需要关心字典内部结构。实际场景里它经常配合“人名笔画排序”“班级名单按姓名笔画排列”这类需求使用,比自己在业务层维护一份笔画表省事得多。

6.2 拼音前缀补全:50行以内的轻量前缀树

前面提到startswith遍历在数据量小的时候够用,但如果要做输入框里的实时补全,前缀树是更合适的结构。下面是一个不依赖任何第三方库的原地实现,构建时把每个读音都插进去,查询时按前缀下钻:

class PinyinTrie: def __init__(self): self.root = {"children": {}, "chars": []} def add(self, pinyin: str, char: str): node = self.root for ch in pinyin: if ch not in node["children"]: node["children"][ch] = {"children": {}, "chars": []} node = node["children"][ch] if char not in node["chars"]: node["chars"].append(char) def query(self, prefix: str) -> list[str]: node = self.root for ch in prefix: if ch not in node["children"]: return [] node = node["children"][ch] res = [] stack = [node] while stack: cur = stack.pop() res.extend(cur["chars"]) stack.extend(cur["children"].values()) return res trie = PinyinTrie() for ch, info in HANZI_DATA.items(): for p in info["pinyin"]: trie.add(p, ch)

构建时把每个汉字的每个读音都加进去,查询时先用for ch in prefix一路下钻到前缀对应的节点,再用栈遍历所有子树收集候选字。这里用栈而不是递归,是为了避开Python默认递归深度限制,同时减少调用栈开销。全量构建3500个字的读音树,耗时在毫秒级,内存占用也不大。做完这一步,输入法候选、搜索联想这些场景就都有同一套基础设施支撑了。

6.3 收尾

我给自己养成了一个习惯:每次改动字表后跑一遍“三查”——数量断言是不是3500、高频字抽查是否都在、首尾字符打印出来确认顺序没乱。这套流程成本极低,但它保证了一份字典在任何时候被加载,行为都是可预期的。后来做模拟项目X时,随机用户名生成、按笔画排序、拼音搜索三个功能都引用了同一份数据源,没有一个地方再硬编码汉字表。数据只维护一份,调用方永远从字典接口走,反而比之前各处写死字符列表更省心。希望这份构建思路帮到你,踩过的那些坑,你也就不用再踩一遍了。

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

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

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

立即咨询