将数量字符串解析为数字

时间:2009-12-08 14:12:46

标签: c# .net parsing ocr

我正在开发一个使用OCR引擎识别纸质文档的系统。这些凭证是包含总金额,增值税和净金额等金额的发票。我需要将这些数量的字符串解析为数字,但它们有多种格式和风格,使用不同的十进制符号和每张发票中的数字分隔数千。如果我试图在.NET中使用正常的double.tryparse和double.parse方法,那么它们通常会因某些金额而失败

这些是我收到的一些例子

"3.533,65" =>  3533.65 
"-133.696" => -133696
"-33.017" => -33017
"-166.713" => -166713
"-5088,8" => -5088.8 
"0.423" => 0.423
"9,215,200" => 9215200
"1,443,840.00" => 1443840

我需要一些方法来猜测小数分隔符和千位分隔符在数字中的含义,然后将值提供给用户以确定这是否正确。

我想知道如何以优雅的方式解决这个问题。

8 个答案:

答案 0 :(得分:9)

我不确定你是否能够以优雅的方式解决这个问题,因为如果你无法确定数据的来源,它总会变得暧昧。

例如,数字1.234和1,234都是有效数字,但没有确定符号的含义,你将无法分辨哪个是。

就个人而言,我会根据一些规则编写一个试图做出“最佳猜测”的函数......

  • 如果号码在,之前包含.,则,必须为数千,而.必须为小数
  • 如果号码在.之前包含,,则.必须为数千,而,必须为小数
  • 如果有> 1 ,个符号,则千位分隔符必须为,
  • 如果有> 1 .个符号,则千位分隔符必须为.
  • 如果只有1 ,跟随它有多少个数字?如果它不是3,那一定是 小数分隔符(.的相同规则)
  • 如果有3个数字将它分开(例如1,234和1.234),也许您可​​以将这个数字放在一边并解析同一页面上的其他数字以试图找出它们是否使用不同的分隔符,然后回到它? / LI>

一旦你计算出小数分开,删除任何千位分隔符(解析数字不需要)并确保小数分隔符为。在您正在解析的字符串中。然后,您可以将其传递到Double.TryParse

答案 1 :(得分:7)

我可能会设置一个按优先顺序指定的规则列表,这样您就可以按优先级插入规则。然后,您可以根据返回正确规则的正则表达式匹配来解析列表。

快速原型很容易设置类似于:

public class FormatRule
{
    public string Pattern { get; set; }
    public CultureInfo Culture { get; set; }

    public FormatRule(string pattern, CultureInfo culture)
    {
        Pattern = pattern;
        Culture = culture;
    }
}

现在用于按优先顺序存储规则的FormatRule列表:

List<FormatRule> Rules = new List<FormatRule>()
{
    /* Add rules in order of precedence specifying a culture
     * that can handle the pattern, I've chosen en-US and fr-FR
     * for this example, but equally any culture could be swapped
     * in for various formats you may need to use */
    new FormatRule(@"^0.\d+$", CultureInfo.GetCultureInfo("en-US")),
    new FormatRule(@"^0,\d+$", CultureInfo.GetCultureInfo("fr-FR")),
    new FormatRule(@"^[1-9]+.\d{4,}$", CultureInfo.GetCultureInfo("en-US")),
    new FormatRule(@"^[1-9]+,\d{4,}$", CultureInfo.GetCultureInfo("fr-FR")),
    new FormatRule(@"^-?[1-9]{1,3}(,\d{3,})*(\.\d*)?$", CultureInfo.GetCultureInfo("en-US")),
    new FormatRule(@"^-?[1-9]{1,3}(.\d{3,})*(\,\d*)?$", CultureInfo.GetCultureInfo("fr-FR")),

    /* The default rule */
    new FormatRule(string.Empty, CultureInfo.CurrentCulture)
}

然后,您应该能够迭代列表,寻找要应用的正确规则:

public CultureInfo FindProvider(string numberString)
{
    foreach(FormatRule rule in Rules)
    {
        if (Regex.IsMatch(numberString, rule.Pattern))
            return rule.Culture;
    }
    return Rules[Rules.Count - 1].Culture;
}

此设置允许您轻松管理规则并设置何时应以某种方式处理某些内容的优先级。它还允许您指定不同的文化,以单向处理一种格式,以另一种格式处理不同的格式。

public float ParseValue(string valueString)
{
    float value = 0;
    NumberStyles style = NumberStyles.Any;
    IFormatProvider provider = FindCulture(valueString).NumberFormat;
    if (float.TryParse(numberString, style, provider, out value))
        return value;
    else
        throw new InvalidCastException(string.Format("Value '{0}' cannot be parsed with any of the providers in the rule set.", valueString));
}

最后,调用你的ParseValue()方法将你拥有的字符串值转换为float:

string numberString = "-123,456.78"; //Or "23.457.234,87"
float value = ParseValue(numberString);

您可以决定使用字典来保存额外的FormatRule类;概念是一样的...我在示例中使用了一个列表,因为它使查询使用LINQ更容易。此外,如果需要,您可以轻松替换我用于单,双或十进制的浮点类型。

答案 2 :(得分:3)

您必须创建自己的函数来猜测小数点分隔符和千位分隔符。然后你就可以使用相应的CultureInfo来加倍.Parse。

我建议做这样的事情(只是这不是生产测试的功能):

private CultureInfo GetNumbreCultureInfo(string number)
    {
        CultureInfo dotDecimalSeparator = new CultureInfo("En-Us");
        CultureInfo commaDecimalSeparator = new CultureInfo("Es-Ar");

        string[] splitByDot = number.Split('.');
        if (splitByDot.Count() > 2) //has more than 1 . so the . is the thousand separator
            return commaDecimalSeparator; //return a cultureInfo where the thousand separator is the .

        //the same for the ,
        string[] splitByComma = number.Split(',');
        if (splitByComma.Count() > 2)
            return dotDecimalSeparator;

        //if there is no , or . return an invariant culture
        if (splitByComma.Count() == 1 && splitByDot.Count() == 1)
            return CultureInfo.InvariantCulture;

        //if there is only 1 . or 1 , lets check witch is the last one
        if (splitByComma.Count() == 2)
            if (splitByDot.Count() == 1)
                if (splitByComma.Last().Length != 3) // , its a decimal separator
                    return commaDecimalSeparator;
                else// here you dont really know if its the dot decimal separator i.e 100.001 this can be thousand or decimal separator
                    return dotDecimalSeparator;
            else //here you have something like 100.010,00 ir 100.010,111 or 100,000.111
            {
                if (splitByDot.Last().Length > splitByComma.Last().Length) //, is the decimal separator
                    return commaDecimalSeparator;
                else
                    return dotDecimalSeparator;
            }
        else
            if (splitByDot.Last().Length != 3) // . its a decimal separator
                return dotDecimalSeparator;
            else
                return commaDecimalSeparator; //again you really dont know here... i.e. 100,101
    }

你可以这样做一个快速测试:

string[] numbers = { "100.101", "1.000.000,00", "100.100,10", "100,100.10", "100,100.100", "1,00" };

        decimal n;
        foreach (string number in numbers)
        {
            if (decimal.TryParse(number, NumberStyles.Any, GetNumbreCultureInfo(number), out n))
                MessageBox.Show(n.ToString());//the decimal was parsed
            else
                MessageBox.Show("there was problems parsing");
        }

另外看看你真的不知道巫婆是分隔符(如100,010或100.001),其中可以是小数或千位分隔符。

您可以在文档中查找一个数字,其中包含了知道女巫是文档文化所需的数据量,保存文化并始终使用相同的文化(如果您可以认为该文档全部在同样的文化...)

希望这会有所帮助

答案 3 :(得分:2)

你应该可以使用Double.TryParse。我认为你最大的问题是你解释数字的方式不一致。

例如,怎么可以

"-133.696" => -133696  

何时

"-166.713" => -166.713

答案 4 :(得分:2)

如果转换数字的规则不一致,那么您将无法在代码中解决此问题。正如克劳斯比斯科所指出的,为什么“-133.696”中的句号与“-166.713”中的句号有不同的含义?在给出这两个示例的情况下,您如何知道如何处理包含小数点的数字,其中一个是按预期使用它,另一个是将它用作千分隔符?

答案 5 :(得分:2)

您需要定义您可能遇到的各种情况,创建一些逻辑以将每个传入字符串与您的一个案例相匹配,然后解析它指定适当的FormatProvider。例如 - 如果您的字符串在逗号之前包含小数点,那么您可以假设对于此特定字符串,它们使用小数点作为千位分隔符而逗号作为小数分隔符,因此您可以构造格式提供程序应对这种情况。

尝试以下几点:

public IFormatProvider GetParseFormatProvider(string s) {
  var nfi = new CultureInfo("en-US", false).NumberFormat;
  if (/* s contains period before comma */) {
    nfi.NumberDecimalSeparator = ",";
    nfi.NumberGroupSeparator = ".";
  } else if (/* some other condition */) {
     /* construct some other format provider */
  }
  return(nfi);
}

然后使用Double.Parse(myString,GetParseFormatProvider(myString))来执行实际的解析。

答案 6 :(得分:1)

  

“然后将值呈现给用户以确定这是否正确。”

如果有多种可能性,为什么不向用户显示它们?

您可以使用多种方法调用TryParse,使其具有您希望能够处理的不同文化,并收集在列表中成功的那些方法的解析结果(删除重复项)。

您甚至可以根据文档中其他地方使用各种格式的频率来估计不同可能性的正确性,并在列表中按照正确的可能性排序。例如,如果你已经看过很多像3,456,231.4这样的数字那么你可以猜到,当你在同一个文档中看到4,675时,逗号可能是数千个分隔符,并且在列表中首先显示“4675”,并且“4.675”秒

答案 7 :(得分:0)

如果您的点或逗号后跟不超过两位数,则为小数点。否则,请忽略它。