微软 BI 系列随笔 - SSIS 2012 高级应用 - Script Component处理不规则平面文件
场景介绍
在使用SSIS从平面文件导入源数据时,最常遇到的是以下两种情况:
- 导入规则的平面文件,这种文件的每一行数据的解析规则都是一样的
- 导入不规则的平面文件,这种文件可能包含多种数据结构,比如某些行是头(Head),某些行是内容(Content),某些行是尾(Tail),三种数据的解析格式都不一样
- 源文件的字符编码集不一样,例如文件是来自于不同的系统(或区域),这些文件的编码有可能是ANSI,Unicode或者UTF-8中的任何一种。
对于第一种情况,可以使用平面文件连接器(FlatFile Connection)来解析,通过配置他的字符集,格式,分隔符等就能做到。
对于第二种情况,要是使用平面文件连接器(FlatFile Connection)的话,需要使用临时文件,即使用平面文件连接器从源文件中中将每一行的数据读为一个类型,然后根据内容使用条件分离(Conditional Split)将内容分离,重新组织新的文件。然后正对新生成的文件做常规处理实现数据导入。
对于第三种情况,比较复杂,目前是不能使用平面文件连接器来解析的。平面文件连接器虽然支持表达式设置CodePage和是否是Unicode,以及其他的设置,但是具体操作起来比较麻烦。设计时有严重的干扰,经常常常导致编译不通过。
本文将针对第二种情况和第三种情况给读者一个思路,即使用Script Component作为数据源解析文件,输出数据,同时结合处理第二种情况。
实施步骤
分析平面文件格式
假设我们有如下格式的平面文件:
首先这个文件的编码是不规则的,他含有西班牙文(还有其他的文件,是不含这个文字集的)。
第二,他有02H和02D两种内容格式,姑且认为是Header和Detail。
所以说这种类型的文件使用平面文件连接器根本无法解析。
那么我们怎么办呢?看下面。
Script Component源
定义输入与输出
SSIS中支持使用Script Component作为数据源,你可以使用C#或者VB代码去做任何事情,然后将构造一个或多个数据集作为输出。如下所示:
本例中我定义了一个EDCOrder的输出用来放Header的内容,OrderDetail的内容我不需要所以没有定义。
我另外定义了一个FailedRows来放Header解析失败的的内容。没有输入。
编写脚本
在脚本页选择要使用的变量和脚本语言。
点击编辑脚本(Edit Script...)
这时候会打开一个Vsta project的脚本编辑项目。
这个项目包含三个文件
- ComponentWrapper.cs - 包含变量类,连接类和用户控件类
- BufferWrapper.cs - 定义了输出类
- main.cs - 脚本文件的入口函数,继承自用户控件类。
main.cs这个文件有三个方法
- PreExecute - 执行之前
- PostExecute - 执行之后
- CreateNewOutputRows 创建输出行
为了解析平面文件,我们定义了如下参数,主要是一个StreamReader来读文件,时间记录和行号:
System.IO.StreamReader sr; DateTime now; int rowNumber,failedRows; Guid fileId; string ContentLengthError; string CoposIdNullError; string errorMsg;
在PreExecute中这么写,根据传进来的文件名打开文件,定义错误消息,初始化变量
/// <summary> /// This method is called once, before rows begin to be processed in the data flow. /// /// You can remove this method if you don‘t need to do anything here. /// </summary> public override void PreExecute() { base.PreExecute(); now = DateTime.Now; rowNumber = 1; failedRows = 0; fileId = Guid.Parse(Variables.pvFileID.ToString()); ContentLengthError = "Line content validation failed. Line content length is not equal to 1141."; CoposIdNullError = "Line content validation failed. Copos id is required."; errorMsg = "Filename: " + Variables.pvLoadFile + System.Environment.NewLine; sr = new System.IO.StreamReader(Variables.pvLoadFile); }
在PostExecute中这么写,关闭文件,返回错误信息给脚本调用的变量
/// <summary> /// This method is called after all the rows have passed through this component. /// /// You can delete this method if you don‘t need to do anything here. /// </summary> public override void PostExecute() { base.PostExecute(); sr.Close(); Variables.pvFailedRows = failedRows; if (errorMsg != "Filename: " + Variables.pvLoadFile + System.Environment.NewLine) Variables.pvErrorMessage += System.Environment.NewLine + errorMsg; }
在CreateNewOutputRows这么写,根据每一行的内容判断消息类型,验证消息内容,生成输出行。
public override void CreateNewOutputRows() { /* Add rows by calling the AddRow method on the member variable named "<Output Name>Buffer". For example, call MyOutputBuffer.AddRow() if your output was named "MyOutput". */ while (!sr.EndOfStream) { string line = sr.ReadLine(); if (line.StartsWith("02H")) { if (line.Length != 1141) { FailedRowsBuffer.AddRow(); FailedRowsBuffer.RowNumber = rowNumber; FailedRowsBuffer.ErrorDescription = ContentLengthError; FailedRowsBuffer.Content = line; failedRows++; errorMsg += "At Row: " + rowNumber.ToString() + System.Environment.NewLine + "Error Description: " + ContentLengthError + System.Environment.NewLine; } else { if (!string.IsNullOrEmpty(line.Substring(3, 30)) && !string.IsNullOrWhiteSpace(line.Substring(3, 30))) //Coposid is required { EDCOrderBuffer.AddRow(); EDCOrderBuffer.Coposid = line.Substring(3, 30); EDCOrderBuffer.Custname = line.Substring(33, 64); EDCOrderBuffer.Custtel = line.Substring(257, 20); EDCOrderBuffer.Dlvytitle = line.Substring(287, 4); EDCOrderBuffer.Dlvyinitial = line.Substring(291, 1); EDCOrderBuffer.Dlvyname = line.Substring(292, 30); EDCOrderBuffer.Dlvyhouse = line.Substring(322, 30); EDCOrderBuffer.Dlvytel = line.Substring(482, 20); EDCOrderBuffer.Dsptchsrvc = line.Substring(882, 9); EDCOrderBuffer.Dlvydate = line.Substring(891, 8); EDCOrderBuffer.Dmworder = line.Substring(959, 30); EDCOrderBuffer.Dsptchemail = line.Substring(989, 70); EDCOrderBuffer.OrderDate = line.Substring(1059, 12); EDCOrderBuffer.FileId = fileId; EDCOrderBuffer.CreatedOn = now; } else { FailedRowsBuffer.AddRow(); FailedRowsBuffer.RowNumber = rowNumber; FailedRowsBuffer.ErrorDescription = CoposIdNullError; FailedRowsBuffer.Content = line; failedRows++; errorMsg += "At Row: " + rowNumber.ToString() + System.Environment.NewLine + "Error Description: " + CoposIdNullError + System.Environment.NewLine; } } } rowNumber++; } }
编译下下这个Vsta项目。
处理Script Component输出
这样,一个不规则的文件就被我们使用Script Component轻松的处理了。
- 由于使用StreamReader,使用默认编码格式他会自动识别文件编码,避免了使用平面文件解析器出现乱码的情况
- 由于针对文件内容分别处理,可以解析不规则文件,产生多个输出
- 由于加入了自定义验证信息,可以验证消息的内容
疑问:
- 使用Script Component能否平行执行,EDCOrderBuffer.AddRow()的时候是不是就已经产生了一行输出了呢?这个有待研究
- 要写代码解析文件,有没有可以长度类型都是可以配置的,如果需要格式转换是否方便,或者在外部再做转换呢?
将这些问题留给读者朋友们去思考吧。
也可以给我留言大家一起讨论哦。