引言
在上篇文章(http://www.cnblogs.com/lightluomeng/p/7212577.html
)中,初步實現(xiàn)了一個可配置的網(wǎng)頁信息分析組件。但是由于是奔著解決事情的目的去的,所以寫的比較匆忙,很多細節(jié)方面的問題沒有仔細考慮,所以存在不少問題。主要問題有:
配置非常不人性化。不人性化到什么程度呢...我自己配置了一個需要抓取多重列表同時中間需要分析連接然后跳轉(zhuǎn)的頁面,足足寫了500行的配置文件。而且每個節(jié)點的類型名稱的編寫這里沒有做優(yōu)化,導致名稱空間很長,很累,而且容易出錯。
關(guān)于返回信息降維沒有處理好,所以在實現(xiàn)新的功能節(jié)點的時候很容易出錯。
日志做的不夠好,不能夠通過錯誤信息推斷哪里出了問題。
配置還不夠到位。仍然需要不少的代碼來把整個流程串聯(lián)起來。沒有做到外部程序集加載。
設(shè)計上的改進
不再靜默處理降維(集合收斂),現(xiàn)在使用一個
DimReduceConvertor
來將二維數(shù)組降維到一位數(shù)組,或者將更高維度的數(shù)組降維到低一個維度的數(shù)組不再靜默的判斷是否是集合,現(xiàn)在使用一個
ProcessedList
來將數(shù)據(jù)顯著的標記為數(shù)組,降維操作也會基于這個判斷進行移除了
ICollector
,將此接口上的Key
屬性定義放在了基礎(chǔ)的IValueConvertor
上,這樣更好的保證了整個樹形結(jié)構(gòu)的一致性,同時可以顯著的減少嵌套結(jié)構(gòu)引入了
IValuePersistence
,用來解決處理后的值的持久化的問題所有元件都通過構(gòu)造函數(shù)注入的方式引用了
ILogger
大量應(yīng)用了
IOptions
模式,從而可以以全局的方式配置一些必要的信息,減少單個處理節(jié)點的配置的復雜度引入了
ITypeNameResolver
從而使得單個節(jié)點在指定名稱的時候可以使用簡寫,降低配置難度;引入了其他的ITypeResolver
從而使自動化注入和配置成為可能
目前整體的類型繼承關(guān)系如下(部分類型未展示):
IOptions模式
IOptions建設(shè)在.net core的ioc的基礎(chǔ)之上。這個模式結(jié)合了.net core的配置系統(tǒng)之后,非常優(yōu)雅。通過類型繼承和配置類型的組合注入(在一個類型中同時注入自身的定制化配置和基類的配置),可以很方便的做到全局配置和個別配置。同時,由于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
來覆蓋基類的配置。當然,在上面的代碼中,并沒有這么做,出于其他原因,節(jié)點的初始化操作是通過其他方式實現(xiàn)的。
幾個實例
現(xiàn)在,把一個控制臺程序的代碼限定為:
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("錯誤的輸入,程序退出,回車以繼續(xù)..."); 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("處理完成..."); } } }
通過配置來抓取不同網(wǎng)站的信息。比如,我們使用以下配置來抓取博客園新聞的前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" } ] } ] }
很顯然,通過配置上的改進,這個配置文件已經(jīng)縮短了不知道多少,配置起來也更加清晰明了。下面是輸出的內(nèi)容,這里使用了一個在控制臺輸出的倉儲實現(xiàn):
處理節(jié)點支持并行運算,基礎(chǔ)的ConvertorOptions
可以配置這個功能,但是有些實現(xiàn)會忽略這個配置。例如,就上述操作而言,開啟并行和不開啟并行的情況下的耗時分別是:500ms 和 949ms。如果是前100頁的抓取任務(wù)的話,那么結(jié)果分別是:5374ms 和 9077ms。實驗機器的配置是:
注意,這個性能數(shù)據(jù)可能會因為站點的安全防護措施以及網(wǎng)絡(luò)帶寬的影響變得極其不穩(wěn)定。
一點心得
要想復雜必須先簡單。這些節(jié)點之所以能夠運轉(zhuǎn)起來,原因是他們的出發(fā)點非常簡單,就是一個入口一個出口。
要想簡單必須單一。在前面的設(shè)計中,一個節(jié)點仍然考慮了太多的問題,比如如何判斷是否要輸出集合,在什么時候應(yīng)該對集合進行降維等等?,F(xiàn)在的做法是不做這些特殊處理,讓特殊的節(jié)點來做這些處理。整個流程更加流暢了。
打包的源代碼
在附件中打包了文章中描述的代碼的源碼,同時包含一個可運行的程序和若干配置。由于代碼中使用了局域網(wǎng)內(nèi)部署的nuget服務(wù)器,所以有些包是無法還原的,這里直接把程序集附上??上螺d的鏈接是 :
http://www.cnblogs.com/lightluomeng/p/7221495.html