【问题标题】:How to parse a dirty CSV with Node.js?如何使用 Node.js 解析脏 CSV?
【发布时间】:2016-01-23 13:05:36
【问题描述】:

由于许多错误,我无法正确解析 CSV 文件。我提取了一个示例,您可以在这里下载:Test CSV File

主要错误(或产生错误的原因)是:

  • 引号和逗号(尝试使用 R 解析文件时出现许多错误)
  • 空行
  • 字段内出现意外换行

我首先决定在将数据加载到 R 之前逐行使用正则表达式来清理数据,但无法解决问题,而且速度很慢(200Mo 文件)

所以我决定在 Node.js 下使用CSV parser,代码如下:

'use strict';

const Fs  = require('fs');
const Csv = require('csv');

let input       = 'data_stack.csv';
let readStream  = Fs.createReadStream(input);
let option      = {delimiter: ',', quote: '"', escape: '"', relax: true};

let parser = Csv.parse(option).on('data', (data) => {
    console.log(data)
});

readStream.pipe(parser)

但是:

  • 某些行被正确解析(字符串数组)
  • 有些没有解析(所有字段都是一个字符串)
  • 有些行仍然是空的(可以通过在选项中添加skip_empty_lines: true 来解决)
  • 我不知道如何处理意外的换行符。

我不知道如何使这个 CSV 变得干净,无论是使用 R 还是使用 Node.js。

有什么帮助吗?

编辑:

按照@Danny_ds 解决方案,我可以正确解析它。现在我无法正确地将其串回。

console.log(); 我得到了一个正确的对象,但是当我尝试对其进行字符串化时,我没有得到一个干净的 CSV(仍然有换行符和空行)。

这是我正在使用的代码:

'use strict';

const Fs  = require('fs');
const Csv = require('csv');


let input  = 'data_stack.csv';
let output = 'data_output.csv';

let readStream  = Fs.createReadStream(input);
let writeStream = Fs.createWriteStream(output);

let opt  = {delimiter: ',', quote: '"', escape: '"', relax: true, skip_empty_lines: true};


let transformer = Csv.transform(data => {
    let dirty = data.toString();
    let replace = dirty.replace(/\r\n"/g, '\r\n').replace(/"\r\n/g, '\r\n').replace(/""/g, '"');

    return replace;
});

let parser = Csv.parse(opt);
let stringifier = Csv.stringify();

readStream.pipe(transformer).pipe(parser).pipe(stringifier).pipe(writeStream);

编辑 2:

这是最终的工作代码:

'use strict';

const Fs  = require('fs');
const Csv = require('csv');


let input  = 'data_stack.csv';
let output = 'data_output.csv';

let readStream  = Fs.createReadStream(input);
let writeStream = Fs.createWriteStream(output);

let opt  = {delimiter: ',', quote: '"', escape: '"', relax: true, skip_empty_lines: true};


let transformer = Csv.transform(data => {
    let dirty = data.toString();
    let replace = dirty
        .replace(/\r\n"/g, '\r\n')
        .replace(/"\r\n/g, '\r\n')
        .replace(/""/g, '"');

    return replace;
});

let parser = Csv.parse(opt);

let cleaner = Csv.transform(data => {
    let clean = data.map(l => {
        if (l.length > 100 || l[0] === '+') {
            return l = "Encoding issue";
        }
        return l;
    });
    return clean;
});

let stringifier = Csv.stringify();

readStream.pipe(transformer).pipe(parser).pipe(cleaner).pipe(stringifier).pipe(writeStream);

谢谢大家!

【问题讨论】:

  • 哇,这是一个搞砸的 CSV!您将需要分多个阶段对其进行修复。首先是修复似乎嵌入在某些行中的换行符。接下来,我将整理随机引用。如果您不希望数据中包含逗号,请删除引号。
  • 您可以将 csv 文件上传到其他地方吗?要点可能
  • 这里是另一个链接:Test CSV FIle

标签: javascript node.js parsing csv


【解决方案1】:

我不知道如何使这个 CSV 变得干净,无论是使用 R 还是使用 Node.js。

其实并没有看起来那么糟糕。

可以使用以下步骤轻松将此文件转换为有效的 csv:

  • 将所有"" 替换为"
  • 将所有\n" 替换为\n
  • 将所有"\n 替换为\n

\n 表示换行符,而不是文件中也出现的字符“\n”。

请注意,在您的示例文件中\n 实际上是\r\n0x0d0x0a),因此根据您使用的软件,您可能需要在上述示例中替换\r\n 中的\n .此外,在您的示例中,最后一行之后有一个换行符,因此最后一个字符的引号也将被替换,但您可能需要在原始文件中进行检查。

这应该会生成一个有效的 csv 文件:

仍然会有多行字段,但这可能是有意的。但是现在这些都被正确引用了,任何像样的 csv 解析器都应该能够处理多行字段。


看起来原始数据有一个额外的转义引号字符:

  • 如果原始字段包含 ,,则它们被引用,并且如果这些字段已包含引号,则引号会被另一个引号转义 - 这是正确的做法。

  • 但随后所有包含引号的行似乎都被再次引用(实际上将这些行转换为一个带引号的字段),并且该行内的所有引号都被另一个引号转义了。

  • 很明显,多行字段出了点问题。在多行之间也添加了引号,这不是正确的做法。

【讨论】:

  • 刚刚用Fs.readFile('data_stack.csv', (err, data) => { data.toString().replace(/""/g, '"').replace(/[\r\n]"/g, '\n').replace(/"[\r\n]/g, '\n'); Fs.writeFile('data_output.csv', data); })试过了,还是不行。
  • 您需要类似:.replace(/\r\n"/g, '\r\n').replace(/\n"/g, '\n')。最后替换的同上。
  • @Synleb - 好吧,仍然有换行符和空行(不应该是空的 csv 行)是正常的,因为您的数据中有一个多行字段(第 8 列 / R2)-如果引用了多行字段,则为有效的 csv,第一次清理后应该是这种情况。如果您不希望这样,您可以仅在解析文件后删除该字段中的换行符。
  • @Synleb - 但是您必须确保您的 csv 解析器当然支持多行字段(我在您链接到的站点上没有看到该选项)。但是既然你说它被正确解析了,我想是这样的。
  • 你说得对,该字段仍在多行上,但解析时 Csv 现在有效。我刚刚删除了格式错误的字段,它很干净。非常感谢!
【解决方案2】:

数据不会太混乱而无法处理。有一个清晰的模式。

一般步骤:

  1. 暂时删除混合格式的内部字段(以双(或更多)引号开头并包含各种字符。
  2. 从引号行的开头和结尾删除引号以提供干净的 CSV
  3. 将数据拆分为列
  4. 替换已删除的字段

上面的第 1 步是最重要的。如果您应用此步骤,那么新行、空行、引号和逗号的问题就会消失。如果您查看数据,您会看到第 7、8 和 9 列包含混合数据。但它总是由 2 个引号分隔 或更多。例如

good,clean,data,here,"""<-BEGINNING OF FIELD DATA> Oh no
++\n\n<br/>whats happening,, in here, pages of chinese
characters etc END OF FIELD ->""",more,clean,data

这是一个基于提供的文件的工作示例:

fs.readFile('./data_stack.csv', (e, data) => {

    // Take out fields that are delimited with double+ quotes
    var dirty = data.toString();
    var matches = dirty.match(/""[\s\S]*?""/g);
    matches.forEach((m,i) => {
        dirty = dirty.replace(m, "<REPL-" + i + ">");
    });

    var cleanData =   dirty
        .split('\n') // get lines

        // ignore first line with column names
        .filter((l, i) => i > 0)

        // remove first and last quotation mark if exists
        .map(l => l[0] === '"' ? l.substring(1, l.length-2) : l) // remove quotes from quoted lines

        // split into columns
        .map(l => l.split(','))

        // return replaced fields back to data (columsn 7,8 and 9)
        .map(col => {

            if (col.length > 9) {
                col[7] = returnField(col[7]);
                col[8] = returnField(col[8]);
                col[9] = returnField(col[9]);
            }
            return col;

            function returnField(f) {
                if (f) {
                    var repls = f.match(/<.*?>/g)
                    if (repls)
                        repls.forEach(m => {
                            var num = +m.split('-')[1].split('>')[0];
                            f = f.replace(m, matches[num]);
                        });
                }
                return f;
            }
        })

    return cleanData
});

结果:

数据看起来很干净。所有行都会产生与标题匹配的预期列数(显示最后 2 行):

  ...,
  [ '19403',
    '560e348d2adaffa66f72bfc9',
    'done',
    '276',
    '2015-10-02T07:38:53.172Z',
    '20151002',
    '560e31f69cd6d5059668ee16',
    '""560e336ef3214201030bf7b5""',
    'a+�a��a+�a+�a��a+�a��a+�a��',
    '',
    '560e2e362adaffa66f72bd99',
    '55f8f041b971644d7d861502',
    'foo',
    'foo',
    'foo@bar.com',
    'bar.com' ],
  [ '20388',
    '560ce1a467cf15ab2cf03482',
    'update',
    '231',
    '2015-10-01T07:32:52.077Z',
    '20151001',
    '560ce1387494620118c1617a',
    '""""""Final test, with a comma""""""',
    '',
    '',
    '55e6dff9b45b14570417a908',
    '55e6e00fb45b14570417a92f',
    'foo',
    'foo',
    'foo@bar.com',
    'bar.com' ],

【讨论】:

    【解决方案3】:

    继续我的评论:

    数据太乱,无法一步修复,不要尝试。

    首先确定双引号和/或逗号是否可能是数据的一部分。如果不是,请使用简单的正则表达式删除双引号。

    接下来,每行应该有 14 个逗号。将文件读取为文本并依次计算每行的逗号数。如果少于 14,请检查以下行,如果逗号之和为 14,则合并 2 行。如果总和小于 14,请检查下一行并继续,直到有 14 个逗号。如果下一行超过 14,则存在严重错误,因此请记下行号 - 您可能必须手动修复。保存生成的文件。

    幸运的是,您现在将拥有一个可以作为 CSV 处理的文件。如果没有,请带回部分整理的文件,我们可以尝试进一步提供帮助。

    不言而喻,您应该处理原件的副本,您不太可能第一次就做好 :)

    【讨论】:

    • 谢谢朱利安。关于您的第一点(顺便说一下第二点),只有一件事。如何通过不考虑可以包含在带引号的字符串中的封闭逗号来计算逗号。并且通过应用正则表达式来删除双引号,我留下了封闭的逗号。
    • 这就是为什么我问数据是否可以包含逗号。如果可以,我不确定您是否可以在不手动检查的情况下修复数据,甚至可能无法修复。并非所有 CSV 数据都包含嵌入式逗号,这就是为什么数据周围的引号实际上是可选的。尽管在您的情况下,您有许多不匹配的引号,这是一个令人担忧的问题,因为它要么表示数据损坏,要么数据本身实际上是二进制的,其中一些显示为引号。
    • 我也应该说,在不知道数据来源的情况下,几乎不可能给出明确的答案。
    猜你喜欢
    • 2021-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-24
    • 2011-08-09
    • 2011-11-14
    相关资源
    最近更新 更多