这个例子是关于如何将一个文本数据导入导出控件作一些简单的修改之后拿来使用的。
◆程序功能
将图书馆的ISO文件中的部分数据转到Oracle8数据库中。
◆设计要求
1.显示导入进度条。
2.在导入过程中,如果某条纪录导入失败,不显示异常,而将导入失败的纪录记入日志。
◆设计思路
程序的关键在文本数据字段的分离。通常的做法,都是先将字符串进行处理(RegulateString),然后把串中每个字符同分割符(可以是空格,逗号等)比较,将不是分割符的字符追加到一个串中(GetRecordItem),得到一个字段的内容。通过一个循环(循环次数由GetItemNum来定),就可以将一个字符串分成几个字段。最后的工作就是将分离出来的数据对号入座加入数据库.
按照上面的思路,利用Delphi提供的已有函数和过程,实现起来应该不难,但问题是,我可不想每次编文本导入程序的时候,都把什么这啊那的函数过程重新定义一遍,哎,最烦的就是重复性的工作了.那么有没有现成的控件将上述过程都封装起来呢?PS:又不用我编呢?
答案是肯定的!前几天刚刚下了一个免费控件TPgCSV,据说可以实现文本的导入和导出.翻出来一看,正是我想要的.
在深入到下面的内容之前,有必要对该控件的类声明部分作一定了解
(经作者Khashayar Sadjadi(khashi@pragena.8m.com)同意发表):
//中文部分为笔者所作的注释
//注意:
//在该控件中,Export代表将文本数据导入到数据库,Import代表从数据库导出到文本。??? 怎么和我理解的
//导入导出概念刚好是反的 :)
type
//在处理数据产生异常时,可选择继续还是中止
TPgCSVErrorResponse = (pgcsvAbort, pgcsvIgnore);
//进程监控事件声明,可以将导入/导出的进度作为参数传出
TPgCSVProgressEvent = procedure (Sender : TObject; AProgress: LongInt; var StopIt: Boolean) of object;
//发生异常时的事件处理声明,异常信息通过该接口传给程序员。
TPgCSVExportErrorEvent = procedure (Sender : TObject; Mess: string; RecNo: LongInt; var Response:TPgCSVErrorResponse) of object;
TPgCSV = class(TComponent)
private
FDataset : TDataset;
FCSVMap,
FCSVFile,
FDateFormat,
FIgnoreStr : string;
FSeprator,
FDelimiter,
FFieldIndicator : Char;
FAutoOpen,
FUseDelimiter,
FSilentExport,
FTrimData,
FStop,
FEmptyTable : Boolean;
FBeforeOpenTable,
FAfterOpenTable,
FBeforeCloseTable,
FAfterCloseTable,
FBeforeEmptyTable,
FAfterEmptyTable,
FBeforeExport,
FAfterExport,
FBeforeImport,
FAfterImport,
FOnAddRecord : TNotifyEvent;
FExportProgress,
FImportProgress : TPgCSVProgressEvent;
FExportError : TPgCSVExportErrorEvent;
FMapItems,
FDefaultInt : Integer;
FBufferSize : LongInt;
FFieldCache : TList;
protected
FFile : TextFile;
//以下就是我所说的希望封装的部分
function CountMapItems:Integer;//计算映射字符串的字段个数
function GetMapItem(ItemIndex:Integer;var AField:Boolean):string;//提取映射字符串的字段
function GetCSVRecordItem(ItemIndex:Integer;CSVRecord:string):string;//提取CSV文件字符串中的某一字段
function BuildMap:string;//自动创建映射,如果CSVMap一栏为空的话,会由它来产生映射字符串
function ExtractWord(Item: Integer;S, WordDelim: string): string;//提取文本数据字符串/映射字符串中的某一字段
function WordCount(const S ,WordDelim: string): Integer;//计算文本数据字符串/映射字符串中的字段数目
function WordPosition(Item: Integer; const S, SubStr: string): Integer;//计算子字符串在字符串中的位置
public
constructor Create(AOwner: TComponent); override;
published
//properties
property Dataset : TDataset read FDataset write FDataset;
//设置要导入或导出的目标数据集.
property CSVMap : string read FCSVMap write FCSVMap;
//CSV 文本数据文件到数据库字段值的映射字符串.控件通过该映射决定文本中的哪些数据要导入及要导入哪个字段.
property CSVFile : string read FCSVFile write FCSVFile;
//CSV 文件格式,其实就是文本数据文件。CSV代表什么意思?呵呵,我也不知道
property Seprator : Char read FSeprator write FSeprator;
//分隔符,可以是空格,也可以是,、;、#等符号
property FieldIndicator : Char read FFieldIndicator write FFieldIndicator;
//字段标识符.
property AutoOpen : Boolean read FAutoOpen write FAutoOpen;
//将AutoOpen设为True可以在处理数据前自动打开要导入的数据表并在操作完毕后自动关掉它。
property IgnoreString : string read FIgnoreStr write FIgnoreStr;
//忽略纪录的标识串.
//举例来说
//IgnoreString:='(ignore)';
//CSVMap:='$Name,(ignore),$Age';
//在这种情况下,CSVToDataSet方法,即导入数据方法将忽略文本文件中的第二列的字段。
property Delimiter : Char read FDelimiter write FDelimiter;
//在某些CSV文档中标识字符串纪录的标识符,比如,"john","boy",12中的",在这种情况下,TPgCSV
//会忽略这些标识符。
property EmptyTable : Boolean read FEmptyTable write FEmptyTable;
//只在从数据库导出(DataSetToCSV)方法中有效,作用是创建一个新的CSV文件。
property UseDelimiter : Boolean read FUseDelimiter write FUseDelimiter;
//是否有Delimiter。
property SilentExport : Boolean read FSilentExport write FSilentExport;
//若该属性为True,应用程序将不显示数据操作时的异常,而将异常信息通过一个接口传给程序员处理.
property DateFormat : string read FDateFormat write FDateFormat;
//设定CSV文件中日期数据的格式。
property TrimData : Boolean read FTrimData write FTrimData;
//是否去掉数据头尾的空格.
property DefaultInt : Integer read FDefaultInt write FDefaultInt;
//整/实形数据转换出错后的默认值
property BufferSize : LongInt read FBufferSize write FBufferSize;
//CSV 文件的缓冲值,以字节为单位,可以加快导入和导出数据的速度。
//events
property BeforeOpenTable : TNotifyEvent read FBeforeOpenTable write FBeforeOpenTable;
property AfterOpenTable : TNotifyEvent read FAfterOpenTable write FAfterOpenTable;
property BeforeCloseTable : TNotifyEvent read FBeforeCloseTable write FBeforeCloseTable;
property AfterCloseTable : TNotifyEvent read FAfterCloseTable write FAfterCloseTable;
property BeforeEmptyTable : TNotifyEvent read FBeforeEmptyTable write FBeforeEmptyTable;
property AfterEmptyTable : TNotifyEvent read FAfterEmptyTable write FAfterEmptyTable;
property BeforeImport : TNotifyEvent read FBeforeImport write FBeforeImport;
property AfterImport : TNotifyEvent read FAfterImport write FAfterImport;
property BeforeExport : TNotifyEvent read FBeforeExport write FBeforeExport;
property AfterExport : TNotifyEvent read FAfterExport write FAfterExport;
property ExportProgress : TPgCSVProgressEvent read FExportProgress write FExportProgress;
//进程监控事件。每完成一条文本数据的导入就触发该事件。
property ImportProgress : TPgCSVProgressEvent read FImportProgress write FImportProgress;
property OnAddRecord : TNotifyEvent read FOnAddRecord write FOnAddRecord;
property ExportError : TPgCSVExportErrorEvent read FExportError write FExportError;
//发生异常时交由该事件处理,异常信息通过该接口传给程序员。
//methodes
//整个控件的核心内容
procedure CSVToDataset;//将文本导入到数据集的方法
procedure DatasetToCSV;//将数据集的数据导入到文本的方法
end;
procedure Register;
{略}
implementation
{略}
end.
从声明部分中我们可以看到,TPgCSV将文本数据的导入导出全部封装到了CSVToDataSet(文本数据导入)和DataSetToCSV(文本数据导出)两个方法中.开发者可以在设计阶段直接将文本文件同要导入/出的数据库相连,然后在程序运行当中调用这两个方法就可以了,根本不用理会那些函数什么的,相当的方便.
那我是不是可以直接拿来使用呢?这里就出现问题了.
----问题一及解决方案
在该控件自带的Demo中,所使用的文本数据文件,格式如下
test.csv
"11","12","13","14"
"21","22","23","24"
"31","32","33","34"
...
而ISO文件中的数据格式(部分)为
sm01632.ISO
... a7507310175b特精装d¥1893"‑1 a毛泽东珍品典藏(上、下册)f中共中央文献研究室编著‑c中献d011200"...
...a7119029193b平装 d¥20"‑1 a中国:加入WTO与经济改革f王 梦 奎编著‑ c外文d011200"‑ ...
我们看到,同test.csv相比,sm01632.ISO文件中的纪录很不规则。
我们所需要的是这样的纪录
sm01632.ISO
... 7507310175,特精装,1893,1,毛泽东珍品典藏(上、下册),中共中央文献研究室编著,中献,011200...
...7119029193,平装,20,1,中国:加入WTO与经济改革,王 梦 奎编著,外文,011200‑ ...
TPgCSV能否对这种情况进行自动的处理呢?哇噻,这种万能的控件好像不大可能有吧!(事实上也不需要)既然直接用TPgCSV处理无法实现正确导入的,我们就需要在每一条从文件中读取出来的纪录导入数据库之前,用程序对它们进行处理,转换之后再交由TPgCSV进行操作。怎么样处理这里就不赘述了。我们所关心的是TPgCSV有没有开放出这样一个处理的接口呢?
我们来看DataSetToCSV方法的实现代码(主要是注释部分):
procedure TPgCSV.CSVToDataSet;
var
RecordString,
Temp : string;
i : Integer;
C : LongInt;
D : Boolean;
F : Real;
ErrorResponse : TPgCSVErrorResponse;
Buffer : Pointer;
begin
//create field cache
FFieldCache:=TList.Create;
//initiate map items
FMapItems:=0;
//allocate buffer size
GetMem(Buffer,FBufferSize);
//assign and open CSV file
AssignFile(FFile,FCSVFile);
SetTextBuf(FFile,Buffer^,FBufferSize);
Reset(FFile);
//open table if nessecary
if FAutoOpen then
begin
if Assigned(FBeforeOpenTable) then
FBeforeOpenTable(Self);
FDataset.Open;
if Assigned(FAfterOpenTable) then
FAfterOpenTable(Self);
end;
//export to table from CSV file
if Assigned(FBeforeExport) then
FBeforeExport(Self);
//set the counter to zero
C:=0;
Temp:=ShortDateFormat;
ShortDateFormat:=FDateFormat;
{*************************以下是文本数据导入的核心代码部分,也是我要关心的部分**************************}
FDataset.DisableControls;
while (not Eof(FFile)) and (not FStop) do
begin
//read from CSV
Readln(FFile,RecordString);
//注意,这里好像差了一点什么东西
//add new record
try
FDataset.Append;
for i:=1 to CountMapItems do
if Uppercase(GetMapItem(i,D)) <> Uppercase(FIgnoreStr) then
case FDataset.FieldByName(GetMapItem(i,D)).DataType of
ftInteger:
FDataset.FieldByName(GetMapItem(i,D)).AsInteger:=StrToIntDef(Trim(GetCSVRecordItem(i,RecordString)),FDefaultInt);
ftFloat:
begin
try
F:=StrToFloat(Trim(GetCSVRecordItem(i,RecordString)));
except
F:=FDefaultInt;
end;
FDataset.FieldByName(GetMapItem(i,D)).AsFloat:=F;
end;
else
if FTrimData then
FDataset.FieldByName(GetMapItem(i,D)).AsString:=Trim(GetCSVRecordItem(i,RecordString))
else
FDataset.FieldByName(GetMapItem(i,D)).AsString:=GetCSVRecordItem(i,RecordString);
end;
//post record
FDataset.Post;
except
on E:Exception do
if not FSilentExport then
raise
else
if Assigned(FExportError) then
begin
FExportError(Self,E.Message,C,ErrorResponse);
if ErrorResponse = pgcsvAbort then
Break;
end;
end;
if Assigned(FOnAddRecord) then
FOnAddRecord(Self);
if Assigned(FExportProgress) then
FExportProgress(Self, C, FStop);
Inc(C);
end;
FDataset.EnableControls;
{*************************以上是文本数据导入的核心代码部分**************************}
if Assigned(FAfterExport) then
FAfterExport(Self);
//close table if nessecary
if FAutoOpen then
begin
if Assigned(FBeforeCloseTable) then
FBeforeCloseTable(Self);
FDataset.Close;
if Assigned(FAfterCloseTable) then
FAfterCloseTable(Self);
end;
//close CSV file
CloseFile(FFile);
//disallocate buffer
FreeMem(Buffer);
ShortDateFormat:=Temp;
//free cache
for i:=FFieldCache.Count - 1 downto 0 do
Dispose(FFieldCache.Items[i]);
FFieldCache.Free;
end;
(这么长!看得我眼都花了.好在找到了我所关心的核心代码,其他的?别管了吧,我这个懒惰的人.)差的是什么呢?很明显,我们希望把这个时候的RecordString开放出去,经过处理之后再回来进行导入的操作.所以,这里差的只是一个事件处理过程,在这个过程中,我们需要通过Delphi把RecordString的值传出去让用户处理.
既然没有提供这个接口,那就自己动手添加了。由于需要传参数,这个事件不能用标准的TNotifyEvent来定义,而需要重新声明。
新的事件声明和事件属性如下:
type
...
TPgCSVRegulateStrEvent = procedure (Sender : TObject; var ARecordString: string) of object;
...
TPgCSV = class(TComponent)
Published
property RegulateString : TPgCSVRegulateStrEvent read FRegulateString write FRegulateString;
//写完后别忘了按一下ctrl+shift+c
...
End;
好了,现在可以在我刚才注释的地方写事件调用方法的程序了.
...
while (not Eof(FFile)) and (not FStop) do
begin
//read from CSV
Readln(FFile,RecordString);
//xm4014's modification
if Assigned(FRegulateString) then
FRegulateString(self,RecordString);
//add new record
try
FDataset.Append;
...
重新编译包文件通过后,你就会在TPgCSV控件的事件页面中发现RegulateString这个事件,双击它就可以添加你的处理代码了。这样一来,无论文本数据中有什么样怪异的字符或格式,我们都通过这个事件处理预先过滤一编,让控件放心的处理导入的操作。简单吧(也太简单了点,居然还写了这么长!汗)
但是,这样就行了吗?