1. 这个项目到底在解决什么问题
你可能很难想象,对视力正常的人来说,打开银行App看一眼余额、翻一翻上月账单,是几秒钟的事儿。但对视障用户来说,这一连串操作充满了门槛。读屏软件(屏幕阅读器)确实能朗读屏幕上的文字,可一旦遇上银行App里复杂的图表、加密控件、验证码滑块,或者账单PDF里排版混乱的表格,读屏软件经常读得断断续续,甚至直接跳过关键数字。
我最初做这个小工具,是因为一位视障朋友的抱怨,他说每次想查一下每月水电支出多少,都得让家人帮忙盯着屏幕念数字,特别不方便。后来我又去问了几个做无障碍支持的朋友,发现这根本不是个例。银行账单里那些“收入”“支出”“结余”关键字段,恰恰是视障用户最需要、也最难拿到的信息。
所以这个项目的目标很明确:把银行账单里的文字信息自动提取出来,按固定的播报逻辑组织成一段话,再用语音合成(TTS)放给用户听。用户不用睁眼看屏幕,也不用费劲操作手机,按一个键就能知道“这个月总收入多少、最大一笔支出花在哪、卡里还剩多少钱”。
这个项目适合谁参考?主要有三类人:一是做无障碍应用开发的工程师,二是银行、金融类产品的无障碍测试人员,三是想给家里老人或视障亲友做个小工具的个人开发者。技术门槛不算高,但涉及的坑相当多,从账单解析、格式处理到语音播报节奏,每一步都有细节,我会把我在实际开发中踩过的坑都写清楚。
2. 整体设计与思路拆解
2.1 为什么选“本地解析+语音播报”而不是塞进App里
最早我想的是做成一款手机App,直接调银行接口,自动拉账单再播报。想法是好的,但现实很骨感:银行开放接口的申请门槛高,个人开发者根本没有权限;就算能用,用户每次都要输入账号密码,安全问题上完全没法交代。
所以我把方案改成了更务实的路径:账单文件本地处理 + 离线TTS播报。用户只需要导出银行账单(PDF或CSV格式),放到指定文件夹,工具自动解析并生成语音文件,用户随时按需播放。
这个方案的好处很明显:
- 隐私安全:账单文件全程本地处理,不会上传到任何服务器,也不需要接入银行系统,把安全风险降到最低。
- 门槛低:不需要开发者账号,不需要银行审批,用户自己就能导出账单文件。
- 离线可用:语音合成完全在本地跑,不依赖网络。对很多视障用户来说,联网、登录、验证这一套操作本身就很麻烦,离线是刚需。
- 可控性强:用户可以自己调整播报模板,比如只播报“结余”和“最大支出”,不播多余信息。
方案选型本质上就是一场取舍:要功能完整还是要简单可靠?对目标用户来说,稳定能用的工具远胜过花里胡哨的App。事实证明这个方向是对的,后续测试中不少用户体验下来,都觉得“能用、够用、好用”。
2.2 技术栈选型:Python为主力,但也没少折腾
技术选型上我最终定了Python,原因很现实:生态成熟、社区案例多、处理PDF和文本的库非常齐全,而且写原型速度快。
核心组件如下:
| 功能模块 | 工具/库 | 选型理由 |
|---|---|---|
| PDF账单解析 | pdfplumber + 正则表达式 | 能保留表格结构,定位“收入”“支出”等关键字比较准确 |
| CSV账单解析 | Python内置csv模块 | 银行导出的CSV格式比较规整,直接按行列读取即可 |
| 语音合成 | pyttsx3(离线) | 支持本地引擎,不需要联网,Windows/macOS都能跑 |
| 播报控制 | 简单的按键映射 | 实体按键触发播报,比手机触屏更好操作 |
这里得额外说一句:pyttsx3在Windows上依赖SAPI5语音引擎,在macOS上依赖NSSpeechSynthesizer,不同系统的音色和语速表现差异很大。后面我会讲,语音合成这块其实是“听起来简单做起来碎”的重灾区。
2.3 播报内容的组织逻辑:不是逐字朗读,而是“讲人话”
直接把账单里的每一行念出来,用户是崩溃的,几十行流水账谁能听得下去?必须对内容做结构化提炼。
我把播报内容设计成三个层级:
- 摘要播报:先说结论,比如“本月总收入八千二百元,总支出五千三百元,结余二千九百元”。
- 明细播报:用户需要时再念逐笔明细,按金额从大到小排序,方便重点听大额收支。
- 异常提示:识别支出金额明显偏大或收入异常波动的项,主动播报提醒。
这个设计思路来自我自己的使用习惯:正常人看账单也不会从头看到尾,都是先看总数、再看大头。视障用户更需要这种“抓重点”式的信息组织方式,而不是被流水明细淹没。
3. 核心细节解析与实操要点
3.1 账单解析:格式五花八门,这才是项目的重头戏
银行账单的格式极其不统一,表面上看都是PDF,实际里面什么妖魔鬼怪都有。有的账单是表格结构,有的纯粹是文本排版;有的带页眉页脚水印,有的数字间喜欢加空格或逗号;还有的银行根本不给PDF,只能导出CSV或Excel。
解析这块我拆成了两条管线:
第一条管线:PDF账单
用pdfplumber把每页内容提取成文本,再按行切分,用正则去匹配关键字段。实际写代码的时候你会发现,不同银行的口径还不一样,有的写“支出”,有的写“取款”,有的写“转出”,这些都得靠关键词表去兼容。
这里分享一个我调了很久的细节:正则匹配金额时,很容易被“小数点”“千分位逗号”绕晕。比如“1,234.56”,如果直接按逗号拆分,就会拆成“1”和“234.56”两个错误数字。我最后是先把千分位逗号都去干净,再统一用\d+\.\d{2}去匹配,这才稳定下来。
第二条管线:CSV账单
CSV其实是最好处理的格式。大多数银行的CSV导出都带标准表头,比如日期、摘要、收入、支出、余额。直接按行读取,判断“收入”“支出”列是否为空即可。
不过CSV也有坑:有的银行会用BOM头(就是文件开头几个不可见字符),直接用csv.reader读会把表头第一个字段带上一堆乱码字符,处理判断时经常匹配失败。解决办法是读文件时加encoding='utf-8-sig',把BOM洗掉。
3.2 数字转中国式读法:金额播报千万别直接念阿拉伯数字
这是整个项目里最容易被低估的环节。你以为直接把“8253.60”读出来就完事了?TTS引擎会念成“八千二百五十三点六零”,勉强能听,但一碰上“1234567.89”这种大数,用户根本反应不过来是多少钱。
我写了一个金额格式化函数,核心逻辑是:把数字拆成“亿、万、元整数、角分”结构,再按中文读法重新组装,类似“一百二十三万四千五百六十七元八角九分”。同时注意“零”的处理规则,比如“1005元”应该读“一千零五元”,而不是“一千零五元零角零分”。
这一步建议做成独立的工具函数,因为以后不管是播报余额、收入还是大额支出,都要复用同一套金额转中文逻辑。
3.3 语音播报:做得好不好听,直接影响用户用不用
TTS这块是用户最直接的体验入口,做得好,工具才有人愿意用。我用了两种方式都测过:
- 云端TTS(如各类云服务商):音色自然、感情丰富,但需要联网、有延迟、有接口费用,还要把可能敏感的账单文本传到第三方服务。隐私上我不放心,最终没有采用。
- 本地TTS(pyttsx3 / SAPI5):完全离线、速度极快、免费,但音色偏机械,断句和语气需要自己调。好在播报的是结构化短句,不是长篇小说,机械一点完全能接受。
实际测试下来,pyttsx3的语速不能直接拉到最快,否则“二零二零年”会被读成“二零二与年”(中间吞字)。我最终把语速设在平常语速的0.9倍左右,再配合在文本里加逗号、句号做停顿控制,播报清晰度会好很多。
另外,金额数字和日期数字的读法完全不同。“2024年”要读“二零二四年”(逐字念),而“2400元”要读“二千四百元”(数值念),这必须在送入TTS前自己处理好。否则你会发现一个特别出戏的场面:系统播报“本月结余二千九百元”听着挺正常,下一句“收入八千二百元”突然念成“收入八二零零元”就瞬间出戏了。
3.4 交互方式:让用户能用“一根手指”完成操作
对不少视障用户来说,在手机上触摸屏操作是很困难的事——按钮太小、容易误触、摸不到焦点。所以我把交互做得极简:只保留一个物理按键(或两个)。
按键逻辑很简单:
- 单击=播报摘要
- 双击=播报大额支出明细
- 长按=重新读取最新账单
如果有条件,最好接入外置蓝牙按键,成本几十块,但体验提升非常明显。没有外接按键也没关系,做一个极简界面,整个屏幕只有一个大按钮,把触控区域做大到几乎占满全屏,误触率会大幅下降。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
我是在Windows环境开发的,Python版本建议3.9或更高,太老的版本对pdfplumber支持不好。
安装依赖只需要几个包:
pip install pdfplumber pyttsx3pyttsx3在Windows上会自动检测SAPI5,如果系统没开“语音识别”功能,需要先去控制面板里确认语音引擎是否正常。我之前在一台精简版系统上遇到过,结果pyttsx3初始化直接报错,查了半天才发现是系统语音组件缺失。
4.2 账单解析核心代码
第一步,先写一个通用的PDF账单解析器,按关键词去匹配表格里的字段:
import pdfplumber import re def extract_text_from_pdf(pdf_path): lines = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: text = page.extract_text() if text: lines.extend(text.split("\n")) return lines拿到文本行之后,用关键词词典去匹配:
KEY_WORDS = { "income": ["收入", "存入", "入账", "转账存入", "工资"], "expense": ["支出", "取出", "出账", "消费", "转出", "取款"], "balance": ["余额", "结余", "账户余额"], } def parse_bank_statement(lines): records = [] for line in lines: numbers = re.findall(r"\d+\.\d{2}", line.replace(",", "")) for keyword, aliases in KEY_WORDS.items(): if any(alias in line for alias in aliases): if numbers: records.append({ "type": keyword, "amount": float(numbers[0]), "desc": line.strip() }) break return records这段代码的核心思想很简单:先找关键词,再提金额。注意我先把逗号去掉再提取数字,就是为了避免“1,234.56”被正则拆成“1”和“234.56”两个数的问题。
CSV账单解析则简单得多:
import csv def parse_csv_statement(csv_path): income_total = 0.0 expense_total = 0.0 records = [] with open(csv_path, "r", encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: income = row.get("收入", "") or row.get("存入", "") expense = row.get("支出", "") or row.get("取出", "") if income: income_total += float(income) records.append(("income", float(income), row.get("摘要", ""))) if expense: expense_total += float(expense) records.append(("expense", float(expense), row.get("摘要", ""))) return income_total, expense_total, records注意encoding="utf-8-sig",这是CSV解析最容易踩的坑之一,少了它,第一列表头会带着BOM导致匹配失败。
4.3 金额转中文播报文本
这段代码是整个播报效果的核心,直接决定用户听得舒服不舒服。我把逻辑分两层:整数部分按“亿/万/元”分级,角分单独取出来:
CN_NUMS = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"] CN_UNITS = ["", "十", "百", "千"] def num_to_cn(num): if num == 0: return "零" result = "" unit_pos = 0 need_zero = False while num > 0: digit = num % 10 if digit == 0: need_zero = True else: if need_zero: result = "零" + result need_zero = False result = CN_NUMS[digit] + CN_UNITS[unit_pos] + result num //= 10 unit_pos += 1 return result def amount_to_speech(amount): amount = round(amount, 2) yuan = int(amount) jiao = int((amount - yuan) * 10) fen = int(round((amount - yuan) * 100)) % 10 parts = [] if yuan >= 100000000: yi = yuan // 100000000 yuan = yuan % 100000000 parts.append(num_to_cn(yi) + "亿") if yuan >= 10000: wan = yuan // 10000 yuan = yuan % 10000 parts.append(num_to_cn(wan) + "万") if yuan > 0: parts.append(num_to_cn(yuan) + "元") else: parts.append("零元") if jiao > 0 or fen > 0: if jiao > 0: parts.append(num_to_cn(jiao) + "角") if fen > 0: parts.append(num_to_cn(fen) + "分") else: parts.append("整") return "".join(parts)这段代码里的“零”处理逻辑我调了好几版,最后这样实现才满足中文金额朗读的常规习惯。比如1005元会正确输出“一千零五元整”,而不是“一千零五元零零分”。
4.4 播报模板与合成
播报文本的组织方式直接决定用户体验,我把摘要播报模板固定成三段:总收入、总支出、结余,一段说完再换下一段:
import pyttsx3 def build_report(income_total, expense_total, balance): parts = [ "本月总收入" + amount_to_speech(income_total), "总支出" + amount_to_speech(expense_total), "银行账户结余" + amount_to_speech(balance), ] return "。".join(parts) + "。" def speak_text(text): engine = pyttsx3.init() rate = engine.getProperty("rate") engine.setProperty("rate", int(rate * 0.9)) voices = engine.getProperty("voices") if voices: # 优先选择一个音色更清晰的语音 for v in voices: if "Huihui" in v.name or "Yao" in v.name: engine.setProperty("voice", v.id) break engine.say(text) engine.runAndWait()语速设为0.9倍,是我对比测试后觉得兼顾清晰度和时间成本的选择。太快吞字,太慢让人烦躁。另外在播报日期、卡号这类连续数字时,最好先把数字逐个转换为中文数字字符,避免TTS引擎按数值读法念错。这一步看起来小,但用户反馈里有一半不满意的原因都出在这。
4.5 完整执行流程演示
以一份模拟某银行PDF月账单为例,整个流程执行起来长这样:
python bill_reader.py --input "2024年11月账单.pdf"程序启动后做的事情依次是:
- 读取PDF,提取文本行。
- 用关键词库匹配出所有收支记录,计算总数。
- 找出最大金额支出和最大金额收入。
- 生成摘要文本:“本月总收入八千二百元,总支出五千三百元,银行账户结余二千九百元。最大一笔支出为一千八百元,发生在某超市。”
- 初始化TTS引擎,把文本转为语音并播放。
我在开发过程中还加了一个“写入音频文件”选项,方便用户把播报内容转成MP3或WAV,提前导到播放器里,连按键都不用按,定时播放就行。这个功能后来成了加分项,不少年纪大的用户喜欢用。
5. 常见问题与排查技巧实录
5.1 PDF提取后文字乱码、缺行、错位
这是账单解析里最常遇到的毛病,主要原因是部分银行PDF的文字编码较特殊,或者账单是扫描图片而非真正的文本。
排查思路分三步:
- 先确认PDF能复制文字出来吗?如果是图片扫描件,要用OCR,pdfplumber拿它没辙。
- 如果提取的文字顺序是乱的,尝试按行切分,看页眉页脚是否和正文混在一起,考虑按坐标过滤掉页眉区域。
- 如果关键字匹配不到,打印出原始文本,肉眼检查是否含有全角/半角空格或破折号等特殊字符,优先把这些字符统一替换成标准空格再匹配。
我给个建议:开发期不要只拿一家银行的账单测试,至少找三种不同格式的账单,哪怕自己造数据也要把“槽点”覆盖到。你会发现正则表达式和关键词表会随着账单格式的变化不断调整,这才是项目真正的核心维护成本。
5.2 金额重复计数或漏计
有时候同一笔交易在PDF里出现了两次,比如明细表里有一行“支出:100元”,表格下方又有一行“合计支出100元”,程序会把“支出”关键词匹配两次,导致总支出变成200元。
解决办法是在解析时增加“只解析明细行、排除合计行”的逻辑。常见的判断方法是:如果行内出现“合计”“总计”“本期”等词,直接跳过。同时,如果行内同时包含“收入”和“支出”关键词且金额一致,大概率是同一行内容里包含多个字段,需要按列拆分而不是按行匹配。
5.3 TTS播报时突然没声音
很多人遇到这个问题会以为是代码有问题,其实多半是系统语音引擎配置出错。Windows下最常见的两种情况:
- SAPI5语音引擎没安装完整,pyttsx3报错“No voices found”,解决方法是去系统设置里开启“文本转语音”功能,或者安装一个额外的中文语音包。
- 系统默认输出设备被切换到了音频端点(比如蓝牙耳机已断开),代码本身没问题,但没声音。建议在代码里增加一个提示,“如果三秒内没有开始播放,请检查音频输出设备”。
5.4 日期播报读得很生硬
日期“2024-11-03”如果不做处理,TTS引擎可能念成“二零二四减十一减零三”,非常出戏。
我写了一个简单转换函数,把“-”和“/”替换为“年”“月”“日”,同时去掉前导零:
import re def date_to_speech(date_str): match = re.match(r"(\d{4})[-/](\d{1,2})[-/](\d{1,2})", date_str) if match: y, m, d = match.groups() return f"{y}年{int(m)}月{int(d)}日" return date_str金额、日期、卡号这三类信息,在播报标准上完全是三套逻辑,千万不要图省事直接丢给TTS。
5.5 大文件处理慢
一个月账单其实不大,但有的用户会导出一整年的流水,PDF可能几百页。这时候逐页解析会明显变慢。
优化办法有两个:一是只提取包含关键词的行,过滤掉无关内容后再送后续逻辑处理;二是用内存存储替代频繁磁盘读写,实测一年份的账单,优化后处理时间从十几秒降到几秒以内,体感差异非常大。
6. 后续扩展与个人经验补充
6.1 这个工具还能怎么扩展
做完了基础播报,后面可以做的事情还很多,我列几个我觉得有价值的方向:
- 多银行账单格式适配:做一个“格式配置文件”,每种银行对应一套关键词和正则规则,通过配置文件热更新,不动代码也能适配新银行。
- 播报历史回放:保存每天/每周的播报音频,用户按日期回听,相当于一个“语音账本”。
- 语音交互控制:如果后续接入语音识别,用户在播报结束后直接说“上一条”“下一条”“念第十条明细”,就更自然了。
- 定时播报:设定每周日晚上八点自动播报本周收支情况,让用户养成理财习惯。
6.2 我做这个项目最大的体会
开发过程中我反复思考一个问题:无障碍工具的核心价值是不是功能本身?后来发现不是,核心价值是尊重用户的注意力和操作习惯。视障用户不缺能读文字的软件,缺的是“把信息整理成人话再讲出来”的工具。正常人看账单会抓重点,视障用户听账单也应该能抓重点,播报工具不能只做“文字的搬运工”,要做“信息的提炼器”。
另外一个很实在的感触是:这种小项目,技术难度真的不高,真正的挑战全在“糊裱”细节上——不同账单格式的兼容、金额中文转换里的各种边界情况、语音播报的停顿和语速调节。这些琐碎功夫花下去,用户口碑才能起来。
最后分享一个小技巧:开发时一定要准备一个“真实账单样本库”,哪怕是自己伪造的也行,关键是包含各种奇怪格式。拿到真实账单后再跑一遍解析,把暴露出的错误全部修正到正则和关键词表里。这个库会随项目一起越来越值钱,它是这个工具的核心资产,比代码本身更重要。
我用这个思路做的工具,内部测试时还顺手给那位最初提出需求的朋友体验了一下。他一听开头那句“本月总收入八千二百元”,就笑着说了一句“这回不用喊人帮忙了”。听到这句话,我就知道这个项目的方向是对的。