提问人:Does Deos 提问时间:6/27/2023 最后编辑:Does Deos 更新时间:6/27/2023 访问量:185
如何正确地从大型二进制文件中读取/写入随机块?
How to read/write random chunks from large binary files properly?
问:
我正在编写一个用于处理二进制文件的库。特别是“日志信息标准 (LIS) 79 子集”,它具有各种类型的记录和各种数据类型的条目。每个条目可以是单个值、数组或具有更复杂的结构。文件大小可能从 3-5 KB 到几 GB 不等。
目标:读取和修改任何大小的文件的任何部分。
尝试过的内容:
- 首先实现只是读取整个文件,然后将每个记录和数据类型及其缓冲区写入相应的类实例。对于小文件来说,它非常好,但是当文件大小大于 1GB 时,速度非常慢且 RAM 饥饿。
- 然后我试图完全停止使用缓冲区。读取每个组件的文件、存储的偏移量、大小和类型。这种方法在阅读方面效果很好,但正如我在经过一些研究后了解到的那样,没有办法在文件的随机位置插入数据。
那么问题来了,如何在不过度使用RAM的情况下正确处理这些数据?
答:
我对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;
}
下一个:C# 使用目录获取文件名
评论