当前位置: 首页 > news >正文

将电子书文本转换为盲文格式,生成可打印的盲文文档,供视障用户阅读。

电子书转盲文转换器

一、实际应用场景与痛点

应用场景

视障学生小李需要阅读教材和课外书籍。虽然市面上有少量盲文书籍,但种类有限、价格昂贵、更新缓慢。当前的数字阅读器如读屏软件虽然能朗读文本,但无法替代盲文的触觉阅读体验。盲文具有独特的优势:帮助视障人士学习正确的拼写、理解文本结构、进行深度学习。小李急需一款能将普通电子书转换为盲文格式的工具,以便通过盲文点字显示器阅读或打印成盲文书籍。

核心痛点

1. 盲文资源匮乏:市面盲文书籍种类少,更新慢

2. 转换工具缺失:缺乏高效的中文盲文转换工具

3. 格式兼容性差:不同电子书格式转换困难

4. 盲文印刷昂贵:专用盲文打印机价格高昂

5. 转换准确性低:自动化转换容易出错

6. 排版复杂:盲文特殊的排版规则难以处理

7. 多语言支持:中英文混合文本处理困难

二、核心逻辑设计

1. 输入电子书文件(txt, epub, pdf等)

2. 提取和清理文本内容

3. 中文文本转拼音(汉语盲文基于拼音)

4. 拼音转盲文符号(按盲文规则)

5. 应用盲文缩写和缩略规则

6. 自动分段和分页处理

7. 生成盲文格式文档

8. 支持多种输出格式(文本、PDF、BRF)

9. 提供打印模板和预览

三、模块化代码实现

主程序文件:ebook_to_braille_converter.py

#!/usr/bin/env python3

# -*- coding: utf-8 -*-

"""

电子书转盲文转换器

将电子书转换为盲文格式,生成可打印的盲文文档

版本:3.0.0

作者:无障碍智能助手

"""

import os

import sys

import re

import json

import zipfile

import xml.etree.ElementTree as ET

from typing import Dict, List, Tuple, Optional, Any, Set

from dataclasses import dataclass, asdict, field

from enum import Enum

import warnings

warnings.filterwarnings('ignore')

# 文本处理

try:

import jieba

import pypinyin

from pypinyin import pinyin, Style

JIEBA_AVAILABLE = True

PYPINYIN_AVAILABLE = True

except ImportError:

JIEBA_AVAILABLE = False

PYPINYIN_AVAILABLE = False

print("警告: 中文处理库未安装")

# PDF处理

try:

import PyPDF2

PDF_AVAILABLE = True

except ImportError:

PDF_AVAILABLE = False

print("警告: PDF处理库未安装")

# 文档生成

try:

from reportlab.lib.pagesizes import A4, letter

from reportlab.lib.units import mm, inch

from reportlab.pdfgen import canvas

from reportlab.pdfbase import pdfmetrics

from reportlab.pdfbase.ttfonts import TTFont

REPORTLAB_AVAILABLE = True

except ImportError:

REPORTLAB_AVAILABLE = False

print("警告: PDF生成库未安装")

class BrailleSystem(Enum):

"""盲文体系枚举"""

CHINESE_MANDARIN = "chinese_mandarin" # 中文普通话盲文

ENGLISH_UEB = "english_ueb" # 英文统一盲文

NUMERIC = "numeric" # 数字盲文

MUSIC = "music" # 音乐盲文

MATH = "math" # 数学盲文

class BrailleCell:

"""盲文单元格(6点或8点)"""

def __init__(self, dots: List[int] = None, is_6dot: bool = True):

"""

初始化盲文单元格

Args:

dots: 点的列表,如[1,3,5]表示第1、3、5点凸起

is_6dot: 是否为6点盲文(True为6点,False为8点)

"""

self.is_6dot = is_6dot

self.max_dots = 6 if is_6dot else 8

if dots is None:

self.dots = []

else:

# 验证并排序点

self.dots = sorted([d for d in dots if 1 <= d <= self.max_dots])

def __str__(self) -> str:

"""字符串表示"""

if not self.dots:

return "⠀" # 空盲文单元格

# 转换为Unicode盲文字符

return self.to_unicode()

def to_unicode(self) -> str:

"""转换为Unicode字符"""

if not self.dots:

# 空单元格

return "⠀" if self.is_6dot else "⣀"

# 计算Unicode码点

# 盲文Unicode范围:U+2800 - U+28FF

base = 0x2800

if self.is_6dot:

# 6点盲文

dot_map = {1: 0x01, 2: 0x02, 3: 0x04, 4: 0x08, 5: 0x10, 6: 0x20}

else:

# 8点盲文

dot_map = {1: 0x01, 2: 0x02, 3: 0x04, 4: 0x08,

5: 0x10, 6: 0x20, 7: 0x40, 8: 0x80}

code = base

for dot in self.dots:

code += dot_map.get(dot, 0)

return chr(code)

def to_ascii(self) -> str:

"""转换为ASCII表示(如134表示1,3,4点凸起)"""

if not self.dots:

return "0"

return ''.join(str(d) for d in self.dots)

def to_binary(self) -> str:

"""转换为二进制表示"""

binary = ['0'] * self.max_dots

for dot in self.dots:

if 1 <= dot <= self.max_dots:

binary[dot-1] = '1'

return ''.join(binary)

@classmethod

def from_binary(cls, binary_str: str, is_6dot: bool = True) -> 'BrailleCell':

"""从二进制字符串创建"""

max_dots = 6 if is_6dot else 8

if len(binary_str) != max_dots:

raise ValueError(f"二进制字符串长度必须为{max_dots}")

dots = []

for i, bit in enumerate(binary_str):

if bit == '1':

dots.append(i + 1)

return cls(dots, is_6dot)

@classmethod

def from_unicode(cls, char: str) -> 'BrailleCell':

"""从Unicode字符创建"""

code = ord(char)

if 0x2800 <= code <= 0x28FF:

# 盲文Unicode字符

dot_value = code - 0x2800

# 判断是6点还是8点

is_6dot = dot_value <= 0x3F # 6点盲文范围:0x2800-0x283F

if is_6dot:

dots = []

dot_map = {0x01: 1, 0x02: 2, 0x04: 3, 0x08: 4, 0x10: 5, 0x20: 6}

else:

dots = []

dot_map = {0x01: 1, 0x02: 2, 0x04: 3, 0x08: 4,

0x10: 5, 0x20: 6, 0x40: 7, 0x80: 8}

for value, dot_num in dot_map.items():

if dot_value & value:

dots.append(dot_num)

return cls(dots, is_6dot)

raise ValueError(f"不是有效的盲文Unicode字符: {char}")

def __eq__(self, other) -> bool:

"""相等比较"""

if not isinstance(other, BrailleCell):

return False

return self.dots == other.dots and self.is_6dot == other.is_6dot

@dataclass

class BrailleCharacter:

"""盲文字符(可能由多个单元格组成)"""

cells: List[BrailleCell]

original_char: str

description: str = ""

def __str__(self) -> str:

"""字符串表示"""

return ''.join(str(cell) for cell in self.cells)

def to_ascii(self) -> str:

"""转换为ASCII表示"""

return '-'.join(cell.to_ascii() for cell in self.cells)

def is_contracted(self) -> bool:

"""是否为缩写形式"""

return len(self.cells) > 1

class BrailleTable:

"""盲文对照表基类"""

def __init__(self, system: BrailleSystem):

self.system = system

self.table = {}

self.contractions = {} # 缩写表

self.init_table()

def init_table(self):

"""初始化对照表(子类实现)"""

pass

def get_braille(self, char: str) -> Optional[BrailleCharacter]:

"""获取字符的盲文表示"""

return self.table.get(char)

def get_contraction(self, word: str) -> Optional[BrailleCharacter]:

"""获取单词的缩写形式"""

return self.contractions.get(word.lower())

class ChineseBrailleTable(BrailleTable):

"""中文盲文对照表"""

def __init__(self):

super().__init__(BrailleSystem.CHINESE_MANDARIN)

# 加载配置文件

self.load_config()

def load_config(self):

"""加载配置文件"""

config_path = "config/chinese_braille.json"

default_config = self.get_default_config()

try:

if os.path.exists(config_path):

with open(config_path, 'r', encoding='utf-8') as f:

config = json.load(f)

else:

config = default_config

# 保存默认配置

os.makedirs(os.path.dirname(config_path), exist_ok=True)

with open(config_path, 'w', encoding='utf-8') as f:

json.dump(config, f, indent=2, ensure_ascii=False)

self.load_from_config(config)

except Exception as e:

print(f"加载配置文件失败,使用默认配置: {e}")

self.load_from_config(default_config)

def get_default_config(self) -> Dict:

"""获取默认配置"""

return {

"system": "chinese_mandarin",

"description": "中文普通话盲文(现行盲文)",

"tone_marks": True,

"use_contractions": True,

"characters": {

# 声母

"b": [[1]],

"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,5]],

"zh": [[1,3,5,6]],

"ch": [[1,6]],

"sh": [[1,4,6]],

"r": [[2,4,5,6]],

"z": [[1,3,5]],

"c": [[1,3,4,6]],

"s": [[2,3,4]],

"y": [[1,3,4,5,6]],

"w": [[2,4,5,6]],

# 韵母

"a": [[3,5]],

"o": [[1,3,5]],

"e": [[2,6]],

"i": [[2,4]],

"u": [[1,3,6]],

"v": [[1,2,4,5,6]],

"ai": [[1,6]],

"ei": [[2,3,4,6]],

"ao": [[3,5,6]],

"ou": [[2,3,5,6]],

"an": [[3,6]],

"en": [[2,6], [3,6]],

"ang": [[1,3,5,6]],

"eng": [[2,6], [2,3,4,6]],

"er": [[2,3,4,5,6]],

"i_": [[2,4]], # 单独i

"ia": [[2,4], [3,5]],

"iao": [[2,4], [3,5,6]],

"ie": [[2,4], [2,6]],

"iu": [[2,4], [1,3,6]],

"ian": [[2,4], [3,6]],

"in": [[2,4], [2,3,4,6]],

"iang": [[2,4], [1,3,5,6]],

"ing": [[2,4], [2,6], [2,3,4,6]],

"u_": [[1,3,6]], # 单独u

"ua": [[1,3,6], [3,5]],

"uo": [[1,3,6], [1,3,5]],

"uai": [[1,3,6], [1,6]],

"ui": [[1,3,6], [2,4]],

"uan": [[1,3,6], [3,6]],

"un": [[1,3,6], [2,3,4,6]],

"uang": [[1,3,6], [1,3,5,6]],

"ong": [[1,3,6], [2,3,5,6]],

"v_": [[1,2,4,5,6]], # 单独ü

"ve": [[1,2,4,5,6], [2,6]],

"van": [[1,2,4,5,6], [3,6]],

"vn": [[1,2,4,5,6], [2,3,4,6]],

# 声调

"1": [[3,4,5]], # 阴平

"2": [[3,4]], # 阳平

"3": [[3,5,6]], # 上声

"4": [[3,4,5,6]], # 去声

"5": [[3,4,6]], # 轻声

# 数字

"0": [[3,5,6]],

"1": [[1]],

"2": [[1,2]],

"3": [[1,4]],

"4": [[1,4,5]],

"5": [[1,5]],

"6": [[1,2,4]],

"7": [[1,2,4,5]],

"8": [[1,2,5]],

"9": [[2,4]],

# 标点符号

",": [[2]], # 逗号

"。": [[2,5,6]], # 句号

"!": [[2,3,5]], # 感叹号

"?": [[2,3,6]], # 问号

";": [[2,3]], # 分号

":": [[2,5]], # 冒号

"、": [[2,6]], # 顿号

"「": [[2,3,5,6]], # 左引号

"」": [[3,5,6]], # 右引号

"(": [[2,3,5,6]], # 左括号

")": [[3,5,6]], # 右括号

"《": [[2,3,5,6]], # 左书名号

"》": [[3,5,6]], # 右书名号

"—": [[3,6]], # 破折号

"…": [[2,3,6]], # 省略号

"·": [[3]], # 间隔号

# 特殊符号

" ": [[0]], # 空格

"\n": [], # 换行

"\t": [[0,0,0,0]], # 制表符

},

"contractions": {

"的": [[1,4,5,6]], # 的

"了": [[1,2,3,5,6]], # 了

"是": [[2,3,4,6]], # 是

"不": [[1,2]], # 不

"在": [[1,2,6]], # 在

"有": [[1,2,4,6]], # 有

"和": [[1,2,3,4,6]], # 和

"这": [[1,4,5,6], [3,5]], # 这

"个": [[1,2,3,4,5]], # 个

"我": [[2,4,6]], # 我

"们": [[1,2,3,4,5,6]], # 们

}

}

def load_from_config(self, config: Dict):

"""从配置加载"""

characters = config.get("characters", {})

contractions = config.get("contractions", {})

# 加载字符表

for char, dots_list in characters.items():

cells = []

for dots in dots_list:

if dots == 0 or dots == [0]:

cells.append(BrailleCell([]))

else:

cells.append(BrailleCell(dots))

self.table[char] = BrailleCharacter(cells, char)

# 加载缩写表

for word, dots_list in contractions.items():

cells = []

for dots in dots_list:

cells.append(BrailleCell(dots))

self.contractions[word] = BrailleCharacter(cells, word, f"缩写: {word}")

def get_braille_for_pinyin(self, pinyin_str: str, tone: int = 0) -> BrailleCharacter:

"""

获取拼音的盲文表示

Args:

pinyin_str: 拼音字符串

tone: 声调(1-5,0表示无调)

Returns:

盲文字符

"""

# 标准化拼音

pinyin_str = pinyin_str.lower()

# 特殊处理ü

pinyin_str = pinyin_str.replace('ü', 'v')

# 分割声母和韵母

initials = ['b', 'p', 'm', 'f', 'd', 't', 'n', 'l', 'g', 'k', 'h',

'j', 'q', 'x', 'zh', 'ch', 'sh', 'r', 'z', 'c', 's', 'y', 'w']

# 查找最长匹配的声母

initial = ""

remainder = pinyin_str

for init in sorted(initials, key=len, reverse=True):

if pinyin_str.startswith(init):

initial = init

remainder = pinyin_str[len(init):]

break

# 获取声母盲文

cells = []

if initial and initial in self.table:

cells.append(self.table[initial].cells[0])

elif initial: # 如果没有单独的声母,可能需要特殊处理

# 尝试分解

pass

# 获取韵母盲文

if remainder in self.table:

cells.append(self.table[remainder].cells[0])

elif remainder: # 尝试匹配部分

for i in range(len(remainder), 0, -1):

part = remainder[:i]

if part in self.table:

cells.append(self.table[part].cells[0])

remainder = remainder[i:]

break

# 添加声调

if tone > 0 and str(tone) in self.table:

cells.append(self.table[str(tone)].cells[0])

if not cells:

# 如果没有找到匹配,返回空单元格

cells.append(BrailleCell())

return BrailleCharacter(cells, pinyin_str, f"拼音: {pinyin_str} 声调: {tone}")

class EnglishBrailleTable(BrailleTable):

"""英文盲文对照表(UEB)"""

def __init__(self):

super().__init__(BrailleSystem.ENGLISH_UEB)

self.init_table()

def init_table(self):

"""初始化英文盲文表"""

# 基本字母

letters = {

'a': [1],

'b': [1,2],

'c': [1,4],

'd': [1,4,5],

'e': [1,5],

'f': [1,2,4],

'g': [1,2,4,5],

'h': [1,2,5],

'i': [2,4],

'j': [2,4,5],

'k': [1,3],

'l': [1,2,3],

'm': [1,3,4],

'n': [1,3,4,5],

'o': [1,3,5],

'p': [1,2,3,4],

'q': [1,2,3,4,5],

'r': [1,2,3,5],

's': [2,3,4],

't': [2,3,4,5],

'u': [1,3,6],

'v': [1,2,3,6],

'w': [2,4,5,6],

'x': [1,3,4,6],

'y': [1,3,4,5,6],

'z': [1,3,5,6],

}

for char, dots in letters.items():

upper_char = char.upper()

self.table[char] = BrailleCharacter([BrailleCell(dots)], char)

self.table[upper_char] = BrailleCharacter([BrailleCell([6]), BrailleCell(dots)], upper_char)

# 数字前缀

self.table['#'] = BrailleCharacter([BrailleCell([3,4,5,6])], '#')

# 标点符号

punctuation = {

'.': [2,5,6],

',': [2],

'?': [2,3,6],

'!': [2,3,5],

';': [2,3],

':': [2,5],

'"': [2,3,5,6],

"'": [3],

'(': [2,3,5,6],

')': [3,5,6],

'[': [2,3,5,6],

']': [3,5,6],

'{': [2,3,5,6],

'}': [3,5,6],

'-': [3,6],

'_': [3,6,3,6], # 下划线

}

for char, dots in punctuation.items():

self.table[char] = BrailleCharacter([BrailleCell(dots)], char)

# 常用缩写

contractions = {

'the': [2,3,4,6],

'and': [1,2,3,4,6],

'for': [1,2,3,4,5,6],

'with': [2,3,4,5,6],

'ing': [3,4,6],

'ed': [1,2,4,5,6],

'sh': [1,4,6],

'th': [1,4,5,6],

'wh': [1,5,6],

'ou': [1,2,5,6],

'st': [3,4],

'ar': [3,4,5],

'er': [1,2,4,5,6],

'gh': [1,2,6],

'ow': [2,4,6],

}

for word, dots in contractions.items():

self.contractions[word] = BrailleCharacter([BrailleCell(dots)], word, f"缩写: {word}")

class TextExtractor:

"""文本提取器(支持多种格式)"""

def __init__(self, config: Dict):

"""

初始化文本提取器

Args:

config: 提取器配置

"""

self.config = config

def extract_text(self, filepath: str) -> Tuple[str, Dict]:

"""

提取文本内容

Args:

filepath: 文件路径

Returns:

(文本内容, 元数据)

"""

if not os.path.exists(filepath):

raise FileNotFoundError(f"文件不存在: {filepath}")

ext = os.path.splitext(filepath)[1].lower()

if ext == '.txt':

return self.extract_txt(filepath)

elif ext == '.pdf':

return self.extract_pdf(filepath)

elif ext == '.epub':

return self.extract_epub(filepath)

elif ext in ['.doc', '.docx']:

return self.extract_doc(filepath)

elif ext in ['.html', '.htm']:

return self.extract_html(filepath)

else:

raise ValueError(f"不支持的格式: {ext}")

def extract_txt(self, filepath: str) -> Tuple[str, Dict]:

"""提取纯文本"""

try:

with open(filepath, 'r', encoding='utf-8') as f:

content = f.read()

metadata = {

'format': 'txt',

'encoding': 'utf-8',

'size': len(content),

'chars': len(content),

'lines': content.count('\n') + 1

}

return content, metadata

except UnicodeDecodeError:

# 尝试其他编码

encodings = ['gbk', 'gb2312', 'big5', 'latin-1']

for encoding in encodings:

try:

with open(filepath, 'r', encoding=encoding) as f:

content = f.read()

metadata = {

'format': 'txt',

'encoding': encoding,

'size': len(content),

'chars': len(content),

'lines': content.count('\n') + 1

}

return content, metadata

except:

continue

raise ValueError("无法解码文本文件")

def extract_pdf(self, filepath: str) -> Tuple[str, Dict]:

"""提取PDF文本"""

if not PDF_AVAILABLE:

raise ImportError("PyPDF2未安装,无法处理PDF文件")

try:

text = ""

metadata = {}

with open(filepath, 'rb') as f:

pdf_reader = PyPDF2.PdfReader(f)

# 提取元数据

if pdf_reader.metadata:

metadata = {

'title': pdf_reader.metadata.get('/Title', ''),

'author': pdf_reader.metadata.get('/

如果你觉得这个工具好用,欢迎关注我!

http://www.jsqmd.com/news/205793/

相关文章:

  • 深度学习计算机毕设之基于python深度学习的餐桌美食识别卷神经网络
  • AI城市管理综合执法系统:让城市治理有“智”更有“度”
  • 高通推出Dragonwing Q-7790 和 Q-8750 处理器,工业及嵌入式物联网布局已成型
  • 2026最新自行车花鼓/三轮车差速器企业首选推荐HOVERIC泓瑞凯:专注中高端领域,HOVERIC泓瑞凯实力领航 - 全局中转站
  • 课程论文 “速通” 指南!虎贲等考 AI 让学术输出又快又稳
  • JAVA基础语法与Spring笔记
  • 超越CRUD:在2026年AI重塑的行业里,程序员如何抢占新赛道与高价值生态位?
  • 《3万字+512GPU!Hugging Face这本“AI修炼秘籍“让小白秒变分布式训练高手,附4000次实验数据+可视化图解》
  • 【保姆级教程】从“陪聊“到“打工“,Google教你构建自己的AI智能体,代码示例全在这!
  • PPO过时了?GRPO/DAPO/GSPO/SAPO四大算法全面对比,揭秘最新强化学习技术趋势!
  • 强脑科技的核心硬件模组为何选择蓝思量产?
  • 全网最全专科生AI论文网站TOP9:开题报告文献综述必备
  • Claude Code之父Boris提出的 9 条 Claude Code 实战技巧
  • 震惊!AI已悄悄内化为你的编程伙伴,小白开发者必知的5大生存法则
  • 懒人福音!2025年Agent工具大盘点,小白程序员也能秒变AI大神!
  • CSDN资源等级如何提升?综合贡献分如何提高?
  • 楼宇运维线路管理标准:保障ICT设施与服务稳定性的核心支撑
  • 电子器件烧毁的底层逻辑与避坑指南
  • 2026上海留学中介实力大比拼,十大靠谱机构引领留学新程 - 留学机构评审官
  • 卷不动了?2025年AI编程工具大盘点:DeepSeek-Coder V3夺冠,代码生成效率提升300%,小白秒变大神!
  • Cache写机制Write-through与Write-back
  • DeepSeek R1引爆开源狂潮!国产大模型“十强混战“,小白程序员如何上车?
  • 开源的包管理和环境管理工具conda详解、应用场景及案例分析
  • 从人工智障到真香!LLM三重觉醒:Tool+Plan+Memory让大模型开窍,小白程序员也能秒变大神
  • 年底 Claude 官方直连 Key 缺货?官转 poloai.top 成开发者首选方案 - poloapi-ai大模型
  • 2025衬氟球阀厂家权威推荐榜单:焊接球阀/不锈钢球阀/电动球阀/V型球阀/螺纹球阀/保温球阀及气动球阀源头厂家精选。 - 品牌推荐官
  • 在线式油液污染度检测仪哪个公司实力强?企业口碑好?2025推荐榜单 - 品牌推荐大师
  • 收藏!AI大模型人才缺口超千万,6岗抢1人,00后硕士50万起薪揭秘
  • 2025年底告别无效控卡!低热量代餐品牌精选,饱腹又控能 - 品牌2026
  • 2026新加坡留学中介综合实力排行榜:公认高效的十大推荐 - 留学机构评审官