
简介Python 将阿拉伯数字转换为中文大写金额的实现代码面向财务软件开发、会计数据处理或需要中文金额展示的自动化脚本场景。代码按数字区间拆分为 _to_chinese4、_to_chinese8、_to_chinese16 等辅助函数再通过主函数 to_chinese 统一调用覆盖 0 到 9999999999999999 的整数范围并处理“零”“十”“万”“亿”等中文数字拼接规则可直接用于批量金额转换或嵌入业务系统。资源包为 1 个 pdf 文件共 30KB内容包含完整源码、函数逻辑说明与运行示例适合初中级 Python 学习者理解数字处理与递归拆分思想。该资源发布以来已有 11359 人浏览学习说明其在数字转换类问题上有较高参考价值。通过阅读该 PDF读者可以快速摘取可运行代码了解每一位转换、高位低位拆分以及“一十”转“十”等边界处理细节在实际项目中稍加修改即可适配大写金额需求。1. 数字转中文一个老 Python 脚本为什么值得拆把阿拉伯数字转成中文听起来是个不起眼的小功能真做起来全是边界条件。上周处理某财务系统的报销单导出金额列要按中文大写展示我从旧文件夹里翻出一个 Python 实现核心思路是用_to_chinese4、_to_chinese8、_to_chinese16三个函数把数字按万、亿分段转换上限支持到 9999999999999999。代码很短但作者自己也说“没经过优化也没经过详细测试”实际跑起来确实踩了好几个坑。这篇文章就把这个实现拆开讲透先看四位转换的零位规则再看万和亿怎么拼接最后补一份金额大写适配。适合写报表、发票、付款单的开发者刚学 Python 的也能跟着把边界问题一次看懂。2. 四位转换 _to_chinese4从反转拼接改到按位拼接2.1 先看常量和分段阈值原实现的第一个优点是常量定义得很清楚。中文字符、位权、分段阈值全部抽成全局变量后续所有函数都围着它们转。_MAPPING (零, 一, 二, 三, 四, 五, 六, 七, 八, 九) _P0 (, 十, 百, 千) _S4, _S8, _S16 10 ** 4, 10 ** 8, 10 ** 16_MAPPING存的是 0 到 9 的中文写法_P0存的是个十百千这四级位名下标 0 对应“个位”因为是空字符串所以个位不显示位名。_S4是 10000_S8是一亿_S16是 10 的 16 次方。这三个阈值决定了数字在哪一段用哪个函数处理。常量值用途_MAPPING零到九数字与中文字符映射_P0空、十、百、千四位以内的位权名_S410 ** 4一万四位转换的上界_S810 ** 8一亿万级转换的上界_S1610 ** 16亿级转换的上界2.2 四位以内的按位拼接写法原帖的四位转换用了一个“低位先拼、最后整体反转”的技巧思路很巧但边界问题不少。我复现时保留它的分段和补零规则把拼接顺序改成高位到低位逻辑更直白也更容易测。def _to_chinese4(num): 转换 0 到 9999 之间的数字返回中文字符串 if num 10: return _MAPPING[num] digits [] while num 10: digits.append(num % 10) num // 10 digits.append(num) digits.reverse() # 现在 digits[0] 是最高位 result length len(digits) for i, val in enumerate(digits): pos length - 1 - i # 当前位对应的位权下标 if val 0: # 遇到零时只在“后面还有非零位”的情况下补一个零 if i ! length - 1 and digits[i 1] ! 0: result 零 continue result _MAPPING[val] _P0[pos] return result这段代码先把数字从个位开始拆进列表reverse()之后变成高位到低位。循环里pos计算当前位的位权比如四位数的千位pos3对应_P0[3]就是“千”。零的补位规则是重点连续零只补一次而且只在后面还有非零位时才补避免出现“一千零零一”。for n in (5, 10, 11, 20, 100, 101, 1001, 1010): print(n, -, _to_chinese4(n))输出结果5 - 五 10 - 一十 11 - 一十一 20 - 二十 100 - 一百 101 - 一百零一 1001 - 一千零一 1010 - 一千零一十你可能注意到了10 和 11 在这里输出的是“一十”“一十一”不是口语里的“十”“十一”。这个处理是有意为之四位函数只负责把数字精确拼出来把“一十”缩成“十”放到最外层统一做因为如果在这里直接替换会误伤“一千零一十”这种中间位置的“一十”。这个坑在原帖里真实存在后面避坑章我会专门讲。2.3 原版反转拼接为什么会在整十数上翻车原帖的四位转换不是按位顺序拼的它先把个位、十位、百位、千位的字符从低位往高位累加最后result[::-1]整体反转。对 1234 这个数原逻辑先拼出“四十三百二千一”反转为“一千二百三十四”看着很巧妙。但换成 20 就出问题了个位是 0 被跳过拼出来只剩“十二”反转之后还是“十二”。这就是反转法的本质缺陷它依赖字符串反转把低位片段倒过来一旦个位为 0整个字符串失去反转参照物结果就错了。类似的还有 30、40 一直到 90全部会变成“十三”“十四”。所以我在复现时没有沿用反转拼法改成高位到低位直接拼接虽然代码多几行但每位的顺序都是确定的不再依赖反转这个黑匣子。3. 万与亿分段把 16 位数字切成可维护的三块3.1 _to_chinese8万级转换与补零判断四位转换解决的是 0 到 9999更大的数字要分段处理。原帖把 10000 到 99999999 之间的数字交给_to_chinese8它把数字拆成 high 和 low 两段high 是万级以上部分low 是万级以下部分。def _to_chinese8(num): 转换 0 到 99999999 之间的数字 if num _S4: return _to_chinese4(num) high, low num // _S4, num % _S4 if low 0: return _to_chinese4(high) 万 if low _S4 // 10: return _to_chinese4(high) 万零 _to_chinese4(low) return _to_chinese4(high) 万 _to_chinese4(low)num // _S4得到的是万以上部分num % _S4是余下的低四位。low 等于 0 时直接在高位后面加“万”比如 10000 就是“一万”。low 不等于 0 时要看 low 是不是小于 1000。如果 low 小于 1000说明万位和低四位之间缺位必须补一个“零”否则 10001 会被拼成“一万一”正确的写法是“一万零一”。如果 low 大于等于 1000说明低四位完整直接拼就行比如 11000 是“一万一千”不是“一万零一千”。print(_to_chinese8(10001)) # 一万零一 print(_to_chinese8(11000)) # 一万一千 print(_to_chinese8(12345678)) # 一千二百三十四万五千六百七十八这个补零判断是中文数字转换里最容易漏的地方很多人第一次写都直接 high 万 low结果 10001 变成“一万零一”还是小事100000001 这种跨亿的会更乱。所以分段时先把补零条件想清楚比事后修字符串靠谱得多。3.2 _to_chinese16亿级转换的同一套模式亿级的_to_chinese16和万级结构完全一样只是把模数换成了_S8也就是 10 的 8 次方。def _to_chinese16(num): 转换 0 到 9999999999999999 之间的数字 high, low num // _S8, num % _S8 if low 0: return _to_chinese8(high) 亿 if low _S8 // 10: return _to_chinese8(high) 亿零 _to_chinese8(low) return _to_chinese8(high) 亿 _to_chinese8(low)high 是亿级以上部分low 是亿级以下部分。low 等于 0 时直接加“亿”例如 100000000 是“一亿”。low 小于_S8 // 10也就是小于 10000000 时需要在中间补“零”例如 100000001 是“一亿零一”不是“一亿一”。这里有个细节high 本身可能超过 9999所以_to_chinese8(high)内部还会再走一次万级分段。比如 123456789012 这个数high 是 1234low 是 56789012亿级函数会把 low 交给_to_chinese8处理得到“五千六百七十八万九千零一十二”整体就是“一千二百三十四亿五千六百七十八万九千零一十二”。这种嵌套分段不需要额外的递归深度因为最多只有三层亿、万、四位。3.3 主函数 to_chinese类型检查与范围校验主函数to_chinese做两件事校验输入、按大小路由到对应分段函数。def to_chinese(num): 对外主入口支持 0 到 9999999999999999 if not isinstance(num, int): raise TypeError(输入必须是整数: %s % num) if num 0 or num 9999999999999999: raise ValueError(数值超出范围: %s % num) if num _S4: text _to_chinese4(num) elif num _S8: text _to_chinese8(num) else: text _to_chinese16(num) return text原帖用的是types.IntType和types.LongType那是 Python 2 的写法Python 3 里这两个属性已经不存在了直接用isinstance(num, int)更干净。范围判断放在最前面避免数字超界走到 assert 里弹出一堆堆栈。数字范围处理函数示例0 到 9999_to_chinese49000 - 九千10000 到 99999999_to_chinese810001 - 一万零一100000000 到 9999999999999999_to_chinese16100000001 - 一亿零一调用方式很简单print(to_chinese(9000)) # 九千 print(to_chinese(10001)) # 一万零一 print(to_chinese(9999999999999999))最后一个 16 位的最大值输出是“九千九百九十九万九千九百九十九亿九千九百九十九万九千九百九十九”对照一下位数刚好 16 个数字说明分段边界没有漏。4. 金额大写从“九千”到“玖仟元整”的落地写法4.1 普通数字和大写金额的映射差异很多场景下要的不是“九千”而是“玖仟元整”。直接做字符串替换是不行的因为普通数字转换里有“一十变十”的简写规则而财务大写恰恰要求 10 写成“壹拾”不能简写。语义普通中文财务大写数字映射零一二三四五六七八九零壹贰叁肆伍陆柒捌玖位权十百千万拾佰仟万10十壹拾20二十贰拾末尾无小数不加后缀加“整”所以金额大写必须用一套独立映射重新转换不能复用普通转换结果再做替换。下面这个_to_capital4就是大写的四位转换逻辑和普通版一致只是换了常量。_CAPITAL_MAPPING (零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖) _CAPITAL_P0 (, 拾, 佰, 仟) def _to_capital4(num): 大写数字的四位转换不做任何‘一十’简写 if num 10: return _CAPITAL_MAPPING[num] digits [] while num 10: digits.append(num % 10) num // 10 digits.append(num) digits.reverse() result length len(digits) for i, val in enumerate(digits): pos length - 1 - i if val 0: if i ! length - 1 and digits[i 1] ! 0: result 零 continue result _CAPITAL_MAPPING[val] _CAPITAL_P0[pos] return result这个函数里 10 输出“壹拾”11 输出“壹拾壹”不会触发普通转换里的“十”简写因为财务场景要求逐位还原。4.2 带元角分的金额转换函数有了大写四位转换还需要把万、亿两级拼起来再处理角分。我一般会写一个_to_capital负责整数部分的大写分段然后to_chinese_amount处理元角分。def _to_capital(num): 支持 0 到 9999999999999999 的大写整数转换 if num _S4: return _to_capital4(num) if num _S8: high, low num // _S4, num % _S4 if low 0: return _to_capital4(high) 万 if low _S4 // 10: return _to_capital4(high) 万零 _to_capital4(low) return _to_capital4(high) 万 _to_capital4(low) high, low num // _S8, num % _S8 if low 0: return _to_capital(high) 亿 if low _S8 // 10: return _to_capital(high) 亿零 _to_capital(low) return _to_capital(high) 亿 _to_capital(low) def to_chinese_amount(value): 金额转中文大写value 单位是元最多支持角分 if not isinstance(value, (int, float)): raise TypeError(金额必须是 int 或 float) if value 0 or value 9999999999999999.99: raise ValueError(金额超出可转换范围) integer_part int(value) decimal_part int(round((value - integer_part) * 100)) text _to_capital(integer_part) 元 if decimal_part 0: text 整 elif decimal_part % 10 0: text _CAPITAL_MAPPING[decimal_part // 10] 角整 elif decimal_part 10: text 零 _CAPITAL_MAPPING[decimal_part] 分 else: text _CAPITAL_MAPPING[decimal_part // 10] 角 \ _CAPITAL_MAPPING[decimal_part % 10] 分 return textdecimal_part是把小数部分转成以“分”为单位的整数。比如 1010.01 的整数部分是 1010decimal_part 是 1输出就是“壹仟零壹拾元零壹分”。这里单独处理了只有角、只有分、角分都有三种情况。提示float 在表示 0.29 这类小数时会有精度误差round((value - integer_part) * 100)能兜住大多数情况。如果是正经财务系统建议用Decimal(str(value))解析别让二进制浮点参与金额计算。4.3 金额用例验证用一组典型用例验证一下for amt in (0, 9000, 1010.01, 100000001.10): print(amt, -, to_chinese_amount(amt))输出结果0 - 零元整 9000 - 玖仟元整 1010.01 - 壹仟零壹拾元零壹分 100000001.10 - 壹亿零壹元壹角整如果公司票据要求用“圆”而不是“元”把元改成圆就行“整”也可以换成“正”各地习惯略有差异。核心的零位规则和分段逻辑不用动。5. 避坑指南这个老代码上车的五个翻车点5.1 Python 2 代码换到 Python 3第一眼就是四个报错现象原帖代码复制到 Python 3 里第一行运行就报SyntaxError因为老代码写的是print to_chinese(9000)而不是print(...)。改成函数调用形式后又报AttributeError: module types has no attribute IntType。继续修完类型判断num / 10在 Python 3 里变成浮点除法while num 10永远为真程序陷入死循环。原因这份代码是按 Python 2 写的types.IntType、print语句、整数除法三个语法点全变了。不是代码逻辑不对是语言版本差异。解决统一换成isinstance(num, int)除法用//打印用print(...)。改完之后在主函数入口加一行范围校验避免异常类型不明确。if not isinstance(num, int): raise TypeError(输入必须是整数: %s % num)5.2 粘贴后缩进错乱格式化比调试更重要现象从网页复制代码到本地运行时报IndentationError: unexpected indent具体行指向 while 循环里面的赋值语句。原因网页排版把代码缩进打乱了尤其是while循环内的num num // 10和函数末尾的return经常对不齐。这种问题肉眼很难一次看出。解决拿到这类代码先做一次全量格式化不要急着跑。多数编辑器内置格式化工具或者用 autopep8 跑一遍。缩进统一成 4 空格后再把类型判断、返回值逐一核对。5.3 整十数转换变成“十二”现象调用to_chinese(20)返回“十二”30返回“十三”十位和个位顺序反了。原因原实现四位转换时先把低位拼到result最后整体反转。20 的个位是 0拼串时被跳过结果只剩“十二”反转后仍然“十二”。解决换用第 2 章的高位按位拼接法或者保留反转法但单独处理整十数if num % 10 0 and 20 num 100: return _MAPPING[num // 10] 十这类边界不靠测试是发现不了的所以后面验证阶段一定要把 20、30、90 这些整十数放进断言列表。5.4 全局替换“一十”误伤“零一十”现象原代码用replace(一十, 十)把 10 变成“十”但 1010 会输出“一千零十”10010 输出“一万零十”书面标准应该是“一千零一十”“一万零一十”。原因replace是全局替换字符串里所有“一十”都被换掉了包括“零一十”里那部分。这不是“一十”该不该简写的问题而是替换范围太大。解决把简写限制在整个字符串开头用正则锚定import re def _normalize(text): return re.sub(r^一十, 十, text)这样只处理“一十”出现在句子开头的情况10 变成“十”11 变成“十一”而“一千零一十”里的“一十”在末尾不会被碰。5.5 bool 也是整数类型检查的漏网之鱼现象to_chinese(True)返回“一”to_chinese(False)返回“零”明明传的是布尔值。原因Python 里bool是int的子类isinstance(True, int)返回 True类型检查直接放行了。解决对布尔值单独拦截或者用严格类型判断if type(num) is not int: raise TypeError(输入必须是 int不能是 bool 或其他类型)如果项目中确实需要支持 Python 2 的long类型就写成isinstance(num, (int, long))并在 Python 3 里把long替换成int。这个坑不致命但接口一旦被别人调用传个True进去报表里出现“零元整”就很难排查。6. 验证与缓存放进生产代码前先跑这组断言6.1 用 lru_cache 缓存高频转换报表导出时经常遇到同一批金额反复转换比如合计行、明细行、汇总页各转一次。转换本身是纯函数同一个数字永远返回同一个结果非常适合加缓存。from functools import lru_cache lru_cache(maxsize256) def to_chinese_cached(num): return to_chinese(num)maxsize256意味着最多缓存 256 个不同数字的结果超过之后按 LRU 策略淘汰最久没用的。对金额段落这类重复较多的场景256 足够覆盖一张报销单上的全部金额种类。6.2 一组可复制的断言用例不要靠 print 目测输出对不对把典型用例写成断言每次改动后跑一遍。下面这组覆盖了零、整十、跨万、跨亿、跨级补零几个关键点cases [ (0, 零), (10, 十), (11, 十一), (20, 二十), (101, 一百零一), (1001, 一千零一), (1010, 一千零一十), (10000, 一万), (10001, 一万零一), (11000, 一万一千), (100000000, 一亿), (100000001, 一亿零一), (9000, 九千), ] for num, expected in cases: result to_chinese_cached(num) assert result expected, (num, result, expected) print(all cases passed)这里特意放了 1010 和 10001一个验证“零一十”不被误伤一个验证万级补零。这两条是当初老代码最容易出问题的地方。6.3 验证时先怀疑边界再相信逻辑我后来做某报销单导出功能时把这组断言直接写进了模块底部的测试段每次改完代码先跑一圈再打包。养成这个习惯以后至少省掉两次上线后才发现金额大写错位的尴尬。尤其是金额场景数字转换一旦出错差一分钱都说不清。如果你也准备拿这个老代码上车建议先跑一遍上面的断言再决定要不要接进业务系统。希望帮到你。本文还有配套的精品资源点击获取