解析制表符分隔的数据。

3

我有一个文本文件(约10GB),格式如下:

data1<TAB>data2<TAB>data3<TAB>data4<NEWLINE>

我希望能够扫描并只处理data2,在C++中提取data2的最佳(最快)方法是什么。
编辑:添加了NEWLINE

有新的行吗?最简单的方法是逐行扫描。 - Jonathan Wood
@Jonathan Wood 是的,有换行符。 - Nemo
@Ajay:每个dataN部分有多长?是固定的还是可变的? - Xeo
@Xeo 这是一个单词,因此是一个变量。 - Nemo
我有点困惑。为什么不打开文件,读取一个字符串,忽略结果,读取空格,然后读取下一个字符串呢? - GManNickG
你正在使用哪个操作系统?考虑到文件的大小,访问它的方式可能会对性能产生很大影响。 - idz
6个回答

4

逐行读取文件。对于每一行,按制表符进行分割。这将产生一个包含字段的数组,使您能够使用第二个字段(data2)。


这很简洁,但做了很多不必要的工作。这个问题优先考虑速度。 - Tony Delroy
唯一不必要的是将其他字段存储在数组中而不是丢弃它们;然而,这大致上是一个恒定的内存开销。Tony D,哪个更快?下面的切割建议应该会慢得多(因为切割将对数据进行一次遍历,您将不得不分叉/执行以运行切割,然后对其输出进行一次遍历)。 - Daniel Papasian

2
这似乎需要使用更高级的工具,比如shell实用程序:
cut -f2           # from stdin
cut -f2 <my_file  # from file

但是,你同样可以使用C++实现这个功能:
void parse(std::istream& in)
{
    std::string word;
    while( in ) {
        std::cin >> word;  // throwaway 1
        std::cin >> word;  // data2
        process(word);
        std::cin >> word >> word;  // throwaway 3 and 4
    }
}

// ...
parse(std::cin);
std::ifstream file("my_file");
parse(file);

哇,我喜欢这个简洁的解决方案。如果帖子作者坚持使用C/C++来完成这个任务,并且cut命令在系统中是保证存在的,那么他们可以使用popen("cut -f2 <my_file","r")来获取所需数据流的句柄。 - zhaian

1

逐行读取文件。从那里解析出制表符非常简单。您可以使用类似于strtok()或类似例程的东西。


1

由于文件大小相当大,您可能需要考虑使用一种技术,允许您将I/O与处理重叠。在回复评论时,您提到正在使用Linux。如果您使用的是2.6或更高版本的内核,则可以考虑使用Linux异步I/O(AIO)。具体而言,您将使用来排队一些读取请求,然后使用等待一个(或多个)请求结束。随着请求完成,您将使用普通的char*扫描缓冲区以定位您感兴趣的数据。对于找到的每个数据,您可以在那时创建一个std::string(尽管避免复制可能会更有益),并对其进行处理。扫描完一个块后,您将重新排队以从文件中读取另一个块。您将继续执行此操作,直到处理完文件中的每个块。

这种方法的代码将比逐行读取文件更复杂,但速度可能会快得多。


"明显更快" ... 不一定...处理速度可能比I/O轻松几个数量级(这完全取决于硬件和处理的确切方法 - std :: strings比原地指针/长度处理要慢),在这种情况下,潜在的好处是微不足道的。事件处理开销甚至可能使它变慢。 如果需要最佳性能,则值得尝试,但不要有太高期望值... ;-P - Tony Delroy
@Tony 所说的都是好观点。天啊,观众中总有一个这样的人;-) - idz
无论如何,对于唯一提出这种方法的人点赞。 :-) - Tony Delroy
@Tony 谢谢。我的经验是,使用逐行 C++ I/O 很慢。我猜测有很多复制操作(或其他什么操作,我从来没有太想深入了解),所以即使您的处理速度相当快(例如校验和),您仍然可以通过这种技术挤出更多的性能。当然,你的效果可能会有所不同! - idz
好的,while (getline(std::cin, my_string)) ... 很慢(我还没有进行调查 - 奇怪的是 my_string 应该只重新分配几次内存,这是最明显的性能风险)。无论如何,aio_read 读取二进制块,更类似于 my_stream.read() …… 我既没有使用过也没有听说过任何有关性能方面的投诉。在这些块读取之后,程序员必须额外工作以跟踪行/第二个字段,在第二个字段本身可能不完整的情况下处理部分行读取等等。这不是火箭科学,但内存映射要容易得多。 - Tony Delroy

1

好的,打开一个文件流(应该能够处理10G的文件),然后跳到第一个制表符之后,也就是'\t',读取你的数据,然后跳到下一个换行符并重复。

#include <fstream>
#include <string>

int main(){
  std::fstream fin("your_file.txt");

  while(fin){
    std::string data2;
    char sink = '\0';

    // skip to first tab
    fin.ignore(1024,'\t');

    fin >> data2;
    // do stuff with data2

    // skip to next line
    fin.ignore(1024,'\n');
  }
}

逐个字符扫描不会消耗更多时间吗? - Nemo
我的意思是,使用getline和strtok会更有效率吗? - Nemo
@Ajay:getline 也必须处理所有这些字符。strtok 也是如此,所以我想不行。 - Xeo
sink永远不会是'\t''\n' - wilhelmtell
@wilhelmtell:已编辑,又忘记跳过空格了,感谢您的提醒。 - Xeo
+1 这是 iostreams 可能的最干净(也可能是最快)的实现。如果性能仍然是一个问题,那么重新实现内存映射文件访问和指针扫描至少是值得的...有时候可以快很多(1-2 个数量级),考虑使用 madvice 或类似的工具来避免缓存失效,但这完全取决于您的驱动器、缓存和内存大小等因素。 - Tony Delroy

0
你可以像其他人建议的那样使用iostream。另一种方法是直接使用fscanf。例如:
#include <stdio.h>

...

FILE* fp = fopen(path_to_file, "r");
char[256] data;

while(fscanf(fp, "%*s<tab>%s<tab>%*s<tab>%*s", data))
{
   do what you want with your data
}

1
为什么要用星号?或者说,为什么要在 %s 外面加星号?使用 %*s 可以抑制赋值。缺点是 %s 会在第一个空格处停止,所以如果数据中有空格,这种方法就不直接适用了。你可以使用 %100*[^\t] 来抑制 data1,使用 %100[^\t] 来捕获 data2 等等,但长度容易出错。 - Jonathan Leffler
啊谢谢你注意到了。我已经更新了代码,把*放在了正确的位置上。是的,只有当数据没有空格时它才能正常工作。 - zhaian
如果你打算推荐使用古老的C函数而不是C++标准库,你至少应该解释一下为什么你认为它们有任何好处...并且在此过程中不会泄漏内存。 - underscore_d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接