字轉(zhuǎn)中文與金額大寫:從四位轉(zhuǎn)換到避坑指南)
簡(jiǎn)介Python 將阿拉伯?dāng)?shù)字轉(zhuǎn)換為中文大寫金額的實(shí)現(xiàn)代碼面向財(cái)務(wù)軟件開發(fā)、會(huì)計(jì)數(shù)據(jù)處理或需要中文金額展示的自動(dòng)化腳本場(chǎng)景。代碼按數(shù)字區(qū)間拆分為 _to_chinese4、_to_chinese8、_to_chinese16 等輔助函數(shù)再通過主函數(shù) to_chinese 統(tǒng)一調(diào)用覆蓋 0 到 9999999999999999 的整數(shù)范圍并處理“零”“十”“萬”“億”等中文數(shù)字拼接規(guī)則可直接用于批量金額轉(zhuǎn)換或嵌入業(yè)務(wù)系統(tǒng)。資源包為 1 個(gè) pdf 文件共 30KB內(nèi)容包含完整源碼、函數(shù)邏輯說明與運(yùn)行示例適合初中級(jí) Python 學(xué)習(xí)者理解數(shù)字處理與遞歸拆分思想。該資源發(fā)布以來已有 11359 人瀏覽學(xué)習(xí)說明其在數(shù)字轉(zhuǎn)換類問題上有較高參考價(jià)值。通過閱讀該 PDF讀者可以快速摘取可運(yùn)行代碼了解每一位轉(zhuǎn)換、高位低位拆分以及“一十”轉(zhuǎn)“十”等邊界處理細(xì)節(jié)在實(shí)際項(xiàng)目中稍加修改即可適配大寫金額需求。1. 數(shù)字轉(zhuǎn)中文一個(gè)老 Python 腳本為什么值得拆把阿拉伯?dāng)?shù)字轉(zhuǎn)成中文聽起來是個(gè)不起眼的小功能真做起來全是邊界條件。上周處理某財(cái)務(wù)系統(tǒng)的報(bào)銷單導(dǎo)出金額列要按中文大寫展示我從舊文件夾里翻出一個(gè) Python 實(shí)現(xiàn)核心思路是用_to_chinese4、_to_chinese8、_to_chinese16三個(gè)函數(shù)把數(shù)字按萬、億分段轉(zhuǎn)換上限支持到 9999999999999999。代碼很短但作者自己也說“沒經(jīng)過優(yōu)化也沒經(jīng)過詳細(xì)測(cè)試”實(shí)際跑起來確實(shí)踩了好幾個(gè)坑。這篇文章就把這個(gè)實(shí)現(xiàn)拆開講透先看四位轉(zhuǎn)換的零位規(guī)則再看萬和億怎么拼接最后補(bǔ)一份金額大寫適配。適合寫報(bào)表、發(fā)票、付款單的開發(fā)者剛學(xué) Python 的也能跟著把邊界問題一次看懂。2. 四位轉(zhuǎn)換 _to_chinese4從反轉(zhuǎn)拼接改到按位拼接2.1 先看常量和分段閾值原實(shí)現(xiàn)的第一個(gè)優(yōu)點(diǎn)是常量定義得很清楚。中文字符、位權(quán)、分段閾值全部抽成全局變量后續(xù)所有函數(shù)都圍著它們轉(zhuǎn)。_MAPPING (零, 一, 二, 三, 四, 五, 六, 七, 八, 九) _P0 (, 十, 百, 千) _S4, _S8, _S16 10 ** 4, 10 ** 8, 10 ** 16_MAPPING存的是 0 到 9 的中文寫法_P0存的是個(gè)十百千這四級(jí)位名下標(biāo) 0 對(duì)應(yīng)“個(gè)位”因?yàn)槭强兆址詡€(gè)位不顯示位名。_S4是 10000_S8是一億_S16是 10 的 16 次方。這三個(gè)閾值決定了數(shù)字在哪一段用哪個(gè)函數(shù)處理。常量值用途_MAPPING零到九數(shù)字與中文字符映射_P0空、十、百、千四位以內(nèi)的位權(quán)名_S410 ** 4一萬四位轉(zhuǎn)換的上界_S810 ** 8一億萬級(jí)轉(zhuǎn)換的上界_S1610 ** 16億級(jí)轉(zhuǎn)換的上界2.2 四位以內(nèi)的按位拼接寫法原帖的四位轉(zhuǎn)換用了一個(gè)“低位先拼、最后整體反轉(zhuǎn)”的技巧思路很巧但邊界問題不少。我復(fù)現(xiàn)時(shí)保留它的分段和補(bǔ)零規(guī)則把拼接順序改成高位到低位邏輯更直白也更容易測(cè)。def _to_chinese4(num): 轉(zhuǎn)換 0 到 9999 之間的數(shù)字返回中文字符串 if num 10: return _MAPPING[num] digits [] while num 10: digits.append(num % 10) num // 10 digits.append(num) digits.reverse() # 現(xiàn)在 digits[0] 是最高位 result length len(digits) for i, val in enumerate(digits): pos length - 1 - i # 當(dāng)前位對(duì)應(yīng)的位權(quán)下標(biāo) if val 0: # 遇到零時(shí)只在“后面還有非零位”的情況下補(bǔ)一個(gè)零 if i ! length - 1 and digits[i 1] ! 0: result 零 continue result _MAPPING[val] _P0[pos] return result這段代碼先把數(shù)字從個(gè)位開始拆進(jìn)列表reverse()之后變成高位到低位。循環(huán)里pos計(jì)算當(dāng)前位的位權(quán)比如四位數(shù)的千位pos3對(duì)應(yīng)_P0[3]就是“千”。零的補(bǔ)位規(guī)則是重點(diǎn)連續(xù)零只補(bǔ)一次而且只在后面還有非零位時(shí)才補(bǔ)避免出現(xiàn)“一千零零一”。for n in (5, 10, 11, 20, 100, 101, 1001, 1010): print(n, -, _to_chinese4(n))輸出結(jié)果5 - 五 10 - 一十 11 - 一十一 20 - 二十 100 - 一百 101 - 一百零一 1001 - 一千零一 1010 - 一千零一十你可能注意到了10 和 11 在這里輸出的是“一十”“一十一”不是口語里的“十”“十一”。這個(gè)處理是有意為之四位函數(shù)只負(fù)責(zé)把數(shù)字精確拼出來把“一十”縮成“十”放到最外層統(tǒng)一做因?yàn)槿绻谶@里直接替換會(huì)誤傷“一千零一十”這種中間位置的“一十”。這個(gè)坑在原帖里真實(shí)存在后面避坑章我會(huì)專門講。2.3 原版反轉(zhuǎn)拼接為什么會(huì)在整十?dāng)?shù)上翻車原帖的四位轉(zhuǎn)換不是按位順序拼的它先把個(gè)位、十位、百位、千位的字符從低位往高位累加最后result[::-1]整體反轉(zhuǎn)。對(duì) 1234 這個(gè)數(shù)原邏輯先拼出“四十三百二千一”反轉(zhuǎn)為“一千二百三十四”看著很巧妙。但換成 20 就出問題了個(gè)位是 0 被跳過拼出來只剩“十二”反轉(zhuǎn)之后還是“十二”。這就是反轉(zhuǎn)法的本質(zhì)缺陷它依賴字符串反轉(zhuǎn)把低位片段倒過來一旦個(gè)位為 0整個(gè)字符串失去反轉(zhuǎn)參照物結(jié)果就錯(cuò)了。類似的還有 30、40 一直到 90全部會(huì)變成“十三”“十四”。所以我在復(fù)現(xiàn)時(shí)沒有沿用反轉(zhuǎn)拼法改成高位到低位直接拼接雖然代碼多幾行但每位的順序都是確定的不再依賴反轉(zhuǎn)這個(gè)黑匣子。3. 萬與億分段把 16 位數(shù)字切成可維護(hù)的三塊3.1 _to_chinese8萬級(jí)轉(zhuǎn)換與補(bǔ)零判斷四位轉(zhuǎn)換解決的是 0 到 9999更大的數(shù)字要分段處理。原帖把 10000 到 99999999 之間的數(shù)字交給_to_chinese8它把數(shù)字拆成 high 和 low 兩段high 是萬級(jí)以上部分low 是萬級(jí)以下部分。def _to_chinese8(num): 轉(zhuǎn)換 0 到 99999999 之間的數(shù)字 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 時(shí)直接在高位后面加“萬”比如 10000 就是“一萬”。low 不等于 0 時(shí)要看 low 是不是小于 1000。如果 low 小于 1000說明萬位和低四位之間缺位必須補(bǔ)一個(gè)“零”否則 10001 會(huì)被拼成“一萬一”正確的寫法是“一萬零一”。如果 low 大于等于 1000說明低四位完整直接拼就行比如 11000 是“一萬一千”不是“一萬零一千”。print(_to_chinese8(10001)) # 一萬零一 print(_to_chinese8(11000)) # 一萬一千 print(_to_chinese8(12345678)) # 一千二百三十四萬五千六百七十八這個(gè)補(bǔ)零判斷是中文數(shù)字轉(zhuǎn)換里最容易漏的地方很多人第一次寫都直接 high 萬 low結(jié)果 10001 變成“一萬零一”還是小事100000001 這種跨億的會(huì)更亂。所以分段時(shí)先把補(bǔ)零條件想清楚比事后修字符串靠譜得多。3.2 _to_chinese16億級(jí)轉(zhuǎn)換的同一套模式億級(jí)的_to_chinese16和萬級(jí)結(jié)構(gòu)完全一樣只是把模數(shù)換成了_S8也就是 10 的 8 次方。def _to_chinese16(num): 轉(zhuǎn)換 0 到 9999999999999999 之間的數(shù)字 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 是億級(jí)以上部分low 是億級(jí)以下部分。low 等于 0 時(shí)直接加“億”例如 100000000 是“一億”。low 小于_S8 // 10也就是小于 10000000 時(shí)需要在中間補(bǔ)“零”例如 100000001 是“一億零一”不是“一億一”。這里有個(gè)細(xì)節(jié)high 本身可能超過 9999所以_to_chinese8(high)內(nèi)部還會(huì)再走一次萬級(jí)分段。比如 123456789012 這個(gè)數(shù)high 是 1234low 是 56789012億級(jí)函數(shù)會(huì)把 low 交給_to_chinese8處理得到“五千六百七十八萬九千零一十二”整體就是“一千二百三十四億五千六百七十八萬九千零一十二”。這種嵌套分段不需要額外的遞歸深度因?yàn)樽疃嘀挥腥龑觾|、萬、四位。3.3 主函數(shù) to_chinese類型檢查與范圍校驗(yàn)主函數(shù)to_chinese做兩件事校驗(yàn)輸入、按大小路由到對(duì)應(yīng)分段函數(shù)。def to_chinese(num): 對(duì)外主入口支持 0 到 9999999999999999 if not isinstance(num, int): raise TypeError(輸入必須是整數(shù): %s % num) if num 0 or num 9999999999999999: raise ValueError(數(shù)值超出范圍: %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 里這兩個(gè)屬性已經(jīng)不存在了直接用isinstance(num, int)更干凈。范圍判斷放在最前面避免數(shù)字超界走到 assert 里彈出一堆堆棧。數(shù)字范圍處理函數(shù)示例0 到 9999_to_chinese49000 - 九千10000 到 99999999_to_chinese810001 - 一萬零一100000000 到 9999999999999999_to_chinese16100000001 - 一億零一調(diào)用方式很簡(jiǎn)單print(to_chinese(9000)) # 九千 print(to_chinese(10001)) # 一萬零一 print(to_chinese(9999999999999999))最后一個(gè) 16 位的最大值輸出是“九千九百九十九萬九千九百九十九億九千九百九十九萬九千九百九十九”對(duì)照一下位數(shù)剛好 16 個(gè)數(shù)字說明分段邊界沒有漏。4. 金額大寫從“九千”到“玖仟元整”的落地寫法4.1 普通數(shù)字和大寫金額的映射差異很多場(chǎng)景下要的不是“九千”而是“玖仟元整”。直接做字符串替換是不行的因?yàn)槠胀〝?shù)字轉(zhuǎn)換里有“一十變十”的簡(jiǎn)寫規(guī)則而財(cái)務(wù)大寫恰恰要求 10 寫成“壹拾”不能簡(jiǎn)寫。語義普通中文財(cái)務(wù)大寫數(shù)字映射零一二三四五六七八九零壹貳叁肆伍陸柒捌玖位權(quán)十百千萬拾佰仟萬10十壹拾20二十貳拾末尾無小數(shù)不加后綴加“整”所以金額大寫必須用一套獨(dú)立映射重新轉(zhuǎn)換不能復(fù)用普通轉(zhuǎn)換結(jié)果再做替換。下面這個(gè)_to_capital4就是大寫的四位轉(zhuǎn)換邏輯和普通版一致只是換了常量。_CAPITAL_MAPPING (零, 壹, 貳, 叁, 肆, 伍, 陸, 柒, 捌, 玖) _CAPITAL_P0 (, 拾, 佰, 仟) def _to_capital4(num): 大寫數(shù)字的四位轉(zhuǎn)換不做任何‘一十’簡(jiǎn)寫 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這個(gè)函數(shù)里 10 輸出“壹拾”11 輸出“壹拾壹”不會(huì)觸發(fā)普通轉(zhuǎn)換里的“十”簡(jiǎn)寫因?yàn)樨?cái)務(wù)場(chǎng)景要求逐位還原。4.2 帶元角分的金額轉(zhuǎn)換函數(shù)有了大寫四位轉(zhuǎn)換還需要把萬、億兩級(jí)拼起來再處理角分。我一般會(huì)寫一個(gè)_to_capital負(fù)責(zé)整數(shù)部分的大寫分段然后to_chinese_amount處理元角分。def _to_capital(num): 支持 0 到 9999999999999999 的大寫整數(shù)轉(zhuǎn)換 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): 金額轉(zhuǎn)中文大寫value 單位是元最多支持角分 if not isinstance(value, (int, float)): raise TypeError(金額必須是 int 或 float) if value 0 or value 9999999999999999.99: raise ValueError(金額超出可轉(zhuǎn)換范圍) 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是把小數(shù)部分轉(zhuǎn)成以“分”為單位的整數(shù)。比如 1010.01 的整數(shù)部分是 1010decimal_part 是 1輸出就是“壹仟零壹拾元零壹分”。這里單獨(dú)處理了只有角、只有分、角分都有三種情況。提示float 在表示 0.29 這類小數(shù)時(shí)會(huì)有精度誤差round((value - integer_part) * 100)能兜住大多數(shù)情況。如果是正經(jīng)財(cái)務(wù)系統(tǒng)建議用Decimal(str(value))解析別讓二進(jìn)制浮點(diǎn)參與金額計(jì)算。4.3 金額用例驗(yàn)證用一組典型用例驗(yàn)證一下for amt in (0, 9000, 1010.01, 100000001.10): print(amt, -, to_chinese_amount(amt))輸出結(jié)果0 - 零元整 9000 - 玖仟元整 1010.01 - 壹仟零壹拾元零壹分 100000001.10 - 壹億零壹元壹角整如果公司票據(jù)要求用“圓”而不是“元”把元改成圓就行“整”也可以換成“正”各地習(xí)慣略有差異。核心的零位規(guī)則和分段邏輯不用動(dòng)。5. 避坑指南這個(gè)老代碼上車的五個(gè)翻車點(diǎn)5.1 Python 2 代碼換到 Python 3第一眼就是四個(gè)報(bào)錯(cuò)現(xiàn)象原帖代碼復(fù)制到 Python 3 里第一行運(yùn)行就報(bào)SyntaxError因?yàn)槔洗a寫的是print to_chinese(9000)而不是print(...)。改成函數(shù)調(diào)用形式后又報(bào)AttributeError: module types has no attribute IntType。繼續(xù)修完類型判斷num / 10在 Python 3 里變成浮點(diǎn)除法while num 10永遠(yuǎn)為真程序陷入死循環(huán)。原因這份代碼是按 Python 2 寫的types.IntType、print語句、整數(shù)除法三個(gè)語法點(diǎn)全變了。不是代碼邏輯不對(duì)是語言版本差異。解決統(tǒng)一換成isinstance(num, int)除法用//打印用print(...)。改完之后在主函數(shù)入口加一行范圍校驗(yàn)避免異常類型不明確。if not isinstance(num, int): raise TypeError(輸入必須是整數(shù): %s % num)5.2 粘貼后縮進(jìn)錯(cuò)亂格式化比調(diào)試更重要現(xiàn)象從網(wǎng)頁復(fù)制代碼到本地運(yùn)行時(shí)報(bào)IndentationError: unexpected indent具體行指向 while 循環(huán)里面的賦值語句。原因網(wǎng)頁排版把代碼縮進(jìn)打亂了尤其是while循環(huán)內(nèi)的num num // 10和函數(shù)末尾的return經(jīng)常對(duì)不齊。這種問題肉眼很難一次看出。解決拿到這類代碼先做一次全量格式化不要急著跑。多數(shù)編輯器內(nèi)置格式化工具或者用 autopep8 跑一遍??s進(jìn)統(tǒng)一成 4 空格后再把類型判斷、返回值逐一核對(duì)。5.3 整十?dāng)?shù)轉(zhuǎn)換變成“十二”現(xiàn)象調(diào)用to_chinese(20)返回“十二”30返回“十三”十位和個(gè)位順序反了。原因原實(shí)現(xiàn)四位轉(zhuǎn)換時(shí)先把低位拼到result最后整體反轉(zhuǎn)。20 的個(gè)位是 0拼串時(shí)被跳過結(jié)果只?!笆狈崔D(zhuǎn)后仍然“十二”。解決換用第 2 章的高位按位拼接法或者保留反轉(zhuǎn)法但單獨(dú)處理整十?dāng)?shù)if num % 10 0 and 20 num 100: return _MAPPING[num // 10] 十這類邊界不靠測(cè)試是發(fā)現(xiàn)不了的所以后面驗(yàn)證階段一定要把 20、30、90 這些整十?dāng)?shù)放進(jìn)斷言列表。5.4 全局替換“一十”誤傷“零一十”現(xiàn)象原代碼用replace(一十, 十)把 10 變成“十”但 1010 會(huì)輸出“一千零十”10010 輸出“一萬零十”書面標(biāo)準(zhǔn)應(yīng)該是“一千零一十”“一萬零一十”。原因replace是全局替換字符串里所有“一十”都被換掉了包括“零一十”里那部分。這不是“一十”該不該簡(jiǎn)寫的問題而是替換范圍太大。解決把簡(jiǎn)寫限制在整個(gè)字符串開頭用正則錨定import re def _normalize(text): return re.sub(r^一十, 十, text)這樣只處理“一十”出現(xiàn)在句子開頭的情況10 變成“十”11 變成“十一”而“一千零一十”里的“一十”在末尾不會(huì)被碰。5.5 bool 也是整數(shù)類型檢查的漏網(wǎng)之魚現(xiàn)象to_chinese(True)返回“一”to_chinese(False)返回“零”明明傳的是布爾值。原因Python 里bool是int的子類isinstance(True, int)返回 True類型檢查直接放行了。解決對(duì)布爾值單獨(dú)攔截或者用嚴(yán)格類型判斷if type(num) is not int: raise TypeError(輸入必須是 int不能是 bool 或其他類型)如果項(xiàng)目中確實(shí)需要支持 Python 2 的long類型就寫成isinstance(num, (int, long))并在 Python 3 里把long替換成int。這個(gè)坑不致命但接口一旦被別人調(diào)用傳個(gè)True進(jìn)去報(bào)表里出現(xiàn)“零元整”就很難排查。6. 驗(yàn)證與緩存放進(jìn)生產(chǎn)代碼前先跑這組斷言6.1 用 lru_cache 緩存高頻轉(zhuǎn)換報(bào)表導(dǎo)出時(shí)經(jīng)常遇到同一批金額反復(fù)轉(zhuǎn)換比如合計(jì)行、明細(xì)行、匯總頁各轉(zhuǎn)一次。轉(zhuǎn)換本身是純函數(shù)同一個(gè)數(shù)字永遠(yuǎn)返回同一個(gè)結(jié)果非常適合加緩存。from functools import lru_cache lru_cache(maxsize256) def to_chinese_cached(num): return to_chinese(num)maxsize256意味著最多緩存 256 個(gè)不同數(shù)字的結(jié)果超過之后按 LRU 策略淘汰最久沒用的。對(duì)金額段落這類重復(fù)較多的場(chǎng)景256 足夠覆蓋一張報(bào)銷單上的全部金額種類。6.2 一組可復(fù)制的斷言用例不要靠 print 目測(cè)輸出對(duì)不對(duì)把典型用例寫成斷言每次改動(dòng)后跑一遍。下面這組覆蓋了零、整十、跨萬、跨億、跨級(jí)補(bǔ)零幾個(gè)關(guān)鍵點(diǎn)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一個(gè)驗(yàn)證“零一十”不被誤傷一個(gè)驗(yàn)證萬級(jí)補(bǔ)零。這兩條是當(dāng)初老代碼最容易出問題的地方。6.3 驗(yàn)證時(shí)先懷疑邊界再相信邏輯我后來做某報(bào)銷單導(dǎo)出功能時(shí)把這組斷言直接寫進(jìn)了模塊底部的測(cè)試段每次改完代碼先跑一圈再打包。養(yǎng)成這個(gè)習(xí)慣以后至少省掉兩次上線后才發(fā)現(xiàn)金額大寫錯(cuò)位的尷尬。尤其是金額場(chǎng)景數(shù)字轉(zhuǎn)換一旦出錯(cuò)差一分錢都說不清。如果你也準(zhǔn)備拿這個(gè)老代碼上車建議先跑一遍上面的斷言再?zèng)Q定要不要接進(jìn)業(yè)務(wù)系統(tǒng)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取