自定义配置文件解析

在制作小工具, 绿色软件时, 经常需要定义一些小型配置文件, 这些配置文件格式一般是针对具体业务的,是对业务的精准定义, 因此格式需要清爽简单。

需求描述

这里以一个端口转发的小工具的配置文件为例:

  • 支持若干组端口转发配置同时工作
  • 每组端口转发配置需要配置一个监听端口, 以及一组转发目标地址
  • 每组转发目标地址包括一个ip和一个端口

用json描述如下

[
{
"bind": 27011,
"forwarding":[
{"ip":"192.168.30.1", "port": 27011},
{"ip":"192.168.30.2", "port": 27011}
]
},
{
"bind": 27012,
"forwarding":[
{"ip":"192.168.30.1", "port": 27012},
{"ip":"192.168.30.2", "port": 27012}
]
},
{
"bind": 27013,
"forwarding":[
{"ip":"192.168.30.1", "port": 27013},
{"ip":"192.168.30.2", "port": 27013}
]
}
]

以上配置文件的意思是:
监听27011端口, 并将接收到的数据转发到192.168.30.1:27011上, 若该端口无法连接, 则发送到192.168.30.2:27011上.
接着的27012和27013端口也是同理。

直接使用json当配置文件当然是可以的, 而且有现成的类库解析,不过在这个场景下, 我更加倾向于使用自定义的格式, 这样看起来更加清爽, 比如以下格式:

27011|192.168.30.1:27011|192.168.30.2:27011
27012|192.168.30.1:27012|192.168.30.2:27012
27013|192.168.30.1:27013|192.168.30.2:27013

我们需要把这段文本解析为List<Tuple<IPEndPoint, List<IPEndPoint>>>,
下面就描述如何自己编写代码来解析这段文本

硬编码解析

static List<Tuple<IPEndPoint, List<IPEndPoint>>> GetConfig()
{
var result = new List<Tuple<IPEndPoint, List<IPEndPoint>>>();
var lines = File.ReadAllLines("forward.txt"); //从配置文件中读取所有行
foreach (var line in lines)
{
string[] fields = line.Split('|'); //按|进行切割
if (fields.Length < 2) continue; //过滤空行
IPEndPoint main = new IPEndPoint(IPAddress.Any, int.Parse(fields[0])); //提取监听端口
List<IPEndPoint> forwordingList = new List<IPEndPoint>();
for (int i = 1; i < fields.Length; i++) //提取转发列表
{
int index = fields[i].IndexOf(":");
if (index != -1)
{
forwordingList.Add(new IPEndPoint(IPAddress.Parse(fields[i].Substring(0, index)), int.Parse(fields[i].Substring(index+1))));
}
}
if (forwordingList.Count > 0) //过滤掉空列表
{
result.Add(new Tuple<IPEndPoint, List<IPEndPoint>>(main, forwordingList));
}
}
return result;
}

这段代码的思路是从配置文件读出所有行, 然后对每行以分隔符|进行切割,切割出各个字段后, 再一一分析, 填充进目标数据结构。

可以看到, 硬编码解析的话,代码比较冗长, 而且语义不清, 如果这段代码不是自己亲手写的, 甚至还难以看懂。

正则表达式解析

在解析思路不变的情况下, 我们可以用正则表达式改造以上代码:

static List<Tuple<IPEndPoint, List<IPEndPoint>>> GetConfig1()
{
var result = new List<Tuple<IPEndPoint, List<IPEndPoint>>>();
Regex r = new Regex(@"^(\d+)(\|.+:\d+)+");
Regex r2 = new Regex(@"\G\|([\d\.]+):(\d+)");
var lines = File.ReadAllLines("forward.txt");

foreach (var line in lines)
{
if (r.IsMatch(line))
{
var match = r.Match(line);
IPEndPoint main = new IPEndPoint(IPAddress.Any, int.Parse(match.Groups[1].Value));
List<IPEndPoint> forwordingList = new List<IPEndPoint>();
foreach (Match match2 in r2.Matches(match.Groups[2].Value))
{
forwordingList.Add(new IPEndPoint(IPAddress.Parse(match2.Groups[1].Value), int.Parse(match2.Groups[2].Value)));
}
result.Add(new Tuple<IPEndPoint, List<IPEndPoint>>(main, forwordingList));
}
}
return result;
}

因为我们的示例比较简单, 使用正则表达式并未减少代码行数, 只不过使用正则表达式扩展性更强了

使用lambda表达式

为了压缩代码行数,以及更加清晰的语义表达, 我们还可以使用大杀器lambda表达式, 经过压缩后代码如下:

static List<Tuple<IPEndPoint, List<IPEndPoint>>> GetConfig2()
{
Regex r = new Regex(@"^(\d+)(\|.+:\d+)+");
Regex r2 = new Regex(@"\G\|([\d\.]+):(\d+)");

return File.ReadAllLines("forward.txt").ToList()
.Where( i => r.IsMatch(i))
.Select( i=> r.Match(i))
.Select( i=> new Tuple<IPEndPoint, List<IPEndPoint>>(
new IPEndPoint(IPAddress.Any, int.Parse(i.Groups[1].Value)),
(from Match j in r2.Matches(i.Groups[2].Value) select new IPEndPoint(IPAddress.Parse(j.Groups[1].Value), int.Parse(j.Groups[2].Value))).ToList()
)
).ToList();
}

可以看到代码行数下降了很多, 而且这种方式的表达更加清晰。完整代码