65.9K
CodeProject 正在变化。 阅读更多。
Home

使用自定义模式进行字符串解析

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (11投票s)

2016年10月3日

CPOL

5分钟阅读

viewsIcon

23543

downloadIcon

510

一个简单的库,用于根据自定义模式解析字符串。

引言

StringPatternizer - 是一个简单的库,它允许定义自定义模式(类似于 DateTime 模式)并使用它们进行字符串解析。

背景

最近,我集成了一个提供位置信息的 WebService。位置信息以 XML 格式发送,并表示为两个字符串字段:纬度和经度。挑战在于,这些坐标可以是任何格式,但我需要它们是十进制格式。以下是一些示例:

  • -22.856944
  • 25 15 30
  • 25° 15' 30"
  • -22.856944 (22 51' 25.0" S)

事实证明,位置信息是由人工提供的,输入没有限制,只有一个文本框字段。在这种情况下,硬编码解析是不可能的。

自然而然地,我想到了 XML 配置文件,其中应该包含一个格式模式列表,以便最终用户将来可以添加缺失的模式。但是应该使用哪种模式呢?

我的第一个想法是正则表达式。但我意识到正则表达式对最终用户来说太复杂了。例如,要解析像 "25° 15' 30"" 这样的字符串,需要定义以下正则表达式:

(?<degrees>\d*[,.]?\d*)° (?<minutes>\d*[,.]?\d*)' (?<seconds>\d*[,.]?\d*)"

当然,这是完全不可接受的。理想的解决方案应该类似于 DateTime 解析模式。与其使用复杂的正则表达式,不如写一个类似这样的模式:

d° m' s"

其中,'d' 是度数的占位符,'m' 是分钟的占位符,'s' 是秒的占位符。通过这种方法,最终用户可以获取原始坐标字符串,用占位符字符替换值,然后就可以得到一个可用的模式。这比正则表达式简单得多。

经过一番搜索,我没有找到任何提供此类功能的库。所以我自己写了一个,并想与社区分享我的解决方案。

使用代码

首先,您需要编译源代码(已附加),并将“StringPatternizer.dll”引用添加到您的项目中。

首先应该创建主要的“StringPatternizer”类

StringPatternizer sp = new StringPatternizer();

下一步是定义字符标记,它们将用作模式中的占位符

sp.Markers.Add('d', typeof(int));//marker for degrees
sp.Markers.Add('m', typeof(int));//marker for minutes
sp.Markers.Add('s', typeof(double));//marker for seconds
sp.Markers.Add('D', typeof(double));//marker for decimal value (coordinate may come in decimal format)
sp.Markers.Add('S', typeof(string));//marker for side of the world (North, South, East, West)

更新

对于“StringPatternizer2”,可以使用字符串标记而不是字符。所以上面的代码可能看起来像这样:

sp.Markers.Add("degrees", typeof(int));//marker for degrees
sp.Markers.Add("minutes", typeof(int));//marker for minutes
sp.Markers.Add("seconds", typeof(double));//marker for seconds
sp.Markers.Add("Decimal", typeof(double));//marker for decimal value (coordinate may come in decimal format)
sp.Markers.Add("Side", typeof(string));//marker for side of the world (North, South, East, West)

上面的代码定义了 5 个标记及其预期的类型。在解析过程中,StringPatternizer 将使用指定的类型来验证提取值的格式是否正确。当然,您可以将所有标记注册为“string”类型,但解析精度会降低。

下一步是定义模式列表

sp.Patterns.Add("d° m' s\"");
sp.Patterns.Add("d m s");
sp.Patterns.Add("d°m's\"");

更新

对于“StringPatternizer2”,模式可能看起来像这样:

sp.Patterns.Add("degrees° minutes' seconds\"");

另一种方法是拥有一个模式列表并注册整个列表

var patterns = new List<string>() 
            { 
                "D",
                "d m s S",
                "d m s",
                "d° m' s\" So",
                "d° m' s\" Se",
                "d° m' s\" S",
                "d° m' s\"",
                "d°m's\"S",
                "d°m's\"",
                "d?m's\"S",
                "d?m's\"",

                "d? m' s\"",

                "D (d m' s\" S)",
                "(d m' s\" S)",
                "d m' s\" S\"",
                
                "m' d°  s\"",
                "d m' s'' S",
                "dº m' s\" S",
                "dºm's"
            };

sp.Patterns.AddRange(_patterns);

定义模式有两种规则:

  • 标记的顺序应反映传入字符串中值的顺序
  • 需要指定邻近字符 - 预期值左侧的一个字符和右侧的一个字符。例如,对于坐标“25° 15' 30"",分钟值左侧由空格字符包围,右侧由撇号字符包围,因此模式也应包含它们:“d° m' s""。”

此时,初始化已完成。StringPatternizer 拥有进行解析所需的所有数据。有两种方法可以提供解析。第一种方法称为“Match”,它查找 **第一个** 匹配的模式并使用它进行解析。第二种方法称为“MatchAll”,它返回 **所有** 匹配的模式和解析的数据。这是一个例子:

PatternizationResult pResult = sp.Match("25° 15' 30\"");
...
List<PatternizationResult> pResults = sp.MatchAll("25° 15' 30\"");

解析结果是 PatternizationResult 类。它具有以下属性:

  • Exception - 如果其中一个模式匹配并且解析成功完成,则为“null”;如果未找到指定字符串值的模式,则为“FormatException”。
  • Pattern - 如果未匹配任何模式,则为“string.Empty”;如果匹配了模式,则为“模式值”。
  • Result - Dictionary<char, object>,其中 Key 是标记符号,Value 是提取的值。更新:对于“StringPatternizer2” - Dictionary<string, object>,其中 Key 是标记字符串,

PatternizationResult 类还具有以下方法:

  • bool MarkerHasValue(char marker) - 用于检查特定标记是否具有值的有用方法。
  • TValue GetMarkerValue<TValue>(char marker) - 用于以所需类型提取特定标记值的有用方法。

更新:对于“StringPatternizer2”

  • bool MarkerHasValue(string marker)
  • TValue GetMarkerValue<TValue>(string marker)

这是一个如何处理 PatternizationResult 的示例:

string location = "25° 15' 30\"";

var pResult = sp.Match(location);

if (pResult.Exception == null)
{
    Log.DebugFormat("Value '{0}' matched with pattern '{1}'.", location, pResult.Pattern);

    if (pResult.MarkerHasValue('D'))
    {
          return pResult.GetMarkerValue<double>('D');
    }
    else
    {
          var degrees = pResult.GetMarkerValue<int>('d');
          var minutes = pResult.GetMarkerValue<int>('m');
          var seconds = pResult.GetMarkerValue<double>('s');

          return ConvertToDecimalCoordinate(degrees, minutes, seconds);
    }
}
else
{
    throw pResult.Exception;
}

就是这样!我认为很简单。

关注点

目前,该库支持以下数据类型进行解析:int、double、decimal、float、bool、string。您可以轻松地在 StringPatternizer.ConvertToType 方法中添加缺失的类型。

该库处理十进制值中的本地化问题(点或逗号分隔符均可)。

有时很难在模式中指定非英文字符。对于这种情况,该库支持“内联字符代码”。假设我们需要解析如下字符串:“43�10�12,4\"”。字符 '�' 的代码是 65533。我们可以使用其代码而不是将此字符放入模式中:“d{65533}m{65533}s\"”。该库会将代码‘{65533}’转换为字符‘�’。

一个可能的改进是使用 Parellel.ForEach 检查每个模式以提高速度。现在,我决定保持代码简单,以便即使是 C# 的新手也能理解它。

 

更新

感谢 Emily Heiner 的评论,我通过在内部使用正则表达式来提取值来改进了算法。这使得将“标记”实现为字符串而不是字符变得容易。代码也变得更加简单。因此,现在这个库有点像是正则表达式之上的包装器。

历史

2016 年 10 月 3 日 - 第一个版本

2016 年 10 月 4 日 - 版本 2

  • 在内部使用正则表达式
  • “标记”现在是字符串而不是字符

2016 年 10 月 5 日 - 修复了正则表达式空组的错误,将“.*”替换为“.+”

© . All rights reserved.