標籤:protect 難度 targe 轉換 ase 限制 alc 匹配 設定
針對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表格最大的好處,在於它的格式一致性。只要分析很少的具有代表性的範例資料,就能夠掌握整個資料集的特徵。因此完全可以用比較大的代價獲得一個儘可能高的識別模組,而在執行過程中盡量提升效能。
Hawk原理:通過IEnumerable實現通用的ETL管道