一个用Python编写的便捷网页信息提取工具,编写

作者:计算机知识

引言

不久前项目有供给从多少个老的站点抓撤废息然后倒入到新的体系中。由于老的系统现已远非人珍重,数据又比较分散,而要提取的数量在网页上显现的相反更统壹,所以布署经过网络请求然后深入分析页面包车型地铁方法来领取数额。而两年前的那年,作者就如做过同样的事体——缘分这件业务,真是风趣。

引言

在上篇作品(http://www.cnblogs.com/lightluomeng/p/7212577.html)中,起首落成了多个可配备的网页音讯深入分析组件。可是出于是奔着化解职业的目标去的,所以写的可比匆忙,许多细节方面包车型地铁主题材料并未有仔细思量,所以存在诸多标题。重要难题有:

  • 布置极度不人性化。不人性化到怎么样程度呢...笔者自个儿配置了贰个亟待抓取多种列表同一时间中间须求剖析连接然后跳转的页面,足足写了500行的布局文件。而且每一种节点的项目名称的编写这里没有做优化,导致名称空间十分短,很累,而且便于失误。
  • 关于重返新闻降维未有管理好,所以在达成新的遵守节点的时候很轻便出错。
  • 日志做的远远不足好,不可见通过错误消息推测哪儿出了难点。
  • 布置还缺乏到位。照旧必要过多的代码来把全副工艺流程串联起来。未有实现外部程序集加载。

lmth一 三个方便人民群众的网页新闻提取工具

设想

在采访新闻那件业务中,最麻烦的频仍是例外的页面包车型大巴演说、数据的领取——因为页面包车型客车计划性和协会往往天差地远。同有的时候间,对于有个别页面,平常只好绕着弯子请求(ajax、iframe等),那致使数据提取成了最耗费时间也最难受的历程——因为您需求编写制定多量的逻辑代码将全方位流程串联起来。笔者隐约记得1伍年的十一月,也正是两年前的那年,作者就寻思过那个主题材料。当时引进了1个档期的顺序CommonExtractor来减轻这几个难题。总体的概念是那般的:

    public class CommonExtractor
    {
        public CommonExtractor(PageProcessConfig config)
        {
            PageProcessConfig = config;
        }

        protected PageProcessConfig PageProcessConfig;

        public virtual void Extract(CrawledHtmlDocument document)
        {
            if (!PageProcessConfig.IncludedUrlPattern.Any(i => Regex.IsMatch(document.FromUrl.ToString(), i)))
                return;
            var node = new WebHtmlNode { Node = document.Contnet.DocumentNode, FromUrl = document.FromUrl };
            ExtractData(node, PageProcessConfig);
        }

        protected Dictionary<string, ExtractionResult> ExtractData(WebHtmlNode node, PageProcessConfig blockConfig)
        {

            var data = new Dictionary<string, ExtractionResult>();
            foreach (var config in blockConfig.DataExtractionConfigs)
            {
                if (node == null)
                    continue;
                /*使用'.'将当前节点作为上下文*/
                var selectedNodes = node.Node.SelectNodes("."   config.XPath);
                var result = new ExtractionResult(config, node.FromUrl);
                if (selectedNodes != null && selectedNodes.Any())
                {
                    foreach (var sNode in selectedNodes)
                    {
                        if (config.Attribute != null)
                            result.Fill(sNode.Attributes[config.Attribute].Value);
                        else
                            result.Fill(sNode.InnerText);
                    }
                    data[config.Key] = result;
                }
                else { data[config.Key] = null; }
            }

            if (DataExtracted != null)
            {
                var args = new DataExtractedEventArgs(data, node.FromUrl);
                DataExtracted(this, args);
            }

            return data;
        }

        public EventHandler<DataExtractedEventArgs> DataExtracted;
    }

代码有一点点乱(因为及时接纳的是Abot进行爬网),不过意图照旧挺鲜明的,希望从四个html文件中提抽出有用的消息,然后经过二个铺排来钦点怎么着提取信息。这种处理格局存在的第3难题是:无法应对复杂结构,在应对一定的构造的时候必须引进新的配备,新的流程,同期那些新的流水生产线不负有较高水准的可重用性。

设计上的精耕细作

  • 不再沉默管理降维(群集收敛),今后应用2个DimReduceConvertor一个用Python编写的便捷网页信息提取工具,编写一个可配置的网页信息提取组件。来将二维数组降维到壹人数组,恐怕将更高维度的数组降维到低贰个维度的数组
  • 不再沉默的判断是不是是会集,今后利用贰个ProcessedList来将数据料定的号子为数组,降维操作也会基于这些论断进行
  • 移除了ICollector,将此接口上的Key品质定义放在了基础的IValueConvertor上,那样越来越好的承接保险了整整树形结构的一致性,同一时候能够分明的缩减嵌套结构
  • 引入了IValuePersistence,用来缓和管理后的值的持久化的主题材料
  • 有着元件都经过构造函数注入的主意引用了ILogger
  • 汪洋施用了IOptions形式,从而能够以大局的法子配置部分必需的音信,收缩单个管理节点的安排的复杂度
  • 引入了ITypeNameResolver之所以使得单个节点在钦命名称的时候能够应用简写,下降配置难度;引进了此外的ITypeResolver故而使自动化注入和计划成为恐怕

方今线总指挥部体的连串承继关系如下(部分门类未突显):

图片 1

0, Why lmth1?

玩Python的人拾有八玖用过urllib,扒数据的10有捌九用过BeautifulSoup。作者也不例外,平日抓数据大致全用BeautifulSoup。
BeautifulSoup的职能挺不错,但哪怕API挫了点,用起来不顺。相对于中规中矩的API,笔者更中意jQuery的Fluent API。所以,花了多少个早上,以BeautifulSoup作为基础,搞了八个库lmth和lmth一:lmth提供基本作用,并担负Hpath深入分析;lmth1提供Fluent API,进行数据抓取。

lmth一的接口特别简单,它的兑现更简便易行——不超过300行代码。但它的作用很强劲,你急忙就会看到,lmth一是如何用一行代码完结BeautifulSoup十行代码的效果的,而且,更易读。

设计

IOptions模式

IOptions建设在.net core的ioc的基础之上。那个情势结合了.net core的配置种类之后,非常优雅。通过品种承接和安顿项指标结合注入(在2个连串中相同的时候注入我的定制化配置和基类的布署),能够很方便的成功全局配置和各自配置。同有的时候间,由于IOptions<>支撑可选重视,那样就能够给1个项目提供暗许的作为,而后通过安插在供给的时候改换其作为。譬如:

        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来掩盖基类的安插。当然,在上边的代码中,并不曾这么做,出于其余原因,节点的起初化操作是通过别的措施完结的。

1, 简介

如题。

应用前请将lmth.py, lmth壹.py以及beautifulsoup.py放至Python的条件目录下。

简易的发端

为了应对现真实意况况中的复杂性,最中央的拍卖必须统一计划的简练。从原先代码中捕捉到灵感,对于数据提取,其实大家想要的便是:

  • 给程序提供2个html文书档案
  • 先后给我们回到3个值

透过,给出了最宗旨的接口定义:

    public interface IContentProcessor
    {
        /// <summary>
        /// 处理内容
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        object Process(object source);
    }

多少个实例

以往,把3个调控台程序的代码限定为:

    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("处理完成...");
            }
        }
    }

透过陈设来抓取分化网址的消息。比方,大家利用以下配置来抓取新浪音信的前十页的标题:

"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"
          }
        ]
      }
    ]
  }

很扎眼,通过配备上的立异,那几个布局文件已经减少了不通晓某个,配置起来也愈发清晰明了。下边是出口的开始和结果,这里运用了2个在支配台出口的积累完成:

图片 2
拍卖节点帮助相互运算,基础的ConvertorOptions能够配备那些意义,可是多少完结会忽视那个布局。举例,就上述操作来讲,开启并行和不展开并行的情况下的耗费时间独家是:500ms 和 94玖ms。假诺是前十0页的抓取职分以来,那么结果个别是:5374ms 和 907七ms。实验机器的布局是:

图片 3
注意,这个性能数据可能会因为站点的安全防护措施以及网络带宽的影响变得极其不稳定。

2, Hpath

Hpath是1种自己定义的1连串似于Xpath的HTML路线查询表达式,它的语法极度轻易——多少个例子就能够说驾驭。要是急需从严的定义,请参见二.二的BNF定义。

可组合性

在上述的接口定义中,IContentProcessor接口的兑现格局假设丰盛变得庞大,其实能够缓和其余html页面包车型地铁多少提取,但是,那意味着其可复用性会越来越低,同一时候保证将进一步不方便。所以,大家更期待其艺术实现丰富小。然则,越小代表着其功用越少,那么,为了面对复杂的现实须求,必须让那么些接口可以组合起来。所以,要为接口加多新的要素:子管理器。

    public interface IContentProcessor
    {
        /// <summary>
        /// 处理内容
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        object Process(object source);

        /// <summary>
        /// 该处理器的顺序,越小越先执行
        /// </summary>
        int Order { get; }

        /// <summary>
        /// 子处理器
        /// </summary>
        IList<IContentProcessor> SubProcessors { get; }
    }

那样1来,各样Processor就能够张开同盟了。其嵌套关系和Order本性共同决定了其实践的逐1。同期,整个管理流程也具有了管道的特色:上二个Processor的管理结果能够看成下贰个Processor的管理源。

或多或少经验

  • 要想复杂必须先轻松。那几个节点之所以能够运行起来,原因是他俩的落脚点特别简单,就是2个输入二个开口。
  • 要想大致必须单一。在前头的规划中,1个节点仍然记挂了太多的难题,举个例子怎么着推断是还是不是要出口集结,在什么样时候理应对聚焦进行降维等等。未来的做法是不做那几个特种管理,让特殊的节点来做这个管理。整个工艺流程进一步通畅了。

二.壹 实例演讲

小心,这里的事例所关联的收获成分,均为在指标节点下所获取的因素。

应用的实例HTML:

 

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ";
 2 <html xmlns="" >
 3 <head>
 4     <title>Untitled Page</title>
 5 </head>
 6 <body>
 7 <h1 id="title">Page list</h1>
 8 <div id="content" class="sites">
 9     <a href="" class="good">Google</a>
10     <a href="" class="good">Yahoo</a>
11     <a href="" class="asshole">Baidu</a>
12     <a href="" class="excellent">Bing</a>
13 </div>
14 <div id="tbl">
15     <ul>
16     <li class="odd">1</li>
17     <li class="even">2</li>
18     <li class="odd">3</li>
19     <li class="even">4</li>
20     <li class="odd">5</li>
21     <li class="even">6</li>
22     </ul>
23 </div>
24 </body>
25 </html>

 

二.一.一 基本表达式

 

li        

功用:获取具备li成分
结果:

[
     <li class="odd">1</li>,
     <li class="even">2</li>,
     <li class="odd">3</li>,
     <li class="even">4</li>,
     <li class="odd">5</li>,
     <li class="even">6</li>
 ]

 

 

div[id=tbl]

功能:获取具有id属性为tbl的div元素
晋升:通过品质过滤来进行更加精准的查找
结果:

<div id="tbl">
 <ul>
 <li class="odd">1</li>
 <li class="even">2</li>
 <li class="odd">3</li>
 <li class="even">4</li>
 <li class="odd">5</li>
 <li class="even">6</li>
 </ul>
 </div>

 

 

div[id=content, class=sites]

意义:获取具备id属性为name且class属性为grey的div元素
唤醒:你能够同有的时候间设定三个属性值,属性对中间用逗号分隔
结果:

<div id="content" class="sites">
 <a href="" class="good">Google</a>
 <a href="" class="good">Yahoo</a>
 <a href="" class="asshole">Baidu</a>
 <a href="" class="excellent">Bing</a>
 </div>

 

 

div[@id]    

效能:获取具有div成分的id属性值
提拔:你必要在需获得的属性值前加2个@符
结果:

[
     'content', 
    'tbl'
 ]

 

 

div[id=content]/a[@href]

意义:获取具备id属性为name的因素上边包车型大巴p成分的href属性值
结果:

[
     '',
     '',
     '',
     ''
 ]

 

结果的组合性

尽管减轻了管理流程的可组合性,可是就当下来说,处理的结果要么不行组合的,因为不只怕应对复杂的构造。为了消除那么些难点,引入了IContentCollector,那个接口承接自IContentProcessor,不过提议了额外的必要,如下:

    public interface IContentCollector : IContentProcessor
    {
        /// <summary>
        /// 数据收集器收集的值对应的键
        /// </summary>
        string Key { get; }
    }

该接口需求提供2个Key来标记结果。那样,大家就能够用二个Dictionary<string,object>把复杂的结构管理起来了。因为字典的项对应的值也得以是Dictionary<string,object>,那个时候,如若选用json作为类别化花招的话,是极度轻便将结果反种类化成复杂的类的。

关于为啥要将以此接口传承自IContentProcessor,那是为着确定保证节点类型的一致性,从而有利于通过配备来协会整个拍卖流程。

包装的源代码

在附属类小部件中打包了稿子中描述的代码的源码,同时富含一个可运营的主次和几何安顿。由于代码中利用了局域网内计划的nuget服务器,所以某个包是心有余而力不足苏醒的,这里一向把程序集附上。可下载的链接是 :

二.壹.二 高端表明式

 

a[class={excellent|good}, @class, @href]

效益:获取具备class属性为excellent或good的要素上面包车型客车a成分的class属性和href属性
唤醒:大括号里面包车型大巴是正则表明式,利用它能够达成或操作
结果:

[
     {
         'href': '',
         'class': 'good'
     },
     {
         'href': '',
         'class': 'good'
     },
     {
         'href': '',
         'class': 'excellent'
     }
 ]

 

 

div[id={con. }]/a[class={ass. }, @class, @#]

成效:获取具备id属性以con做前缀的div成分上边包车型地铁class属性以ass为前缀的成分的id属性以及内容
提示:@#表示要得到成分的剧情(innertext)
结果:

{
     '#': 'Baidu',
     'class': 'asshole'
 }

 

 

ul/li[class={e. }, @#]

效率:获取具备id属性以post做前缀的要素上边的以数字为id的p成分上面包车型地铁a成分的href属性及内容
唤醒:也得以行使正则表明式实行模糊查询
结果:

[
     '2',
     '4',
     '6'
 ]

 

配置

从地点的宏图中得以见见,整个拍卖流程其实是1棵树,结构至极规范。那就为布局提供了方向,这里运用1个Content-Processor-Options项目来代表每种Processor节点的门类和必备的先导化信息。定义如下所示:

    public class ContentProcessorOptions
    {
        /// <summary>
        /// 构造Processor的参数列表
        /// </summary>
        public Dictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();

        /// <summary>
        /// Processor的类型信息
        /// </summary>
        public string ProcessorType { get; set; }

        /// <summary>
        /// 指定一个子Processor,用于快速初始化Children,从而减少嵌套。
        /// </summary>
        public string SubProcessorType { get; set; }

        /// <summary>
        /// 子项配置
        /// </summary>
        public List<ContentProcessorOptions> Children { get; set; } = new List<ContentProcessorOptions>();
    }

在Options中引进了SubProcessorType品质来飞速初阶化唯有一个子管理节点的ContentCollector,那样就能够减去配置内容的层级,从而使得配置文件更加的分明。而以下情势则代表了什么样通过一个Content-Processor-Options初始化Processor。这里运用了反光,可是由于不会壹再开端化,所以不会有太大的标题。

        public static IContentProcessor BuildContentProcessor(ContentProcessorOptions contentProcessorOptions)
        {
            Type instanceType = null;
            try
            {
                instanceType = Type.GetType(contentProcessorOptions.ProcessorType, true);
            }
            catch
            {
                foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    if (assembly.IsDynamic) continue;
                    instanceType = assembly.GetExportedTypes()
                        .FirstOrDefault(i => i.FullName == contentProcessorOptions.ProcessorType);
                    if (instanceType != null) break;
                }
            }

            if (instanceType == null) return null;

            var instance = Activator.CreateInstance(instanceType);
            foreach (var property in contentProcessorOptions.Properties)
            {
                var instanceProperty = instance.GetType().GetProperty(property.Key);
                if (instanceProperty == null) continue;
                var propertyType = instanceProperty.PropertyType;
                var sourceValue = property.Value.ToString();
                var dValue = sourceValue.Convert(propertyType);
                instanceProperty.SetValue(instance, dValue);
            }
            var processorInstance = (IContentProcessor) instance;
            if (!contentProcessorOptions.SubProcessorType.IsNullOrWhiteSpace())
            {
                var quickOptions = new ContentProcessorOptions
                {
                    ProcessorType = contentProcessorOptions.SubProcessorType,
                    Properties = contentProcessorOptions.Properties
                };
                var quickProcessor = BuildContentProcessor(quickOptions);
                processorInstance.SubProcessors.Add(quickProcessor);
            }
            foreach (var processorOption in contentProcessorOptions.Children)
            {
                var processor = BuildContentProcessor(processorOption);
                processorInstance.SubProcessors.Add(processor);
            }
            return processorInstance;
        }

2.2 Hpath的BNF定义

没玩过编写翻译的可以忽略那壹节。
玩过编写翻译的看了就精通。

 

 hpath ::= hpart {"/" hpart}
 hpart ::= ele_name [ "[" attrs "]" ]
 attrs ::= pred_attrs [ "," get_attrs ]
 pred_attrs ::= pred_attr { "," pred_attr }
 get_attrs ::= get_attr { "," get_attr }
 get_attr ::= "@"string [ "(" attr_alias ")"]
 attr_alias ::= string
 pred_attr ::= string "=" value
 value ::= string | regex_value
 regex_value ::= "{" string "}"

 

本文由bwin必赢发布,转载请注明来源

关键词: .NET C# 必赢亚洲www36net XPATH 正则表达式