如何正确地从大型二进制文件中读取/写入随机块?

How to read/write random chunks from large binary files properly?

提问人:Does Deos 提问时间:6/27/2023 最后编辑:Does Deos 更新时间:6/27/2023 访问量:185

问:

我正在编写一个用于处理二进制文件的库。特别是“日志信息标准 (LIS) 79 子集”,它具有各种类型的记录和各种数据类型的条目。每个条目可以是单个值、数组或具有更复杂的结构。文件大小可能从 3-5 KB 到几 GB 不等。

目标:读取和修改任何大小的文件的任何部分。

尝试过的内容:

  • 首先实现只是读取整个文件,然后将每个记录和数据类型及其缓冲区写入相应的类实例。对于小文件来说,它非常好,但是当文件大小大于 1GB 时,速度非常慢且 RAM 饥饿。
  • 然后我试图完全停止使用缓冲区。读取每个组件的文件、存储的偏移量、大小和类型。这种方法在阅读方面效果很好,但正如我在经过一些研究后了解到的那样,没有办法在文件的随机位置插入数据。

那么问题来了,如何在不过度使用RAM的情况下正确处理这些数据?

C# IO 二进制文件

评论

0赞 Fildor 6/27/2023
但是,如果您坚持,请查看内存映射文件
0赞 jdweng 6/27/2023
最好的方法是一次读取一个字段。二进制数据,字段大小不一致,因此您必须像第二种方法一样阅读。写入时,唯一的保证方法是解析整个数据,然后写入。阅读ZIP规范将提供其他解决方案。zip 文件具有目录,您可以将文件添加到结构中而无需读取所有内容。但是ZIP是一种特殊类型的二进制文件。
0赞 Fildor 6/27/2023
等等,所以这实际上是关于特定的文件类型?然后,当然,您必须实现该文件类型的规范。这意味着,DB 不在窗口。这些信息对这个问题非常有帮助。
0赞 Matthew Watson 6/27/2023
若要将数据插入二进制文件,可以使用两个文件。将所有数据从源文件复制到插入点到目标文件。然后将要插入的数据写入目标文件。然后将源文件的其余部分写入目标文件。然后删除源文件,并将目标文件重命名为源文件名。
0赞 Matthew Watson 6/27/2023
请注意,在此方法中,临时目标文件应与原始文件位于同一卷上,这一点很重要,否则在过程结束时重命名它实际上会再次复制所有数据。

答:

1赞 JonasH 6/27/2023 #1

我对LIS文件一无所知,所以这将是关于二进制文件的一般内容。

许多二进制文件格式将具有某种类型的索引,以及实际的数据条目本身。因此,读取文件将包括扫描索引,直到找到要查找的内容,然后跳转到索引中指定的偏移量。索引可以在文件开头的区块中定义,也可以作为链表定义,分布在整个文件中。实际格式可能会复杂得多,但它可能作为一个简化的心智模型有用。

如果您知道格式,则只需使用 BinaryReader 读取值,然后相应地在文件中跳转。可能使用某种状态机来跟踪您正在阅读的内容。

在文件的随机位置插入数据

这真的很难做好。您将不得不在浪费空间、移动数据和碎片之间做出选择。数据库花费了大量精力试图在每个极端之间找到一个快乐的媒介。

但是,如果您使用的是现有格式,则可以选择适合您的格式。如果该格式不是为廉价插入而设计的,您可能需要移动文件中的绝大多数数据,本质上需要您重写整个内容。如果幸运的话,该格式可能允许廉价地附加数据。

如果该格式不是为廉价修改而设计的,您很可能需要将其转换为某种修改成本低廉的格式。如果你能把它全部保存在内存中,这可能会简化事情。

您也可以将索引解析为内存结构,并将任何更新保留在内存中,直到需要将数据写回磁盘。所以一个虚构的格式可能看起来像这样。这里的关键是,您只从磁盘中读取所需的最少数据量,并且添加或修改是在内存中完成的。请注意,这仅用于说明目的。

public class Index
{
    private readonly Dictionary<string, IEntry> entries = new();

    public IEnumerable<string> List => entries.Keys;
    public byte[] Read(string key) => entries[key].Read();
    public void UpdateOrAdd(string key, byte[] data) => entries[key] = new MemoryEntry(data);

    public static Index Load(Stream source)
    {
        var br = new BinaryReader(source);
        var numEntries = br.ReadInt32();
        var result = new Index();
        
        for (int i = 0; i < numEntries; i++)
        {
            var key = br.ReadString();
            var length = br.ReadInt32();

            // Note. Mixing index information and data like this will make it 
            // easy to read/append, but slower to load. 
            var offset = (int)br.BaseStream.Position;
            result.entries[key] = new FileEntry(source, offset, length);
            br.BaseStream.Position += length;
        }
        return result;
    }

    public void Save(Stream destination)
    {
        var bw = new BinaryWriter(destination);
        bw.Write(entries.Count);
        var list = entries.ToList();
        foreach (var (key, value) in list)
        {
            bw.Write(key);
            bw.Write(value.Length);
            value.CopyTo(bw.BaseStream);
        }
    }
}

public interface IEntry
{
    public void CopyTo(Stream destination);
    public byte[] Read();
    public int Length { get; }
}

public class MemoryEntry : IEntry
{
    private readonly byte[] data;
    public MemoryEntry(byte[] data) => this.data = data;
    public void CopyTo(Stream destination) => destination.Write(data, 0, data.Length);
    public byte[] Read() => data;
    public int Length => data.Length;
}

public class FileEntry : IEntry
{
    private readonly Stream fileStream;
    private readonly int offset;
    private readonly int length;

    public FileEntry(Stream fileStream, int offset, int length)
    {
        this.fileStream = fileStream;
        this.offset = offset;
        this.length = length;
    }

    public void CopyTo(Stream destination)
    {
        fileStream.Position = offset;
        fileStream.CopyTo(destination, length);
    }

    public byte[] Read()
    {
        fileStream.Position = offset;
        var result = new byte[length];
        fileStream.Position = offset;
        var bytesRead = fileStream.Read(result, 0, length);
        if (bytesRead != length) throw new InvalidOperationException("Invalid binary format");
        return result;
    }

    public int Length => length;
}