最初,我是在开发聊天机器人的时候用到这个功能,比如用户提问 一千米以内有哪些场地可用?,我需要在数据库中查询范围小于一千米的场地,SQL 语句大致为 WHEN distant<1000,但我只能在原语句中提取到 一千 这个词语。数据库的判断条件是无法识别中文数字的,这时就先需要转化一下了。

当时我搜索一些资料,看到有一些零散的代码,并没有找到合适的开源库,于是自己动手实现了一个非常粗糙的转化函数,粗糙到连最基本的异常判断都没有。后来我发现这个功能在自然语言处理领域还挺常用的,所以就打算把它抽象出来,方便自己和他人复用。我抽了一些空闲的时间,完善了部分功能后把它开源了。当时并没有想到会有很多人用,我也知道里面有很多 bug,但懒得修。直到我逐渐发现有人在不同的任务中使用它,比如爬虫、自然语言转SQL、聊天机器人、语音识别等,所以后面就很积极的在更新了。

1 任务简介

这个任务的核心就是「中文数字」和「阿拉伯数字」相互转化,这两种数字的描述方式非常规则,比如 一百二十三 和 123。如果是标准格式的数据,转化算法也许并不难,而真正困难的是如何在数据不规范的情况下做好转化,比如三万五,我们一听就知道要转化成 35000,而机器却可能理解为 30005。那我们今天要讲如何将不规则数据转化吗?不!今天这篇文章我们主要讨论的还是标准情况下的数字转化。

我:在啃骨头之前,先把大块的肉撕下来!

一般来说,我们常见的数字的最大值是 千万亿,即 10**16,写出来大概这么长:10000000000000000,极少有数字会超过这个,因此我们也把算法的输入范围定为 0 - 10**16,下面我们来看如何用算法转化这么长的数字的。

2 核心算法

我们要将核心算法是指剔除了一些无关代码,大致包括数据预处理、异常判断、数据后处理等功能,是只针对数字转化的算法描述,毕竟复杂的细枝末节不是我们今天要讨论的内容。

2.1 正向遍历法

首先,我们以 一百二十三 作为第一个要转化的例子,你大概会说,这个用小学学过的知识就可以做到,的确如此!先把中文数字和单位做个映射,然后正向遍历,用数字乘以单位,然后直接把他们累加起来就搞定了。

一百二十三 的解析式为 1*100 + 2*10 + 3,代码如下:

# 数字映射
number_map = {"零": 0,"一": 1,"二": 2,"三": 3,"四": 4,"五": 5,"六": 6,"七": 7,"八": 8,"九": 9
}# 单位映射
unit_map = {"十": 10,"百": 100,"千": 1000,"万": 10000,"亿": 100000000
}# 正向遍历 1
def forward_cn2an_one(inputs):output = 0unit = 1num = 0for index, cn_num in enumerate(inputs):if cn_num in number_map:# 数字num = number_map[cn_num]# 最后的个位数字if index == len(inputs) - 1:output = output + numelif cn_num in unit_map:# 单位unit = unit_map[cn_num]# 累加output = output + num * unitnum = 0else:raise ValueError(f"{cn_num} 不在转化范围内")return outputoutput = forward_cn2an_one("一百二十三")
# output: 123

是不是如你想象的那么简单,但这种基础算法到十万以上就不行了。比如 一十二万(通常情况下,你会更习惯说 十二万,但那个是口语说法),用上述算法就会得到 20010,而不是 120000

output = forward_cn2an_one("一十二万")
# output: 20010

仔细想想问题出在了哪里。没错!就出在万位那里,由于算法是单位乘以数字然后直接累加,因此一十二万被解析成了 1*10 + 2*10000,而不是 (1*10 + 2)*10000

问题找到了,那解决起来还是比较简单的。我们直接在万位那里做个判断,即当识别到万的时候,先把前面所有数字加起来,再乘以单位万。

# 正向遍历 2
def forward_cn2an_two(inputs):output = 0unit = 1num = 0for index, cn_num in enumerate(inputs):if cn_num in number_map:# 数字num = number_map[cn_num]# 最后的个位数字if index == len(inputs) - 1:output = output + numelif cn_num in unit_map:# 单位unit = unit_map[cn_num]# 判断出万、亿,先把前面的累加再乘以单位万、亿if unit % 10000 == 0:output = (output + num) * unitelse:# 累加output = output + num * unitnum = 0else:raise ValueError(f"{cn_num} 不在转化范围内")return outputoutput = forward_cn2an_two("一十二万")
# output: 120000

这下在万上的问题似乎解决了,那这个方法在亿上能不能运行呢?我们用 一亿二千三百四十五万六千七百八十一 来测试,得到了 1000023456781,又错了!

output = forward_cn2an_two("一亿二千三百四十五万六千七百八十一")
# output: 1000023456781

别急!我们来分析一下,这是 一亿二千三百四十五万六千七百八十一 的解析式: (1*100000000 + 2*1000 + 3*100 + 4*10 + 5)*10000 + 6*1000 + 7*100 + 8*10 + 1,从解析式中可以看到,亿位上的数字被错误得乘以了万,这种情况也需要做处理。

# 正向遍历 3
def forward_cn2an_three(inputs):output = 0unit = 1num = 0# 亿位以上的输出hundred_million_output = 0for index, cn_num in enumerate(inputs):if cn_num in number_map:# 数字num = number_map[cn_num]# 最后的个位数字if index == len(inputs) - 1:# 把亿位和中间输出以及个位上的一起加起来output = hundred_million_output + output + numelif cn_num in unit_map:# 单位unit = unit_map[cn_num]# 判断出万,前面的累加再乘以单位万if unit == 10000:output = (output + num) * unit# 判断出亿,前面累加乘以亿后赋值给 hundred_million_output, output 重置为 0elif unit == 100000000:hundred_million_output = (output + num) * unitoutput = 0else:# 累加output = output + num * unitnum =0else:raise ValueError(f"{cn_num} 不在转化范围内")return outputoutput = forward_cn2an_three("一亿二千三百四十五万六千七百八十一")
# output: 123456781

仔细阅读上文代码,我增加了一个新的变量 hundred_million_output,用于存储亿位以上的数字,等所有计算完成时,再把它和后面的数字相加即可得到正确结果,解析式为:1*100000000 + (2*1000 + 3*100 + 4*10 + 5)*10000 + 6*1000 + 7*100 + 8*10 + 1

同样的,我们用一个几乎接近上限的数字 一千二百三十四万五千六百七十八亿一千二百三十四万五千六百七十八 来测试,结果也是正确的。

output = forward_cn2an_three("一千二百三十四万五千六百七十八亿一千二百三十四万五千六百七十八")
# output: 1234567812345678

目前为止,千万亿规模的数字上,我们的算法似乎已经可以很好的转化了!

2.2 反向遍历法

不知道你看完上面的算法是不是有点膈应,先用变量存储再相加的方式似乎不是那么优雅。有没有更好方法的解决它呢?

雷军:有人说我写的代码,像诗一样优雅。

咦~有了!我们可以使用反向(倒序)遍历,虽然还是数字乘以单位,但我们是不断的和更大的数字累加,只要处理好单位问题,就不会出现需要把部分值先暂存,然后再加到一起的方式,从而节省一个变量的空间!

# 反向遍历 1
def backward_cn2an_one(inputs):output = 0unit = 1num = 0for index, cn_num in enumerate(reversed(inputs)):if cn_num in number_map:# 数字num = number_map[cn_num]# 累加output = output + num * unitelif cn_num in unit_map:# 单位unit = unit_map[cn_num]else:raise ValueError(f"{cn_num} 不在转化范围内")return outputoutput = backward_cn2an_one("一百二十三")
# output: 123

上文是反向遍历的最基本实现,但这种算法依然处理不了十万以上的数字,主要是因为单位问题。

output = backward_cn2an_one("一十二万")
# output: 20010

一十二万 被解析成了 2*10000 + 1*10,仔细看和正向遍历得到的解析式 1*10 + 2*10000 的不同,1*10 中的 10 本应该是十万位。我们需要在遍历到万位时,记录当前的单位是万,后面的单位再乘以万就搞定了!代码如下:

# 反向遍历 2
def backward_cn2an_two(inputs):output = 0unit = 1# 万、亿的单位ten_thousand_unit = 1num = 0for index, cn_num in enumerate(reversed(inputs)):if cn_num in number_map:# 数字num = number_map[cn_num]# 累加output = output + num * unitelif cn_num in unit_map:# 单位unit = unit_map[cn_num]# 判断出万、亿if unit % 10000 == 0:ten_thousand_unit = unitif unit < ten_thousand_unit:unit = ten_thousand_unit * unitelse:raise ValueError(f"{cn_num} 不在转化范围内")return outputoutput = backward_cn2an_two("一十二万")
# output: 120000

当然除了万位以上,亿位以上时我们也这样处理。

output = backward_cn2an_two("一亿二千三百四十五万六千七百八十一")
# output: 123456781

亿以上数字测试通过!反向遍历就真这么简单?!不,还有更大的数字没有测试。

output = backward_cn2an_two("一千二百三十四万五千六百七十八亿一千二百三十四万五千六百七十八")
# output: 567824685678

凉了,输出错误...这么优雅的代码哪里出问题了呢?

我们来看下解析式:8 + 7*10 + 6*100 + 5*1000 + 4*10000 + 3*100000 + 2*1000000 + 1*10000000 + 8*100000000 + 7*1000000000 + 6*10000000000 + 5*100000000000 + 4*10000 + 3*100000 + 2*1000000 + 1*10000000,这么长...估计你已经看晕了。现在仔细看解析式的最后四个单位 4*10000 + 3*100000 + 2*1000000 + 1*10000000,它们都是万亿之后的值,本应该是最大最长的四个,也就是说代码中没有判断出这里的万应该是万亿!

我们需要让代码在判断出万位的时候,朝前面单位看一下,如果有一个比它还大的单位(即亿)时,就把它们乘起来。

# 反向遍历 3
def backward_cn2an_three(inputs):output = 0unit = 1# 万、亿的单位ten_thousand_unit = 1num = 0for index, cn_num in enumerate(reversed(inputs)):if cn_num in number_map:# 数字num = number_map[cn_num]# 累加output = output + num * unitelif cn_num in unit_map:# 单位unit = unit_map[cn_num]# 判断出万、亿if unit % 10000 == 0:# 万、亿if unit > ten_thousand_unit:ten_thousand_unit = unit# 万亿else:ten_thousand_unit = unit * ten_thousand_unitunit = ten_thousand_unitif unit < ten_thousand_unit:unit = ten_thousand_unit * unitelse:raise ValueError(f"{cn_num} 不在转化范围内")return outputoutput = backward_cn2an_three("一千二百三十四万五千六百七十八亿一千二百三十四万五千六百七十八")
# output: 1234567812345678

至此,我们的反向遍历算法也完成了,本来想省掉一个变量,似乎最后没有做到...

如你所见,这两个算法的时间复杂度都是 O(n),空间复杂度都是 O(1),本质上并没有什么优劣,但我依然感觉反向遍历在思维上流畅很多!因此我在 cn2an 这个库中用的就是反向遍历法啦!

另外,这里有上文的所有代码。

3 模式匹配

如果你给正确的算法丢进一个错误的输入,那么你大概率得到也是一个错误的输出!

郭冬临:用谎言验证谎言,得到的一定是谎言!

比如我们把 十七十八 输入上述两个算法的最终版本,你可能期望的结果是 1718,而实际上会是 78,具体原因我就不细说了,你可以自己思考一下。

output = forward_cn2an_three("十七十八")
# output: 78output = backward_cn2an_three("十七十八")
# output: 78

因此我们必须要限制输入,所有格式不正确的输入都要排除在外。俗话说的好:人生苦短,我用正则!,我们现在就来写个正则表达式。

先给大家看我写的第一版有问题的模式:

import renums = "零一二三四五六七八九"
nums_ten = "零一二三四五六七八九十"
units = "十百千万亿"
pattern = re.compile(f"(([{nums_ten}]+[{units}]+)+零?[{nums}]|([{nums_ten}]+[{units}]+)+|十[{nums}]?|[{nums}])$")result = pattern.search("十七十八")
if result:print(result.group())
# 十七十八

这个正则看似很简单,实则问题非常大。我的思路是匹配带零的数字(比如一千零二十三)、不带零的数字(比如一百二十三)、十几(比如十一)和几(比如一)的任意一种,但它会把很多有问题的模式匹配进来,比如刚刚的 十七十八 这种,反正问题很多!

后来这个问题一直有人向我提,虽然我知道这不是算法的问题,但却是这个库需要解决的问题。有一天晚上,我苦苦想了好几个小时,尝试很多方法,最后想到一个笨方法——枚举所有需要匹配的数字!

import re_0 = "[零]"
_1_9 = f"[一二三四五六七八九]"
_10_99 = f"{_1_9}?[十]{_1_9}?"
_1_99 = f"({_10_99}|{_1_9})"
_100_999 = f"({_1_9}[百]([零]{_1_9})?|{_1_9}[百]{_10_99})"
_1_999 = f"({_100_999}|{_1_99})"
_1000_9999 = f"({_1_9}[千]([零]{_1_99})?|{_1_9}[千]{_100_999})"
_1_9999 = f"({_1000_9999}|{_1_999})"
_10000_99999999 = f"({_1_9999}[万]([零]{_1_999})?|{_1_9999}[万]{_1000_9999})"
_1_99999999 = f"({_10000_99999999}|{_1_9999})"
_100000000_9999999999999999 = f"({_1_99999999}[亿]([零]{_1_99999999})?|{_1_99999999}[亿]{_10000_99999999})"
_1_9999999999999999 = f"({_100000000_9999999999999999}|{_1_99999999})"
pattern = re.compile(f"^({_0}|{_1_9999999999999999})$")result = pattern.search("十七十八")
if result:print(result.group())
# 输出为空,因为没有匹配到

看到上面一层又一层的嵌套,我想你应该猜到这个正则表达式会特别长,居然达到了 5323 个字符...令我惊讶的是,它匹配起来依然很快,对性能来说几乎没有影响。

目前为止,我依然是个正则新手,上面的解决方法肯定不是最优的,大家如果有好的方法可以提 PR ,或者开 Issue 和我讨论,一起精进!

4 cn2an 介绍

如果你的需求简单,可以直接把上面代码嵌入到自己的程序中,如果你想有更多的功能,来试试 cn2an 吧!

???? cn2an 是一个快速转化 中文数字 和 阿拉伯数字 的工具包!

4.1 功能

  • 支持 中文数字 => 阿拉伯数字

  • 支持 大写中文数字 => 阿拉伯数字

  • 支持 中文数字和阿拉伯数字 => 阿拉伯数字

  • 支持 阿拉伯数字 => 中文数字

  • 支持 阿拉伯数字 => 大写中文数字

  • 支持 阿拉伯数字 => 大写人民币

  • 支持 中文数字 的句子 => 阿拉伯数字 的句子;

  • 支持 阿拉伯数字 的句子 => 中文数字 的句子;

  • 支持 小数

  • 支持 负数

  • 支持 HTTP API

4.2 安装

# pip 安装
pip install cn2an -U#从代码库安装
git clone https://github.com/Ailln/cn2an.git
cd cn2an && python setup.py install

4.3 使用

# 在文件首部引入包
import cn2an# 查看版本
print(cn2an.__version__)
# 0.5.2# 在 strict 模式(默认)下,只有严格符合数字拼写的才可以进行转化
output = cn2an.cn2an("一百二十三")
# 或者
output = cn2an.cn2an("一百二十三", "strict")
# output:
# 123# 在 normal 模式下,还可以将 一二三 进行转化
output = cn2an.cn2an("一二三", "normal")
# output:
# 123# 在 smart 模式下,还可以将混合拼写的 1百23 进行转化
output = cn2an.cn2an("1百23", "smart")
# output:
# 123# 以上三种模式均支持负数
output = cn2an.cn2an("负一百二十三", "strict")
# output:
# -123# strict 和 normal 模式支持小数,smart 模式暂不支持
output = cn2an.cn2an("一点二三", "strict")
# output:
# 1.23

更多使用方法请访问 https://github.com/Ailln/cn2an。

长按扫码添加“Python小助手” 

进入 P Y 交 流 群

▼点击成为社区会员   喜欢就点个在看吧

用 Python 快速转化「中文数字」和「阿拉伯数字」相关推荐

  1. cn2.php中文,Python使用cn2an实现中文数字与阿拉伯数字的相互转换

    工作中经常遇到阿拉伯数字转换称为中文数字或者大写金额,在网上搜了下,cn2an口碑较好,遂进行了一番学习. 安装 pip install cn2an 依赖库为setuptools.PyYAML 查看版 ...

  2. 已解决(Python cn2an库实现中文数字与阿拉伯数字互转)ValueError: mode 仅支持 [‘low‘, ‘up‘, ‘rmb‘, ‘direct‘]

    已解决(Python cn2an库实现中文数字与阿拉伯数字互转)ValueError: mode 仅支持 ['low', 'up', 'rmb', 'direct'] 文章目录 报错内容 报错翻译 报 ...

  3. js 阿拉伯数字转化为中文数字

    作为一个中国人,总会在某个瞬间觉得中文是那样的美,中华文化是那样的博大精深.当书写数字时,都不忘了使用中文式的数字,比如写单据,章节名时,写上中文数字显得高大上一些.在这里,就和大家分享一下,如何通过 ...

  4. 中文数字文字转换成阿拉伯数字

    在参考率网络上的部分代码后,写了一个简单的转换方法,原本是想用在抓取电子书中的标题使用的,比如,"第一千三百零二章"转换成1302 1 /// <summary> 2 ...

  5. springmvc 将大写转小写_Excel – 快速设置大小写中文数字顺序编号,拖动自动增序...

    将数字转换成中文,做财务相关工作的小伙伴可能会需要用到. Excel 虽然是美国人开发的,但是也很人性化地考虑了庞大中国用户的需求,所以专门开发了一个函数,用于将阿拉伯数字转换成中文. 这是一个隐藏函 ...

  6. 热烈祝贺致欧家居获2022 IDC中国未来企业大奖「未来数字基础架构领军者优秀奖」

    2022 年 9 月 2 日,全球领先的 IT 市场研究和咨询公司 IDC 公布"2022 IDC 中国未来企业大奖优秀奖"获奖名单.其中,致欧家居的"全球数据化基础架构 ...

  7. PHP阿拉伯数字与中文数字的转换,阿拉伯数字转中文数字,中文数字转阿拉伯数字(数字的读写)

    //原创内容 , //主要目的是用于自己记录一些封装好的方法 和 分享以供大家借鉴. 希望大家能提出优化建议. //如若转载 望加来源,谢谢.https://blog.csdn.net/BetterM ...

  8. python练习题--阿拉伯数字转换成中文数字

    python练习题–阿拉伯数字转换成中文数字 用户输入任意阿拉伯数字,如[123456],把它转换成中文数字[壹拾贰万叁仟肆佰伍拾陆] 以前面试时面试官问的问题,当时没能答出来,今天初步解决,把它记录 ...

  9. 如何将数字转化为中文大写 java

    package com.tchf.service.plan.common.utils;public class NumberToChn {static String CHN_NUMBER[] = {& ...

最新文章

  1. php读取html中元素属性,读写HTML元素的css 属性
  2. android上滑隐藏动画,ListView上滑和下滑,显示和隐藏Toolbar的实现方法
  3. php替换文本域中的换行符,文本域中换行符的替换示例
  4. python机械_10分钟掌握Python-机器学习小项目
  5. String封装——读时共享,写时复制
  6. pandas空值填充
  7. android高效简洁的代码实现直播礼物效果
  8. CCSP(Certified Cloud Security Professional) 国际注册云安全专家
  9. 郭盛华如今现状如何呢? 身价早过亿,网友:期待开直播
  10. zk集群和clickhouse集群搭建
  11. 聚合数据API接口调用方法
  12. 解读爱奇艺Q2财报:会员数破亿,其他收入打开新增长空间?
  13. 1002-过河卒-洛谷-luogu-动态规划dp
  14. Ant Design之表格动态合并行
  15. Emc服务器怎么用u盘装系统,用u盘装系统的具体方法
  16. Cortex M3处理器工作模式及中断过程
  17. 王卫不再担任顺丰速运法人 变更为陈雪颖
  18. 三阶魔方还原 two phase 算法 学习笔记
  19. 洛谷 p1000 超级玛丽游戏
  20. 执行Http请求时doGet和doPost的区别

热门文章

  1. 区块链新一代共识算法:VRF分布式可验证随机函数
  2. 十一、【高级篇】无线通信模块(Nrf)
  3. UML是什么意思?类图又是什么?
  4. Font Icon 的资源推荐
  5. macOS系统将App安装至移动硬盘的方法
  6. 陕西计算机好的二本学校排名2015年,2021年陕西二本大学最新排名
  7. 错题本-----高速公路超速处罚
  8. IDEA报错:The injection point has the following annotations:....
  9. finebi分析集团毛利率下滑原因(附pdf下载)
  10. support库中v4 v7版本冲突错误详解