ルールの複雑さによって異なります。唯一の変化する入力が列の名前と使用されるセパレーターである場合、それは非常に簡単ですが、完全に異なる形式 (XML など) も解析できるようにしたい場合は、別の話です。
私自身は、ファイルからレコードを読み取り、データセットまたは CSV に出力する「レコード」リーダーの基本クラスを実装することを選択します。次に、さまざまなソース形式の読み取りを実装する子クラスを実装できます。
必要に応じて、これらの形式に特定のルールを追加して、BaseReader から派生した汎用 XMLReader を作成できますが、これにより、構成可能な列名が可能になります。しかし、あなたが遭遇するかもしれないそれらのフォーマットのどの方言がより明確になるまで、私はあなたが得たフォーマットのハードコードされたリーダーの束から始めます.
編集:リクエストに応じて、どのように見えるかの例。
この例は理想とはかけ離れていることに注意してください。カスタム形式を読み取り、それを 1 つの特定のテーブル構造に転送して、それを CSV ファイルとして保存します。別のテーブル構造にコードを再利用できるように、もう少し分割することもできます。特にフィールド定義は、子孫クラスまたはファクトリ クラスで設定できるようにしたい場合があります。しかし、単純にするために、私はより厳密なアプローチを採用し、1 つの基底クラスに多すぎるインテリジェンスを配置しました。
基本クラスには、メモリ内データセットを作成するために必要なロジックがあります (私は TClientDataSet を使用しました)。ファイルを「移行」できます。実際には、これはファイルを読み取り、検証し、エクスポートすることを意味します。
読み取りは抽象的であり、子クラスで実装する必要があります。インメモリ データセットにデータを読み取る必要があります。これにより、クライアント データセットで必要なすべての検証を行うことができます。これにより、データベース/ファイル形式に依存しない方法で、必要に応じてフィールドの型とサイズを強制し、追加のチェックを行うことができます。
検証と書き込みは、データセット内のデータを使用して行われます。ソース ファイルがデータセットに解析された瞬間から、ソース ファイル形式に関する知識はもう必要ありません。
宣言: を使用することを忘れないでくださいDB, DBClient
。
type
TBaseMigrator = class
private
FData: TClientDataset;
protected
function CSVEscape(Str: string): string;
procedure ReadFile(AFileName: string); virtual; abstract;
procedure ValidateData;
procedure SaveData(AFileName: string);
public
constructor Create; virtual;
destructor Destroy; override;
procedure MigrateFile(ASourceFileName, ADestFileName: string); virtual;
end;
実装:
{ TBaseReader }
constructor TBaseMigrator.Create;
begin
inherited Create;
FData := TClientDataSet.Create(nil);
FData.FieldDefs.Add('ID', ftString, 20, True);
FData.FieldDefs.Add('Name', ftString, 60, True);
FData.FieldDefs.Add('Phone', ftString, 15, False);
// Etc
end;
function TBaseMigrator.CSVEscape(Str: string): string;
begin
// Escape the string to a CSV-safe format;
// Todo: Check if this is sufficient!
Result := '"' + StringReplace(Result, '"', '""', [rfReplaceAll]) + '"';
end;
destructor TBaseMigrator.Destroy;
begin
FData.Free;
inherited;
end;
procedure TBaseMigrator.MigrateFile(ASourceFileName, ADestFileName: string);
begin
// Read the file. Descendant classes need to override this method.
ReadFile(ASourceFileName);
// Validation. Implemented in base class.
ValidateData;
// Saving/exporting. For now implemented in base class.
SaveData(ADestFileName);
end;
procedure TBaseMigrator.SaveData(AFileName: string);
var
Output: TFileStream;
Writer: TStreamWriter;
FieldIndex: Integer;
begin
Output := TFileStream.Create(AFileName,fmCreate);
Writer := TStreamWriter.Create(Output);
try
// Write the CSV headers based on the fields in the dataset
for FieldIndex := 0 to FData.FieldCount - 1 do
begin
if FieldIndex > 0 then
Writer.Write(',');
// Column headers are escaped, but this may not be needed, since
// they likely don't contain quotes, commas or line breaks.
Writer.Write(CSVEscape(FData.Fields[FieldIndex].FieldName));
end;
Writer.WriteLine;
// Write each row
FData.First;
while not FData.Eof do
begin
for FieldIndex := 0 to FData.FieldCount - 1 do
begin
if FieldIndex > 0 then
Writer.Write(',');
// Escape each value
Writer.Write(CSVEscape(FData.Fields[FieldIndex].AsString));
end;
Writer.WriteLine;
FData.Next
end;
finally
Writer.Free;
Output.Free;
end;
end;
procedure TBaseMigrator.ValidateData;
begin
FData.First;
while not FData.Eof do
begin
// Validate the current row of FData
FData.Next
end;
end;
子クラスの例: TIniFileReader は、inifile セクションをデータベース レコードであるかのように読み取ります。ご覧のとおり、ファイルを読み取るためのロジックを実装するだけで済みます。
type
TIniFileReader = class(TBaseMigrator)
public
procedure ReadFile(AFileName: string); override;
end;
{ TIniFileReader }
procedure TIniFileReader.ReadFile(AFileName: string);
var
Source: TMemIniFile;
IDs: TStringList;
ID: string;
i: Integer;
begin
// Initialize an in-memory dataset.
FData.Close; // Be able to migrate multiple files with one instance.
FData.CreateDataSet;
// Parsing a weird custom format, where each section in an inifile is a
// row. Section name is the key, section contains the other fields.
Source := TMemIniFile.Create(AFileName);
IDs := TStringList.Create;
try
Source.ReadSections(IDs);
for i := 0 to IDs.Count - 1 do
begin
// The section name is the key/ID.
ID := IDs[i];
// Append a row.
FData.Append;
// Read the values.
FData['ID'] := ID;
FData['Name'] := Source.ReadString(ID, 'Name', '');
// Names don't need to match. The field 'telephone' in this propriety
// format maps to 'phone' in your CSV output.
// Later, you can make this customizable (configurable) if you need to,
// but it's unlikely that you encounter two different inifile-based
// formats, so it's a waste to implement that until you need it.
FData['Phone'] := Source.ReadString(ID, 'Telephone', '');
FData.Post;
end;
finally
IDs.Free;
Source.Free;
end;
end;