编写一个可配置的网页信息提取组件 (二)—— 优雅的.net core 配置系统
引言
在上篇文章(http://www.cnblogs.com/lightluomeng/p/7212577.html
)中,初步实现了一个可配置的网页信息分析组件。但是由于是奔着解决事情的目的去的,所以写的比较匆忙,很多细节方面的问题没有仔细考虑,所以存在不少问题。主要问题有:
- 配置非常不人性化。不人性化到什么程度呢...我自己配置了一个需要抓取多重列表同时中间需要分析连接然后跳转的页面,足足写了500行的配置文件。而且每个节点的类型名称的编写这里没有做优化,导致名称空间很长,很累,而且容易出错。
- 关于返回信息降维没有处理好,所以在实现新的功能节点的时候很容易出错。
- 日志做的不够好,不能够通过错误信息推断哪里出了问题。
- 配置还不够到位。仍然需要不少的代码来把整个流程串联起来。没有做到外部程序集加载。
设计上的改进
- 不再静默处理降维(集合收敛),现在使用一个
DimReduceConvertor
来将二维数组降维到一位数组,或者将更高维度的数组降维到低一个维度的数组 - 不再静默的判断是否是集合,现在使用一个
ProcessedList
来将数据显著的标记为数组,降维操作也会基于这个判断进行 - 移除了
ICollector
,将此接口上的Key
属性定义放在了基础的IValueConvertor
上,这样更好的保证了整个树形结构的一致性,同时可以显著的减少嵌套结构 - 引入了
IValuePersistence
,用来解决处理后的值的持久化的问题 - 所有元件都通过构造函数注入的方式引用了
ILogger
- 大量应用了
IOptions
模式,从而可以以全局的方式配置一些必要的信息,减少单个处理节点的配置的复杂度 - 引入了
ITypeNameResolver
从而使得单个节点在指定名称的时候可以使用简写,降低配置难度;引入了其他的ITypeResolver
从而使自动化注入和配置成为可能
目前整体的类型继承关系如下(部分类型未展示):
IOptions模式
IOptions建设在.net core的ioc的基础之上。这个模式结合了.net core的配置系统之后,非常优雅。通过类型继承和配置类型的组合注入(在一个类型中同时注入自身的定制化配置和基类的配置),可以很方便的做到全局配置和个别配置。同时,由于IOptions<>
支持可选依赖,这样就可以给一个类型提供默认的行为,而后通过配置在必要的时候改变其行为。例如:
public CollectorConvertor(ILogger logger, IOptions<ConvertorOptions> options,
IOptions<CollectorOptions> collectorOptions) : base(logger, options)
{
if (collectorOptions.Value != null)
{
AutoGenerateKey = collectorOptions.Value.AutoGenerateKey;
AutoResolveComflict = collectorOptions.Value.AutoResolveComflict;
}
}
在这类型CollectorConvertor
中,同时注入了两个配置。其中ConvertorOptions
是基类的配置。我们可以通过CollectorOptions
来覆盖基类的配置。当然,在上面的代码中,并没有这么做,出于其他原因,节点的初始化操作是通过其他方式实现的。
几个实例
现在,把一个控制台程序的代码限定为:
class Program
{
static void Main(string[] args)
{
SwitchConfiguration();
RunCore();
}
public static IServiceCollection ServiceCollection { get; set; }
public static IServiceProvider ServiceProvider { get; set; }
public static IConfigurationRoot ConfigurationRoot { get; set; }
private static void SwitchConfiguration()
{
var allFiles = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory);
var files = allFiles.Where(i => Regex.IsMatch(i, @".*appsettings\.?.*\.json")).ToList();
if (files.Count == 1)
{
Console.WriteLine("仅找到一个配置文件,加载中...");
BuildConfiguration(files[0]);
}
else
{
Console.WriteLine($"找到{files.Count}个配置文件,请选择加载第几个...");
var index = Console.ReadLine().Number<int>();
if (index == null)
{
Console.WriteLine("错误的输入,程序退出,回车以继续...");
Console.ReadLine();
SwitchConfiguration();
}
else
{
var configurationName = files[index.Value];
BuildConfiguration(configurationName);
}
}
}
private static void BuildConfiguration(string fileName)
{
var builder = new ConfigurationBuilder()
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddJsonFile(fileName, true, true);
var configurationRoot = builder.Build();
var collection = new ServiceCollection();
collection.AddOptions();
collection.ConfigureDefault<EnviromentBuilderOptions>(configurationRoot);
var traceSource = new TraceSource("信息提取", SourceLevels.All);
traceSource.Listeners.Add(new ConsoleTraceListener());
collection.AddSingleton<ILogger, TraceSourceLogger>(p => new TraceSourceLogger(traceSource));
collection.AddSingleton<EnviromentBuilder>();
collection.AddSingleton<ConvertorBuilder>();
var enBuilder = collection.BuildServiceProvider().GetService<EnviromentBuilder>();
var enviroment = enBuilder.Build(collection, configurationRoot);
ServiceProvider = enviroment.ServiceProvider;
ConfigurationRoot = configurationRoot;
}
private static void RunCore()
{
var builder = ServiceProvider.GetService<ConvertorBuilder>();
var convertor = builder.Build();
if (convertor == null)
{
Console.WriteLine("无法初始化convertor,程序退出");
}
else
{
AsyncHelper.Synchronize(() => convertor.ProcessAsync(null));
Console.WriteLine("处理完成...");
}
}
}
通过配置来抓取不同网站的信息。比如,我们使用以下配置来抓取博客园新闻的前10页的标题:
"ConvertorBuildOptions": {
"TypeName": "Collector",
"PersistenceTypeName":"ConsoleOutputPersistence",
"Children": [
{
"Key": "博客园前10页所有的文章title",
"TypeName": "Container",
"Children": [
{
"TypeName": "NumberList",
"Properties": {
"From": 1,
"To": 10
}
},
{
"TypeName": "Formatter",
"Properties": {
"Formatter": "https://news.cnblogs.com/n/page/{0}/"
}
},
{"TypeName":"Url2Html"},
{
"TypeName": "Xpath",
"Properties": {
"Xpath": "//h2[@class=\"news_entry\"]/a",
"ValueProvider": "InnerText"
}
},
{
"TypeName": "DimReduce"
}
]
}
]
}
很显然,通过配置上的改进,这个配置文件已经缩短了不知道多少,配置起来也更加清晰明了。下面是输出的内容,这里使用了一个在控制台输出的仓储实现:
处理节点支持并行运算,基础的ConvertorOptions
可以配置这个功能,但是有些实现会忽略这个配置。例如,就上述操作而言,开启并行和不开启并行的情况下的耗时分别是:500ms 和 949ms。如果是前100页的抓取任务的话,那么结果分别是:5374ms 和 9077ms。实验机器的配置是:
注意,这个性能数据可能会因为站点的安全防护措施以及网络带宽的影响变得极其不稳定。
一点心得
- 要想复杂必须先简单。这些节点之所以能够运转起来,原因是他们的出发点非常简单,就是一个入口一个出口。
- 要想简单必须单一。在前面的设计中,一个节点仍然考虑了太多的问题,比如如何判断是否要输出集合,在什么时候应该对集合进行降维等等。现在的做法是不做这些特殊处理,让特殊的节点来做这些处理。整个流程更加流畅了。
打包的源代码
在附件中打包了文章中描述的代码的源码,同时包含一个可运行的程序和若干配置。由于代码中使用了局域网内部署的nuget服务器,所以有些包是无法还原的,这里直接把程序集附上。可下载的链接是 :