简介这是一份面向Python初、中级开发者的实用代码资源核心解决阿拉伯数字转中文大写的编程需求适用于财务报表、发票金额、合同文书等要求中文数字展示的场景。资源压缩包内含1个PDF文件整体大小约30KB内容聚焦下载后即可阅读。目前已有11359人浏览学习受到较多关注。文档完整呈现数字转中文的Python解决方案由四个函数分工协作_to_chinese4处理万以内数字逐位拆解并拼接千百十等单位同时处理一十转十的细节_to_chinese8负责万级区间在万与低位之间正确补零避免连续零出现_to_chinese16扩展至亿级区间沿用同样的补零策略主函数to_chinese统一调度先完成类型检查与范围校验再根据数值区间调用对应函数。整体算法支持16位以内的整数转换示例调用print(to_chinese(9000))输出九千。读者既可快速复制函数用于财务系统也可通过清晰的分段设计学习递归拆分、位权映射和字符串处理技巧是一份兼具实用性与教学意义的参考代码。1. 数字转中文看起来是个查表题做起来全是规则题财务系统要打发票大写金额、语音模块要播报数字、报表摘要要把大额数据转成中文读数需求一句话落地却没法用一张映射表直接糊弄过去。整数读法、小数读法、货币大写是三条独立的语法规则“零”要不要省略、单位在亿和万之间怎么跳、角分位和大写“整”的关系全都不同。这篇文章从整数读数写起逐步扩展到负号、小数和人民币大写每一步都给出可运行的 Python 代码、参数说明和边界验证最后整理我踩过的五个高频翻车点。适合正在写工具函数、中间层接口或者语音合成前处理逻辑的开发者。2. 整数转中文四位分组法是唯一靠得住的套路2.1 为什么逐位拼接总会翻车中文读法的分组习惯中文读数和英文不一样英文三位一组中文四位一组而且“万”和“亿”这两个单位是真实读出来的。很多第一次写数字转中文的人习惯拿到数字后从高位到低位逐位拼接千位读“几千”百位读“几百”最后拼上“万”“亿”。这个方案在小数字上看起来没问题一旦出现 100050001 这种带空位和连续零的数字立刻露馅。逐位拼接最常见的结果是“一亿零五万零一”被拼成“一亿五万一”或者更离谱的“一亿万”。问题的根源在于中文读数不是按位读而是按四位一组读组与组之间再套上“万”“亿”这类大单位。一个组内千位是零、或者整个组为零时需要额外补一个“零”字但如果连续两组都是零又只能出现一个“零”。这些规则没法用循环加判断硬凑至少我在项目里用逐位方式写过两次都倒在边界用例上。后来统一改成四位分组法把问题拆成“组内读数”和“组间补零”两层才稳定下来。四位分组法的核心思路是先把数字按 10000 整除取余拆成若干个四位组从高到低依次处理。每个四位组内部用“千百十”单位组之间用“万亿兆”单位。组内全零就跳过但标记一下让后面遇到的非零组在前面补一个“零”组内非零但首位是零也就是这个组小于 1000时同样要在前面补“零”。这两条规则配合“十”开头的处理基本能把所有整数读对。2.2 最小可运行实现int_to_cn 完整代码我一般把整数转换拆成两个函数一个负责四位组内部转换一个负责组间拼接。这样便于独立测试也方便后续金额大写复用同一套分组逻辑。CN_DIGITS 零一二三四五六七八九 CN_UNIT [, 十, 百, 千] CN_BIG_UNIT [, 万, 亿, 兆] def group_to_cn(group: int) - str: 把一个 0-9999 的四位组转成中文读数不含万/亿等大单位。 s pending_zero False for pos in range(3, -1, -1): d group // (10 ** pos) % 10 if d 0: if s: pending_zero True else: if pending_zero: s 零 pending_zero False s CN_DIGITS[d] CN_UNIT[pos] return s def int_to_cn(num: int) - str: 把整数转成中文读数支持负数最大支持到 9999 亿。 if num 0: return 零 if abs(num) 10 ** 12: raise ValueError(超出兆范围的整数暂不支持) sign 负 if num 0 else num abs(num) groups [] while num: groups.append(num % 10000) num // 10000 parts [] read_any False need_zero False for idx in range(len(groups) - 1, -1, -1): g groups[idx] if g 0: if read_any: need_zero True continue g_str group_to_cn(g) if read_any and (need_zero or g 1000): parts.append(零) parts.append(g_str CN_BIG_UNIT[idx]) read_any True need_zero False result sign .join(parts) if result.startswith(一十): result result[1:] return result逻辑说明group_to_cn内部从千位往个位遍历遇到空位时先标记 pending_zero遇到下一个非零数字才真正插入“零”。这样可以避免“一零零一”这种叠零。int_to_cn把数字按 10000 拆组后从高位往低位拼接这里的关键是need_zero标记只要中间出现过整个为零的组并且后面还有非零组就在该组前补一个“零”。g 1000的判断处理的是“万组和亿组存在但当前组千位为空”的场景比如 100050001 读“一亿零五万零一”没有这个条件会输出“一亿五万零一”。参数说明int_to_cn的入参是 int返回值是 str。group_to_cn的入参是 0 到 9999 的整数不直接对外使用。范围上限是 9999 亿超过会抛 ValueError不是为了省事而是避免“兆”后面单位层级混乱。如果你确实要支持到亿亿级可以扩展CN_BIG_UNIT但一般业务场景用不到。2.3 边界值自测清单用这组用例验证你的实现写完函数第一件事不是接业务而是跑一跑边界用例。下面是我在任何一次重构后都会执行的测试表每组都对应一个独立的坑输入期望输出这个用例在测什么0零零的特判10十“一十”的省略很多实现会输出“一十”100000十万组间衔接不能输出“一十万”100050001一亿零五万零一空组补零 组内首位为零100000001一亿零一连续空组只保留一个零101000十万一千组内满四位时不需要补零-101负一百零一负号处理999999999999九千九百九十九亿九千九百九十九万九千九百九十九上限边界单位完整拼接这组用例建议直接保存成一个测试函数每次改代码后跑一遍。我第一次实现时挂在了 100050001 上输出的“一亿五万零一”在语音播报里听起来尤其别扭后来才补上g 1000这个条件。另外注意int_to_cn吃的是 int如果你拿到的是字符串直接int()转换前要确认里面没有逗号或空格这个在第六章会再展开。3. 加上负号和小数自然读法里最容易混在一起的两条规则3.1 小数部分为什么要逐位读而不是继续分组很多人在整数方案跑通之后顺手就把小数部分也用四位分组法处理结果读出来很奇怪0.1234 被读成“零点一千二百三十四”但中文口语里小数部分永远是逐位读的“零点一二三四”才对。小数点的位置是规则的分水岭点之前按整数规则读点之后的每一位数字单独念单位完全不参与。负号的规则相对简单在整数部分前面加“负”字即可。但要注意一个边界-0.5 应该读“负零点五”而 -0 在整数场景下应该读“零”而不是“负零”。所以负数判断要放在整数转换之前不要在整数字符串拼完之后再判断开头字符否则“-0”会被拼成“负零”。还有一个容易被忽略的地方是小数部分的数字“0”不能吞掉。比如 10.10 读作“十点一零”不是“十点一”。如果按整数思维去省略末尾的零语音播报会读出“十点一”和原金额差一位。小数位必须原样逐字输出包括末尾的零。3.2 number_to_cn字符串输入 整数部分复用到这里你会发现输入类型最好是字符串而不是浮点数。float 会把 0.1 变成 0.100000000000000005也会把大数悄悄转成科学计数法。所以我的统一入口直接收字符串先清洗再分发到不同分支。def number_to_cn(text: str) - str: 把十进制数字字符串转成中文自然读法支持负数和小数。 text text.strip() if not text: raise ValueError(输入不能为空) if e in text.lower(): raise ValueError(请传入普通十进制字符串不要使用科学计数法) sign if text.startswith(-): sign 负 text text[1:] if . in text: int_part, _, dec_part text.partition(.) if int_part : int_part 0 int_cn int_to_cn(int(int_part)) if dec_part : return sign int_cn dec_cn .join(CN_DIGITS[int(d)] for d in dec_part) return sign int_cn 点 dec_cn return sign int_to_cn(int(text))逻辑说明函数先对输入字符串做 strip 和科学计数法检查然后把负号从数字部分剥离开避免“-0”进入int_to_cn。有小数点时拆成整数部分和小数部分整数部分复用上一章的int_to_cn小数部分用CN_DIGITS[int(d)]逐位映射。partition(.)比split(.)安全因为split在遇到多个点时行为不够明确而partition只拆第一处。参数说明入参必须是字符串这是有意设计的。如果你手里是 float建议先转成字符串再调用但不要直接str(0.1 0.2)那会得到那串经典的长尾数。更稳妥的做法是保留业务侧的原始字符串或者用 Decimal 承接浮点运算结果。这个函数返回的读法偏向口语适合语音播报和报表展示不适合作为财务金额大写那套规则在下一章单独讲。3.3 自然读法和货币大写在规则上的分水岭如果你以为“数字转中文”只需要一个通用函数那财务同事大概率会让你返工。人民币大写是另一套完全独立的语法数字用壹贰叁肆伍陆柒捌玖单位变成拾佰仟整数部分后必须带“元”角分规则和“整”字的出现条件也都和口语读法无关。举个例子10.10 在口语里读“十点一零”在发票上要写“壹拾元壹角”这里的“壹拾”不能省略成“拾”和口语读法的“十”不一样。我在接手财务相关需求前试图在number_to_cn上加参数去兼容大写最后发现代码里全是 if 分支维护成本很高。正确的做法是拆成两个函数各自维护自己的数字表和单位表只在“四位分组”这个底层逻辑上复用。分组思想是大写金额和小数自然读法唯一能共享的部分其余规则各写各的谁都别想覆盖谁。4. 金额大写财务场景下数字转中文的正确打开方式4.1 大写数字、单位以及“整”字的完整规则人民币大写有几个硬性要求数字必须用壹贰叁肆伍陆柒捌玖元以内的位数单位用拾佰仟整数部分后面接“元”没有角分时在末尾写“整”有角但没分时不写“整”。这些规则在银行票据和发票上有明确约定做财务系统时务必要对齐不能自己发挥。角分位是容易出错的区域。规则是角和分不同时存在时需要写“零”比如 5.05 写作“伍元零伍分”这个零不能省金额在零到一元之间时比如 0.05直接写“伍分”前面不带“零元”0.50 写“伍角”末尾不写“整”。另外整数部分为零且角分为零时要写“零元整”这是很多实现会漏掉的边界。还有一个中文大写特有的习惯大写金额里“10元”必须写“壹拾元”这个“壹”在金额场景不能省。这和口语读法里“十”不带“一”正好相反所以金额大写绝不能直接复用自然读法的字符串必须单独实现。4.2 rmb_upper 实现从字符串到财务大写金额转换的安全做法是先用 Decimal 做两位小数量化再做整数分组。Decimal 的量化能统一处理进位否则浮点运算会让0.1 0.2的尾数混进分位。from decimal import Decimal, ROUND_HALF_UP CN_UPPER 零壹贰叁肆伍陆柒捌玖 CN_SMALL_UNIT [, 拾, 佰, 仟] CN_BIG_UNIT [, 万, 亿, 兆] def _int_upper(num: int) - str: 整数部分转大写规则和自然读法分开实现。 if num 0: return groups [] while num: groups.append(num % 10000) num // 10000 parts [] read_any False need_zero False for idx in range(len(groups) - 1, -1, -1): g groups[idx] if g 0: if read_any: need_zero True continue s pending False for pos in range(3, -1, -1): d g // (10 ** pos) % 10 if d 0: if s: pending True else: if pending: s 零 pending False s CN_UPPER[d] CN_SMALL_UNIT[pos] if read_any and (need_zero or g 1000): parts.append(零) parts.append(s CN_BIG_UNIT[idx]) read_any True need_zero False return .join(parts) def rmb_upper(amount: str) - str: 金额字符串转人民币大写支持负数输入如 100.10。 amount amount.strip() negative False if amount.startswith(-): negative True amount amount[1:] if e in amount.lower(): raise ValueError(金额不要用科学计数法表达) d Decimal(amount).quantize(Decimal(0.01), roundingROUND_HALF_UP) if negative and d 0: d -d if d 0: return 零元整 if d 0: sign 负 d -d else: sign yuan int(d) jiao int((d * 10) % 10) fen int((d * 100) % 10) result sign if yuan 0: result _int_upper(yuan) 元 elif jiao 0 and fen 0: result 零元 if jiao 0 and fen 0: result 整 else: if jiao 0: result CN_UPPER[jiao] 角 elif yuan 0 and fen 0: result 零 if fen 0: result CN_UPPER[fen] 分 return result逻辑说明_int_upper的整体结构和int_to_cn类似但数字表换成大写、单位换成拾佰仟并且不做“一十”省略。rmb_upper在入口处直接拒绝科学计数法用Decimal.quantize把金额统一到两位小数角分从量化后的值里拆出避免出现0.1 0.2 0.30000000000000004这类浮点尾数污染大写结果。d 0提前返回“零元整”负数先取绝对值转换再加“负”前缀。参数说明入参 amount 是字符串这是硬约束不接受 float。如果你在业务层已经拿到 Decimal直接传str(decimal_obj)即可。quantize(Decimal(0.01), roundingROUND_HALF_UP)表示四舍五入到分位这是财务上报销场景最常用的规则如果你的业务要求银行家舍入需要在这里替换 rounding 参数。4.3 几个容易出错的金额样例和输出对照下面这几个样例是我按真实业务用例整理出来的任何一个输出不对都说明规则没吃透输入期望输出说明100.00壹佰元整末尾整无角分10.10壹拾元壹角有角无分不写整5.05伍元零伍分角位为空时补零0.50伍角零元不写0.05伍分同上且不补零0.00零元整全零特判100050001.01壹亿零伍万零壹元零壹分分组补零 金额补零叠加这些用例和自然读法完全不能互通尤其是 5.05 那条在口语读法里是“五点零五”但在大写里是“伍元零伍分”。每次改完rmb_upper我都拿这组例子过一遍比我记规则可靠得多。5. 数字转中文避坑清单五个最常见的翻车点5.1 现象“一亿万”这种重复单位出现在结果里输入 100000000一亿时输出变成了“一亿万”。原因在于分组拼接时个位组为零但循环没有正确跳过导致在“亿”后面又拼了一个“万”单位。解决方法是把“组为零则跳过并标记 need_zero”这个逻辑放在拼接之前确保零组不产生任何单位输出。我在另一个类似的实现中看到有人直接对每一位数字乘单位结果 100000000 会拼出“一亿零万零万零”这类问题只能靠分组处理根治。5.2 现象漏零和叠零是同一类问题的两种表现100050001 被读成“一亿五万一”或者“一亿零零五万零一”都出现过前者是漏补零后者是补零逻辑被重复触发。原因在于把“组内空位”和“组间空组”混为一谈两处都加了补零逻辑又没有统一标记。解决方法是区分两个变量pending_zero只管组内四位间的零need_zero只管跨组的空档组间拼接时两个条件用“或”合并但每组只补一次。这个区分是那段时间我重构最频繁的地方补上之后连续零用例全部通过。5.3 现象金额大写里出现“零元零角零分”的乌龙输入 0.50 时输出“零元零角伍分”而财务规范要求写“伍角”。原因是在零元分支没有区分“整数部分是否为零”就统一补了“零元”。解决方法是只有当整数部分为零且角分也为零时才输出“零元整”否则直接跳过元位从角或分开始写。类似的还有 5.05 误写“伍元零角伍分”需要明白角位为零但分位非零时补“零”字是为隔开元位和分位而不是为了报出“角”这个单位。5.4 现象科学计数法和浮点长尾把输入搞坏输入 1000000000000000.0 时直接int()转换得到科学计数法字符串输出变成“一点零一加十五次方”之类的垃圾读法输入 0.1 0.2 再转字符串则是“零点三零零零零零零零零零零零零零零四”。原因很简单float 在表示超大数和某些小数时精度不足。解决方法是入口统一收字符串并显式拒绝包含 e/E 的输入涉及金额时使用 Decimal 量化。这个坑在语音播报场景尤其致命读出来的内容用户一听就能发现不对。5.5 现象“负零”这种不存在的读法出现在输出里输入 -0 时输出“负零”读出来非常尴尬。原因是对负数做“先取绝对值再拼负号”的流程时没有先判断零。解决方式是在int_to_cn和rmb_upper里都把num 0或d 0的特判放在最前面零不参与符号逻辑。类似地-0.0也应该走“零”而不是“负零”。这类边界在测试用例里很容易被忽略我建议把 -0 和 -0.00 写进自测清单。6. 进阶统一输入边界并固化自测用例6.1 统一入口让所有输入先走字符串清洗实际项目里你拿到的输入不会像文档里那么干净可能带千分位逗号、首尾空格、人民币符号甚至全角数字。我习惯在入口加一个清洗函数去掉首尾空白、剥离逗号、把全角数字映射成半角。以下是一个极简版本def clean_number_input(text: str) - str: text text.strip().replace(,, ).replace(, ) table str.maketrans(, 0123456789.-) return text.translate(table)清洗之后再交给number_to_cn或rmb_upper可以省掉大量调用方的重复处理。注意清洗函数不要顺手把空格从数字中间删掉因为正常数字字符串中间不该有空格如果有说明输入本身就是脏数据应该让上层感知到而不要静默修正。6.2 自测用例表把这些数字一次性跑全自测是这类工具函数最值得投入的部分。我通常把自然读法和金额大写各维护一组用例存成字典形式让 pytest 或 unittest 循环比对。下面是自然读法的核心用例金额用例用 4.3 节那张表即可输入字符串期望输出0零10十15十五100000十万100050001一亿零五万零一100000001一亿零一101000十万一千0.5零点五100.10一百点一零-3.14负三点一四-0.00负零点零零把所有用例放进一个列表后改一次代码跑一次比手动输入演示强太多。工具函数最大的风险不是逻辑复杂而是改了一处以为没影响结果另一个角落的规则被连带破坏。固化用例相当于给行为上了保险。6.3 我留下的习惯现在只要涉及数字转中文我会先把输入层统一成字符串再把自然读法、金额大写拆成两个独立模块最后把边界用例写成一个 fixture 文件放在测试目录里。第一版直接吃 float 的教训让我记住了精度问题不是靠 round 能解决的必须在输入边界就堵死。如果你也在维护这类函数建议先看看自己手里有多少个“临时补丁”是因为输入类型混乱而打的那才是真正该重构的地方。希望帮到你。本文还有配套的精品资源点击获取