最后,我是在开发聊天机器人的时候用到这个性能,比方用户发问 一千米以内有那些场地可用?,我须要在数据库中查问范畴小于一千米的场地,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}# 正向遍历 1def forward_cn2an_one(inputs):    output = 0    unit = 1    num = 0    for index, cn_num in enumerate(inputs):        if cn_num in number_map:            # 数字            num = number_map[cn_num]            # 最初的个位数字            if index == len(inputs) - 1:                output = output + num        elif cn_num in unit_map:            # 单位            unit = unit_map[cn_num]            # 累加            output = output + num * unit            num = 0        else:            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

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

# 正向遍历 2def forward_cn2an_two(inputs):    output = 0    unit = 1    num = 0    for index, cn_num in enumerate(inputs):        if cn_num in number_map:            # 数字            num = number_map[cn_num]            # 最初的个位数字            if index == len(inputs) - 1:                output = output + num        elif cn_num in unit_map:            # 单位            unit = unit_map[cn_num]            # 判断出万、亿,先把后面的累加再乘以单位万、亿            if unit % 10000 == 0:                output = (output + num) * unit            else:                # 累加                output = output + num * unit            num = 0        else:            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,从解析式中能够看到,亿位上的数字被谬误得乘以了万,这种状况也须要做解决。

# 正向遍历 3def forward_cn2an_three(inputs):    output = 0    unit = 1    num = 0    # 亿位以上的输入    hundred_million_output = 0    for 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 + num        elif cn_num in unit_map:            # 单位            unit = unit_map[cn_num]            # 判断出万,后面的累加再乘以单位万            if unit == 10000:                output = (output + num) * unit            # 判断出亿,后面累加乘以亿后赋值给 hundred_million_output, output 重置为 0            elif unit == 100000000:                hundred_million_output = (output + num) * unit                output = 0            else:                # 累加                output = output + num * unit            num =0        else:            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 反向遍历法

不晓得你看完下面的算法是不是有点膈应,先用变量存储再相加的形式仿佛不是那么优雅。有没有更好办法的解决它呢?

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

咦~有了!咱们能够应用反向(倒序)遍历,尽管还是数字乘以单位,但咱们是一直的和更大的数字累加,只有解决好单位问题,就不会呈现须要把局部值先暂存,而后再加到一起的形式,从而节俭一个变量的空间!

# 反向遍历 1def backward_cn2an_one(inputs):    output = 0    unit = 1    num = 0    for index, cn_num in enumerate(reversed(inputs)):        if cn_num in number_map:            # 数字            num = number_map[cn_num]            # 累加            output = output + num * unit        elif 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 本应该是十万位。咱们须要在遍历到万位时,记录以后的单位是万,前面的单位再乘以万就搞定了!代码如下:

# 反向遍历 2def backward_cn2an_two(inputs):    output = 0    unit = 1    # 万、亿的单位    ten_thousand_unit = 1    num = 0    for index, cn_num in enumerate(reversed(inputs)):        if cn_num in number_map:            # 数字            num = number_map[cn_num]            # 累加            output = output + num * unit        elif cn_num in unit_map:            # 单位            unit = unit_map[cn_num]            # 判断出万、亿            if unit % 10000 == 0:                ten_thousand_unit = unit            if unit < ten_thousand_unit:                unit = ten_thousand_unit * unit        else:            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,它们都是万亿之后的值,本应该是最大最长的四个,也就是说代码中没有判断出这里的万应该是万亿!

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

# 反向遍历 3def backward_cn2an_three(inputs):    output = 0    unit = 1    # 万、亿的单位    ten_thousand_unit = 1    num = 0    for index, cn_num in enumerate(reversed(inputs)):        if cn_num in number_map:            # 数字            num = number_map[cn_num]            # 累加            output = output + num * unit        elif 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_unit                    unit = ten_thousand_unit            if unit < ten_thousand_unit:                unit = ten_thousand_unit * unit        else:            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 是一个疾速转化 中文数字阿拉伯数字 的工具包!

点我拜访 DEMO

4.1 性能

  • 反对 中文数字 => 阿拉伯数字
  • 反对 大写中文数字 => 阿拉伯数字
  • 反对 中文数字和阿拉伯数字 => 阿拉伯数字
  • 反对 阿拉伯数字 => 中文数字
  • 反对 阿拉伯数字 => 大写中文数字
  • 反对 阿拉伯数字 => 大写人民币
  • 反对 中文数字 的句子 => 阿拉伯数字 的句子;
  • 反对 阿拉伯数字 的句子 => 中文数字 的句子;
  • 反对 小数
  • 反对 正数
  • 反对 HTTP API

4.2 装置

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

4.3 应用

# 在文件首部引入包import cn2an# 查看版本print(cn2an.__version__)# 0.4.5# 在 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("负一百二十三")# output:# -123# strict 和 normal 模式反对小数,smart 模式暂不反对output = cn2an.cn2an("一点二三")# output:# 1.23

更多应用办法请拜访 Github。

4.4 API 拜访

Java

import java.net.URL;import java.net.HttpURLConnection;import java.io.BufferedReader;import java.io.InputStreamReader;public class HttpGetExample {    public static void main(String[] args) throws Exception {        HttpGetExample http = new HttpGetExample();          String url = "https://api.dovolopor.com/v1/cn2an";        String params = "?text=123&function=an2cn&method=low";        http.get(url + params);    }    private void get(String url) throws Exception {        URL obj = new URL(url);        HttpURLConnection con = (HttpURLConnection) obj.openConnection();        con.setRequestMethod("GET");        con.setRequestProperty("User-Agent", "Mozilla/5.0");        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));        String inputLine;        StringBuffer response = new StringBuffer();        while ((inputLine = in.readLine()) != null) {            response.append(inputLine);        }        in.close();        System.out.println(response.toString());    }}// { output: "一百二十三", msg: "转化胜利" }

Javascript

const axios = require("axios")axios.get("https://api.dovolopor.com/v1/cn2an", {  params: {    text: "123",    function: "an2cn",    method: "low"  }}).then(  function (res) {    console.log(res.data);  })// { output: "一百二十三", msg: "转化胜利" }

Go

package mainimport (    "fmt"    "io/ioutil"    "net/http"    "net/url")func main(){    params := url.Values{}    Url, err := url.Parse("https://api.dovolopor.com/v1/cn2an")    if err != nil {        return    }    params.Set("text", "123")    params.Set("function", "an2cn")    params.Set("method", "low")    Url.RawQuery = params.Encode()    urlPath := Url.String()    resp,err := http.Get(urlPath)    defer resp.Body.Close()    body, _ := ioutil.ReadAll(resp.Body)    fmt.Println(string(body))}// { output: "一百二十三", msg: "转化胜利" }

Python

import requestsresponse = requests.get("https://api.dovolopor.com/v1/cn2an",  params={    "text": "1234567890",    "function": "an2cn",    "method": "low"  })print(response.json())# { output: "一百二十三", msg: "转化胜利" }

4.5 性能

  • 测试版本:v0.4.3
  • 测试设施:2.3 GHz 双核Intel Core i5 MacBook Pro
  • 测试代码:performance.py
  • 测试方法:

    pip install -r requirements_test.txtpython -m cn2an.performance
  • 测试后果:

    | 序号 | 性能 | 执行次数 | 执行工夫(均匀) | 性能(次/秒)
    | :-: | :-: | :-: | :-: | :-: |
    | 1 | an2cn | 10000 | 0.20 | 50k |
    | 2 | cn2an | 10000 | 0.35 | 29k |

测试时,我应用的是最大长度的测试数据!因而,大多数状况下该库的性能会更好~

4.6 许可证