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






4.95/5 (11投票s)
一个简单的库,用于根据自定义模式解析字符串。
引言
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 日 - 修复了正则表达式空组的错误,将“.*”替换为“.+”