• Hawk原理:通过IEnumerable实现通用的ETL管道


      针对IEnumerable已经有多篇文章,本篇介绍如何使用IEnumerable实现ETL. ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过萃取(extract)、转置(transform)、加载(load)至目的端的过程。通常来说,从原始端采集的数据有很多问题,同时可能业务需求与采集的数据格式不相匹配,所以就必须实现ETL过程。

      ETL可以理解为一条清洗管线,数据从一端流入,从另一端流出。数据量可能很大,所以管线不大可能也没有必要加载全部内容。同时,一般情况下,从管线流出来的数据会进入新的数据池,很少直接修改到原表。

      从管线的概念可以看出,ETL需要构造可组合的链条,首先实现一组组件,然后实现可将这些组件组装为一条ETL管线的框架。IEnumerable一大堆的LINQ扩展,正好帮我们实现了这一思想。

    1. 数据的表达

      我们先讨论清楚如何表达数据,因为数据处理涉及到动态增减属性的问题,因此一般的实体类是做不到的,我们采用字典来实现。为此我包装了一个实现IDictionary<string, object>的类。叫做FreeDocument。它可以简单表示如下:

      /// <summary>
        /// 自由格式文档
        /// </summary>
        public interface IFreeDocument : IDictionarySerializable, IDictionary<string, object>, IComparable
        {
            #region Properties
    
            IDictionary<string, object> DataItems { get; set; }
    
            IEnumerable<string> PropertyNames { get; }
    
            #endregion
        }

      因此数据的处理,本质就是对每一个字典对象中的键值对进行增删改查。

    2 .基本组件

      数据清洗组件的基接口是ICollumProcess. 定义如下:

     public interface ICollumProcess : IDictionarySerializable
        {
    
            string CollumName { get; set; }  //针对的列名
    
            bool ShouldCalculated { get; set; }  //是否需要重新计算
     
            double Priority { get; set; }  //优先级
    
            void Finish();  //处理完成时的回收函数
    
            void Init(IList<IFreeDocument> datas);  //对数据进行初始化的探测行为
    
        }

      更清晰的说,其实派生出四部分:

      (1) 生成器

      生成器即提供/产生数据的组件。这可能包括生成一个从0-1000的数,获取某个数据表中的数据,或从网页检索的结果。它的接口可以表示如下:

    [Interface("ICollumGenerator", "数据生成器", SearchStrategy.FolderSearch)]
        public interface ICollumGenerator : ICollumProcess
          {
    
              /// <summary>
              /// 当前迭代的位置
              /// </summary>
              int Position { get; set; }
              IEnumerable<FreeDocument> Generate();/// <summary>
              /// 生成器能生成的文档数量
              /// </summary>
              /// <returns></returns>
              int? GenerateCount();
          }

      最主要的方法是Generate,它能够枚举出一组数据出来,同时还有可能(有时做不到)得到能够生成文档的总数量。

      (2)过滤器

      过滤器即能够分析一个文档是否满足条件,不满足则剔除的组件。接口也很简单:

      

      [Interface("ICollumDataFilter", "数据列过滤器", SearchStrategy.FolderSearch)]
         public interface ICollumDataFilter :  ICollumProcess
        {
            bool FilteData(IFreeDocument data);
         
        }

       (3)排序器

      顾名思义,对数据实现排序的接口,定义如下:

      [Interface("ICollumDataSorter", "数据排序器", SearchStrategy.FolderSearch)]
        public interface ICollumDataSorter : IDictionarySerializable, ICollumProcess,IComparer<object>
        {
           
           
             SortType SortType { get; set; }
    
             IEnumerable<IFreeDocument> Sort(IEnumerable<IFreeDocument> data);
        }

        排序一般需要升序和降序,但排序最大的问题是破坏了管线的单向流动性和虚拟性。最少LINQ的标准实现上,排序是内存排序,因此必须把数据全部加载进来才能排序,这严重影响了性能。因此目前的排序最好在小数据的情况下进行。

      (4)列转换器

      它最重要的组件。整个ETL过程,实质上就是不同的列进行变换,组成另外一些列的过程(列就是键值对)。 定义实现如下:

    [Interface("ICollumDataTransformer", "数据转换器", SearchStrategy.FolderSearch)]
        public interface ICollumDataTransformer : ICollumProcess
        {
            string NewCollumName { get; set; }
            SimpleDataType TargetDataType { get; set; }
            ObservableCollection<ICollumDataFilter> FilterLogics { get; set; }
            object TransformData(IFreeDocument datas);
            IEnumerable<string> AffectedCollums { get; }
        }

       看着很复杂,但其实就是将文档中的一些列转换为另外一些列。比如对一个字符串的列进行正则替换,或转换其数据类型(如从string变成int)。举个最简单的HTML编解码的例子:

       public override object TransformData(IFreeDocument document)
            {
                object item = document[CollumName];
                if (item == null)
                    return "";
                switch (ConvertType)
                {
                    case ConvertType.Decode:
                        return HttpUtility.HtmlDecode(item.ToString());
                        break;
                    case ConvertType.Encode:
                        return HttpUtility.HtmlEncode(item.ToString());
                        break;
                }
                return "";
            }

      

    3. ETL管线的设计

       相信你已经想到,ETL管线的核心就是动态组装的LINQ了。

       一个最基本的ETL管理类,应当具有以下的属性:

        public ObservableCollection<ICollumProcess> CurrentETLTools { get; set; }  //当前已经加载的ETL工具

        protected List<Type> AllETLTools { get; set; }  //所有能够使用的ETL工具。当然Type只是此处为了方便理解而设定的,更合适的应该是记录了组件元数据,名字和介绍的扩展类。

        以及一个方法:

        public IEnumerable<IFreeDocument> RefreshDatas(IEnumerable<IFreeDocument> docuts)   //从原始数据转换为新的数据

        那么,这个函数的实现可以如下定义:

      public IEnumerable<IFreeDocument> RefreshDatas(IEnumerable<IDictionarySerializable> docuts)
            {
                if (SampleMount <= 0)
                {
                    SampleMount = 10;
                }
    
                IEnumerable<IFreeDocument> ienumable = docuts.Where(d=>d!=null).Select(d => d.DictSerialize());
                Errorlogs = new List<ErrorLog>();
             
                List<IFreeDocument> samples = docuts.Take((int) SampleMount).Select(d => d as IFreeDocument).ToList();
                foreach (ICollumProcess tool in
                    CurrentETLTools.Where(d => d.ShouldCalculated).OrderByDescending(d => d.Priority))
                {
                    tool.SourceCollection = CurrentCollection;
    
                    tool.Init(samples);
    
                    if (tool is ICollumDataTransformer)
                    {
                        var ge = tool as ICollumDataTransformer;
    
                        ienumable = Transform(ge, ienumable);
                    }
                    if (tool is ICollumGenerator)
                    {
                        var ge = tool as ICollumGenerator;
                        if (!ge.CanAppend) //直接拼接
                            ienumable = ienumable.Concat(ge.Generate());
                        else
                        {
                            ienumable = ienumable.MergeAll(ge.Generate());
                        }
                    }
    
                    else if (tool is ICollumDataFilter)
                    {
                        var t = tool as ICollumDataFilter;
                        ienumable = ienumable.Where(t.FilteData);
                    }
                    else if (tool is ICollumDataSorter)
                    {
                        var s = tool as ICollumDataSorter;
    
                        switch (s.SortType)
                        {
                            case SortType.AscendSort:
                                ienumable = ienumable.OrderBy(d => d, s);
                                break;
                            case SortType.DescendSort:
                                ienumable = ienumable.OrderByDescending(d => d, s);
                                break;
                        }
                    }
    
                    tool.Finish();
                }
                return ienumable;
            }

        基本实现思路如上。即通过优先级排序所有加载的ETL组件,并提取一部分样例数据,为组件进行一次初始化。然后通过组装不同的转换器,生成器,排序器和过滤器,最后即可组装为一个新的ienumable对象。注意整个过程都是延迟计算的,只有在真正需要ETL结果时才会进行实质性的操作。

     4. 优化ETL管线和实现虚拟视图

      以上就是ETL的基本思路。但是仅仅做到这些是很不够的。以下才是这篇文章的核心。

      ETL管线破坏了原有集合的特性,原有集合可能是能够支持索引查询甚至能够执行高性能查找的。但ETL将其退化为仅能够枚举。枚举意味着只能从头访问到尾,不能回退和索引。要想使用新集合,就只能访问其前n个元素,或者全部访问。这显然对一些操作是很不利的。

      先考虑索引器。如果能满足以下条件:

      (1) 管线中不包括排序器和过滤器,因为它们使得得集合产生了乱序。

      (2) 原始集合能够支持索引器

      (3) 使用的生成器能够提供生成的大小,同时生成器也能够实现索引器

      (4) 转换器应当只实现1到1转换,没有额外的副作用。

       那么原始集合和新集合元素的对应关系是可计算的。此时索引器就能发挥作用。在实际使用中,转换器是用的最多的。条件不可谓不苛刻。

      关于高性能查找,我们先不考虑针对复杂的SQL查询,先考虑那种最简单的find(item[key]==value)的查询。但这个条件更加苛刻:

      (1) key在原始集合中必须支持高性能查找

      (2) 满足上述索引器的四个条件

      (3) 针对key这一列的操作,转换器必须是可逆的。而且最好能实现1-1映射。

        所谓可逆的意思就是说,转换器能从A转换为B,同时也能通过结果B反推出结果A。 但这种条件何其苛刻!a*5=b,这样的操作是可逆的,然而正则转换,替换以及绝大多数的运算都是不可逆的。

      怎么办呢?可能的做法,就是转换器在转换过程中,就动态地将key的转换结果保存下来。于是,对新集合的查找操作,最后就能一步步回退到原始集合的查找操作。还有更好的办法么?

      如何让新集合应对复杂的SQL查询?首先需要解析SQL, 这可能涉及到大量的数学推导和转换。以至于在实现当中因为限制太多,基本上不可能实现。以筛选key为一定范围的数据为例,每次都需要逆向推导,这种推导难度非常大。

     

    5. 智能ETL和用户体验优化

      整个ETL过程,是人为观察数据的特性,组合和配置不同的ETL组件,这一过程能够实现自动化吗?

      人是很智能的,它能够观察不同数据的格式和类型,发现其中的特征,比如以下数据:

    高楼层/21层,南垡头翠成馨园,2004年建,塔楼
    中楼层/5层,南北豆各庄5号院,2003年建,板楼

      人通过观察这么两行的数据,就可以大概的判断出这些信息分别代表的是什么意思,以及如何去分割和转换。可以用正则,提取第一个出现的数字,即楼层,再使用d{4}提取年份,而用逗号分割,即可得到小区名称。

      但是,这个操作依旧需要最少懂得一定程序基础的人来参与,如果用机器来做的话,又该如何做呢?自动化步骤可以分为两个层次:

      (1) 自动分割和对齐。

      数据尤其是来自web的数据,由于本身是由程序生成的,因此在格式上有高度的统一性,同时分隔符也是类似的,包括逗号,分号,空格,斜杠等。因此,可以统计不同分割符出现的次数,以及对应的位置,通过概率模型,生成最可能的分割方案,使得每一条数据分割出来的长度和子项数量尽可能一致。

      (2) 自动识别内容

      自动识别内容可以依赖于规则或者识别器。一种比较可靠的方法是通过基于正则的文本规则,构造一组规则组。通常200x这样的数值,很容易被理解为年份,而12:32这样的结构,则很容易被识别为时间。通过基于结构的识别引擎,不仅能够识别”这是什么内容“,更能提出其元数据,比如日期中的日月年等信息,为之后的工作做准备。

      Web表格最大的好处,在于它的格式一致性。只要分析很少的具有代表性的样例数据,就能够掌握整个数据集的特征。因此完全可以用比较大的代价获得一个尽可能高的识别模块,而在执行过程中尽量提升性能。

      

           

  • 相关阅读:
    求两个字符串中相同的汉字及字母的个数
    将十进制转成十六进制
    综合模糊查询
    求第一个字符串中第二个串的个数
    去除字符串中连续的分割符
    去除字符串中的html标记及标记中的内容
    sql基础语句
    SQL Server2008函数大全(完整版)
    sql 数字转人民币大写函数(两种方法)
    数字转IP地址函数
  • 原文地址:https://www.cnblogs.com/buptzym/p/4223083.html
Copyright © 2020-2023  润新知