【问题标题】:Most appropriate data structure for a CSV table?最适合 CSV 表的数据结构?
【发布时间】:2011-11-18 18:33:19
【问题描述】:

我正在寻找有关在内存中保存 CSV(逗号分隔值)表的最合适数据结构的建议。 它应该涵盖两种情况:有和没有标题的表格。 如果表包含表头,则所有行的所有字段都由键->值对确定,其中键是表头中的名称,值是字段的适当内容。 如果表不包含标题,则行只是字符串列表或键名的键->值对(如“COL1”、“COL2”、...“COLn”)。

我正在同时寻找最简单(更少代码)和最通用的解决方案。

我正在考虑以下子类化,但怀疑它是否是正确/有效的实现方式:

TCSV = class (TObjectList<TDictionary<string, string>>)
  ...
public
  constructor Create(fileName: string; header: Boolean; encoding: string = '';
                     delimiter: Char = ';'; quoteChar: Char = '"'); overload;
  ...
end;

看起来我必须为每一行字段保留键。 TDictionary&lt;string, TStringList&gt; 呢?这会是一个更好的解决方案吗?

【问题讨论】:

  • 在我看来有点像二维数组......
  • 但是数组只能有一个整数索引。他希望能够按列名引用字段。
  • @kenneth 通过建立索引来解决这个问题
  • 您需要使用数据感知控件吗?如果是这样,我的 JvCsvDataSet 将是完美的。

标签: delphi csv


【解决方案1】:

TClientDataset 呢?看起来很简单。

这里只是一个关于如何use TClientDataSet as an in-memory dataset 的简单指南。

【讨论】:

  • 感谢您的回答。如果没有其他人提出更优雅的解决方案/想法,我会接受这个作为答案。
  • 我个人认为kbmMemTable 是内存数据集比 TClientDataSet 更好的选择。
  • @LURD 我自己没用过,但this review 证实了我的怀疑。
  • 主要是因为它得到很好的支持、成熟和大量的示例、文档。
  • ClientDataSet 也是如此。仍然不确定为什么 kbmMemTable 会更好。
【解决方案2】:

您提议的结构意味着您的 csv 文件中的每一行都有一个 TDictionary 实例。本质上为每一行复制列名。好像有点浪费。

假设使用TDictionary&lt;string, TStringList&gt;,您将使用单个列中的值填充每个 TStringList。这可能行得通,但要遍历每行数据的所有列仍然不容易。

正如 GolezTrol 所建议的,TClientDataSet 是 Delphi 的标准配置,非常强大,并且作为一个旨在用于列数据的数据集。此外,虽然它是一个数据集,但它不需要数据库(连接),并且在许多应用程序中用于您想要实现的目标:内存数据集。

【讨论】:

  • 刚刚研究 TClientDataSet 的文档,看起来很有希望。如果这是 Delphi 中的方式,与我来自的 Python 和 Ruby 中的字典/哈希使用相反,我也有点惊讶。我确实期望一些简单的类似哈希的数据类型,而不是像 TClientDataSet 这样复杂的类。
  • 这可能是因为 Delphi 的主要焦点一直是以数据(基础)为中心的业务应用程序。 IIRC Delphi 是第一个使应用程序的数据库访问变得“轻而易举”的 RAD IDE。并且不要被 TClientDataSet 的复杂性所推迟。添加一些字段定义(列),然后“仅”插入行,从您的 csv 读取。在需要之前,您可以忽略所有其他内容。
【解决方案3】:

我建议您尝试 TJvCsvDataSet,它是我编写并贡献给 JEDI JVCL 的。它适用于带和不带标题的 CSV 文件。它适用于数据感知控件,包括 DB Grids。

它解析 CSV 数据,工作方式与其他人建议的客户端数据集完全一样。

在内部,它使用一个字节记录数组并解析每一行并保持一个整数“查找”,以便它知道每个单独的列从该特定行开始的位置。这使得将一个值更改为另一个值(修改一行中的字段)是一项非常快速的操作。

它支持最常见的字段类型(尽管目前不支持 blob 或货币),它解析 CSV 功能,包括字段值内的嵌入式回车 + 换行符,以及嵌入式 CSV“转义码”,以便您可以将双精度例如,字符串中的引号字符。

它有一个名为 FieldDef 的属性,可用于定义列的类型,或者它可以简单地读取文件的标题,并将其中的每个值视为一个字符串(如果你不告诉它) .

它可以通过添加或删除列来修改 CSV,并执行您希望对 CSV 表执行的大多数常见操作。我已经使用它并对其进行了大量测试,并且效果很好。

【讨论】:

    【解决方案4】:

    根据使用情况而不是 TDataSet,您也可以使用Synopse TSynBigTable,它更高效且限制更少。

    对于没有“时间或大小关键”的应用程序,TDataSet 是可以的。

    【讨论】:

      【解决方案5】:

      所以您基本上希望能够访问以下元素:

      for RowNum := 0 to csv.Count - 1 do
      begin
        Name := csv[RowNum]['Name'];
        // Do something
      end;
      

      TObjectList&lt;TDictionary&lt;string, string&gt;&gt; 肯定能胜任,但效率不高。

      将 csv 加载到数据集中可能是最少的代码量,但开销会稍高。

      您可能需要考虑将简单的TstringlistTList&lt;string&gt; 组合用于标头,并将数据分解为一个新类,该类在其构造函数中采用标头列表。你会得到相同的结果:

      TCSVRow = class
      private
        FHeaders: TList<string>;
        FFields: TList<string>;
      public
        constructor(Headers: TList<string>);
        function GetField(index: string): string;
        property Fields[index: string]: string read GetField; default;
      end;
      
      TCSV = class
      private
        FHeaders: TList<string>;
        FRows:TList<TCSVRow>;
      public
        function GetRow(Index: integer):TCSVRow;
        property Rows[index: integer]:TCSVRow read GetRow; default;
      end;
      
      implementation
      
      function TCSVRow.GetField(index: string): string;
      begin
        Result := FFields[FHeaders.IndexOf(index)];
      end;
      
      function TCSV.GetRow(Index:integer):TCSVRow;
      begin
        Result := FRows[Index];
      end;
      

      这是不完整的,我直接在浏览器中输入,所以我没有测试它的正确性,但你明白了。这样,标题信息只存储一次,而不是每行都重复。

      您可以通过将FFields 设为字符串数组而不是TList&lt;string&gt; 来节省一点内存,但TList&lt;string&gt; 更易于使用恕我直言。

      更新 再三考虑David 有一点。 CSVrow 类可以被消除。您可以简单地拥有TList&lt;TList&lt;string&gt;&gt; 或二维数组。无论哪种方式,我仍然认为您应该将标题保存在单独的列表中。在这种情况下,TCSV 看起来更像:

      TCSV = class
      private
        FHeaders: TList<string>;
        FData:TList<TList<string>>;
      public
        function GetData(Row: integer; Column:string):string;
        property Data[Row: integer; Column:string]:string read GetData; default;
      end;
      
      function TCSV.GetData(Row: integer; Column:string):string;
      begin
        Result := FData[Row][FHeaders.IndexOf(Column)];
      end;
      

      【讨论】:

        【解决方案6】:

        对此有许多可能的解决方案。 如果您想要根据您的要求提供非常简单和通用的东西(不一定是最好的解决方案),为什么不...

        TMyRec =
        record
          HeaderNames: array of string;
          StringValues: array of array of string
        end;
        

        只需根据需要设置数组的长度(使用 SetLength)。

        【讨论】:

        • 不幸的是,这也会重复存储每条记录的标题,此外,字段是通过索引号而不是列名访问的。
        • 嗨,大卫。抱歉,这个例子有点小,因为我只描述了结构而不是它背后的意图。它不会重复标题(它只是一个单维数组)。正如您正确指出的那样,StringValues 确实需要通过整数索引访问(这将是 stringValues [recordnumber,fieldnumber],但想法是您会编写一个函数,例如“FieldByName”,它需要一个字符串,扫描 HeaderNames对于 fieldindex,然后以这种方式访问​​数组。此外,您可能希望将其作为类和方法来执行,而不是记录和函数。
        猜你喜欢
        • 2011-05-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多