资讯详情

汉字转拼音实战:txt与json双格式输出及多音字处理

📅 2026/10/15 1:06:25 | 华诺云谱 👁 阅读
汉字转拼音实战:txt与json双格式输出及多音字处理
简介这份资源面向中文信息处理开发者、语言学习者及教学人员提供汉字与汉语拼音的完整对应数据解决拼音查询、数据集成与批量处理的需求。压缩包共3个文件包含1个json、1个txt和1个py脚本整体约174KB其中txt便于直接阅读编辑json结构清晰适合程序解析Python脚本则用于将文本数据转换为JSON格式。数据覆盖20850个汉字及其拼音可应用于中文输入法、搜索引擎、语音识别及自然语言处理等场景也适合初学者练习发音或教师作为教学工具。已有505人学习下载。借助这份资源读者能快速获得结构化的拼音库并通过脚本灵活完成格式转换与自动化集成还可对汉字使用频率、拼音分布进行统计分析为教育、科研与开发工作提供便利。1. 汉字转拼音这件事为什么txt和json两种格式总得同时准备做中文文本处理的人迟早会撞上一个需求把汉字批量转成拼音。可能是给语音合成做前端文本归一化可能是给检索系统建拼音索引也可能是给数据标注流水线补一层注音特征。需求本身不复杂但真正落地时你会发现一个绕不开的岔路口——上游要的是纯文本下游要的是结构化数据。于是同一批汉字你得同时产出 txt 和 json 两份拼音结果。我最早做这个是在一个文本清洗流程里当时只输出了 txt每行「汉字 拼音」用空格隔开。结果下游做多音字校验的同事直接翻车他需要知道每个字对应的拼音是哪一个而空格分隔在遇到「西安」这种词时拼音写成「xi an」他分不清是「西 安」还是「西安」这个词的整体注音。后来改成 json每个字一个对象问题才解决。但 json 又不能直接喂给某些只认纯文本的老工具。所以现实就是txt 给人看、给简单工具用json 给程序解析、给需要字级对齐的场景用两者都得有。这篇笔记就围绕「汉字对应拼音包括 txt 和 json」这个具体任务把选型、实现、参数和踩坑讲清楚。适合正在做中文 NLP 数据预处理、语音前端、检索索引的工程师也适合需要批量处理注音数据但不想引入重型依赖的开发者。核心工具用 Python 的 pypinyin这是目前最稳的选择下面会讲为什么。2. 选 pypinyin 还是自己查表字级对齐和格式输出的取舍2.1 为什么字级对齐是硬需求汉字转拼音看起来简单但真正决定方案能不能用的是「字级对齐」这个能力。什么叫字级对齐就是给定一个汉字序列你能明确知道第 i 个汉字对应哪个拼音。很多场景下这比拼音本身还重要。比如做多音字消歧的训练数据你需要标注「银行」的「行」读 hang 还是 xing如果拼音结果是一整串「yin hang」你没法直接定位到第二个字。再比如做拼音输入法的候选排序每个按键对应哪个字必须一一对应。还有语音合成里的韵律预测字和音的边界错了合成出来就是灾难。pypinyin 的核心优势就在这里它默认返回的是嵌套列表外层是词内层是每个字的拼音。你调lazy_pinyin得到的是扁平列表调pinyin得到的是带声调的嵌套结构。这个设计直接支持字级对齐不需要你再去做对齐解析。自己查表行不行小规模可以但多音字、轻声、儿化、姓氏特殊读音这些边界情况维护一张表的时间成本远超引入一个成熟库。pypinyin 内置了《汉语拼音正词法基本规则》和大量词典多音字准确率在通用文本上够用特殊场景还能自定义词典覆盖。2.2 txt 和 json 各自该长什么样在动手之前先把两种格式的 schema 定下来不然后面返工很痛苦。txt 格式我一般用两种模式。一种是「汉字\t拼音」逐行对应适合人眼校对和简单脚本处理另一种是「整句拼音」一行一句适合直接喂给下游做索引。两种模式不要混在一个文件里否则解析逻辑会变得很脏。json 格式我推荐按「文档-句子-字」三层组织每个字是一个对象包含char、pinyin、pinyin_tone带声调、pinyin_num数字声调四个字段。这样下游无论要哪种形式都能直接取不用再转换。句子层面加一个text字段保留原文方便回溯。下面是一个最小 json 结构示例先感受一下{ text: 银行行长, chars: [ {char: 银, pinyin: yin, pinyin_tone: yín, pinyin_num: yin2}, {char: 行, pinyin: hang, pinyin_tone: háng, pinyin_num: hang2}, {char: 行, pinyin: hang, pinyin_tone: háng, pinyin_num: hang2}, {char: 长, pinyin: zhang, pinyin_tone: zhǎng, pinyin_num: zhang3} ] }注意这里「行」出现了两次拼音都是 hang但实际「银行」的「行」是 hang「行长」的「长」是 zhang。pypinyin 在词组模式下能正确区分这就是为什么要用词组模式而不是逐字转换。2.3 安装与最小可跑通示例先装库。pypinyin 是纯 Python 实现没有编译依赖pip 直接装pip install pypinyin如果你需要处理大量文本建议再装一个tqdm看进度不是必须但体验好很多pip install tqdm最小示例验证环境是否正常from pypinyin import pinyin, lazy_pinyin, Style # 逐字带声调嵌套列表 result pinyin(银行行长) print(result) # [[yín], [háng], [háng], [zhǎng]] # 扁平不带声调 flat lazy_pinyin(银行行长) print(flat) # [yin, hang, hang, zhang] # 数字声调 num lazy_pinyin(银行行长, styleStyle.TONE3) print(num) # [yin2, hang2, hang2, zhang3]这里的关键参数是style。Style.TONE是带声调符号Style.TONE3是数字声调Style.NORMAL是不带声调。lazy_pinyin默认走Style.NORMALpinyin默认走Style.TONE。选哪个取决于下游做显示用 TONE做检索用 NORMAL做语音前端用 TONE3 比较稳因为数字声调在文本里不会因为编码问题丢符号。提示lazy_pinyin虽然叫 lazy但它返回的是完整列表不是生成器。名字来源是它跳过了声调处理速度更快不是惰性求值。3. 批量转换脚本从原始文本到 txt 和 json 双输出3.1 输入文本的预处理原始文本往往不干净。直接丢给 pypinyin 之前至少要做三件事去掉不可见字符、统一标点、处理空行。不可见字符里最常见的是零宽空格和 BOM。零宽空格在复制粘贴的文本里很常见它会被当成一个汉字去转拼音结果得到一个空字符串污染输出。BOM 在文件开头不处理会导致第一行多一个字符。标点统一主要是全角半角的问题。pypinyin 对标点默认是原样保留但如果你做的是拼音索引标点应该被过滤掉。我一般用正则把非汉字字符先标记出来转换时跳过输出时再按需保留。import re def clean_text(text): # 去掉 BOM 和零宽字符 text text.replace(\ufeff, ).replace(\u200b, ) # 统一换行 text text.replace(\r\n, \n).replace(\r, \n) # 去掉首尾空白但保留行内空格 lines [line.strip() for line in text.split(\n)] # 去掉空行 lines [line for line in lines if line] return lines def is_hanzi(ch): return \u4e00 ch \u9fffis_hanzi这个判断用的是基本汉字区覆盖了绝大多数常用字。生僻字和扩展区汉字如果需要支持要把范围扩到\u3400-\u4dbf和\u20000-\u2a6df但一般场景用基本区就够了。3.2 核心转换函数与双格式输出下面这个函数是整个流程的核心。它接收一行文本返回字级对齐的结果然后分别写出 txt 和 json。import json from pypinyin import pinyin, Style def convert_line(line): 把一行文本转成字级拼音结构 # 只对汉字做拼音转换非汉字保留原字符 chars [] for ch in line: if is_hanzi(ch): # 用 pinyin 函数获取带声调结果取第一个读音 py pinyin(ch, styleStyle.TONE, heteronymFalse) py_tone py[0][0] if py else py_normal pinyin(ch, styleStyle.NORMAL, heteronymFalse)[0][0] py_num pinyin(ch, styleStyle.TONE3, heteronymFalse)[0][0] chars.append({ char: ch, pinyin: py_normal, pinyin_tone: py_tone, pinyin_num: py_num }) else: # 非汉字字符拼音字段留空 chars.append({ char: ch, pinyin: , pinyin_tone: , pinyin_num: }) return { text: line, chars: chars } def write_txt(results, txt_path): 输出逐行 汉字\t拼音 格式 with open(txt_path, w, encodingutf-8) as f: for item in results: for c in item[chars]: if c[pinyin]: f.write(f{c[char]}\t{c[pinyin_tone]}\n) def write_json(results, json_path): 输出结构化 json with open(json_path, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)这里有几个参数需要解释。heteronymFalse表示不返回多音字的所有读音只返回一个最可能的。如果你做多音字消歧要把它设成True然后自己根据上下文选。Style.TONE返回带声调符号的拼音Style.NORMAL不带Style.TONE3用数字标声调。注意我在convert_line里对每个字调了三次pinyin这其实有性能问题。更好的做法是一次调用拿到所有信息但 pypinyin 的 API 设计是 style 决定输出形式没法一次拿三种。如果性能敏感可以只调一次Style.TONE3然后从数字声调反推另外两种但反推逻辑要自己写容易出错。我一般接受这个开销因为批量处理时瓶颈通常在 IO 不在 CPU。3.3 词组模式与多音字处理逐字转换最大的问题是多音字。上面convert_line是逐字调的遇到「银行」会把「行」转成 xing这是错的。正确做法是用词组模式让 pypinyin 根据上下文判断。from pypinyin import pinyin, Style def convert_line_v2(line): 词组模式先整体转再对齐 # 对整行做拼音转换返回嵌套列表 py_result pinyin(line, styleStyle.TONE3, heteronymFalse) # py_result 的长度和 line 的长度一致每个元素是一个列表 chars [] for ch, py in zip(line, py_result): if is_hanzi(ch): py_num py[0] if py else # 从数字声调反推其他形式 py_tone num_to_tone(py_num) py_normal py_num.rstrip(0123456789) chars.append({ char: ch, pinyin: py_normal, pinyin_tone: py_tone, pinyin_num: py_num }) else: chars.append({char: ch, pinyin: , pinyin_tone: , pinyin_num: }) return {text: line, chars: chars}pinyin(line)对整行调用时pypinyin 内部会做分词然后按词查多音字。这样「银行」会被识别为一个词「行」读 hang。这是词组模式的核心价值。但要注意pinyin返回的嵌套列表长度和输入字符串长度一致标点也会占一个位置返回空列表。所以zip(line, py_result)是对齐的不需要额外处理。num_to_tone函数负责把yin2转成yín需要维护一个声调映射表TONE_MAP { a: [ā, á, ǎ, à], o: [ō, ó, ǒ, ò], e: [ē, é, ě, è], i: [ī, í, ǐ, ì], u: [ū, ú, ǔ, ù], v: [ǖ, ǘ, ǚ, ǜ], } def num_to_tone(py_num): if not py_num or not py_num[-1].isdigit(): return py_num tone int(py_num[-1]) if tone 0 or tone 5: return py_num[:-1] base py_num[:-1] # 按 a o e i u v 的优先级找韵母 for v in aoeiuv: if v in base: idx base.index(v) return base[:idx] TONE_MAP[v][tone-1] base[idx1:] return base这个映射表覆盖了标准韵母。v对应 üpypinyin 在 TONE3 模式下用 v 表示 ü。优先级 a o e i u v 是汉语拼音标调规则必须按这个顺序找否则「iu」这种韵母会标错位置。3.4 完整流水线与命令行入口把上面的片段串起来加一个命令行入口就能直接用了。import argparse import json from tqdm import tqdm def main(): parser argparse.ArgumentParser() parser.add_argument(input, help输入文本文件) parser.add_argument(--txt, defaultoutput.txt, helptxt 输出路径) parser.add_argument(--json, defaultoutput.json, helpjson 输出路径) args parser.parse_args() with open(args.input, r, encodingutf-8) as f: raw f.read() lines clean_text(raw) results [] for line in tqdm(lines, desc转换中): results.append(convert_line_v2(line)) write_txt(results, args.txt) write_json(results, args.json) print(f完成{len(results)} 行txt - {args.txt}json - {args.json}) if __name__ __main__: main()跑起来就是python convert.py input.txt --txt out.txt --json out.jsontqdm的进度条在大文件上很有用几千行以上没有进度条会让人焦虑。argparse的参数设计让脚本可以复用不用每次改代码。注意如果输入文件是 GBK 编码open时要指定encodinggbk否则会报 UnicodeDecodeError。不确定编码时可以用chardet探测但生产环境最好统一转成 UTF-8。4. 避坑与排查拼音转换里那些让人翻车的细节4.1 多音字在逐字模式下集体翻车现象转换「重庆」「银行」「音乐」这类词拼音全错「重」变成 zhong 而不是 chong「行」变成 xing 而不是 hang。原因逐字调用pinyin(ch)时pypinyin 没有上下文只能返回最常用的读音。多音字的正确读音依赖词组。解决永远用整行或整词调用pinyin(text)不要逐字调。如果必须逐字处理先用分词工具切词再对每个词整体转拼音。分词可以用 jieba但引入 jieba 会增加依赖简单场景直接用 pypinyin 的词组模式就够。4.2 数字声调反推带声调符号时标错位置现象num_to_tone(liu2)返回líu而不是liú声调标在了 i 上。原因标调优先级搞错了。汉语拼音规则是 a o e i u ü有 a 标 a没 a 标 o以此类推。liu里没有 a、o、e应该标在 u 上但代码里按aoeiuv顺序找先找到了 i。解决优先级顺序必须是a, o, e, i, u, v但iu和ui是特殊情况标在后面的元音上。更稳妥的做法是直接用 pypinyin 的Style.TONE拿带声调结果不要自己反推。反推只在性能极度敏感且能接受少量错误时用。4.3 json 输出中文被转义成 \uXXXX现象打开 json 文件里面全是\u94f6\u884c这样的转义序列人眼没法读。原因json.dump默认ensure_asciiTrue会把所有非 ASCII 字符转义。解决加ensure_asciiFalse。这个参数在写中文 json 时是必须的不加的话文件虽然合法但没法直接看。另外indent2让 json 有缩进方便 diff 和人工检查。4.4 大文件内存爆掉现象处理几十万行的文本时脚本跑着跑着被 OOM killer 杀掉。原因把所有结果攒在results列表里最后一次性写文件。几十万行每行几十个字每个字一个 dict内存占用轻松上 GB。解决改成流式写入。txt 可以边转边写json 如果必须是单个数组可以用json.dump的流式写法或者改成 JSON Lines 格式每行一个独立 json 对象。JSON Lines 的好处是可以逐行读取不用一次性加载。def write_jsonl(results, path): with open(path, w, encodingutf-8) as f: for item in results: f.write(json.dumps(item, ensure_asciiFalse) \n)如果下游必须要求单个 json 数组那就分批处理每批写一个临时文件最后合并。但更推荐推动下游接受 JSON Lines这是大数据场景的标准做法。4.5 生僻字和异体字返回空拼音现象某些生僻字转换后拼音是空字符串或者返回原字。原因pypinyin 的词典覆盖的是常用字和通用规范汉字表超出范围的生僻字没有收录。解决对返回空的字可以查 Unihan 数据库的 kMandarin 字段或者用pypinyin的errors参数指定默认行为。errorsdefault会返回原字errorsignore会跳过。我一般设errorsdefault然后在输出里标记出来人工确认。生僻字在通用文本里占比极低不值得为它引入额外依赖。5. 进阶自定义词典、性能优化与格式校验5.1 用自定义词典修正领域专有名词通用词典搞不定的领域词pypinyin 支持自定义词典。比如某个项目里「阿房宫」必须读 e pang gong但通用词典可能给 a fang gong。这时候加载自定义词典覆盖。from pypinyin import load_phrases_dict, pinyin, Style load_phrases_dict({ 阿房宫: [[ē], [páng], [gōng]], 虚与委蛇: [[xū], [yǔ], [wēi], [yí]], }) print(pinyin(阿房宫, styleStyle.TONE)) # [[ē], [páng], [gōng]]load_phrases_dict接收一个字典key 是词组value 是每个字的拼音列表。注意 value 是嵌套列表每个字一个列表因为多音字可能有多个读音。加载后全局生效后续所有转换都会优先匹配自定义词典。这个机制在垂直领域非常有用。医疗文本里的「卒中」读 cu zhong 不是 zu zhong法律文本里的「法人」读 fa ren 没问题但「行」在「行长」里读 hang这些都可以通过自定义词典固化下来。词典可以放在一个独立的 py 文件里启动时加载方便维护。5.2 性能批量转换的吞吐量优化pypinyin 的性能瓶颈主要在词组匹配和声调处理。几个优化方向第一用lazy_pinyin代替pinyin。lazy_pinyin跳过声调符号生成速度快 30% 左右。如果下游只需要不带声调的拼音直接用lazy_pinyin。第二减少 style 切换。每次调用pinyin都指定 style不要在一个循环里反复切。如果同时需要多种 style先转一次 TONE3然后从 TONE3 反推其他形式虽然反推有出错风险但在性能敏感场景值得。第三用多进程。pypinyin 是纯 PythonGIL 限制多线程但多进程可以线性加速。按行切分文件每个进程处理一段最后合并。from multiprocessing import Pool def process_chunk(lines): return [convert_line_v2(line) for line in lines] def parallel_convert(lines, workers4): chunk_size len(lines) // workers 1 chunks [lines[i:ichunk_size] for i in range(0, len(lines), chunk_size)] with Pool(workers) as pool: results pool.map(process_chunk, chunks) # 展平 return [item for sublist in results for item in sublist]workers一般设成 CPU 核数。注意多进程有启动开销行数少于几千行时反而更慢小文件直接单进程。5.3 输出格式校验确保 txt 和 json 一致双格式输出最大的风险是两份数据不一致。txt 写成功了 json 写失败或者两边字段对不上。上线前必须做一致性校验。校验思路从 txt 读回「汉字\t拼音」对从 json 读回 chars 数组逐条比对。如果不一致定位到具体行。def validate(txt_path, json_path): # 读 txt txt_pairs [] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split(\t) if len(parts) 2: txt_pairs.append(tuple(parts)) # 读 json with open(json_path, r, encodingutf-8) as f: data json.load(f) json_pairs [] for item in data: for c in item[chars]: if c[pinyin]: json_pairs.append((c[char], c[pinyin_tone])) # 比对 if len(txt_pairs) ! len(json_pairs): print(f数量不一致txt {len(txt_pairs)} vs json {len(json_pairs)}) return False for i, (a, b) in enumerate(zip(txt_pairs, json_pairs)): if a ! b: print(f第 {i} 条不一致txt {a} vs json {b}) return False print(校验通过) return True这个校验函数应该作为流水线的最后一步每次生成后自动跑。我吃过亏有一次 json 写的时候ensure_ascii忘了关txt 正常但 json 全是转义下游解析出来拼音字段是空的排查了半天才发现是编码参数问题。从那以后校验就成了固定步骤。5.4 一个容易忽略的细节儿化和轻声儿化音在拼音里通常写成r附在前一个音节后比如「花儿」是huar。pypinyin 默认会把「儿」单独转成er不会自动合并。如果需要儿化合并要自己处理检测「儿」前面是不是汉字如果是且构成儿化词就把er合并到前一个音节。轻声在 TONE3 里标为 5比如「妈妈」的第二个「妈」是ma5。num_to_tone里对 tone 5 的处理是去掉数字返回原音节这是对的因为轻声不标调。但有些下游系统期望轻声标为 0 或空这个要按下游约定来。这两个细节在通用文本里出现频率不低尤其是口语化文本。如果做的是对话系统或语音合成前端必须处理。处理方式没有统一标准取决于下游的拼音规范建议在项目初期就和下游对齐。我现在的习惯是任何拼音转换任务先拿一段包含多音字、儿化、轻声、生僻字的测试文本跑一遍人工检查输出确认边界情况都符合预期再上批量。这个测试文本我固定用「银行行长在重庆的音乐厅里和面花儿开了妈妈来了」这一句基本覆盖了最常见的坑。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑