Python实现汉字转盲文:从拼音拆分到Unicode点位编码全解析
2026/9/23 10:52:10 网站建设 项目流程

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 专门留了一块区域给盲文:U+2800U+28FF。其中U+2800是"空方",一个凸点都没有;U+2801是点 1 凸起;U+2803是点 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 是点 1,b 是点 1-2,c 是点 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('你好世界', style=Style.TONE3)) # 输出: ['ni3', 'hao3', 'shi4', 'jie4']

这里要提一个坑:lazy_pinyin默认对多音字只取一个读音,通常是按词频或常用语料排序后的最常见读音。对绝大多数场景够用,但对"银行"的"行"、"长春"的"长"这种词,就可能不如预期。后面我会讲怎么兜底处理。

3.2 拼音拆分的核心逻辑:先匹配双字母声母

拿到类似ni3这样的拼音串后,要拆成声母、韵母和声调。声母有单个字母的,也有双字母的,例如zhchsh。拆的时候必须优先匹配双字母声母,否则会把zhi错误地拆成zhi

我的实现是先判断最后一个字符是不是数字,是的话取出来作为声调,然后遍历一个声母列表,找到第一个能匹配的声母,剩下的部分就是韵母。如果整个音节开头没有匹配到任何声母,比如aaiouer这样的零声母音节,就返回空声母、整个拼音作为韵母。

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,不过加了yw这些声母后,yiwu会被拆成y + iw + 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, style=Style.TONE3, errors='ignore') 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转换后是空盲文方U+2800,视觉上是一个空白格。这么设计的好处是程序不会因为生僻字或映射缺失而崩溃,而且后续如果想加"未登录词提示",只需要判断返回字符串里是否包含空方就行。

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, style=Style.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, style=Style.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 hang2

pypinyin 很聪明地选择了常见读音,在这里被读作háng。但换成"自行车":

请输入要转换的汉字:自行车 汉语拼音: zi4 xing2 che1

同一个"行",在"自行车"里读xíng,pypinyin 也处理对了。大部分常用词它都能靠词库搞定,但总有例外,比如人名、地名、古文里的生僻读法。

我的解决办法是加一个"用户纠错词典":在调用lazy_pinyin之前,先查一遍自定义词典,命中就直接用。比如:

CUSTOM_DICT = { '长安': 'chang2 an1', '解忧': 'jie4 you1', '单于': 'chan2 yu2', } def smart_pinyin(text, style=Style.TONE3): for word, py in CUSTOM_DICT.items(): if word in text: text = text.replace(word, py) return lazy_pinyin(text, style=style)

这个方案简单粗暴,但确实有效。实际项目中如果要做得更完善,可以接入基于分词的多音字消歧模型,或者干脆给用户一个交互式选择界面,让用户碰到读错的地方手动选读音。作为教学示例,维护一个小词典是最划算的。

5.2 非汉字字符和标点:保留还是映射,取决于使用场景

输入里难免有数字、英文、标点。我在convert_text里默认保留所有非汉字字符,数字和英文直接透传,标点也原样输出。这对"看懂大概意思"足够,但严格来说不够盲文规范。

盲文里有标准的数字符号和标点方案。比如盲文数字通常用"数字前缀 + 字母 a-j"来表示,所以数字 1 是⠼⠁,数字 2 是⠼⠃。英文标点也有对应点位,句号是,逗号是,问号是

如果你的翻译器要输出真正给盲人朋友读的文本,这一步就不能省。我在代码里预留了扩展点:只要在convert_text里对非汉字字符也查一张PUNCT_MAPDIGIT_MAP,就能逐步逼近完整方案。

5.3 盲文符号在电脑上显示成豆腐块,不是代码的错

第一次在 Windows 记事本里跑代码,输出一串□□□,我差点以为是编码出了问题。后来查了一下,是系统字体不支持盲文Unicode块。这个问题很常见,不是代码 bug。

解决办法有三个:

  1. 用点阵可视化函数render_dots,它只依赖,任何终端都能显示,这是最稳的方案。
  2. 换一个支持盲文字形的字体,比如 Google 的 Noto Sans Braille,或者系统自带的 Segoe UI Symbol。
  3. 把盲文转成 SVG 或图片,用Pillow按点位坐标画圆点,这样在任何设备上都能看。

我实际开发时是三种方案并行:终端里默认用 Unicode 字符,调试映射时用点阵图,需要对外展示时就导出图片。

5.4 映射表撞码是最隐蔽的坑,一定要写校验

前面提到过撞码问题,实际操作中它真的会出现。我在整理韵母表的时候,发现iniu一度都映射到了[1, 4, 6],如果不做检查,代码不会报错,但输出的盲文就是错的,而且很难排查——因为错误藏在数据里,不在逻辑里。

这就是为什么我强烈建议所有映射表都配上check_conflicts校验函数。每次改完表跑一遍,看到"检查完成"没有冲突提示,才敢继续往下走。这个小习惯帮我避免了好几次低级错误。

5.5 性能上可以忽略,但有个地方值得优化

当输入文本很长时,lazy_pinyin会被反复调用。convert_text里每个汉字都单独调一次lazy_pinyin,对几百字的短文完全没问题,但如果要处理整本书,最好先对整个文本调用一次lazy_pinyin,再按字数切分,避免重复初始化拼音引擎的开销。我在大文件转换版本里是这么做的:

pinyin_all = lazy_pinyin(text, style=Style.TONE3)

然后用一个计数器遍历汉字并逐个用掉pinyin_all里的结果。这样耗时能下降不少,批量处理时体感更明显。

6. 从教学示例到实用工具:后续扩展的几个方向

6.1 对齐国家标准盲文方案,替换教学映射表

最优先的扩展项是替换映射表。上搜索引擎查"国家通用盲文方案 拼音",能找到官方发布的声母、韵母、声调点位对照表。拿到表后,把SM_MAPYM_MAPTONE_MAP里的值逐项更新,再跑一遍check_conflicts校验,基本就是一个能用于真实教学场景的版本了。不要小看这一步,映射表对了,整个翻译器的正确率立刻就从"演示级"跳到"可用级"。

6.2 加入反向翻译:从盲文到汉字

既然能正向转换,理论上就能反向。盲文转汉字的核心思路是:把盲文 Unicode 字符转回点位掩码,再查反向映射表得到声母、韵母、声调,然后拼出拼音,最后用拼音反查汉字候选集。难点在于一个拼音对应多个汉字,需要结合上下文做消歧。最简单的实现是输出所有候选汉字让用户选择,进阶做法是接一个语言模型做拼音到汉字的自动转换。这个方向练手价值很高,推荐一试。

6.3 做一个带界面的小工具,或者接到树莓派上

命令行版方便,但给不懂编程的人用还是有点门槛。可以用 Tkinter 或 Gradio 套一个简单的图形界面:左边输入汉字,右边实时显示盲文和点阵图。更进一步,如果手头有树莓派和盲文显示器,可以把转换结果通过串口直接发到盲文显示器上,让用户真的"摸到"自己输入的每一个字。这种硬件联动的项目,特别适合做无障碍相关的毕业设计或创客作品。

最后的实际感受

这个项目做完,我对盲文的认知彻底变了。以前觉得它是一门外语,做完之后发现它其实是一套编码系统,跟 ASCII 码、Unicode 没有本质区别,只是信息的呈现介质是纸上的凸点。写代码的过程里,最深的体会是:数据映射表这种基础结构,往往比算法更值得花心思。拼音拆分、Unicode 位运算都是现成的套路,真正让这个翻译器"能用"的,是那一张张经过校验、不断修正的点位表。

如果你也想动手做点什么,建议从"最小可用版本"开始,先别管标准、别名、多音字这些复杂问题,就按这篇文章的链路把代码跑通,再一步一步往里加东西。等你的映射表越补越全,多音字词典越攒越多,你会明显感觉到这个项目的价值在一点点变大。盲文不该是只有专业人士才能接触的东西,用代码把它变成人人都能生成的格式,这件事本身就挺有意义的。

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

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

立即咨询