NodeJS,promises,streams - 处理大型CSV文件

时间:2015-10-14 15:35:49

标签: node.js promise bluebird pg-promise

我需要构建一个处理大型CSV文件的函数,以便在bluebird.map()调用中使用。鉴于文件的潜在大小,我想使用流媒体。

此函数应接受流(CSV文件)和函数(处理流中的块),并在读取文件结束(已解决)或错误(拒绝)时返回承诺。

所以,我从:

开始
'use strict';

var _ = require('lodash');
var promise = require('bluebird');
var csv = require('csv');
var stream = require('stream');

var pgp = require('pg-promise')({promiseLib: promise});

api.parsers.processCsvStream = function(passedStream, processor) {

  var parser = csv.parse(passedStream, {trim: true});
  passedStream.pipe(parser);

  // use readable or data event?
  parser.on('readable', function() {
    // call processor, which may be async
    // how do I throttle the amount of promises generated
  });

  var db = pgp(api.config.mailroom.fileMakerDbConfig);

  return new Promise(function(resolve, reject) {
    parser.on('end', resolve);
    parser.on('error', reject);
  });

}

现在,我有两个相互关联的问题:

  1. 我需要限制正在处理的实际数据量,以免造成内存压力。
  2. 作为processor param传递的函数通常是异步的,例如通过基于promise的库将文件内容保存到db(现在:pg-promise) 。因此,它将在记忆中创造一种承诺并反复继续前进。
  3. pg-promise库具有管理此功能的功能,例如page(),但我无法将如何将流事件处理程序与这些promise方法混合起来。现在,我在每个readable之后在read()部分的处理程序中返回一个promise,这意味着我创建了大量承诺的数据库操作并最终因为我达到进程内存限制而出错。

    有没有人有一个可以用作跳跃点的工作示例?

    更新:可能有不止一种方法可以给猫皮肤涂抹,但这样做有效:

    'use strict';
    
    var _ = require('lodash');
    var promise = require('bluebird');
    var csv = require('csv');
    var stream = require('stream');
    
    var pgp = require('pg-promise')({promiseLib: promise});
    
    api.parsers.processCsvStream = function(passedStream, processor) {
    
      // some checks trimmed out for example
    
      var db = pgp(api.config.mailroom.fileMakerDbConfig);
      var parser = csv.parse(passedStream, {trim: true});
      passedStream.pipe(parser);
    
      var readDataFromStream = function(index, data, delay) {
        var records = [];
        var record;
        do {
          record = parser.read();
          if(record != null)
            records.push(record);
        } while(record != null && (records.length < api.config.mailroom.fileParserConcurrency))
        parser.pause();
    
        if(records.length)
          return records;
      };
    
      var processData = function(index, data, delay) {
        console.log('processData(' + index + ') > data: ', data);
        parser.resume();
      };
    
      parser.on('readable', function() {
        db.task(function(tsk) {
          this.page(readDataFromStream, processData);
        });
      });
    
      return new Promise(function(resolve, reject) {
        parser.on('end', resolve);
        parser.on('error', reject);
      });
    }
    

    有人发现这种方法存在潜在问题吗?

4 个答案:

答案 0 :(得分:7)

您可能需要查看promise-streams

var ps = require('promise-streams');
passedStream
  .pipe(csv.parse({trim: true}))
  .pipe(ps.map({concurrent: 4}, row => processRowDataWhichMightBeAsyncAndReturnPromise(row)))
  .wait().then(_ => {
    console.log("All done!");
  });

使用背压和一切。

答案 1 :(得分:3)

在下面找到一个完整的应用程序,它可以正确地执行相同类型的任务:它将文件作为流读取,将其解析为CSV并将每行插入数据库。

const fs = require('fs');
const promise = require('bluebird');
const csv = require('csv-parse');
const pgp = require('pg-promise')({promiseLib: promise});

const cn = "postgres://postgres:password@localhost:5432/test_db";
const rs = fs.createReadStream('primes.csv');

const db = pgp(cn);

function receiver(_, data) {
    function source(index) {
        if (index < data.length) {
            // here we insert just the first column value that contains a prime number;
            return this.none('insert into primes values($1)', data[index][0]);
        }
    }

    return this.sequence(source);
}

db.task(t => {
    return pgp.spex.stream.read.call(t, rs.pipe(csv()), receiver);
})
    .then(data => {
        console.log('DATA:', data);
    }
    .catch(error => {
        console.log('ERROR:', error);
    });

请注意,我唯一更改的内容是:使用库csv-parse代替csv,作为更好的选择。

添加了stream.read库中方法spex的使用方法,该方法正确地为Readable流提供了与promises一起使用的方法。

答案 2 :(得分:1)

那么说你不想要流式传输而是某种数据块? ; - )

您知道https://github.com/substack/stream-handbook吗?

我认为在不改变您的架构的情况下最简单的方法是某种承诺池。例如https://github.com/timdp/es6-promise-pool

答案 3 :(得分:1)

我发现做同样事情的更好的方法。具有更多控制权。这是具有精确并行控制的最小框架。将并行值作为一个,所有记录将按顺序处理,而无需将整个文件存储在内存中,因此我们可以增加并行值以加快处理速度。

      const csv = require('csv');
      const csvParser = require('csv-parser')
      const fs = require('fs');

      const readStream = fs.createReadStream('IN');
      const writeStream = fs.createWriteStream('OUT');

      const transform = csv.transform({ parallel: 1 }, (record, done) => {
                                           asyncTask(...) // return Promise
                                           .then(result => {
                                             // ... do something when success
                                             return done(null, record);
                                           }, (err) => {
                                             // ... do something when error
                                             return done(null, record);
                                           })
                                       }
                                     );

      readStream
      .pipe(csvParser())
      .pipe(transform)
      .pipe(csv.stringify())
      .pipe(writeStream);

这允许为每条记录执行一个异步任务。

要返回一个承诺,我们可以返回一个空的承诺,并在流完成时完成它。

    .on('end',function() {
      //do something wiht csvData
      console.log(csvData);
    });