【问题标题】:How can I read and write CSV in a way similar to NET FileHelpers?如何以类似于 NET FileHelpers 的方式读写 CSV?
【发布时间】:2011-06-18 18:23:09
【问题描述】:

任何人都知道如何以类似于 NET FileHelpers 的方式导入/导出 csv、txt 文件,但使用 Delphi,将空格和引号考虑在内,并以类似于 CSV 转义的方式处理传统的 CSV 转义规则Excel?

参考。链接http://www.filehelpers.com/

如果你的答案往往是:“为什么这个懒惰的家伙不写一个简单的 CSV 解析器”,考虑这 5 分钟的阅读,然后你就会知道为什么 CSV 解析不是微不足道的:

http://secretgeek.net/csv_trouble.asp

【问题讨论】:

  • 其实任务分为两部分:首先写一个分词器来计算引号和空格(扔掉正则表达式,大多数时候没有它们你会活得更轻松),然后使用分词器来解析CSV 逐行。编写一个正确而完整的分词器花了我一个小时左右。
  • 并且导出更加容易。我最近编写了自己的导出器,目标是高性能。
  • 为什么你链接页面咆哮那些明显的事情?与在互联网上寻求随机建议相比,实际调查问题所花费的时间更少。
  • 导出绝对比解析容易。对于任何认为 CSV 解析微不足道的人来说;把你的 CSV 解析器发给我,我会告诉你十个真实世界的 CSV 输入文件,它们会破坏你的解析器。
  • @WarrenP 我们希望保持 Stack Overflow 干净且没有垃圾邮件。推荐问题会吸引垃圾邮件。

标签: delphi csv import export


【解决方案1】:

我为 Jedi 项目编写了一个名为 TJvCsvDataSet 的数据集(类似 TTable 的对象),它遵循所有 CSV 解析规则,其方式类似于 Excel 以及各种导入和导出 CSV 的数据库和报告工具使用的 CSV 解析规则。

您可以安装 JVCL,在表单上放置一个 TJvCsvDataSet。

它还包含一个流类,该类将非常快速地将文件加载到磁盘上,并使用 CSV 文件所需的正确转义规则逐行解析它,即使是包含在其中编码的回车/换行代码的文件一个字段。

您只需将其放在表单上,​​然后像这样设置 FieldDefs 属性:

CsvFieldDef=ABC:%,DEF:#,GHI:$,....

整数、浮点数、iso 日期时间和其他字段有特殊代码。它甚至允许您将宽字符串字段映射到 CSV 文件中的 utf8 字段。

有一个设计时属性编辑器可以让您不必使用上述语法声明 CSV 字段定义,而是您可以直观地选择列类型是什么。

如果您不设置 CSV 字段定义,它只会将文件中存在的任何内容映射到字符串类型字段。

Jedi JVCL: http://jvcl.delphi-jedi.org/

JvCsvDataSet 文档:

http://help.delphi-jedi.org/unit.php?Id=3107

http://help.delphi-jedi.org/item.php?Id=174896

【讨论】:

  • P,是否能够或容易实现“N 行”布局。我的意思是第一行有一个布局,第二行有一个布局,就像一个主细节?
  • 组件可以容忍缺失的字段和值。就目前而言。您可以拥有一个 CSV 数据库,并且可以继续向其中添加字段,它将加载在添加该字段之前创建的文件。它甚至不在乎您是否对字段重新排序。它非常聪明。但这不是魔术,而且不会在每一行上都有完全不同的数据集,不会。
  • 如果你想在每一行上实现不同的布局,你不会想要一个 TTable 组件。但也许你会发现很容易从获取我的流类和我的 CSV-splitter 类开始,然后将它们包装成你想要做的任何事情,这不是 csv,而是分隔文本。跨度>
  • 不错的组件沃伦。当然非常快速和可靠。只是好奇-如何优化内存使用?仅仅通过定义字段?或者是否有 TextStreamBuffer 的最佳值(计算方式)? (D7)
  • @WarrenP 我尝试使用此组件,但它在包含双引号的字段上阻塞(用测试用例报告)。无论如何,它看起来相当不错。
【解决方案2】:

这是非常基本的,但TStringList 具有Delimiter、DelimitedText 和QuoteChar 属性,它们可以解决其中的一些问题。

更新添加,每个 cmets:不要被 CommaText 属性所诱惑,它在向后兼容古老的 Delphi 版本方面有一些令人惊讶的限制。

【讨论】:

  • @Craig 我正在阅读帮助文件,其中指出:“分配 DelimitedText 时,该值被解析为 SDF 格式的文本。对于 SDF 格式,字符串由分隔符字符或 空格”,我的重点。但后来它指出“如果 StrictDelimiter 设置为 false,则空格字符也被解释为分隔符,无论 Delimiter 的值如何。当空格字符出现在引号之间时,这不是真的。”这个 StrictDelimiter 属性并不总是存在,这可以解释我的误解和随之而来的错误 cmets。
  • @Craig 我修正了你的回答中的一个错字,因此也给了我一个机会来删除我的反对票。
  • @Craig 实际上我不确定 StrictDelimiter 是否相关。我想我过去只是被 CommaText 引诱,我对它的失望影响了我对 TStringList 能够做到这一点的看法。请接受我的道歉。反对票转换为赞成票!
  • @David 确实,CommaText 和 DelimitedText 有(令人惊讶的)不同的规则,我可能应该在我的回答中指出这一点。
  • 尝试使用 TStringList 解析非平凡的现实世界 CSV 文件最终会令人沮丧。使用一个专门的类,比如下面的 Misha 帖子,或者我写的(JVCL JvCsvDataSet)。 TStringList 无法处理 csv 文件中嵌入的 cr-lf esaped,因此建议在现实世界中使用是不安全的。
【解决方案3】:

我的框架在 CsiTextStreamsUnt.pas 文件中有这方面的代码(参见 http://www.csinnovations.com/framework_delphi.htm)

【讨论】:

  • 它是否处理嵌入在 csv 行中的回车换行符?
  • 大约一年前我有意删除了这个功能,因为没有标准的方法来处理文件读取和写入的多行记录。相反,我引入了一个可覆盖的方法:
  • 再试一次:大约一年前,我故意删除了这个功能,因为没有标准的方法来处理跨越多行的读取和写入记录(实际上没有必要尝试)。相反,我引入了两种可覆盖的方法,一种用于读取,一种用于写入,使开发人员能够添加他们需要的任何类型的多行记录处理。
  • 同意,Misha,没有一种标准的方式,但有一种最常见的方式。这就是我所说的“事实上的”规则。无论 Excel 做什么。所以,也许你可以启用“excel flag”。
  • 好点,虽然鉴于我从未在任何实际场景中使用过它,但我将它置于 YAGNI 原则之下,如果我确实需要它,我将覆盖我的方法。我至少有 20,000 LOC 用于实现过去 10 年我在实时应用程序中实际使用的实用程序和类 - 添加我可能不需要的东西将是太多的努力;-)
【解决方案4】:

这是我编写的一些读取 CSV 文件的代码,它也处理引号内的回车。

unit CSV;

interface
uses
  SysUtils, Generics.Collections, IOUtils;

type
  TParseState = (psRowStart, psFieldStart, psUnquotedFieldData,
    psQuotedFieldData, psQFBranch, psEndOfQuotedField, psQFEndSearch,
    psEndOfLine, psEndOfFile);

  TCSVField = class
  strict private
    FText: String;
  public
    constructor Create;
    destructor Destroy; override;
    property Text: string read FText write FText;
    procedure Clear;
  end;

  TCSVFieldList = class(TObjectList<TCSVField>)
  public
    function AddField(const AText: string): TCSVField;
    procedure ClearFields;
  end;

  TCSVRow = class
  strict private
    FFields: TCSVFieldList;
  public
    constructor Create;
    destructor Destroy; override;
    property Fields: TCSVFieldList read FFields;
  end;

  TCSVParser = class
  strict private
    FRow: TCSVRow;
    FContent: String;
    FCIdx: Integer;
    FParseState: TParseState;
    FEOF: Boolean;
    procedure ParseRow;
  public
    function First: Boolean;
    function EOF: Boolean;
    function Next: Boolean;
    procedure OpenFile(AFileName: String);
    procedure OpenText(const AText: string);
    property Row: TCSVRow read FRow;
    constructor Create;
    destructor Destroy; override;
  end;

implementation



{implementation of TCSVField}

procedure TCSVField.Clear;
begin
  FText:= '';
end;

constructor TCSVField.Create;
begin
  inherited Create;
end;

destructor TCSVField.Destroy;
begin
  inherited Destroy;
end;

{implementation of TCSVRow}

constructor TCSVRow.Create;
begin
  inherited Create;
  FFields:= TCSVFieldList.Create;
end;

destructor TCSVRow.Destroy;
begin
  FreeAndNil(FFields);
  inherited Destroy;
end;

{implementation of TCSVParser}

constructor TCSVParser.Create;
begin
  inherited Create;
  FRow:= TCSVRow.Create;
  FCIdx:= 1;
  FParseState:= psEndOfFile;
end;

destructor TCSVParser.Destroy;
begin
  FreeAndNil(FRow);
  inherited Destroy;
end;



function TCSVParser.EOF: Boolean;
begin
  Result:= FEOF;
end;

function TCSVParser.First: Boolean;
begin
  FEOF:= False;
  FCIdx:= 1;
  FParseState:= psRowStart;
  Result:= Next;
end;

function TCSVParser.Next: Boolean;
begin
  if not EOF then
    ParseRow;
  Result:= not EOF;
end;

procedure TCSVParser.OpenFile(AFileName: String);
begin
  OpenText(TFile.ReadAllText(AFileName));
end;

procedure TCSVParser.OpenText(const AText: string);
begin
  FContent:= AText;
  FRow.Fields.Clear;
  First;
end;

procedure TCSVParser.ParseRow;
var
  FieldIdx: Integer;

  procedure AddField(const AText: string);
  begin
    if FieldIdx > FRow.Fields.Count-1 then
      FRow.Fields.AddField(AText)
    else
      FRow.Fields[FieldIdx].Text:= AText;

    Inc(FieldIdx);
  end;

var
  FieldText: string;
  Curr: Char;
  LastIdx: Integer;
begin
  if FParseState = psEndOfFile then
  begin
    FEOF:= True;
    FRow.Fields.ClearFields;
    Exit;
  end;

  if not (FParseState in [psRowStart]) then
    raise Exception.Create('ParseRow requires ParseState = psRowState');

  FieldIdx:= 0;
  FRow.Fields.ClearFields;
  LastIdx:= Length(FContent);
  while True do
  begin
    case FParseState of
      psRowStart:
        begin
          if FCIdx > LastIdx then
          begin
            FEOF:= True;
            FParseState:= psEndOfFile;
          end
          else
          begin
            FParseState:= psFieldStart;
          end;
          Dec(FCIdx); // do not consume
        end;
      psFieldStart:
        begin
          FieldText:= '';
          if FContent[FCIdx] = '"' then
            FParseState:= psQuotedFieldData
          else
          begin
            FParseState:= psUnquotedFieldData;
            Dec(FCIdx); // do not consume
          end;
        end;
      psUnquotedFieldData:
        begin
          if FCIdx > LastIdx then
          begin
            AddField(FieldText);
            FParseState:= psEndOfFile;
          end
          else
          begin
            Curr:= FContent[FCIdx];
            case Curr of
              #13, #10:
                begin
                  AddField(FieldText);
                  FParseState:= psEndOfLine;
                end;
              ',':
                begin
                  AddField(FieldText);
                  FParseState:= psFieldStart;
                end;
            else
              FieldText:= FieldText + Curr;
            end;
          end;
        end;
      psQuotedFieldData:
        begin
          if FCIdx > LastIdx then
            raise Exception.Create('EOF in quoted Field.');

          Curr:= FContent[FCIdx];
          if Curr = '"' then
            FParseState:= psQFBranch
          else
            FieldText:= FieldText + Curr;
        end;
      psQFBranch:
        begin
          Curr:= FContent[FCIdx];
          if Curr = '"' then
          begin
            FieldText:= FieldText + Curr;
            FParseState:= psQuotedFieldData;
          end
          else
          begin
            AddField(FieldText);
            FParseState:= psEndOfQuotedField;
            Dec(FCIdx); // do not consume
          end;
        end;
      psEndOfQuotedField:
        begin
          if FCIdx > LastIdx then
            FParseState:= psEndOfFile
          else
          begin
            Curr:= FContent[FCIdx];
            if CharInSet(Curr, [#13, #10]) then
              FParseState:= psEndOfLine
            else
            begin
              FParseState:= psQFEndSearch;
              Dec(FCIdx); // do not consume
            end;
          end;
        end;
      psQFEndSearch:
        begin
          if FCIdx > LastIdx then
            FParseState:= psEndOfFile
          else
          begin
            Curr:= FContent[FCIdx];
            if CharInSet(Curr, [#13, #10]) then
              FParseState:= psEndOfLine
            else if Curr = ',' then
              FParseState:= psFieldStart;

            // skips white space or other until end
          end;
        end;
      psEndOfLine:
        begin
          if FCIdx > LastIdx then
            FParseState:= psEndOfFile
          else
          begin
            Curr:= FContent[FCIdx];
            if not CharInSet(Curr, [#13, #10]) then
            begin
              FParseState:= psRowStart;
              Break; // exit loop, we are done with this row
            end;
          end;
        end;
      psEndOfFile:
        begin
          Break;
        end;
    end;
    Inc(FCIdx);
  end;
end;


{ TCSVFieldList }

function TCSVFieldList.AddField(const AText: string): TCSVField;
begin
  Result:= TCSVField.Create;
  Add(Result);
  Result.Text:= AText;
end;

procedure TCSVFieldList.ClearFields;
var
  F: TCSVField;
begin
  for F in Self do
    F.Clear;
end;

end.

【讨论】:

    【解决方案5】:

    按照 VCL TXMLTransform 逻辑,我编写了一个 TCsvTransform 类帮助器,它将 .csv 格式结构转换为 / 来自 TClientDataSet。
    有关 TCsvTransform 的更多详细信息,请参阅http://didier.cabale.free.fr/delphi.htm#uCsvTransform。
    注意:我设置了与 Warren 的 TJvCsvDataSet 相同的字段类型符号

    【讨论】:

      【解决方案6】:

      我的功能

      function ParseCSVString(s: string; const delimiter: Char = ','; const enclosure: Char = '"'): TStrings;
      var
          i,len: Integer;
          f: string;
          inQuoted: Boolean;
      begin
          Result := TStringList.Create;
          len := Length(s);
          if len = 0 then Exit;
          //Test,Test;"Test;Test";"Test""Test";;
          f := '';
          inQuoted := False;
          i:=0;
          while i < len do
          begin
              Inc(i);
              if s[i] = enclosure then
              begin
                  if inQuoted and (i<len) and (s[i+1] = enclosure) then
                  begin
                      f := f + '"';
                      i:=i+1;
                  end
                  else
                      inQuoted := not inQuoted;
              end
              else if s[i] = delimiter then
              begin
                  if inQuoted then
                      f := f+s[i]
                  else
                  begin
                      Result.Add(f);
                      inQuoted := false;
                      f := '';
                  end;
              end
              else
                  f := f + s[i];
          end;
          Result.Add(f);
      end;
      
      function EscapeCSVString(s: string; const delimiter: Char = ','; const enclosure: Char = '"'): string;
      var
          i: Integer;
      begin
          Result := StringReplace(s,enclosure,enclosure+enclosure,[rfReplaceAll]);
          if (Pos(delimiter,s) > 0) OR (Pos(enclosure,s) > 0) then
              Result := enclosure+Result+enclosure;
      end;
      

      【讨论】:

        【解决方案7】:

        今天在 CodeProject 上遇到了这个Delphi CSV File and String Reader Classes for Delphi 2009 and later,我还没有尝试过,但是从示例代码来看它有点酷。由 Vladimir Nikitenko 编写,主要类是 TnvvCSVReader。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-11-03
          • 1970-01-01
          • 2012-08-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-12-04
          • 1970-01-01
          相关资源
          最近更新 更多