管道

时间:2017-01-27 19:50:49

标签: javascript node.js csv error-handling

作为我正在构建的应用程序的一部分,我正在使用csv-parse阅读和操作大型(大约5.5GB,800万行)csv文件。我让这个过程运行得比较顺利,但我坚持一个项目 - 通过不一致的列数捕获错误。

我正在使用管道功能,因为它适用于应用程序的其余部分,但我的问题是,如何将解析器抛出的错误重定向到日志并允许进程继续?

我认识到我可以使用relax_column_count选项跳过列数不一致的记录,而且该选项几乎已足够。问题在于,为了进行数据质量评估,我需要记录这些记录,以便我可以返回并查看导致列数不正确的原因(该过程是一个包含许多潜在故障点的进程)。

作为旁注,我知道解决这个问题的最简单方法是清理此过程的上游数据,但不幸的是我无法控制数据源。

例如,在示例集中,我收到以下错误:

  

events.js:141个
  扔掉//未处理的“错误”事件
  错误:行(行号)上的列数与标题

不匹配

示例数据(实际上不是我的数据,但证明了同样的问题):

year, month, value1, value2
2012, 10, A, B
2012, 11, B, C,
2012, 11, C, D,
2013, 11, D, E,
2013, 11, E, F,
2013, 11, F, 
2013, 11, G, G,
2013, 1, H, H,
2013, 11, I, I,
2013, 12, J, J,
2014, 11, K, K,
2014, 4, L, L,
2014, 11, M, M,
2014, 5, N, 
2014, 11, O, N,
2014, 6, P, O,
2015, 11, Q, P,
2015, 11, R, Q,
2015, 11, S, R,
2015, 11, T, S, 

代码:

const fs = require('fs');
const parse = require('csv-parse');
const stringify = require('csv-stringify');
const transform = require('stream-transform');

const paths = {
    input: './sample.csv',
    output: './output.csv',
    error: './errors.csv',
}

var input  = fs.createReadStream(paths.input);
var output = fs.createWriteStream(paths.output);
var error  = fs.createWriteStream(paths.error);

var stringifier = stringify({
    header: true,
    quotedString: true,
});
var parser = parse({
    relax: true,
    delimiter: ',', 
    columns: true, 
    //relax_column_count: true,
})
var transformer = transform((record, callback) => {
    callback(null, record);
}, {parallel: 10});

input.pipe(parser).pipe(transformer).pipe(stringifier).pipe(output);

思想?

1 个答案:

答案 0 :(得分:1)

我开发了这个问题的解决方案。 它不使用管道API ,而是使用CSV包的回调API 。它不像我希望的那样优雅,但是它具有功能性,并且具有显式错误处理的好处,并且不会导致过程停止。列数不一致。

该进程逐行读取文件,根据settings对象(settings.mapping)中的预期字段列表解析该行,然后对结果行进行转换,字符串化和写入输出到新的csv。

我将其设置为记录错误,因为与文件头不一致的列数以及一些额外数据(执行的日期时间,行号和整行作为诊断信息的文本.I没有设置其他错误类型的日志记录,因为它们都是csv结构错误的下游,但您也可以修改代码来编写这些错误。(您也可以将它们写入JSON或MySQL)数据库,但一次只有一件事。)

好消息似乎表明,采用这种方法而不是直接的方法会造成巨大的性能损失。我还没有进行任何正式的性能测试,但是在60MB文件上,两种方法的性能大致相同(假设文件没有不一致的行)。 明确的下一步是研究将写入捆绑到磁盘以减少I / O.

我仍然非常感兴趣,如果有更好的方法可以做到这一点,那么如果你有一个想法,肯定发布一个答案!与此同时,我认为我发布这个有效的答案,以防其他人在使用相同类型的格式不一致的资源时有用。

信用到期的信用,特别是两个问题/答案:

  • parsing huge logfiles in Node.js - read in line-by-line
    • 这个答案适应了一些核心代码,这些核心代码是分开逐行读取文件的答案,这可以防止csv-parse组件在发生故障的行中关闭(代价是分割文件的代码开销)进一步上游)。我实际上建议使用iconv-lite,因为它在该帖子中已经完成,但它与最低限度可重复的示例没有密切关系,所以我删除了这篇帖子。
  • Error handling with node.js streams
    • 这通常有助于更好地了解管道的潜力和局限性。从理论上讲,似乎有一种方法可以将基本相当于管道拆分器的内容从解析器放到出站管道上,但考虑到我当前的时间限制以及与异步过程相关的挑战,这在流方面是相当不可预测的终止,我使用了回调API。

示例代码:

'use strict'
// Dependencies
const es     = require('event-stream');
const fs     = require('fs');
const parse = require('csv-parse');
const stringify = require('csv-stringify');
const transform = require('stream-transform');

// Reference objects
const paths = {
    input: 'path to input.csv',
    output: 'path to output.csv',
    error: 'path to error output.csv',
}
const settings = {
    mapping: {
        // Each field is an object with the field name as the key
        // and can have additional properties for use in the transform 
        // component of this process
        // Example
        'year' : {
            import: true,
        }
    }
}

const metadata = {
    records: 0,
    error: 0
}

// Set up streams
var input  = fs.createReadStream(paths.input);
var errors  = fs.createWriteStream(paths.error,  {flags: 'ax'});
var output = fs.createWriteStream(paths.output, {flags: 'ax'});

// Begin process (can be refactored into function, but simplified here)
input
  .pipe(es.split()) // split based on row, assumes \n row endings
  .pipe(es.mapSync(line => { // synchronously process each line

    // Remove headers, specified through settings
    if (metadata.records === 0) return metadata.records++;
    var id = metadata.records;

    // Parse csv by row 
    parse(line, {
        relax: true,
        delimiter: ',', 
        columns: Object.keys(settings.mapping),
    }, (error, record) => {

        // Write inconsistent column error 
        if (error) {
            metadata.error++;
            errors.write(
                new Date() + ', Inconsistent Columns, ' + 
                 id + ', `' +  
                 line + '`\n'
            );
        }

    // Apply transform / reduce
    transform(record, (record) => {
        // Do stuff to record
        return record;
    }, (error, record) => {

        // Throw tranform errors
        if (error) {
            throw error;
        }

    // Stringify results and write to new csv
    stringify(record, {
           header: false,
           quotedString: true,
    }, (error, record) => {

        // Throw stringify errors
        if (error) {
            console.log(error);
        }

        // Write record to new csv file
        output.write(record);
    });
    });
    })

    // Increment record count
    metadata.records++;

  }))  
  .on('end', () => {
    metadata.records--;
    console.log(metadata)
  })