news 2026/10/17 19:58:36

Python数字转中文:四位分组与零规则避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python数字转中文:四位分组与零规则避坑指南

简介:在Python开发中,将阿拉伯数字转换为中文大写是一项常见且易错的编程任务,常用于财务金额处理、票据打印和报表生成等场景。内容面向Python新手及需要快速实现数字转换的开发者,系统讲解0至9999999999999999范围内整数的中文大写算法,并提供了可直接运行的完整代码。包体为1个PDF文件,容量约30KB,包含源码、注释以及分模块的转换思路,便于离线阅读或打印对照。资源已有11359人学习/下载,实用度较高。方案通过_to_chinese4、_to_chinese8、_to_chinese16和主函数to_chinese四级递进实现:小范围先拆位,大范围借助“万”“亿”分组,并处理“零”的插入规则及“一十”变“十”的细节;同时包含类型检查和越界异常提示,结构清晰,便于二次修改。

1. 数字转中文在财务场景里的高频诉求:一个函数搞定十六位以内的整型转换

数字转中文这个需求,在财务类项目里出现频率比想象中高得多。做报销单打印、发票台账、合同金额回显时,甲方经常要求阿拉伯数字旁边必须有一行中文大写,防止被篡改。我见过不少同事第一次接手这类需求,从网上扒一段代码直接塞进项目,输入 10001 返回"一万零一"看着没问题,切到 100010001 就多出一个"零",整套逻辑当场翻车。这套 Python 实现把 0 到 9999999999999999 范围内的整数按四位一组拆解,逐段转中文再拼单位,源码不到一百行,适合 Web 后端、桌面工具甚至脚本里直接引用。它解决的核心问题是:不用自己反复抠"万位以下到底要不要补零"这类边界规则,一份代码覆盖个、十、百、千、万、亿、万亿七级单位。适合刚接触 Python 的开发者,也适合手头正好有金额转换需求、想直接拿现成方案改的人。

2. 转换逻辑拆解:四位一组的进制设计与零的拼接规则

2.1 为什么以四位为一组:中英文数字分组的本质差异

中文读数字的位权是"个十百千"一组,往上才是万、亿、万亿;英文则是按千(thousand)、百万(million)分组。这个差异直接决定了转换程序应该选多大进制。把 123456789 用中文读出来是"一亿两千三百四十五万六千七百八十九",肉眼观察会发现它天然被"亿"和"万"切成三段:1 | 2345 | 6789。如果按 1000 切分,段内还要再处理"百、十、个"三层,代码会多一层判断;按 10000 切分,每个段正好落进"个十百千"同一套循环里,段内逻辑可以完全复用。

原始代码里用三个常量控制层级:

_S4, _S8, _S16 = 10 ** 4, 10 ** 8, 10 ** 16 _MIN, _MAX = 0, 9999999999999999

_S4 表示一个完整四位数段的容量,_S8 是万级段的容量,_S16 是亿级段的容量。数字 99999999 落在 _S4 和 _S8 之间,程序把它拆成 high=9999、low=9999,先译出"九千九百九十九",拼一个"万",再补一段"九千九百九十九"。等你看到 16 位的 9999999999999999,它被拆成 9999|99999999|99999999 三层,每一层内部都走同一个 _to_chinese4,层间用"亿""万"衔接。这种分层设计的好处是:想扩展更高位时,只需要仿照 _to_chinese16 再包一层,把 _S32 设成 10**32,新增"兆"单位,原有核心逻辑一行都不用改。财务系统经常把参数调来调去,这种"改一处不牵连整体"的结构,在维护阶段特别省心。

2.2 段与段之间的零规则:什么时候必须补"零"

跨段拼接时最容易出错的就是"零"。中文数字读法里,相邻两个位权段之间如果出现空位,要读一个"零";连续多个空位也只读一个。代码里对这段逻辑的处理非常直接:看低段数字是否小于本段容量的十分之一。_to_chinese8 里的判断是low < _S4 / 10,也就是 low 小于 1000 时,拼接"万零",否则直接拼"万"。

为什么临界点是 1000?因为低段满值是 9999,如果 low 落在 1000 到 9999 之间,说明低段的千位不是空位,两段之间不需要补零;如果 low 落在 0 到 999 之间,千位一定是空位,段间跳级发生,中文必须补一个零来衔接。例如 10001 拆成 high=1、low=1,低段为 1,小于 1000,结果为"一万零一";11001 拆成 high=1、low=1001,低段千位有值,结果为"一万一千零一",不再额外补零。

这个规则单独看并不复杂,但手工实现很容易把"段内零"和"段间零"混在一起。原代码的优势在于职责分离:段间零由 _to_chinese8 根据 low 大小判断,段内零由 _to_chinese4 内部遍历时的插入逻辑负责,互不干扰。例如 10010001 这种数字,段间不补零,但段内出现空位,_to_chinese4 会在"一千"和"一"之间补出"零",两段组合起来就是正确的"一千零一"。

到了 _to_chinese16 里,同一套规则再套一层:low < _S8 / 10 时拼"亿零",否则拼"亿"。加上 _to_chinese16 内部还要调 _to_chinese8,所以 100000001 会被拆成 high=1、low=1,low 远小于 10**7,高位部分得到"一",拼"亿零",再拼低段的"一",最终输出"一亿零一"。零的规则在最底层和最顶层各管一段,反而比那种"一个函数处理所有位数"的写法更容易核对。

2.3 常见输入的预期输出对照

建议把下面这组用例当作验收基准。第 4 章会讲到,其中个别用例在原始代码里会翻车,正好用来验证修复效果。

输入四位分组拆段预期输出
00零
1111十一
1515十五
100100一百
90009000九千
100011|0001一万零一
110011|1001一万一千零一
1000000011|00000001一亿零一

这些用例覆盖了个位、十位开头、百位零值、万段零值、亿段零值五种典型模式。自己改造函数时,建议先把这张表跑通再谈优化,否则很容易在一个看似人畜无害的输入上浪费一晚上。

3. 源码逐段解读:从个位到亿位的完整调用链

3.1 全局常量与两个异常类

原始代码开头定义了一些基础元素,先看这一段:

#!/usr/bin/python # -*- encoding: utf-8 -*- class NotIntegerError(Exception): pass class OutOfRangeError(Exception): pass _MAPPING = (u'零', u'一', u'二', u'三', u'四', u'五', u'六', u'七', u'八', u'九') _P0 = (u'', u'十', u'百', u'千') _S4, _S8, _S16 = 10 ** 4, 10 ** 8, 10 ** 16 _MIN, _MAX = 0, 9999999999999999

_MAPPING 是数字 0 到 9 对应的中文单字,_P0 是四位数段内的位权。注意 _P0 的第一个元素是空字符串,这对应个位——个位上不需要再拼位权。两个异常类分别用于类型错误和越界错误,这样调用方可以用 except 精确捕获,而不是拿到一串莫名其妙的拼接结果。

很多人容易忽略 _MIN 和 _MAX 的边界语义:_MAX 是 9999999999999999,一共 16 位,不是"亿"级,而是"万亿"级。我见过有人拿它转 10000000000000000,直接抛 OutOfRangeError,以为是程序坏了,其实只是超出设计范围。

3.2 _to_chinese4:段内核心转换

这是整个工具最核心的函数,负责把一个 0 到 9999 之间的数字转成中文:

def _to_chinese4(num): '''转换[0, 10000)之间的阿拉伯数字''' assert(0 <= num and num < _S4) if num < 10: return _MAPPING[num] else: lst = [] while num >= 10: lst.append(num % 10) num = num / 10 lst.append(num) c = len(lst) # 位数 result = u'' for idx, val in enumerate(lst): if val != 0: result += _P0[idx] + _MAPPING[val] if idx < c - 1 and lst[idx + 1] == 0: result += u'零' return result[::-1].replace(u'一十', u'十')

先说参数:num 被约束在 [0, 10000) 区间,函数开头用 assert 做防御式检查,越界直接中断。接着用 while 循环把数字从低位到高位逐个取出,例如 1234 会得到 lst=[4,3,2,1]。遍历时从低位开始拼,idx=0 拼出"四",idx=1 拼出"四十三",idx=2 拼出"四十三百二",idx=3 拼出"四十三百二千一",最后整体反转,得到"一千二百三十四"。

这段代码的巧劲在后面一行:result[::-1].replace(u'一十', u'十')。反转之前的结果是逆序拼接的,反转后 11 会变成"一十一",规范中文读法要写成"十一",所以用 replace 把开头的"一十"换成"十"。这个写法能处理 10 到 19 的"十"字开头问题,但注意它是全串替换,不是锚定开头,后面第 4 章会专门讨论这个坑。

3.3 _to_chinese8 与 _to_chinese16:万位与亿位拼接

这两个函数结构几乎一样,区别只在 mod 值和单位字:

def _to_chinese8(num): assert(num < _S8) to4 = _to_chinese4 if num < _S4: return to4(num) else: mod = _S4 high, low = num / mod, num % mod if low == 0: return to4(high) + u'万' else: if low < _S4 / 10: return to4(high) + u'万零' + to4(low) else: return to4(high) + u'万' + to4(low) def _to_chinese16(num): assert(num < _S16) to8 = _to_chinese8 mod = _S8 high, low = num / mod, num % mod if low == 0: return to8(high) + u'亿' else: if low < _S8 / 10: return to8(high) + u'亿零' + to8(low) else: return to8(high) + u'亿' + to8(low)

_to_chinese8 先用 10000 整除取 high 和 low,high 是"万"字前面的数字,low 是"万"字后面的数字。low==0 时直接拼"万",例如 30000 得到"三万";low 小于 1000 时拼"万零",例如 10001 得到"一万零一";low 大于等于 1000 时不补零直接拼"万"。_to_chinese16 把 mod 换成 10**8,逻辑完全复用,只是把单位换成"亿"。

用一句话概括调用链推演,以 123456789 为例:

# to_chinese(123456789) # num >= _S8,进入 _to_chinese16 # high = 123456789 / 10**8 = 1 # low = 123456789 % 10**8 = 23456789 # low != 0 且 low >= 10**7 / 10 # 拼接:_to_chinese8(1) + u'亿' + _to_chinese8(23456789) # 再展开 _to_chinese8(23456789) # high = 23456789 / 10000 = 2345 # low = 23456789 % 10000 = 6789 # 拼接:_to_chinese4(2345) + u'万' + _to_chinese4(6789) # 最终:一亿二千三百四十五万六千七百八十九

这种"每层只拆一次"的写法,比递归套到底更容易做日志追踪。出问题时,只要看 low 落在哪个分支,就能定位到是段间零的判断出问题,还是段内转换出问题。

3.4 to_chinese:主入口的类型与范围分派

def to_chinese(num): if type(num) != types.IntType and type(num) != types.LongType: raise NotIntegerError(u'%s is not a integer.' % num) if num < _MIN or num > _MAX: raise OutOfRangeError(u'%d out of range[%d, %d)' % (num, _MIN, _MAX)) if num < _S4: return _to_chinese4(num) elif num < _S8: return _to_chinese8(num) else: return _to_chinese16(num)

入口函数先做类型检查,避免浮点数和字符串混进来产生奇怪拼写。原始代码用的是 types.IntType 和 types.LongType,这是 Python 2 时代的写法,在 Python 3 里 types.IntType 已经被移除,直接运行会抛 AttributeError。判断完类型后进入范围检查,然后按数量级分三路调用。

这种三段式分派和三个辅助函数的边界一一对应:0 到 9999 走 _to_chinese4,10000 到 99999999 走 _to_chinese8,100000000 到 9999999999999999 走 _to_chinese16。模块化程度很高,单元测试只需要分别覆盖三个区间的临界值,就能保证整体可靠。

4. 避坑实战:类型兼容、零值处理与边界范围三个翻车点

4.1 坑一:Python3 下 types.IntType 直接抛 AttributeError

现象:把原始文件拿过来在 Python 3 环境里跑,import 不报错,一调用to_chinese(123)就报AttributeError: module 'types' has no attribute 'IntType'。原因:Python 3 把内置类型重新组织,types.IntType 和 types.LongType 都被移除,int 和 long 合并成了一个类型。解决:把类型检查换成都兼容的写法,推荐用 numbers.Integral 或直接 isinstance:

import numbers def to_chinese(num): if not isinstance(num, numbers.Integral): raise NotIntegerError(u'%s is not a integer.' % num) if num < _MIN or num > _MAX: raise OutOfRangeError(u'%d out of range[%d, %d)' % (num, _MIN, _MAX)) # 后续分派不变

这里选 numbers.Integral 而不是isinstance(num, int),是因为它还能兼容 Python 2 的 long 类型,以及 numpy 里的整数类型。如果项目里正好用了 numpy,这个选择能省掉不少兼容性报错。范围检查的 _MIN 和 _MAX 没变,超限行为跟原版一致。

4.2 坑二:replace("一十","十") 误伤"一百一十五"这类中间读法

现象:输入 115,期望输出"一百一十五",实际输出"一百十五"。原因:_to_chinese4 最后一行result[::-1].replace(u'一十', u'十')是全串替换,没有限定位置。字符串"一百一十五"中间恰好包含"一十"两个字,被整体换成了"十",所以丢了一个"一"。这个 bug 在 11、111 这类小数字上不触发,因为替换目标出现在开头;一旦数字超过百位且十位是 1,就会踩中。解决:改成只处理开头匹配:

result = result[::-1] if result.startswith(u'一十'): result = result[1:] return result

这样 11 会从"一十一"去掉开头的"一"变成"十一",115 反转后是"一百一十五",开头不是"一十",原样保留,不再误伤。修复后建议把 11、15、110、115、1115 全部跑一遍,这五个值正好覆盖"不修会错、修了要防止过度替换"的场景。

4.3 坑三:assert 在 -O 优化模式下会被整体移除

现象:用python -O main.py运行时,传入 10000 给 _to_chinese4,函数没有抛 AssertionError,反而开始执行 while 循环。原因:Python 的 assert 在优化模式下会被编译器剥离,所有用 assert 做的防御检查全部失效。原始代码里 _to_chinese4、_to_chinese8、_to_chinese16 三个函数都依赖 assert 拦边界,一旦以优化模式部署,越界数字会带着错误的分组进入循环,拼出"一万零万"这类离谱结果。解决:把关键边界检查从 assert 改成显式 if+raise:

def _to_chinese4(num): if not (0 <= num < _S4): raise OutOfRangeError(u'%d out of range[%d, %d)' % (num, 0, _S4)) # 其余逻辑不变

生产环境还有一个更隐蔽的好处:换个异常类型不会影响主入口的 OutOfRangeError 捕获逻辑,调用方可以通过同一个 except 统一处理越界。血泪经验是:凡是线上要跑的数字转换类代码,别用 assert 做输入校验,它只适合在测试阶段做内部不变量检查。

4.4 坑四:16 位上限与"兆"级业务需求的冲突

现象:某个项目要转换的金额达到 10 的 16 次方量级,调用 to_chinese 直接抛 OutOfRangeError。原因:_MAX 被写死为 9999999999999999,也就是只能覆盖到万亿级;财务单据中偶尔会出现"兆"级别的大额数据,超过了设计范围。解决:不要硬改 _MAX,而是按同样的模式加一层 _S32 和对应的转换函数:

_S32 = 10 ** 32 _MAX = 99999999999999999999999999999999 # 32位 def _to_chinese32(num): mod = _S16 high, low = num / mod, num % mod if low == 0: return _to_chinese16(high) + u'兆' else: if low < _S16 / 10: return _to_chinese16(high) + u'兆零' + _to_chinese16(low) else: return _to_chinese16(high) + u'兆' + _to_chinese16(low)

注意这里的除法在 Python 2 是整除,在 Python 3 要改成//,否则 high 会变成浮点数,类型检查直接卡住。扩展后主入口的 else 分支落到 32 位函数,原有 16 位以内的行为完全不变。这个坑的教训是:拿到这类工具先看 _MAX,别等线上抛异常才发现范围不够。

5. 从数字转换到金额大写:把整数转换升级为财务专用工具

5.1 金额大写与普通读法的映射差异

数字转中文只能解决"读法"问题,财务场景要的是"大写金额"。两者有两处明显差异:一是用字不同,阿拉伯数字 1 到 9、以及"十百千万"都要替换成壹贰叁肆伍陆柒捌玖拾佰仟;二是金额必须有单位闭环,整数部分末尾要加"元整",有小数时要加"角分"。如果直接把数字转中文的结果扔给甲方,对方看到"一"而不是"壹",第一轮评审就会被驳回。映射关系可以做成一张字典,而不是手动写一堆 if:

_UPPER_MAP = { u'零': u'零', u'一': u'壹', u'二': u'贰', u'三': u'叁', u'四': u'肆', u'五': u'伍', u'六': u'陆', u'七': u'柒', u'八': u'捌', u'九': u'玖', u'十': u'拾', u'百': u'佰', u'千': u'仟', u'万': u'万', u'亿': u'亿' }

这里有个细节:零在大写金额里仍然是"零",不能换成"〇"或"零"以外的字形。万和亿保持不变,只有十百千需要换成带单人旁的金融专用字。做映射时记得把"万亿"组合也考虑进去,因为原函数输出里可能出现"一万亿"这种连续单位,映射后是"壹万亿"。

5.2 基于原函数的金额大写改造

最稳妥的改造方式是复用原函数转出中文,再做逐字映射:

def to_chinese_money(num): if num < 0: return u'负' + to_chinese_money(-num) integer_part = int(num) chinese_str = to_chinese(integer_part) for old, new in _UPPER_MAP.items(): chinese_str = chinese_str.replace(old, new) return chinese_str + u'元整'

先处理负数,把负号单独拼在前面;再把数字截断成整数部分,走原函数;然后逐字替换成大写;最后补"元整"。这里要注意int(num)的精度风险:如果 num 是浮点数,转 int 会直接丢掉小数部分,比如 123.9 会变成 123,金额少了九毛。实际业务里常见的做法是先把金额乘以 100 再拆整数和小数,或者直接把输入改成 Decimal,从根源上避开浮点误差。

这套方案的最小可用版本就这么几行,原函数内部的零处理和单位拼接逻辑完全没动,只需要保证 115 这类坑已经被第 4.2 节的修复处理掉,否则金额大写会把"一百一十五"错写为"一百十五",在合同回显场景是很严重的字面差错。

5.3 角分与负数处理的补丁做法

整数金额好办,带角分的金额需要单独处理小数部分。我一般会这样做:

def to_chinese_money_with_fen(num): if num < 0: return u'负' + to_chinese_money_with_fen(-num) integer_part = int(num) fen_part = round((num - integer_part) * 100) upper_map = _UPPER_MAP chinese_str = to_chinese(integer_part) for old, new in upper_map.items(): chinese_str = chinese_str.replace(old, new) if fen_part == 0: return chinese_str + u'元整' jiao = fen_part / 10 fen = fen_part % 10 result = chinese_str + u'元' if jiao > 0: result += _UPPER_MAP[str(jiao)] + u'角' if fen > 0: result += _UPPER_MAP[str(fen)] + u'分' return result

注意这里用的是round((num - integer_part) * 100),而不是直接int(num * 100),原因是浮点数乘 100 后很容易出现 123.999 这种尾巴,int 会再丢一分。如果项目里对金额精度要求高,强烈建议调用方传入 Decimal 字符串,转成 Decimal 后再做拆分,这能避免整个金额转换链路被 0.1+0.2 不等于 0.3 的经典问题拖下水。

这个补丁还有个隐藏约定:不足一元的金额,整数部分转换出来可能是"零",映射后是"零元",读起来是"零元伍角",财务上更常见的写法是直接"伍角"。要不要加"零元"前缀,取决于你们系统的出纳习惯,建议做成可配置参数,而不是写死在函数里。

6. 用边界值给转换函数做一轮验证:代码与预期结果对照

把前面几章的理论落到实测,最有效的方法不是拿一堆随机数乱试,而是固定一组边界用例。我习惯把这串用例直接写成测试脚本:

cases = [ (0, u'零'), (9, u'九'), (10, u'十'), (11, u'十一'), (15, u'十五'), (100, u'一百'), (101, u'一百零一'), (110, u'一百一十'), (115, u'一百一十五'), # 原始代码会错成'一百十五' (1000, u'一千'), (10001, u'一万零一'), (11001, u'一万一千零一'), (100000001, u'一亿零一'), (9000, u'九千'), (9999999999999999, u'九千九百九十九万九千九百九十九亿九千九百九十九万九千九百九十九'), ] for num, expected in cases: actual = to_chinese(num) status = 'PASS' if actual == expected else 'FAIL' print('{}: input={}, expected={}, actual={}'.format(status, num, expected, actual))

运行方法很简单:把原代码和修复后的代码保存为同一个文件,开头放这段测试,直接python test.py。重点看三个位置:十位为 1 的数(11、15、115),跨万段和跨亿段零值(10001、100000001),以及最大值边界(9999999999999999)。15 是"十五"而不是"一十五",115 是"一百一十五"而不是"一百十五"——这两个用例能把第 4.2 节的 replace 误伤问题一次性测出来。最大值用例同时验证了 16 位上限内三段式分派都能正确工作。

调试时如果某个用例 FAIL,不要盯着输出发呆,按第 3.3 节的调用链推演,在 _to_chinese8 或 _to_chinese16 的 low 分支处加一行print(low),立刻能判断是段间零判断错了,还是段内 _to_chinese4 拼错了。从那以后我每次拿到这类数字转换函数,都强制走一遍从 0 到最大值的边界用例,尤其是十位与百位交界、段与段交界这些最容易翻车的位置。这套习惯帮我挡下了至少三次线上金额错字事故,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 19:57:39

StyleMapGAN代码级拆解:空间映射机制实现局部图像编辑

StyleMapGAN 这个模型在 2021 年 CVPR 上发布时&#xff0c;我正好在做一个图像编辑相关的项目&#xff0c;当时看到它能做到"在像素级别上对生成图像进行局部编辑"这个能力&#xff0c;第一反应是——终于有人把 StyleGAN 的 latent space 玩明白了。但真正打开代码…

作者头像 李华
网站建设 2026/10/17 19:57:01

SQL三小时笔记:从SELECT到多表JOIN的完整知识骨架

简介&#xff1a;这份资源是B站Mosh老师SQL三小时入门课程的配套笔记&#xff0c;以PDF形式整理&#xff0c;面向零基础或希望系统梳理SQL核心语法的开发者与数据分析初学者。内容覆盖SQL基础知识、SELECT语句、WHERE子句&#xff0c;以及逻辑操作符、IN、BETWEEN、LIKE、REGEX…

作者头像 李华
网站建设 2026/10/17 19:50:49

地面无人平台性能评价:机动性与自主性双核心指标体系

简介&#xff1a;本资源是一篇聚焦地面无人作战平台性能评价体系构建的学术论文&#xff0c;面向军事装备研发人员、智能无人系统研究者及高校相关专业师生&#xff0c;旨在解决当前缺乏统一评价标准导致的需求方与研制方沟通不畅、性能描述模糊等实际问题。论文从平台内涵出发…

作者头像 李华
网站建设 2026/10/17 19:49:21

《Attention Is All You Need》拆解:从自注意力到Transformer架构

2017年那篇标题只有一句话的论文——《Attention is All You Need》&#xff0c;当时我拿到手的第一反应是&#xff1a;这标题起得真狂。注意力机制在之前的序列模型里不过是个辅助组件&#xff0c;你说只要注意力就够了&#xff1f;但等我从摘要开始&#xff0c;一段一段把Enc…

作者头像 李华
网站建设 2026/10/17 19:44:22

Python 3.8 入门实战:从环境搭建到写出第一个小工具

简介&#xff1a;这份资源是面向零基础编程学习者的Python 3.8入门教程PDF&#xff0c;适合希望系统掌握Python基础语法、准备进入Web开发、数据分析或自动化运维等方向的自学者与在校学生。教程从环境搭建讲起&#xff0c;覆盖Windows与Linux下的安装步骤、IDLE与PyCharm两种开…

作者头像 李华