最近工作中遇到一个问题,就是CSV文件有近200万个。 每个 CSV 文件包含数千个实验数据。 CSV使用不连续的整数值作为文件名,如:1.CSV、2.CSV、3.CSV、5.CSV等。此外,还有200万个XML文件。 每个 XML 文件的文件名与 CSV 文件名相对应。 在这些XML文件中,定义了对应的CSV实验数据文件的实验描述信息(例如实验名称、实验类型等),即每个XML都包含其对应的CSV文件的元数据。 目前的需求之一是,当软件中列出部分CSV文件(例如数千个或数万个)时,需要在每个文件名旁边显示相应的实验名称。
乍一看,这个要求似乎比较简单。 当显示CSV文件时,直接找到对应的XML文件,解析XML得到名称。 然而,问题是:
这就需要软件本身自带这200万个XML文件。 文件数量太大。 如果压缩成ZIP,ZIP的大小会比较大。 当程序请求实验名称时,需要解压,性能极差。
解析XML本身就需要一定的性能损失。 如果要显示CSV对应的成百上千个实验名称,那么每个XML都需要解析,性能也很不理想。
这里介绍一种方法,通过预处理将需要的信息提取到结构化数据结构(Data)中,然后通过索引快速定位。
问题分析
虽然XML文件的数量比较多,并且每个XML文件提供的信息也比较多,但我们需要的信息只是XML文件中的实验名称。 因此,一个想法是首先预处理所有 XML 文件,然后提取实验名称并将其保存到另一个文件。 当需要根据CSV文件名获取实验名称时,查询实验名称数据文件,然后显示对应的实验名称。 这里的问题是,使用哪种格式来生成实验名称数据文件? 我们还有几个选择:
使用JSON来存储“CSV文件名实验名称”的键值对性能不会很好,因为这样的键值对有200万个,解析JSON文件本身的CPU和IO负载会很高。
例如,使用桌面数据库需要应用程序中内置引擎。 它的CPU架构(x86、x64)有问题,中间有一层数据库访问操作被阻塞,所以性能不一定特别高。
定制存储结构比较灵活,但是需要自己实现,难度较大,出现问题的几率也比较高。
综合分析后,我们还是打算选择第三种方案,自己定义数据存储结构。
假设CSV文件名是连续的,比如从1.CSV、2.CSV到.CSV,那么我们可以以CSV文件名值作为索引值,通过查表方法找到对应的实验名称字符串。 例如内存中有如下字符串数组:
假设CSV文件名为1535.CSV,那么我们只需要[1534]即可获取第1535个CSV对应的实验名称(即1535.CSV)。 这样做的效率非常高,它直接使用数组的索引。 然而,现实并没有那么美好:
我们不可能把200万条数据全部放在一个数组内存中。 这样做会消耗非常高的内存。
原始CSV文件的文件名标签不连续
解决问题一的方式比较直接:我们需要将数据放到磁盘上,然后按需访问; 针对问题二,我们需要引入数据库实现中的一个概念:索引。
解决这个问题
假设每个实验名称数据作为固定长度记录存储在二进制文件中。 但是,由于文件名中的数字标识符不是连续的,因此无法简单地通过文件名推断出数据记录的位置(即数组的底部)。 标量值),例如:
1.csv 和 2.csv 仍然有规则可寻。 二进制文件中记录实验名称数据的位置是文件名减1。从4.csv开始,后续位置值与文件名无关。 这时候我们就需要有一个映射来定义文件名中的值和数据记录位置之间的关系。 为此,我引入了另一个二进制文件,其中定义了 200 万条记录。 每条记录仅占用4个字节。 每条记录(每4个字节)将记录的偏移值保存为带有文件名值的CSV文件,对应的实验名称数据记录在上述二进制文件中的记录位置。 例如:
然后,假设CSV文件的文件名为4.csv,则可以先在索引文件中找到偏移值为4(即index=3)的记录位置值(即2),并然后在二进制文件中找到索引值为2的记录,即为4.csv对应的实验名称数据。
代码
我使用..命名空间下的类和类以及.IO命名空间下的类来实现结构化二进制文件的读写。 包装代码如下:
班级
T ( , int idx = 0)
其中 T :
var buff = 新字节[.()];
如果 (..)
..Seek(idx * buff., .Begin);
..读取(buff, 0, buff.);
var = .Alloc(buff, .);
尝试
var = .(.());
;
。自由的();
void ( , T 项)
其中 T :
var buff = 新字节[.()];
var = .Alloc(buff, .);
尝试
.(项目, .(), false);
.Write(buff, 0, buff.);
。自由的();
接下来再编写一个测试程序,测试结构化二进制文件的读取性能:
[(.)]
指数
[(0)]
[(.U4, = 4)]
整数索引;
[(.)]
回声
[(0)]
[(., = 256)]
姓名;
无效主([]参数)
var = new();
使用(var = new(“.bin”,.Open,.Read))
使用(var = new(“.idx”,.Open,.Read))
使用 (var = new ())
使用 (var = new ())
而(真)
.Write("请输入CSV文件名(直接回车退出程序):");
var 行 = .();
if (.(line)) 中断;
if (!int.(路径.(行), out var )) ;
.();
var = .(, );
if (.Index == -1)
.($"数据文件不包含{行}记录。");
.();
;
var = .(, .Index);
。停止();
.($"耗时:{.}毫秒,实验名称:{.Name}。");
.();
执行结果如下:
可以看到,无论CSV文件名中的值是大还是小,从近200万条数据中读取实验名称信息的速度都非常快,基本上只要零点几毫秒,达到了预期目标。 。
总结
所谓结构化数据,是指每条数据所占用的存储空间是一致的,即每条记录所占用的字节数相等,这样就很容易将记录的索引值和每条记录的索引值传递出去。记录。 通过记录的大小来计算位置偏移以快速读取数据。 这是一个以空间换时间的方案。 一个明显的问题是,每条记录占用的存储空间需要根据实际数据合理选择:如果太大,那么超过200万条记录的积累就会占用大量的存储空间。 ,造成空间浪费; 如果太小,某些数据可能无法正确存储,从而导致信息丢失。 因此,本文介绍的解决方案仍然需要根据实际情况来考虑,选择合理的记录存储结构。