简介:面向需要处理财务或会计数据的 Python 开发者,这份 PDF 资源提供一套数字转中文大写代码,解决将金额等阿拉伯数字转换为大写中文的需求。实现采用分位函数与统一入口结合的方式,分别处理万以内、亿以内及更大范围整数,并自动处理空位补“零”与“一十”转“十”等中文表达细节,可读性和可维护性较好。资源包内仅含 1 个 PDF 文件,大小约 30KB,轻量易用,便于离线查看或打印代码说明。目前已有 11359 人浏览学习,受到有类似转换需求的开发者关注。PDF 内附完整源码结构及调用示例,读者可对照学习分位转换、边界检查和递归调用逻辑,也可将其直接迁移至财务软件或报表工具中,节省重复造轮子的时间。
1. 数字转换成中文:财务大写与读码场景里的刚需
“Python实现把数字转换成中文”这个标题看着简单,实际背后是一条非常具体的业务线:发票金额大写、合同结算单、银行票据、报销系统的校验位,以及语音读码、财务审计里的中文读数。很多从业务侧过来的读者,第一反应是“这不就是做个映射表吗”,但这恰恰是翻车的起点——数字1到9映射成“一”到“九”只是最外层的一层皮,真正的复杂度在位数分段、零值处理和金额单位的组合规则上。我见过某公司结算系统因为“100010”被转成“一十万零一十”而被财务打回,也见过A同学写的大写转换函数在“10.50”这种常见金额上多输出一个“零角”。这篇文章就按我实际做这类功能时的思路来拆:先讲清楚中文数字的语法规则,再给出可直接复用的Python实现,最后把边界、坑位和测试方法一次说透。适合正在做财务、票据、语音合成、报表导出的读者,也适合想把这类基础能力做进工具库的开发者。
2. 中文数字的读法规则:先看懂“分段”和“零”的语法
2.1 为什么不能只靠“个十百千万”的映射
最简单的直觉做法是:把数字逐位拆开,每一位配上“个十百千万”的单位,例如 1234 直接拼成“一二三四”再补位“一千二百三十四”。这样对 4 位以内的数字大致可用,但一超过“万”就开始失控。比如 10000 如果逐位拼,会得到“一万零零零零”这种明显不对的结果;而 100010000 按逐位处理根本无法判断应该在哪个位置断句。中文数字的本质是“四位一节”的读数系统:从低位起每 4 位为一节,节与节之间插入“万”、“亿”等节权单位,每一节内部再按“千、百、十”的位权组织。理解了这一点,实现思路就清晰了——不要逐位处理,要按节处理。
中文数字读法的核心规则可以收敛为三条:
| 规则 | 说明 | 示例 |
|---|---|---|
| 四位一节 | 从低位起每 4 位一组,组间加“万”“亿” | 12345678 → 1234万5678 |
| 节内零处理 | 节内某一位为空且该节整体不为 0 时,补一个“零” | 1001 → 一千零一 |
| 尾零不读 | 节内末尾的 0 不读出来 | 1200 → 一千二百 |
这三条规则是所有数字转中文方案的基石。第一节(最低节)叫做“个节”,第二节叫“万节”,第三节叫“亿节”。超过亿节之后,原则上可以继续按四位一节往上推,但实际业务里极少遇到,后面我会给出无限扩展的实现方式。
2.2 位权表与节权表:先定义数据,再谈算法
在动手写代码之前,先把数据表定下来。中文数字的基本数字是“零一二三四五六七八九”,位权是“十百千万”的逻辑要分层理解:节内的“十百千”是位权,节间的“万”“亿”是节权。一个常见的误解是把“万”放在位权表里和“十百千”并列,这样在处理 100000000(一亿)时会因为位权叠加而出错。我建议把位权与节权分成两张表:
# 节内位权,从个位向左 UNIT_INNER = ["", "十", "百", "千"] # 节权,从个节向上 UNIT_SECTION = ["", "万", "亿", "兆", "京", "垓"] # 基础数字 DIGITS = "零一二三四五六七八九"这里有个容易被忽略的细节:在“万节”或“亿节”内,如果该节的千位是 0,但万节本身有值,读法上要补一个“零”,例如 100100001 读作“一亿零一十万零一”。这个“零”不是节内补的,而是“节衔接”时补的——因为亿节和万节之间跨了一个完整的 4 位段,段间为空时要补零,这是后面代码里最需要小心判断的地方。
2.3 读法组合逻辑:从“一万零一”到“十亿零五”
我用自己的话说一下组合逻辑:先把数字按四位一截切分,得到一个节列表。然后从高节向低节处理。处理每一节时,将该节的四位数字按“千、百、十、个”拆开,遇到 0 时先标记“需要补零”,但不要立刻输出零——因为如果 0 后面还有非零数字,才需要输出一个“零”;如果 0 后面全是 0(即处于该节末尾),则不输出。每一节处理完后,如果该节整体不为 0(即四位不全是 0),就加上对应的节权“万”“亿”等。最后把各节结果拼接起来。
两个要重点对待的特例:
- 十位为 1 且百位千位均为 0 时,“一十八”要读“十八”而不是“一十八”。这是中文习惯问题,代码里需要单独处理。
- 高节全为 0 时,整体要避免输出连续的“零”,例如 100000001 应读作“一亿零一”,而不是“一亿零零一”。
这两条我会在下一章的代码里直接实现。
3. 整数转中文:Python实现与参数说明
3.1 基础版本:四位一节、逐节拼装
有了前面的规则,代码可以做到很短。下面是一个把非负整数转换为中文读法的函数,兼容到“垓”(10 的 20 次方)量级,普通业务足够用:
DIGITS = "零一二三四五六七八九" UNIT_INNER = ["", "十", "百", "千"] UNIT_SECTION = ["", "万", "亿", "兆", "京", "垓"] def num_to_cn(num: int) -> str: """整数转中文读法,支持 0 到极大整数。""" if num == 0: return "零" # 1. 按四位一节切分,低位在前 sections = [] while num > 0: sections.append(num % 10000) num //= 10000 # 2. 从高节向低节处理 parts = [] need_zero = False # 当前处理到的位置之前是否出现过空节 for i in range(len(sections) - 1, -1, -1): sec = sections[i] if sec == 0: # 当前节整体为 0,标记需要补零,但不输出 need_zero = True continue # 3. 处理节内四位 sec_str = "" # 千位 thousand = sec // 1000 hundred = (sec % 1000) // 100 ten = (sec % 100) // 10 unit = sec % 10 if thousand: sec_str += DIGITS[thousand] + "千" if hundred: if thousand: sec_str += "零" if hundred == 0 else "" sec_str += DIGITS[hundred] + "百" # 十位与个位的组合,需要处理“一十”变“十” if ten: if sec_str and (hundred == 0 and thousand != 0): sec_str += "零" if ten != 1 or sec_str: sec_str += DIGITS[ten] sec_str += "十" if unit: if sec_str and (ten == 0): sec_str += "零" sec_str += DIGITS[unit] # 4. 节间补零:如果之前出现空节或节内以零开头,补一个零 if need_zero and sec_str: parts.append("零") need_zero = False # 5. 接上节权 parts.append(sec_str + UNIT_SECTION[i]) return "".join(parts)这段代码的逻辑分三块:第 1 步用取余循环把整数切成 4 位一节的列表,切完之后低位在前面,处理时倒序遍历。第 3 步是节内拼装,核心是每一位的“非零才输出、中间零补读”的判断:比如 1001,千位是 1 输出“一千”,百位十位是 0 不输出,个位是 1 且前面已有内容且十位为 0,就补一个“零”,于是得到“一千零一”。第 4 步的need_zero是节间零处理的关键——当某一节整体为 0(例如 10000 的万节以下是 0)时,先标记,等下一节有值时再补“零”,避免输出“一万零零”这种重复零。
参数说明:num必须是非负整数。如果需要支持负数,可以在函数外层做符号判断,不建议在函数内部混入符号逻辑。节权列表UNIT_SECTION可以继续往后加,中文大数单位还有“秭、穰、沟、涧、正、载”,按需扩展即可。这个版本的复杂度是 O(n),n 为节数,对于 10 的 20 次方以内的数字,循环次数不超过 6 次,性能完全不是问题。
3.2 负数、0 与特殊输入的处理
实际业务很少只传正整数。金额、报表里经常出现负数和 0,我的做法是在封装层统一处理符号,核心函数保持纯净。
def num_to_cn_signed(num: float | int) -> str: """带符号的整数/整数部分转换。""" if isinstance(num, float): # 只取整数部分,小数部分由调用方另行处理 num = int(num) if num == 0: return "零" if num < 0: return "负" + num_to_cn(-num) return num_to_cn(num)这里有几个决策点:
- 传入 0 直接返回“零”,不要走主函数的循环,因为主函数对 0 返回空串会出 bug。
- 负数先取出符号位,再对绝对值调用主函数。注意
num_to_cn(-num)里如果num是-0,在 Python 里-0 == 0为真,所以不会误判为负数。 - 浮点数传入时先取整,小数部分不进这个函数。常见做法是在上一层拆成“整数部分”和“小数部分”分别处理,这个我放到第 4 章讲金额大写时一起说。
3.3 与“直接用库”的差别:没有标准库,但别乱 pip install
有个容易踩的坑:很多开发者会去搜“python 数字转中文库”,然后安装一个名字类似cn2an或者chinese的开源包。第三方库确实能解决一部分需求,尤其是中文读法转数字的逆向功能,但我建议核心逻辑自己维护——原因有三:一是金融场景对转换规则的合规性有要求,第三方库的“一十”处理方式不一定符合你们财务的规范;二是很多库为了支持口语化会做容错,比如输入“一万二”也能解析,这听起来好,实际上会让异常数据悄悄通过校验;三是自己维护的代码在出问题时能立刻定位,不依赖开源包的更新节奏。我一般会把自研函数做成一个工具模块,上面跑单元测试,比引第三方包可控得多。
4. 金额大写与小数处理:财务场景的进阶实现
4.1 金额大写为什么和整数读法不一样
如果只是做读数功能,第 3 章的代码已经够了。但标题里“数字转换成中文”在财务语境里通常指的是“金额大写”,这和普通读法是一套不同的语法:金额大写使用“壹贰叁肆伍陆柒捌玖”这套防篡改数字,单位是“圆、角、分”,整数部分的后缀是“整”或“正”,而且“零”的补位规则更复杂——例如 100.50 应写成“壹佰圆零伍角”,3000.00 应写成“叁仟圆整”,1.03 要写成“壹圆零叁分”。
做金额大写时,最好的策略不是复用整数转中文的代码,而是单独写一套“按位定位”的逻辑,因为金额的特殊点在“圆”和“角分”之间,这个位置的零处理规则和整数内部不一样。
4.2 金额大写函数:逐位映射、圆角分单独成段
下面是我常用的金额大写实现,输入是浮点数或字符串形式的金额,输出是财务大写字符串:
UPPER_DIGITS = "零壹贰叁肆伍陆柒捌玖" UPPER_UNIT_INT = ["圆", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿"] UPPER_UNIT_DEC = ["角", "分"] def amount_to_cn(amount) -> str: """金额转财务大写,支持到亿。""" # 1. 统一转为字符串处理,避免浮点误差 text = f"{amount:.2f}" int_part, dec_part = text.split(".") # 2. 处理整数部分 int_len = len(int_part) result_int = "" zero_flag = False # 记录是否需要补零(用于连续的 0) for i, ch in enumerate(int_part): digit = int(ch) pos = int_len - i - 1 # 从个位往上数,0 为个位 if digit == 0: zero_flag = True # 如果当前位是“万”或“亿”位,且前面有值,单位仍要写 if pos == 4 and any(int_part[:i]): result_int += "万" zero_flag = False elif pos == 8 and any(int_part[:i]): result_int += "亿" zero_flag = False continue if zero_flag and result_int: result_int += "零" zero_flag = False # 十位为 1 且前面没有值时,省略“壹拾”中的“壹” if pos == 1 and digit == 1 and not result_int: result_int += "拾" else: result_int += UPPER_DIGITS[digit] + UPPER_UNIT_INT[pos] if not result_int: result_int = "零" # 3. 处理小数部分 result_dec = "" for i, ch in enumerate(dec_part): digit = int(ch) if digit: if i > 0 and int(dec_part[0]) == 0: result_dec += "零" result_dec += UPPER_DIGITS[digit] + UPPER_UNIT_DEC[i] # 4. 拼接规则:有角分则不加整,无小数部分加整 if result_dec: return result_int + "圆" + result_dec return result_int + "圆整"这段代码比整数版本长,但逻辑更直观。处理小数部分时先统一用:.2f把金额格式化为两位小数,根本目的是避开浮点运算误差。整数部分逐位处理,用zero_flag记录是否遇到连续的零;但“万”和“亿”两个位置特殊——即使该位是 0,只要前面已经有值,单位仍然要写出来,例如 100001000 应写成“壹亿零壹仟圆”,中间的万位虽然是 0,但亿位已经出现,万位那个 0 要通过后面的“零”来体现,而不是直接丢掉。
UPPER_UNIT_INT的索引设计是:pos=0对应“圆”,pos=1对应“拾”,以此类推,到亿位是pos=8。这里有个细节:我把“万”“亿”也放在同一张表里,而不是像整数读法那样用节权单独处理,原因是金额大写的“万”和“亿”不能像读法那样省略——即便某一节全为 0,单位也不能丢,例如 10000 万节为 1、个节为 0,如果按节处理会得到“壹万”而非“壹万圆整”。逐位处理在金额场景更稳。
参数说明:amount支持传入int、float或数字字符串。传入100会先格式化为"100.00",整数部分处理得到“壹佰”,小数部分为 0,拼接后输出“壹佰圆整”。传入100.50得到“壹佰圆伍角”。一个常见边界是0.01:整数部分为“零”,小数部分输出“零壹分”,最终是“零圆零壹分”,符合财务写法。
4.3 金额大写里“整”和“零”的取舍逻辑
金额大写最容易错的地方在“整”字。财务规范是:金额到分位时不需要写“整”,整数金额或小数部分为 0 时才写“整”。上面代码用if result_dec:判断实现了这一点。但如果传入100.00,result_dec为空字符串,输出“壹佰圆整”,这是对的;如果传入100.01,输出“壹佰圆零壹分”,这里有零是因为百分位为 1,但十分位为 0,需要在“壹分”前补一个“零”。还有一种特殊写法是“壹佰圆零壹分”和“壹佰圆壹分”都有人用,但严格按财务票据的书写习惯,“零”要写,表示角位空缺。这里如果你们公司有自己的财务规范,以规范为准,代码里的if i > 0 and int(dec_part[0]) == 0:这个判断可以按需调整。
5. 常见踩坑与排查:数字转中文最容易翻车的 5 个位置
5.1 “一十”变“十”的规则与例外
现象:调用整数转中文函数,输入 10、15,得到“一十”“一十五”,但中文习惯是“十”“十五”。
原因:我在第 3 章代码里已经处理了这个逻辑:当十位为 1、前面没有任何已输出内容(即sec_str为空)时,省略“一”。但很多自己写的版本只判断首位是否为 1,没有结合“前面是否有内容”这个条件,于是 110 会被误转成“一百一十”(正确是“一百一十”,没问题),但 10 却被转成“一十”。
解决:判断条件要同时看两个维度——当前处理位置是十位,且十位数字为 1,且当前节内前面没有输出过任何内容。写成if ten == 1 and not sec_str:就够了。如果是 10010,万节是 1,个节是 10,个节前面没有内容,所以个节的“10”读作“十”,整体是“一万零十”,这个逻辑同样适用。
5.2 连续零压缩失败,出现“零零零”
现象:输入 10010001,得到“一千万零一十零一”之类的错误结果,多个零没有被压缩为一个。
原因:在逐位处理的结构里,每个 0 都触发了补零标记,但没有在真正输出前检查“这个零是否紧跟前一个零”。我的代码里用zero_flag加if zero_flag and result_int:做了双重校验,但很多初版实现会漏掉result_int非空这个条件,导致前导零也被输出。
解决:补零前必须确认两点——之前确实遇到过零(zero_flag=True),并且当前结果串非空(说明前面真的有数值内容)。这两个条件缺一不可。另外要注意:补完零后记得把zero_flag复位,否则下一个非零数字会再补一次零,同样出错。
5.3 金额大写的浮点精度坑
现象:调用amount_to_cn(100000000.01),输出“壹亿元整”,小数部分的 0.01 丢失;或者输入0.29,得到“零圆贰角玖分”但实际可能是“零圆贰角捌分”。
原因:浮点数在计算机里无法精确表示 0.01 这样的十进制小数,100000000.01实际存储可能是100000000.01000001,经过:.2f格式化后变成"100000000.01",有时会碰巧正确,有时会进位或截断出错。
解决:统一用字符串传参,或者内部强制Decimal。我一般建议调用方直接传字符串或整数(以“分”为单位),这是金融系统里最稳的做法。如果调用方坚持传浮点数,函数内部用Decimal(str(amount))而不是float(amount)来做格式化。上面代码里我用f"{amount:.2f}"也是一种可行方案,但要注意它对float的舍入规则是银行家舍入还是四舍五入,Python 3 的 round 是银行家舍入,100000000.015会被格式化为"100000000.01",这在票据上可能是错的。更稳妥的是在外部先把金额乘以 100 转成整数分,再传入函数。
5.4 超大数字的“单位越界”
现象:输入 10 的 30 次方,程序抛 IndexError 或者输出一段不伦不类的“一万万…”。
原因:UNIT_SECTION列表不够长,节权索引越界;或者没有做位数上限检查。
解决:两个方案任选。一是主动检查位数,超过 16 位直接抛异常或提示“超出支持范围”;二是扩展节权表到“载”(10 的 44 次方),这样多数场景都不会越界。我的习惯是采取“可扩展表 + 越界抛错”双保险,因为业务上如果真出现超过 10 的 20 次方的金额,先怀疑数据源是否异常,而不是默默转换。
5.5 金额大写的“零圆”与“整”冲突
现象:输入 0.50,得到“零圆伍角整”或“零圆伍角”不一致。
原因:我的代码里result_dec非空时不会加“整”,所以输出“零圆伍角”。但很多人在拼接时先加“整”,再拼小数,导致“整”和“角分”同时出现。财务规范里“整”字只出现在金额末尾且小数部分为 0 时;如果小数部分有角分,末尾写“角”或“分”即可,不再加“整”。
解决:代码里需要把“整”的判断从“是否有小数部分”改为“小数部分是否全为 0”。我上面给出的实现是用result_dec是否为空来判断,等价于“小数部分全为 0”。如果你实现的方式是先拼“圆整”再拼小数,那必须改成先拼小数、最后统一决定是否补“整”。
6. 进阶:把转换封装成可复用模块,并用测试用例锁住边界
做这类工具函数,我习惯在交付之前做三件事:封装、打日志、写测试用例。封装的意义不只是复用,而是把“转换规则”和“业务调用方”解耦——财务系统、报表模块、语音读码用的是同一套规则,如果规则要调整(比如把“零圆”改成“零元”),只需改一个文件。
我一般会把函数收敛到一个模块里,对外暴露三个接口:num_to_cn(整数读法)、amount_to_cn(金额大写)、parse_currency(金额字符串拆分与校验)。接口内部不用 print,统一返回字符串,由上层决定是记录日志还是直接展示。这样最大的好处是可以做单元测试。
下面是一组我常用的测试用例,覆盖前面提到的所有边界:
import unittest class TestNumToCn(unittest.TestCase): def test_basic(self): self.assertEqual(num_to_cn(0), "零") self.assertEqual(num_to_cn(5), "五") self.assertEqual(num_to_cn(15), "十五") self.assertEqual(num_to_cn(100), "一百") self.assertEqual(num_to_cn(1234), "一千二百三十四") def test_zero_rules(self): self.assertEqual(num_to_cn(1001), "一千零一") self.assertEqual(num_to_cn(10010), "一万零一十") self.assertEqual(num_to_cn(100000001), "一亿零一") self.assertEqual(num_to_cn(100100001), "一亿零一十万零一") def test_amount(self): self.assertEqual(amount_to_cn(100), "壹佰圆整") self.assertEqual(amount_to_cn(100.50), "壹佰圆伍角") self.assertEqual(amount_to_cn(1.03), "壹圆零叁分") self.assertEqual(amount_to_cn("100000000.01"), "壹亿圆零壹分")写测试用例时有一个习惯对我帮助很大:把“业务验收时容易被财务打回”的用例优先写进去,比如100100001这种连续跨节零的案例,而不是只测简单整数。因为这些用例正是当初让我翻车的地方,也是财务最较真的地方。
除了测试,我还建议做一个很小的命令行验证工具,方便在不写代码的情况下快速核对结果。做法是在模块尾部加两行:
if __name__ == "__main__": sample = [1001, 10010, 100000001, 0.50, 1.03, 100] for s in sample: print(s, "->", amount_to_cn(s) if isinstance(s, float) else num_to_cn(s))输出之后人工逐条核对,比写一堆断言更快发现规则漏洞。这也是我的一个习惯:再完善的单元测试,也抵不上把真实业务里的金额样本跑一遍,因为你会看到那些“语法上正确但业务上不符合习惯”的输出,比如“壹拾万零伍仟元”这种读法——它没违反零位规则,但在某些财务格式里被要求写成“壹拾万伍仟元”。这类差异只有业务方说了算,测试用例只能守住“语法正确”这条底线。
最后说一个我自己的教训:刚做这套转换时,我偷懒直接用第三方库,结果上线后遇到一批“万”节为 0 的金额大写全部多了一个“零”,排查了很久最后发现是第三方库对“零”的处理策略和我们财务规范不同。后来我被要求全部换回自研实现,那一次替换花了一整天改接口、写适配、补测试。现在我的建议是:这类基础转换函数,规则不复杂,自研加测试的性价比远高于引库,而且你能确保规则永远听你的。希望这篇对你有帮助,自己动手写一遍,踩过那些零的坑,才会真正理解为什么中文数字转换不是一张映射表那么简单。
本文还有配套的精品资源,点击获取