是否可以从大型固定宽度 CSV 文件中有效地获取行的子集?

Is it possible to efficiently get a subset of rows from a large fixed-width CSV file?

我有一个非常大的固定宽度 CSV 文件(130 万行和 80K 列)。它的大小约为 230 GB。我需要能够获取这些行的一个子集。我有一个我需要的行索引向量。但是,我现在需要弄清楚如何遍历如此庞大的文件来获取它们。

按照我的理解,C++ 将逐行检查文件,直到遇到换行符(或给定的定界符),此时,它将清除缓冲区,然后移至下一个线。我还听说过 seek() 函数可以转到流中的给定位置。那么是否有可能以某种方式使用这个函数来快速获得指向正确行号的指针?

我认为由于程序基本上不需要 运行 数十亿个 if 语句来检查换行符,如果我简单地告诉程序在固定宽度中去哪里可能会提高速度文件。但是我不知道该怎么做。

假设我的文件宽度为 n 个字符,行号为 {l_1, l_2, l_3, ... l_m}(其中 l_1 < l_2 < l_3, ... < l_m)。在那种情况下,我可以简单地告诉文件指针转到 (l_1 - 1) * n,对吗?但是对于下一行,我是从 l_1 行的末尾还是从下一行的开头计算下一个跳转?我应该在计算跳跃时包括换行符吗?

这是否有助于提高速度,还是我只是误会了什么?

感谢您抽出时间提供帮助

编辑:文件将如下所示:

id0000001,AB,AB,AA,--,BB
id0000002,AA,--,AB,--,BB
id0000003,AA,AA,--,--,BB
id0000004,AB,AB,AA,AB,BB

正如我在评论中建议的那样,您可以将数据字段压缩为两位:

-- 00
AA 01
AB 10
BB 11

这会将您的文件大小减少 12 倍,因此约为 20GB。考虑到你的处理很可能IO-bound,你可能会加快处理同样的12倍。

生成的文件的记录长度为 20,000 字节,因此很容易计算出任何给定记录的偏移量。无需考虑换行符号 :)

以下是我构建二进制文件的方式:

#include <fstream>
#include <iostream>
#include <string>
#include <chrono>

int main()
{
    auto t1 = std::chrono::high_resolution_clock::now();
    std::ifstream src("data.txt", std::ios::binary);
    std::ofstream bin("data.bin", std::ios::binary);
    size_t length = 80'000 * 3 + 9 + 2; // the `2` is a length of CR/LF on my Windows; use `1` for other systems
    std::string str(length, '[=11=]');
    while (src.read(&str[0], length))
    {
        size_t pos = str.find(',') + 1;
        for (int group = 0; group < 2500; ++group) {
            uint64_t compressed(0), field(0);
            for (int i = 0; i < 32; ++i, pos += 3) {
                if (str[pos] == '-')
                    field = 0;
                else if (str[pos] == 'B')
                    field = 3;
                else if (str[pos + 1] == 'B')
                    field = 2;
                else
                    field = 1;

                compressed <<= 2;
                compressed |= field;
            }
            bin.write(reinterpret_cast<char*>(&compressed), sizeof compressed);
        }
    }
    auto t2 = std::chrono::high_resolution_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count() << std::endl;

    // clear `bad` bit set by trying to read past EOF
    src.clear();
    // rewind to the first record
    src.seekg(0);
    src.read(&str[0], length);
    // read next (second) record
    src.read(&str[0], length);
    // read forty second record from start (skip 41)
    src.seekg(41 * length, std::ios_base::beg);
    src.read(&str[0], length);
    // read next (forty third) record
    src.read(&str[0], length);
    // read fifties record (skip 6 from current position)
    src.seekg(6 * length, std::ios_base::cur);
    src.read(&str[0], length);

    return 0;
}

这可以在一秒钟内对大约 1,600 条记录进行编码,因此整个文件大约需要 15 分钟。你现在处理需要多长时间?

更新:

添加了如何从 src.

中读取单个记录的示例

我只设法让 seekg() 在二进制模式下工作。

<iostream> 类 中的 seek 函数族通常是 byte-oriented。您可以使用它们,前提是您绝对确信您的记录(在本例中为行)具有固定的字节数;在这种情况下,您可以将文件作为二进制文件打开,并使用 .read 代替 getline,它可以将指定数量的字节读入具有足够容量的字节数组中。但是——因为文件毕竟存储的是文本——万一一条记录的大小不同,你就会失去对齐;如果 id 字段保证等于行号 - 或者 at-least 它的递增映射 - 有根据的猜测和 follow-up 试错会有所帮助。您需要快速切换到一些更好的数据库管理;即使是 10GB 的单个二进制文件也太大并且容易快速损坏。您可以考虑将其切成更小的片(可能是 100MB 的数量级),以尽量减少损坏传播的机会。另外,您还需要 recovery/correction.

的一些冗余机制