
简介在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 之间程序把它拆成 high9999、low9999先译出九千九百九十九拼一个万再补一段九千九百九十九。等你看到 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 拆成 high1、low1低段为 1小于 1000结果为一万零一11001 拆成 high1、low1001低段千位有值结果为一万一千零一不再额外补零。这个规则单独看并不复杂但手工实现很容易把段内零和段间零混在一起。原代码的优势在于职责分离段间零由 _to_chinese8 根据 low 大小判断段内零由 _to_chinese4 内部遍历时的插入逻辑负责互不干扰。例如 10010001 这种数字段间不补零但段内出现空位_to_chinese4 会在一千和一之间补出零两段组合起来就是正确的一千零一。到了 _to_chinese16 里同一套规则再套一层low _S8 / 10 时拼亿零否则拼亿。加上 _to_chinese16 内部还要调 _to_chinese8所以 100000001 会被拆成 high1、low1low 远小于 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]。遍历时从低位开始拼idx0 拼出四idx1 拼出四十三idx2 拼出四十三百二idx3 拼出四十三百二千一最后整体反转得到一千二百三十四。这段代码的巧劲在后面一行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 和 lowhigh 是万字前面的数字low 是万字后面的数字。low0 时直接拼万例如 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_chinese410000 到 99999999 走 _to_chinese8100000000 到 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 或直接 isinstanceimport 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 改成显式 ifraisedef _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.10.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 到最大值的边界用例尤其是十位与百位交界、段与段交界这些最容易翻车的位置。这套习惯帮我挡下了至少三次线上金额错字事故希望帮到你。本文还有配套的精品资源点击获取