使用C ++解析非常大的CSV文件

时间:2014-12-10 12:47:19

标签: c++ qt csv large-files

我的目标是在OSX环境中的QT项目中使用C ++解析大型csv文件。 (当我说csv我的意思是tsv和其他变种1GB~5GB)。

这似乎是一项简单的任务,但是当文件大小变大时,事情会变得复杂。我不想编写自己的解析器,因为与解析csv文件有关的许多边缘情况。

我找到了各种csv处理库来处理这项工作,但是在我的机器上解析1GB文件需要大约90~120秒,这是不可接受的。我现在没有对数据做任何事情,我只是为了测试目的而处理和丢弃数据。

cccsvparser是我尝试过的图书馆之一。但唯一足够快的库fast-cpp-csv-parser给出了可接受的结果:我的机器上有15秒,但只有在知道文件结构时才有效。

使用示例:fast-cpp-csv-parser

#include "csv.h"

int main(){
    io::CSVReader<3> in("ram.csv");
    in.read_header(io::ignore_extra_column, "vendor", "size", "speed");
    std::string vendor; int size; double speed;
    while(in.read_row(vendor, size, speed)){
    // do stuff with the data
    }
}

正如您所见,我无法加载任意文件,我必须专门定义变量以匹配我的文件结构。我不知道任何允许我在运行时动态创建这些变量的方法。

我尝试的另一种方法是逐行读取csv文件fast-cpp-csv-parser LineReader类非常快(读取整个文件大约需要7秒),然后使用cccsvparser解析每一行可以处理字符串的lib。但这需要大约40秒才能完成,与第一次尝试相比这是一个改进,但仍然是不可接受的。

我已经看到了与csv文件解析相关的各种stackoverflow问题,它们都没有将大文件处理到帐户中。

此外,我花了很多时间用谷歌搜索来找到这个问题的解决方案,我真的很想念包裹经理喜欢 npm pip 在搜索时提供的自由开箱即用的解决方案。

我将不胜感激任何有关如何处理此问题的建议。

修改

当使用@fbucek的方法时,处理时间减少到25秒,这是一个很大的改进。

我们可以对此进行更优化吗?

3 个答案:

答案 0 :(得分:8)

我假设你只使用一个帖子。

多线程可以加速您的过程。

到目前为止,最佳成就是 40秒。让我们坚持下去。

我假设您首先阅读然后处理 - &gt; (大约7秒读取整个文件)

7秒供阅读 33秒进行处理

首先您可以将文件分成块,让我们说50MB。 这意味着您可以在读取50MB文件后开始处理。您无需等到整个文件完成。 读数为0.35秒(处理时为0.35 + 33秒= cca 34秒)

使用多线程时,可以一次处理多个块。从理论上讲,这可以加速核心数量。我们假设您有4个核心。 那是33/4 = 8.25秒。

我认为您可以使用4个核心加速处理,最多可达 9 s。

查看QThreadPoolQRunnableQtConcurrent 我更喜欢QThreadPool

将任务分成几部分:

  1. 首先尝试循环文件并将其分成块。并且不做任何事情。
  2. 然后创建&#34; ChunkProcessor&#34;可以处理该块的类
  3. 制作&#34; ChunkProcessor&#34; QRunnable的子类,并在重新实现的run()函数中执行您的进程
  4. 当你有块时,你有可以处理它们的类,并且该类与QThreadPool兼容,你可以将它传递给
  5. 看起来像这样

    loopoverfile {
      whenever chunk is ready {
         ChunkProcessor *chunkprocessor = new ChunkProcessor(chunk);
         QThreadPool::globalInstance()->start(chunkprocessor);
         connect(chunkprocessor, SIGNAL(finished(std::shared_ptr<ProcessedData>)), this, SLOT(readingFinished(std::shared_ptr<ProcessedData>)));
      }   
    }
    

    您可以使用std :: share_ptr传递已处理的数据,以便不使用QMutex或其他内容,并避免多线程访问某些资源时出现序列化问题。

    注意:为了使用自定义信号,您必须在使用前注册

    qRegisterMetaType<std::shared_ptr<ProcessedData>>("std::shared_ptr<ProcessedData>");
    

    编辑:(基于讨论,我的回答并不清楚) 使用什么磁盘或速度有多关系并不重要。读取是单线程操作。 建议使用此解决方案只是因为它需要7秒才能读取,并且无论它是什么磁盘都无关紧要。 7秒是最重要的。唯一的目的是尽快开始处理,而不是等到阅读结束。

    您可以使用:

    QByteArray data = file.readAll();
    

    或者您可以使用主要想法:(我不知道为什么需要7秒才能阅读,背后是什么)

     QFile file("in.txt");
     if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
       return;
    
     QByteArray* data = new QByteArray;    
     int count = 0;
     while (!file.atEnd()) {
       ++count;
       data->append(file.readLine());
       if ( count > 10000 ) {
         ChunkProcessor *chunkprocessor = new ChunkProcessor(data);
         QThreadPool::globalInstance()->start(chunkprocessor);
         connect(chunkprocessor, SIGNAL(finished(std::shared_ptr<ProcessedData>)), this, SLOT(readingFinished(std::shared_ptr<ProcessedData>)));
         data = new QByteArray; 
         count = 0;
       }
     }
    

    一个文件,一个线程,读取速度几乎和第34行一样快;没有&#34;中断。 您对数据的处理是另一个问题,但与I / O无关。它已经在记忆中了。 因此,只关注机器上的 5GB文件和RAM

    这是非常简单的解决方案所有你需要的是子类QRunnable,重新实现运行函数,完成时发出信号,使用共享指针传递处理数据,并在主线程中将数据联合成一个结构或其他。简单的线程安全解决方案。

答案 1 :(得分:2)

我建议一个多线程建议,稍有不同的是一个线程专用于以预定义(可配置)大小的块读取文件,并继续向一组线程(多个基于cpu核心)提供数据。我们假设配置如下:

块大小= 50 MB
磁盘线程= 1
处理线程= 5

  1. 创建一个用于从文件中读取数据的类。在这个类中,它包含一个用于与进程线程通信的数据结构。例如,该结构将包含每个进程线程的读缓冲区的起始偏移量,结束偏移量。对于读取文件数据,reader类包含2个缓冲区,每个块大小(在这种情况下为50 MB)
  2. 创建一个进程类,它包含读缓冲区和偏移数据结构的指针(共享)。
  3. 现在创建驱动程序(可能是主线程),创建所有线程并等待它们完成并处理信号。
  4. 使用reader类调用Reader线程,读取50 MB的数据并根据线程数创建偏移量数据结构对象。在这种情况下,t1处理0 - 10 MB,t2处理10 - 20 MB,依此类推。准备好后,它会通知处理器线程。然后它立即从磁盘读取下一个块,并等待处理器线程从进程线程完成通知。
  5. 通知上的处理器线程,从缓冲区读取数据并对其进行处理。完成后,它会通知读者线程完成并等待下一个块。
  6. 此过程完成,直到读取和处理整个数据。然后读取器线程通知主线程关于完成,在所有线程退出时发送PROCESS_COMPLETION。或主线程选择处理队列中的下一个文件。
  7. 请注意,为了便于说明,需要进行偏移,需要以编程方式处理线分隔符映射的偏移。

答案 2 :(得分:0)

如果您使用的解析器未明确分发,则该方法不可扩展。

我会投票支持下面这样的技术

  • 将文件分块为可由机器/时间约束处理的大小
  • 将块分发到可满足您的时间/空间要求的机器群集(1 .. *)
  • 考虑处理给定块的块大小
  • 避免相同资源上的线程(即给定块)以避免所有与线程相关的问题。
  • 使用线程实现非竞争(在资源上)操作 - 例如在一个线程上读取和在不同的线程上写入另一个文件。
  • 进行解析(现在对于这个小块你可以调用你的解析器)。
  • 进行操作。
  • 将结果合并回/如果可以按原样分发它们。

现在,已经说过了,为什么不能像框架一样使用Hadoop?