【问题标题】:How to store dynamic data (unknown number of fields) to a file?如何将动态数据(未知数量的字段)存储到文件中?
【发布时间】:2011-02-11 17:32:26
【问题描述】:

我需要将一些数据存储在一个文件中。目前,每条记录(数据集)包括:

  • 一个字符串(可变长度),
  • 整数数组(可变长度),
  • 字节数组(可变长度),
  • 一些整数值。

将所有这些内容保存在二进制文件中一点也不难。但是,我确定(不幸的是)我的数据格式会及时改变,我希望有可能为每个“记录”添加更多字段。所以,显然我的文件格式无法修复。我想最好的解决方案是将我的数据保存在(DB)表中,但我不想搞砸大枪(SQL,ADO,BDE,Nexus ...)。 我需要一个可以做到这一点的基本库(如果可能的话,单个 PAS 文件)。 既然这样做的目的是存储数据而不是处理数据,那么在没有 DB 表的情况下可以做到吗?

此库的要求:

  • 它需要轻松支持超过 100 万行
  • 真的很轻
  • 如果可能,单个 PAS 文件
  • 强制:易于安装在新机器上(连同它编译的项目)
  • 强制:为了使用它,我不需要重新分发任何东西
  • 强制:为了使用它,用户不必安装/设置东西
  • 可以是免费软件/共享软件
  • 它不必支持 SQL 查询或类似的高级东西

我用 D7

【问题讨论】:

  • 我肯定会为此设计自己的二进制文件格式,但我不确定我是否敢对此做出回答......
  • 此数据的生命周期是多少。在您停止开发工具后,它的消费者会想要使用它吗?如果是这样,那么基于标准的格式将使您的产品更具吸引力。在这种情况下,可能与您无关,但您永远不知道。
  • @david - 用户不必知道这个文件。我在内部使用它。用户将只使用我的程序来处理数据。
  • @Andreas R - 是的,我也是这么想的。我只是假设使用现有的库会使事情变得更容易(似乎这不一定是真的)。​​
  • 我讨厌自定义二进制文件格式。 :-)

标签: delphi


【解决方案1】:

看看our Synopse Big Table单元。

通过最近的升级,它可以完全满足您的需求。

以下是创建字段布局的方法:

var Table: TSynBigTableRecord;
    FieldText, FieldInt: TSynTableFieldProperties;
begin
  Table := TSynBigTableRecord.Create('FileName.ext','TableName');
  FieldText := Table.AddField('text',tftWinAnsi,[tfoIndex]);
  FieldInt := Table.AddField('Int',tftInt32,[tfoIndex,tfoUnique]);
  Table.AddFieldUpdate;

对于存储字节或整数数组,只需使用tftWinAnsi 甚至更好的tftBlobInternal 类型的字段(这是一个真正的可变长度字段),然后将其映射到动态数组或从动态数组映射,就像RawByteString

您以后可以放心地添加字段,数据文件将为您处理。

有几种处理数据的方法,但我实现了一种基于变体的使用记录的方法,具有真正的后期绑定:

var vari: Variant;

  // initialize the variant
  vari := Table.VariantVoid;
  // create record content, and add it to the database
  vari.text := 'Some text';
  vari.int := 12345;
  aID := Table.VariantAdd(vari);
  if aID=0 then
    ShowMessage('Error adding record');
  // how to retrieve it
  vari := Table.VariantGet(aID);
  assert(vari.ID=aID);
  assert(vari.INT=12345);
  assert(vari.Text='Some text');

关于速度,you can't find anything faster IMHO。 在我的笔记本电脑上,使用一些文本和一个整数值创建 1,000,000 条记录,这两个字段都使用索引,并且将整数字段设置为唯一字段不到 880 毫秒。它将使用非常少的磁盘空间,因为所有存储都是可变长度编码的(类似于 Google 的协议缓冲区)。

它只需要两个单元,适用于 Delphi 6 到 XE(并且已经准备好 Unicode,因此使用这个单元,您可以在需要时安全地升级到较新的 Delphi 版本)。无需安装,只需将几 KB 添加到您的可执行文件中。它只是一个用纯 Delphi 编写的小而强大的 NoSQL 引擎,但具有数据库的使用能力(即纯字段布局)和内存引擎的速度,大小没有限制。

而且它是完全开源的,具有许可许可。

请注意,我们还提供了SQLite3 wrapper,但这是另一个项目。速度较慢但功能更强大,具有 SQL 支持和集成的客户端/服务器 ORM。

【讨论】:

  • 我花了很长时间才弄清楚从哪里下载它。
  • @Altar 你有来自博客主页的链接。
  • 另一个想法:我们的 SQLite3 框架可以在没有 SQLite3 引擎的情况下使用。您可以将数据作为 TList 加载到内存中,然后在磁盘上序列化为 JSON 内容。您将拥有我们框架的所有 ORM 功能。定义具有已发布属性的 TSQLRecord 类类型,然后使用 TSQLRestServerStaticInMemory 实例以本地或客户端/服务器方式处理数据。我认为实现的 JSON 解析器/生成器是纯 Delphi 中最快的。在所有情况下,它都比 Delphi 序列化(类似 dfm)或 XML 更快。
【解决方案2】:

使用 Synopse BigTable,http://synopse.info/ a key=>value 数据库,在这种情况下,value 是您数据的序列化(json、二进制、xml、...)。

这是疯狂的快速、轻量级和免费。

【讨论】:

  • 概要是用 Delphi 编写的?
  • 这很有趣,我刚刚写了相同的答案......但更准确,因为我写了这个库,而且我刚刚添加了字段支持。感谢您的关注! :)
  • @A.Bouchez 我把它做成了一个 wiki,随时添加更多信息。
  • 当我还在输入我的(长)答案时,你发布了你的。谢谢! :)
【解决方案3】:

我认为您不需要为此建立数据库。如果您使用数据库,我看不出它如何解决数据结构更改的问题。

我个人会存储为非常容易扩展的 YAML 格式。这需要相当多的工作来链接到一些 LIBYAML,所以一个非常轻量级的替代方案是存储到 INI 文件。这些很容易扩展,同时保持与旧文件的兼容性。

您可以很容易地推出自己的可扩展二进制格式。你所做的就是将每条记录写入一个。每个块都有一个包含其长度的短标题。

当您读取数据时,您会一直读取到块的末尾,然后如果您期望更多数据,您只需停止读取并使用数据的默认值即可。如果您已经阅读了所有您知道的数据但不在块的末尾,则该文件必须来自您的程序的更高版本,您只需跳到块的末尾。也许您警告该文件包含您不知道的数据。

通过始终按照与以前版本相同的顺序写入数据来实现可扩展性。任何新数据都放在每个块的末尾。

【讨论】:

  • YAML、INI、XML 或 JSON 需要使用解析器,对于超过 1,000,000 行的数据可能会很慢。 JSON解析器可以很快,但是在加载过程中创建内存中的所有对象并不是立即的,并且会产生一些内存碎片。
  • @A.Bouchez 我原以为 I/O 会成为瓶颈,即使对于 YAML/JSON/XML 也是如此,但您显然比我知道的要多得多。我不明白为什么使用 YAML/JSON/XML 意味着碎片。听起来你有一个非常好的工具!
  • YAML/INI/XML/JSON有2种使用方式: 1.解析和读取记录一个一个。 2. 加载内存中的所有内容,为每个解析的项目创建一个对象。 1. 可能有利于日志记录机制,但它不是类似 DB 的方法。 2.每个信息节点都会在内存中创建为一个TObject,其中包含字符串,因此它会创建很多小的内存缓冲区......这里是内存碎片。使用 SynBigTable,您只需创建指向二进制内存映射数据的指针,而永远不要复制它。当然,*ML 方法可以与现代硬件一起使用。但它不会很好地扩展。
  • @A.Bouchez 我认为您可以为高度规则的表(如 *ML 数据)制作无碎片解析器,但您需要努力做到这一点。我可以看到,如果您不读取所有 1M 记录,则传输紧凑的二进制文件并按需解包会更有效。这当然是一个非常有趣的话题!不过,我确实喜欢你 BigTable 的声音,即使我不是你 SynGdiPlus 的忠实粉丝!! ;-)
  • @David SynGdiPlus 根本不是一个权威的 GdiPlus 库。它是为任何 TMetaFile 的抗锯齿渲染而设计的。恕我直言,对此功能的错误处理就足够了。至少没有内存和资源泄漏,而且过程非常快。用于生产,没有任何问题(比GDI+1.1更完善)。 GdiPlus 本身有点过时(它缺乏 Gdi 的一些基本功能:没有直接的文本旋转,也没有下划线/罢工,没有 RoundRect...)。而且 Direct2D 仍然存在性能问题(硬件图形驱动程序效率不高)。 AggPas 或 Cairo 可以用作渲染器。
【解决方案4】:

我建议按照您实施的努力程度,按以下顺序:

  1. CSV 或 INI 文件(TMemIniFile 或 TJvCsvDataSet)。这对你来说是最少的工作。您可以在单个文件中支持数百万行,但消耗的内存将是巨大的。我设想了一个“数据编写器”组件来替换我的 TJvCsvDataSet,它只附加记录,而不是将它们加载到内存中。这将允许您写出 CSV 文件,甚至可以逐行读取它们,但不能一次全部加载。这种方法可能适合您。一个简单的 CSV 读取器/写入器类,它不是数据集对象。

  2. 每行一个 XML 标记文件。这比 INI 文件更灵活,可以分层。 INI 文件是非分层的。如果您只是打开一个文件流,并附加一行文本,则不需要 SAX 或 DOM,也就是这种形式,以 cr+lf 结尾:

  3. 列表项

  4. 某种二进制 nosql db,如 bsddb、couchdb 等。

【讨论】:

  • 要解码1000万个数据集,XML是不是很慢?
  • > 并且可以分层- - - 我不知道这是否与我有关。
  • +1 用于 INI 文件。原始发帖人应该测试大型数据集的性能,但如果正如他所说,他将其用于存储而不是处理数据,INI 文件可能足够快。 IMO,它们最容易使用。
  • 我使用 TIniFile 将数据附加到内存文件中,当文件变大时,这比使用 TMemIniFile 快得多。您甚至可以使用 Windows API WritePrivateProfileString,并以这种方式记录数百万行。
  • altar,如果您将您的 XML 写成每条记录一行,而没有任何结束标记怎么办?然后,您永远不必解析 100 万条记录。只需做一个 readline 并解析一行。这是一种自定义的类似 XML 的 CSV 行日志记录系统。
【解决方案5】:

任何时候您需要将可变长度数据存储为二进制格式,您应该将数据的长度存储在实际数据的前面。

由于您以后还需要添加新字段,因此您应该存储每条记录的字段数(或至少在末尾有一个记录结束标记),以便在文件中移动时保持正确的位置读取和查找操作。

至于实际的记录数据,我会建议每个字段的类型-长度-数据格式,这样您就可以在不知道它们的数据类型的情况下提前添加新字段,并允许代码识别和读取/无论内容如何,​​都可以根据需要跳过各个字段(即,如果旧应用尝试读取具有较新字段的文件,它可以跳过它无法识别的内容)。

最后,您将以这样的方式结束,然后您可以根据需要对其进行扩展、优化等:

const
  cTypeUnknown     = $00;
  cTypeString      = $01;
  cTypeInteger     = $02;
  cTypeByte        = $03;

  cTypeArray        = $80;
  cTypeStringArray  = cTypeStringArray or cTypeArray;
  cTypeIntegerArray = cTypeIntegerArray or cTypeArray;
  cTypeByteArray    = cTypeByteArray or cTypeArray;

type
  Streamable = class
  public
    procedure Read(Stream: TStream); virtual; abstract;
    procedure Write(Stream: TStream); virtual; abstract;
  end;

  Field = class(Streamable)
  public
    function GetType: Byte; virtual; abstract;
  end;

  FieldClass = class of Field;

  StringField = class(Field)
  public
    Data: String;
    function GetType: Byte; override;
    procedure Read(Stream: TStream); override;
    procedure Write(Stream: TStream); override;
  end;

  StringArrayField = class(Field)
  public
    Data: array of String;
    function GetType: Byte; override;
    procedure Read(Stream: TStream); override;
    procedure Write(Stream: TStream); override;
  end;

  IntegerField = class(Field)
  public
    Data: Integer;
    function GetType: Byte; override;
    procedure Read(Stream: TStream); override;
    procedure Write(Stream: TStream); override;
  end;

  IntegerArrayField = class(Field)
  public
    Data: array of Integer;
    function GetType: Byte; override;
    procedure Read(Stream: TStream); override;
    procedure Write(Stream: TStream); override;
  end;

  ByteField = class(Field)
  public
    Data: Byte;
    function GetType: Byte; override;
    procedure Read(Stream: TStream); override;
    procedure Write(Stream: TStream); override;
  end;

  ByteArrayField = class(Field)
  public
    Data: array of Byte;
    function GetType: Byte; override;
    procedure Read(Stream: TStream); override;
    procedure Write(Stream: TStream); override;
  end;

  AnyField = class(ByteArrayField)
  public
    Type: Byte;
    function GetType: Byte; override;
  end;

  Record = class(Streamable)
  public
    Fields: array of Field;
    procedure Read(Stream: TStream); override;
    procedure Write(Stream: TStream); override;
  end;

  RecordArray = class(Streamable)
  public
    Records: array of Record;
    procedure Read(Stream: TStream); override;
    procedure Write(Stream: TStream); override;
  end;

procedure WriteByte(Stream: TStream; Value: Byte);
begin
  Stream.WriteBuffer(@Value, SizeOf(Byte));
end;

function ReadByte(Stream: TStream): Byte;
begin
  Stream.ReadBuffer(@Result, SizeOf(Byte));
end;

procedure WriteInteger(Stream: TStream; Value: Integer);
begin
  Stream.WriteBuffer(@Value, SizeOf(Integer));
end;

function ReadInteger(Stream: TStream): Integer;
begin
  Stream.ReadBuffer(@Result, SizeOf(Integer));
end;

procedure WriteString(Stream: TStream; Value: String);
var
  S: UTF8String;
begin
  S := UTF8Encode(Value);
  WriteInteger(Stream, Length(S));
  if Length(S) > 0 then
    Stream.WriteBuffer(S[1], Length(S));
end;

function ReadString(Stream: TStream): String;
var
  S: UTF8String;
begin
  SetLength(S, ReadInteger(Stream));
  if Length(S) > 0 then
    Stream.ReadBuffer(S[1], Length(S));
  Result := UTF8Decode(S);
end;

function StringField.GetType: Byte;
begin
  Result := cTypeString;
end;

procedure StringField.Read(Stream: TStream);
begin
  Data := ReadString(Stream);
end;

procedure StringField.Write(Stream: TStream);
begin
  WriteString(Data);
end;

function StringArrayField.GetType: Byte;
begin
  Result := cTypeStringArray;
end;

procedure StringArrayField.Read(Stream: TStream);
var
  I: Integer;
begin
  SetLength(Data, ReadInteger(Stream));
  for I := 0 to High(Data) do
    Data[I] := ReadString(Stream);
end;

procedure StringArrayField.Write(Stream: TStream);
var
  I: Integer;
begin
  WriteInteger(Stream, Length(Data));
  for I := 0 to High(Data) do
    WriteString(Stream, Data[I]);
end;

procedure IntegerField.GetType: Byte;
begin
  Result := cTypeInteger;
end;

procedure IntegerField.Read(Stream: TStream);
begin
  Assert(ReadInteger(Stream) == SizeOf(Integer));
  Data := ReadInteger(Stream);
end;

procedure IntegerField.Write(Stream: TStream);
begin
  WriteInteger(Stream, SizeOf(Integer));
  WriteInteger(Stream, Data);
end;

function IntegerArrayField.GetType;
begin
  Result := cTypeIntegerArray;
end;

procedure IntegerArrayField.Read(Stream: TStream);
var
  Num: Integer;
begin
  I := ReadInteger(Stream);
  Assert((I mod SizeOf(Integer)) == 0);
  SetLength(Data, I);
  if Length(Data) > 0 then
    Stream.ReadBuffer(Data[0], I * SizeOf(Integer));
end;

procedure IntegerArrayField.Write(Stream: TStream);
begin
  WriteInteger(Stream, Length(Data));
  if Length(Data) > 0 then
    Stream.WriteBuffer(Data[0], Length(Data) * SizeOf(Integer));
end;

procedure ByteField.GetType: Byte;
begin
  Result := cTypeByte;
end;

procedure ByteField.Read(Stream: TStream);
begin
  Assert(ReadInteger(Stream) == SizeOf(Byte));
  Data := ReadByte(Stream);
end;

procedure ByteField.Write(Stream: TStream);
begin
  WriteInteger(Stream, SizeOf(Byte));
  WriteByte(Stream, Byte);
end;

function ByteArrayField.GetType: Byte;
begin
  Result := cTypeByteArray;
end;

procedure ByteArrayField.Read(Stream: TStream);
begin
  SetLength(Data, ReadInteger(Stream));
  if Length(Data) > 0 then
    Stream.ReadBuffer(Data[0], Length(Data));
end;

procedure ByteArrayField.Write(Stream: TStream); override;
begin
  WriteInteger(Stream, Length(Data));
  if Length(Data) > 0 then
    Stream.WriteBuffer(Data[0], Length(Data));
end;

function AnyField.GetType: Byte;
begin
  Result := Type;
end;

procedure Record.Read(Stream: TStream);
const
  PlainTypes = array[1..3] of FieldClass = (StringField, IntegerField, ByteField);
  ArrayTypes = array[1..3] of FieldClass = (StringArrayField, IntegerArrayField, ByteArrayField);
var
  I: Integer;
  RecType, PlainType: Byte;
begin
  SetLength(Fields, ReadInteger(Stream));
  for I := 0 to High(Fields) do
  begin
    RecType := ReadByte(Stream);
    PlainType := RecType and (not cTypeArray);
    if (PlainType >= cTypeString) and (PlainType <= cTypeByte) then
    begin
      if (RecType and cTypeArray) <> cTypeArray then
        Fields[I] := PlainTypes[PlainType].Create
      else
        Fields[I] := ArrayTypes[PlainType].Create;
    end else
      Fields[I] := AnyField.Create;
    Fields[I].Read(Stream);
  end;
end;

procedure Record.Write(Stream: TStream)
var
  I: Integer;
begin
  WriteInteger(Stream, Length(Fields));
  for I := 0 to High(Fields) do
  begin
    WriteByte(Stream, Fields[I].GetType);
    Fields[I].Write(Stream);
  end;
end;

procedure RecordArray.Read(Stream: TStream);
var
  I: Integer;
begin
  SetLength(Records, ReadInteger(Stream));
  for I := High(Records) do
  begin
    Records[I] := Record.Create;
    Records[I].Read(Stream);
  end;
end;

procedure RecordArray.Write(Stream: TStream);
begin
  WriteInteger(Stream, Length(Records));
  for I := High(Records) do
    Records[I].Write(Stream);
end;

【讨论】:

  • 这就是序列化的根源。但是你的提议有点冗长。标准的 TPersistent 序列化会成功。 ORM 会让它变得更好。
  • TPersistent 本身不会序列化任何内容。它只是为 DFM 流媒体系统完成其工作打开了大门。我更喜欢编写自定义序列化代码,因为它在数据格式上提供了最大的灵活性,并且可以根据需要针对特定​​数据类型进行优化。
  • 你说得对,TPersistent 为已发布的属性启用了 RTTI。这是可序列化的直接子 TComponent。关于编写自己的序列化代码,这当然是个人喜好问题。但是手动序列化往往有点冗长。多年来我不这样做了。
【解决方案6】:

我可以想到每个“记录”有一个 ini 或 XML 文件,INI 文件存储在像我们的 SolFS 这样的虚拟文件系统中。但是,我不知道您所说的“轻松支持 100 万行”是什么意思,即必须支持哪些操作。如果您主要计划随机访问一些较少数量的记录,那么解析文本文件并不是什么大问题。在其他情况下,您可能希望查看一些二进制格式,例如二进制 XML。我可以说我们的另一个产品 MsgConnect 的 TMCDataTree 类支持以二进制格式保存的分层 ini 文件。

【讨论】:

  • “轻松支持100万行” - - - 意思是允许我存储超过100万个数据集/记录。那么问题来了:当您要解码 100 万(或 10 个)数据集时,XML 是不是很慢?
  • @Altar “当您要解码 100 万(或 10 个)数据集时,XML 不是很慢吗?” - 那是 问题。如果您需要一次阅读它们,那么它会很慢,任何文本格式(可能是大多数二进制格式)也是如此。如果您只需要保持 100 万可用,并偶尔阅读它们,那么文本文件就可以了。这就是我在答案中提到的。
猜你喜欢
  • 2014-05-13
  • 2011-03-10
  • 1970-01-01
  • 1970-01-01
  • 2014-10-05
  • 2020-11-05
  • 2014-04-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多