资讯动态

Python实现汉字转盲文:从拼音拆分到Unicode点位编码全解析

发布时间:2026/9/23 10:52:17 来源:尧图企业网站定制
1. 起因与目标我为什么要碰汉字生成盲文这个冷门方向1.1 盲文其实离我们很近只是平时看不见前阵子整理旧物翻出一本以前出于好奇买的盲文练习册页面上是密密麻麻的凸点。说实话当时买回来翻了两页就搁置了因为完全读不懂。可那天再看到它我突然意识到一件事电梯按钮、药盒包装、地铁站牌、人民币右下角盲文一直都在只是我们这些视力正常的人平时根本不会注意到。更让我触动的是身边真正能读写盲文的人少之又少。盲文印刷成本高、学习门槛不低很多低视力或全盲的朋友更依赖读屏软件而读屏软件能读的内容未必都有对应的盲文版本。如果我能写一个工具把普通的汉字句子直接转换成盲文点位既可以拿来学习盲文编码规则也能给做无障碍设计的人当参考甚至用在教育场景里教孩子认识盲文那不是挺好这个想法最终落地成一个 Python 小项目输入一句中文输出对应的拼音、盲文编码、Unicode 盲文字符和可视化点位图。主要依赖是 pypinyin 这个拼音库外加 Python 内置的 Unicode 字节操作。整条链路不复杂但有几个地方相当值得展开讲。1.2 这个翻译器到底能做到什么程度先把预期管理做好这不是一个生产级的国家通用盲文翻译系统而是一个把汉字到盲文的转换逻辑讲清楚、能运行的编程示例。它能正确处理常见汉字的拼音拆解、声调提取、盲文点位映射、Unicode 盲文输出和图形化示意也能处理多音字的常见读音。但盲文方案本身是有一套国家标准的完整的词库、简写规则、标点方案和专用符号非常多这篇文章实现的是核心框架和教学简化映射表扩展点我会在最后一节详细说。适合来看这篇文章的人有三类第一是对无障碍编程感兴趣、想做个相关小项目的开发者第二是刚学 Python、想找一个能跑起来又有点实际意义的练习项目的朋友第三是从事特殊教育或无障碍设计、需要快速把汉字转成盲文点位做演示素材的人。2. 盲文编码不神秘六个点位就是六位二进制2.1 一个盲文方等于一个 6 位的二进制数盲文的基本单位是一个方就是指尖能摸到的一个长方形凸点组合。标准盲文方有 6 个点位左边从上到下是 1、2、3 点右边从上到下是 4、5、6 点。每个点只有凸起和凹陷两种状态这就好像 6 个开关每个开关开或关就构成一个编码理论上一共有 2 的 6 次方也就是 64 种组合。用计算机的视角看这就是一个 6 位的二进制数。点 1 对应最低位点 2 对应第二位以此类推。比如点位[1, 2]就代表二进制000011十进制是 3。而 Unicode 专门留了一块区域给盲文U2800到U28FF。其中U2800是空方一个凸点都没有U2801是点 1 凸起U2803是点 1 和点 2 同时凸起。规律非常直观把点位做按位或运算得到一个掩码加上0x2800就是对应的盲文字符。def dots_to_char(dots): mask 0 for dot in dots: mask | 1 (dot - 1) return chr(0x2800 mask)测试一下dots_to_char([1, 2])得到⠃这正好是英文字母 b 的盲文写法。在英文盲文里字母 a 到 j 就是点位的简单组合比如 a 是点 1b 是点 1-2c 是点 1-4。中文盲文没有直接采用这种按字母拼写的方式而是走了另一条更符合汉语特点的路线。2.2 中文盲文是拼音盲文一个汉字由三段组成中文盲文不直接编码汉字字形而是编码汉语拼音。一个普通汉字的盲文通常由这么几部分组成声母方、韵母方再加一个可选的声调符号。比如你字拼音是 nǐ声母是 n韵母是 i声调是第三声那么它的盲文就是声母 n 的盲文方 韵母 i 的盲文方 第三声的声调方。这个设计的深层逻辑是汉字数量上万但拼音音节只有 400 多个带声调的音节也不过 1300 多个。用拼音做媒介盲文点位组合完全够用学习成本也比每个汉字单独编码低得多。但这也给编程带来了一个关键任务把一个汉字转换成拼音后还要把拼音正确地拆成声母、韵母和声调三部分才能查表输出盲文。所以这个项目的核心链路其实是汉字 → 拼音带声调 → 拆成声母/韵母/声调 → 查盲文映射表 → 输出盲文方理解了这条链路汉字生成盲文就不再是玄学而是一个纯粹的数据处理问题。接下来要解决的就是三个小问题怎么得到带声调的拼音、怎么拆拼音、怎么维护映射表。3. 技术选型与数据处理pypinyin 加自定义映射表3.1 为什么我选了 pypinyin 而不是自己造拼音轮子写汉字转拼音第一反应可能是自己维护一张汉字拼音对照表。我劝你放弃这个念头。常用汉字几千个多音字几百个还有繁体、生僻字、轻声变调手工维护表既枯燥又容易出错而且完全没有必要。pypinyin是一个很成熟的汉字转拼音库支持多种拼音风格。我用的版本是 0.51 以上安装一行命令搞定pip install pypinyin关键点是选择拼音输出风格。pypinyin 的Style里常见的有三种风格输出示例说明Style.NORMALni hao不带声调Style.TONEnǐ hǎo声调是 Unicode 注音符号Style.TONE3ni3 hao3声调用数字 1-5 标注轻声是 5 或不标我选了Style.TONE3原因是后续拆声母韵母时数字结尾最好处理。Style.TONE的注音符号需要额外的 Unicode 转换绕了一圈没必要Style.NORMAL又把声调信息丢了中文盲文必须有声调符号所以不能选。这个选择看着不起眼实际操作中能省掉大量麻烦。用法很简单from pypinyin import lazy_pinyin, Style print(lazy_pinyin(你好世界, styleStyle.TONE3)) # 输出: [ni3, hao3, shi4, jie4]这里要提一个坑lazy_pinyin默认对多音字只取一个读音通常是按词频或常用语料排序后的最常见读音。对绝大多数场景够用但对银行的行、长春的长这种词就可能不如预期。后面我会讲怎么兜底处理。3.2 拼音拆分的核心逻辑先匹配双字母声母拿到类似ni3这样的拼音串后要拆成声母、韵母和声调。声母有单个字母的也有双字母的例如zh、ch、sh。拆的时候必须优先匹配双字母声母否则会把zhi错误地拆成z加hi。我的实现是先判断最后一个字符是不是数字是的话取出来作为声调然后遍历一个声母列表找到第一个能匹配的声母剩下的部分就是韵母。如果整个音节开头没有匹配到任何声母比如a、ai、ou、er这样的零声母音节就返回空声母、整个拼音作为韵母。INITIALS [ zh, ch, sh, b, p, m, f, d, t, n, l, g, k, h, j, q, x, r, z, c, s, y, w ] def split_pinyin(syllable): tone 5 # 默认轻声 if syllable and syllable[-1] in 12345: tone int(syllable[-1]) syllable syllable[:-1] for init in INITIALS: if syllable.startswith(init) and len(syllable) len(init): return init, syllable[len(init):], tone return , syllable, tone注意一个细节len(syllable) len(init)这个条件必须有。如果不加遇到饿的拼音e4遍历到声母e的时候也会匹配成功但声母列表里根本没有单独的 e不过加了y、w这些声母后yi、wu会被拆成y i和w u这没问题拼音方案里它们本来就可以当声母看待。另外pypinyin 对ü的默认输出是字母v比如绿返回lv4。所以韵母映射表里我会单独放一个v对应拼音中的ü。3.3 映射表怎么设计才能避免撞码盲文点位映射是整个翻译器的心脏。我在工程里维护了两张字典声母映射表SM_MAP和韵母映射表YM_MAP。每个键是声母或韵母值是一个点位列表。这里必须坦白我这份映射表是教学简化版主要目的是让程序逻辑完整可跑。真正做正式无障碍产品时务必去查《国家通用盲文方案》或《汉语盲文方案》的官方字表逐项校准不能拿我这套表直接上线。原因很简单盲文点位一共有 64 种组合声母韵母加起来上百个如果不按标准排布很容易出现两个不同音共用同一点位也就是撞码。我写了一个校验函数用来检查映射表内部是否有冲突def check_conflicts(mapping): seen {} for name, dots in mapping.items(): key tuple(dots) if key in seen: print(f冲突: {name} 和 {seen[key]} 都对应点位 {key}) else: seen[key] name print(检查完成)整理映射表的时候撞码是很常见的别慌改点位重新跑一遍校验就行。这个小函数看着不起眼但我在好几个类似项目里都靠它省下了大量排查时间。下面是我这份教学简化表的节选SM_MAP { b: [1, 2], p: [1, 2, 3, 4], m: [1, 3, 4], f: [1, 2, 4], d: [1, 4, 5], t: [2, 3, 4, 5], n: [1, 3, 4, 5], l: [1, 2, 3], g: [1, 2, 4, 5], k: [1, 3], h: [1, 2, 5], j: [2, 4, 5], q: [1, 2, 3, 4, 5], x: [1, 3, 4, 6], zh: [1, 2, 3, 4, 6], ch: [1, 2, 3, 5, 6], sh: [2, 3, 4, 6], r: [1, 2, 3, 5], z: [2, 3, 4], c: [1, 3, 5, 6], s: [3, 4, 6], y: [1, 3, 4, 5, 6], w: [2, 4, 5, 6], } YM_MAP { a: [1], o: [1, 3, 5], e: [1, 5], i: [2, 4], u: [1, 3, 6], v: [1, 2, 3, 6], ai: [2, 4, 6], ei: [1, 2, 4, 6], ui: [2, 4, 5, 6], ao: [1, 3, 5, 6], ou: [2, 3, 5, 6], iu: [1, 4, 6], ie: [1, 2, 4, 5], ve: [1, 2, 3, 4, 6], er: [1, 2, 3, 5], an: [1, 2, 3, 4, 6], en: [2, 3, 4, 6], in: [1, 4, 5, 6], un: [1, 3, 4, 5, 6], vn: [1, 2, 4, 5, 6], ang: [1, 3, 4, 5, 6], eng: [3, 4, 5, 6], ing: [1, 2, 5, 6], ong: [1, 3, 4, 6], } TONE_MAP { 1: [], # 一声不标简化处理 2: [2, 6], 3: [3, 5], 4: [4, 6], 5: [], # 轻声不标 }声调的处理我做了简化第一声和轻声不额外输出声调方第二声到第四声各用一个点位组合表示。实际盲文方案里声调符号的规则更细致有的会跟韵母合并在一个方里但作为编程示例这种分离式的输出更直观也方便学习者对照。4. 核心代码实现从一行句子到一串可触摸的盲文4.1 单字转换主流程万事俱备现在把整条流水线串起来。单个汉字的转换逻辑是先调 pypinyin 拿带声调的拼音再拆分成声母、韵母、声调然后分别查表最后把每个盲文方字符拼接起来。import re from pypinyin import lazy_pinyin, Style CN_RE re.compile(r[\u4e00-\u9fff]) def dots_to_char(dots): mask 0 for dot in dots: mask | 1 (dot - 1) return chr(0x2800 mask) def convert_char(ch): pinyin_list lazy_pinyin(ch, styleStyle.TONE3, errorsignore) if not pinyin_list: return ch py pinyin_list[0] init, final, tone split_pinyin(py) braille_cells [] braille_cells.append(SM_MAP.get(init, [])) braille_cells.append(YM_MAP.get(final, [])) if TONE_MAP.get(tone): braille_cells.append(TONE_MAP[tone]) return .join(dots_to_char(c) for c in braille_cells)这里有个细节值得说一下SM_MAP.get(init, [])在查不到映射时返回空列表。空列表经dots_to_char转换后是空盲文方U2800视觉上是一个空白格。这么设计的好处是程序不会因为生僻字或映射缺失而崩溃而且后续如果想加未登录词提示只需要判断返回字符串里是否包含空方就行。4.2 整句转换逐个字符处理保留非汉字处理整句话的时候我的策略很简单遍历每个字符如果是汉字就走convert_char否则原样保留。这样做的好处是空格、标点、数字不会丢文本结构能基本保持住。def convert_text(text): result [] for ch in text.strip(): if CN_RE.match(ch): result.append(convert_char(ch)) else: result.append(ch) return .join(result)等等这个 .join(result)会在每个元素之间都加一个空格包括非汉字之间。如果输入是你好世界输出会变成⠝⠊⠔ ⠓⠵⠔ ⠮⠊⠨ ⠚⠛⠨逗号被空格包夹看着有点散。实测下来更自然的做法是把连续的汉字作为一个块块内每个汉字转换后加空格但标点符号紧贴前一个块。我在后续版本里改成了这样def convert_text(text): result [] for ch in text.strip(): if CN_RE.match(ch): result.append(convert_char(ch)) result.append( ) else: if result and result[-1] : result.pop() # 去掉汉字末尾的空格让标点贴上去 result.append(ch) result.append( ) return .join(result).strip()这个细节不算复杂但体验差别很大。盲文排版本身对空格就有约定标点和汉字之间不加空格是更接近实际阅读习惯的做法。4.3 ASCII 点阵可视化不依赖字体也能看懂盲文Unicode 盲文字符在多数系统字体里能正常显示但总有一些环境把⠝渲染成方块。为了让结果在任何设备上都直观可读我加了一个点阵可视化函数用●表示凸起点○表示凹陷位def render_dots(dots): cells [● if i in dots else ○ for i in range(1, 7)] return f{cells[0]} {cells[3]}\n{cells[1]} {cells[4]}\n{cells[2]} {cells[5]}布局对应关系是第一行是点 1 和点 4第二行是点 2 和点 5第三行是点 3 和点 6。比如点位[1, 2]的可视化输出就是● ○ ● ○ ○ ○如果你看的是这个点阵图而不是 Unicode 盲文字符一样能手工摸出它的形状。这个函数在调试映射表的时候也极其好用——直接肉眼就能看出两个音是否撞码。4.4 命令行入口与运行效果最后加一个简单的命令行入口if __name__ __main__: text input(请输入要转换的汉字).strip() pinyins lazy_pinyin(text, styleStyle.TONE3) braille_text convert_text(text) print(原始文本:, text) print(汉语拼音:, .join(pinyins)) print(盲文编码:, braille_text) print() print(点位明细:) for ch in text: if CN_RE.match(ch): py lazy_pinyin(ch, styleStyle.TONE3)[0] init, final, tone split_pinyin(py) dots_list [] if init: dots_list.append(SM_MAP.get(init, [])) dots_list.append(YM_MAP.get(final, [])) if TONE_MAP.get(tone): dots_list.append(TONE_MAP[tone]) print(ch, py, -, / .join( f{.join(map(str, d))} for d in dots_list ))实际运行效果请输入要转换的汉字你好世界 原始文本: 你好世界 汉语拼音: ni3 hao3 shi4 jie4 盲文编码: ⠝⠊⠔ ⠓⠵⠔ ⠮⠊⠨ ⠚⠛⠨ 点位明细: 你 ni3 - 1345 / 24 / 35 好 hao3 - 125 / 1356 / 35 世 shi4 - 2346 / 24 / 46 界 jie4 - 245 / 1245 / 46看到这个输出一个完整的盲文句子就诞生了。虽然这几个盲文方点位是我教学简化映射的结果不是标准方案但整条链路从输入到输出的每一步都是透明、可校验的。5. 实测中踩过的坑多音字、渲染方块、映射冲突5.1 多音字和轻声pypinyin 帮你做了一半另一半得自己兜实测第一件事就是拿多音字开刀。输入银行请输入要转换的汉字银行 汉语拼音: yin2 hang2pypinyin 很聪明地选择了常见读音行在这里被读作háng。但换成自行车请输入要转换的汉字自行车 汉语拼音: zi4 xing2 che1同一个行在自行车里读xíngpypinyin 也处理对了。大部分常用词它都能靠词库搞定但总有例外比如人名、地名、古文里的生僻读法。我的解决办法是加一个用户纠错词典在调用lazy_pinyin之前先查一遍自定义词典命中就直接用。比如CUSTOM_DICT { 长安: chang2 an1, 解忧: jie4 you1, 单于: chan2 yu2, } def smart_pinyin(text, styleStyle.TONE3): for word, py in CUSTOM_DICT.items(): if word in text: text text.replace(word, py) return lazy_pinyin(text, stylestyle)这个方案简单粗暴但确实有效。实际项目中如果要做得更完善可以接入基于分词的多音字消歧模型或者干脆给用户一个交互式选择界面让用户碰到读错的地方手动选读音。作为教学示例维护一个小词典是最划算的。5.2 非汉字字符和标点保留还是映射取决于使用场景输入里难免有数字、英文、标点。我在convert_text里默认保留所有非汉字字符数字和英文直接透传标点也原样输出。这对看懂大概意思足够但严格来说不够盲文规范。盲文里有标准的数字符号和标点方案。比如盲文数字通常用数字前缀 字母 a-j来表示所以数字 1 是⠼⠁数字 2 是⠼⠃。英文标点也有对应点位句号是⠲逗号是⠂问号是⠦。如果你的翻译器要输出真正给盲人朋友读的文本这一步就不能省。我在代码里预留了扩展点只要在convert_text里对非汉字字符也查一张PUNCT_MAP和DIGIT_MAP就能逐步逼近完整方案。5.3 盲文符号在电脑上显示成豆腐块不是代码的错第一次在 Windows 记事本里跑代码输出一串□□□我差点以为是编码出了问题。后来查了一下是系统字体不支持盲文Unicode块。这个问题很常见不是代码 bug。解决办法有三个用点阵可视化函数render_dots它只依赖●和○任何终端都能显示这是最稳的方案。换一个支持盲文字形的字体比如 Google 的 Noto Sans Braille或者系统自带的 Segoe UI Symbol。把盲文转成 SVG 或图片用Pillow按点位坐标画圆点这样在任何设备上都能看。我实际开发时是三种方案并行终端里默认用 Unicode 字符调试映射时用点阵图需要对外展示时就导出图片。5.4 映射表撞码是最隐蔽的坑一定要写校验前面提到过撞码问题实际操作中它真的会出现。我在整理韵母表的时候发现in和iu一度都映射到了[1, 4, 6]如果不做检查代码不会报错但输出的盲文就是错的而且很难排查——因为错误藏在数据里不在逻辑里。这就是为什么我强烈建议所有映射表都配上check_conflicts校验函数。每次改完表跑一遍看到检查完成没有冲突提示才敢继续往下走。这个小习惯帮我避免了好几次低级错误。5.5 性能上可以忽略但有个地方值得优化当输入文本很长时lazy_pinyin会被反复调用。convert_text里每个汉字都单独调一次lazy_pinyin对几百字的短文完全没问题但如果要处理整本书最好先对整个文本调用一次lazy_pinyin再按字数切分避免重复初始化拼音引擎的开销。我在大文件转换版本里是这么做的pinyin_all lazy_pinyin(text, styleStyle.TONE3)然后用一个计数器遍历汉字并逐个用掉pinyin_all里的结果。这样耗时能下降不少批量处理时体感更明显。6. 从教学示例到实用工具后续扩展的几个方向6.1 对齐国家标准盲文方案替换教学映射表最优先的扩展项是替换映射表。上搜索引擎查国家通用盲文方案 拼音能找到官方发布的声母、韵母、声调点位对照表。拿到表后把SM_MAP、YM_MAP、TONE_MAP里的值逐项更新再跑一遍check_conflicts校验基本就是一个能用于真实教学场景的版本了。不要小看这一步映射表对了整个翻译器的正确率立刻就从演示级跳到可用级。6.2 加入反向翻译从盲文到汉字既然能正向转换理论上就能反向。盲文转汉字的核心思路是把盲文 Unicode 字符转回点位掩码再查反向映射表得到声母、韵母、声调然后拼出拼音最后用拼音反查汉字候选集。难点在于一个拼音对应多个汉字需要结合上下文做消歧。最简单的实现是输出所有候选汉字让用户选择进阶做法是接一个语言模型做拼音到汉字的自动转换。这个方向练手价值很高推荐一试。6.3 做一个带界面的小工具或者接到树莓派上命令行版方便但给不懂编程的人用还是有点门槛。可以用 Tkinter 或 Gradio 套一个简单的图形界面左边输入汉字右边实时显示盲文和点阵图。更进一步如果手头有树莓派和盲文显示器可以把转换结果通过串口直接发到盲文显示器上让用户真的摸到自己输入的每一个字。这种硬件联动的项目特别适合做无障碍相关的毕业设计或创客作品。最后的实际感受这个项目做完我对盲文的认知彻底变了。以前觉得它是一门外语做完之后发现它其实是一套编码系统跟 ASCII 码、Unicode 没有本质区别只是信息的呈现介质是纸上的凸点。写代码的过程里最深的体会是数据映射表这种基础结构往往比算法更值得花心思。拼音拆分、Unicode 位运算都是现成的套路真正让这个翻译器能用的是那一张张经过校验、不断修正的点位表。如果你也想动手做点什么建议从最小可用版本开始先别管标准、别名、多音字这些复杂问题就按这篇文章的链路把代码跑通再一步一步往里加东西。等你的映射表越补越全多音字词典越攒越多你会明显感觉到这个项目的价值在一点点变大。盲文不该是只有专业人士才能接触的东西用代码把它变成人人都能生成的格式这件事本身就挺有意义的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价