2

我正在开发一个使用 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

我需要一些方法来猜测数字中的小数分隔符和千位分隔符是什么,然后将值呈现给用户以决定这是否正确。

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

4

8 回答 8

9

我不确定你是否能够以一种优雅的方式来解决这个问题,因为如果你不能告诉它数据来自哪里,它总是会模棱两可。

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

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

  • 如果数字包含,BEFORE .,则,必须为千且.必须为小数
  • 如果数字包含.BEFORE ,,则.必须为千且,必须为小数
  • 如果有 >1 个,符号,千位分隔符必须是,
  • 如果有 >1 个.符号,千位分隔符必须是.
  • 如果只有 1,后面有多少个数字?如果它不是 3,那么它必须是小数分隔符(相同的规则.
  • 如果有 3 个数字分隔它(例如 1,234 和 1.234),也许你可以把这个数字放在一边,然后解析同一页面上的其他数字,尝试找出它们是否使用不同的分隔符,然后再回到它?

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

于 2009-12-08T14:26:09.863 回答
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() 方法将字符串值转换为浮点数:

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

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

于 2009-12-08T16:35:31.777 回答
3

您必须创建自己的函数来猜测小数分隔符和千位分隔符是什么。然后您将能够使用相应的 CultureInfo 进行 double.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),其中可以是小数或千位分隔符。

您可以保存这个在文档中查找所需数据量的数字,以知道女巫是文档的文化,保存该文化并始终使用相同的文化(如果您可以假设文档都在相同的文化中...)

希望这会有所帮助

于 2009-12-08T15:00:17.147 回答
2

你应该能够做到这一点Double.TryParse。在我看来,您最大的问题是您解释数字的方式不一致。

例如,怎么能

"-133.696" => -133696  

什么时候

"-166.713" => -166.713

?

于 2009-12-08T14:21:02.277 回答
2

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

于 2009-12-08T14:29:09.673 回答
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)) 执行实际的解析。

于 2009-12-08T14:31:25.877 回答
1

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

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

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

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

于 2009-12-08T14:41:25.623 回答
0

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

于 2009-12-08T15:03:58.493 回答