解析平面文本文件

pio*_*pio 3 delphi

我正在开发一个应用程序,我必须将CSV文件中的数据上传到数据库表中.问题是,我没有CSV文件,但我有平面文本文件要转换为CSV.另一个问题是,由于应用程序由具有不同系统的多个客户使用,因此我使用不同布局的不同平面文本文件.

我想要实现的是创建一个从特殊文件加载"规则"的应用程序; 这些规则将使用平面文本文件进行处理,以生成CSV文件.从平面文件转换为CSV的应用程序将是相同的,只是规则集将是不同的.

我怎样才能做到这一点?你推荐的最佳做法是什么?

Gol*_*rol 7

这取决于规则的复杂性.如果唯一不同的输入是列的名称和使用的分隔符,那么它很容易,但如果你想要能够完全解析不同的格式(如XML等),那么它就是另一回事.

我自己会选择为"记录"阅读器实现一个基类,该阅读器从文件中读取记录并将其输出到数据集或CSV.然后,您可以实现实现读取不同源格式的子类.

如果您愿意,可以为这些格式添加特定规则,这样您就可以创建一个从BaseReader继承的通用XMLReader,但允许使用可配置的列名.但是我会从一堆硬编码的读者开始,为你所获得的格式,直到你可能会遇到哪些格式的方言更清楚.

编辑:根据要求,举例说明它的外观.

注意,这个例子远非理想!它读取自定义格式,将其传输到一个特定的表结构并将其保存为CSV文件.您可能希望将其进一步拆分,以便可以将代码重用于不同的表结构.特别是字段定义,您可能希望能够设置后代类或工厂类.但是为了简单起见,我采取了更严格的方法,并在一个基类中放置了太多的智能.

基类具有创建内存数据集所需的逻辑(我使用了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;
Run Code Online (Sandbox Code Playgroud)

执行:

{ 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;
Run Code Online (Sandbox Code Playgroud)

一个示例子类: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;
Run Code Online (Sandbox Code Playgroud)