作为我正在构建的应用程序的一部分,我正在使用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);
思想?
答案 0 :(得分:1)
我开发了这个问题的解决方案。 它不使用管道API ,而是使用CSV包的回调API 。它不像我希望的那样优雅,但是它具有功能性,并且具有显式错误处理的好处,并且不会导致过程停止。列数不一致。
该进程逐行读取文件,根据settings
对象(settings.mapping
)中的预期字段列表解析该行,然后对结果行进行转换,字符串化和写入输出到新的csv。
我将其设置为记录错误,因为与文件头不一致的列数以及一些额外数据(执行的日期时间,行号和整行作为诊断信息的文本.I没有设置其他错误类型的日志记录,因为它们都是csv结构错误的下游,但您也可以修改代码来编写这些错误。(您也可以将它们写入JSON或MySQL)数据库,但一次只有一件事。)
好消息似乎表明,采用这种方法而不是直接的方法会造成巨大的性能损失。我还没有进行任何正式的性能测试,但是在60MB文件上,两种方法的性能大致相同(假设文件没有不一致的行)。 明确的下一步是研究将写入捆绑到磁盘以减少I / O.
我仍然非常感兴趣,如果有更好的方法可以做到这一点,那么如果你有一个想法,肯定发布一个答案!与此同时,我认为我发布这个有效的答案,以防其他人在使用相同类型的格式不一致的资源时有用。
信用到期的信用,特别是两个问题/答案:
示例代码:
'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)
})