4

如果以下正则表达式可以逐行拆分 csv 字符串。

var lines = csv.split(/\r|\r?\n/g);

这怎么能适应跳过包含在 CSV 值中的换行符(即引号/双引号之间)?

例子:

2,"Evans & Sutherland","230-132-111AA",,"Visual","P
CB",,1,"Offsite",

如果您没有看到它,这是一个带有可见换行符的版本:

2,"Evans & Sutherland","230-132-111AA",,"Visual","P\r\nCB",,1,"Offsite",\r\n 

我试图跳过的部分是包含在“PCB”条目中间的换行符。

更新:

我之前可能应该提到过这一点,但这是一个名为jquery-csv的专用 CSV 解析库的一部分。为了提供更好的上下文,我在下面添加了当前的解析器实现。

这是验证和解析条目的代码(即一行):

$.csvEntry2Array = function(csv, meta) {
  var meta = (meta !== undefined ? meta : {});
  var separator = 'separator' in meta ? meta.separator : $.csvDefaults.separator;
  var delimiter = 'delimiter' in meta ? meta.delimiter : $.csvDefaults.delimiter;

  // build the CSV validator regex
  var reValid = /^\s*(?:D[^D\\]*(?:\\[\S\s][^D\\]*)*D|[^SD\s\\]*(?:\s+[^SD\s\\]+)*)\s*(?:S\s*(?:D[^D\\]*(?:\\[\S\s][^D\\]*)*D|[^SD\s\\]*(?:\s+[^SD\s\\]+)*)\s*)*$/;
  reValid = RegExp(reValid.source.replace(/S/g, separator));
  reValid = RegExp(reValid.source.replace(/D/g, delimiter));

  // build the CSV line parser regex
  var reValue = /(?!\s*$)\s*(?:D([^D\\]*(?:\\[\S\s][^D\\]*)*)D|([^SD\s\\]*(?:\s+[^SD\s\\]+)*))\s*(?:S|$)/g;
  reValue = RegExp(reValue.source.replace(/S/g, separator), 'g');
  reValue = RegExp(reValue.source.replace(/D/g, delimiter), 'g');

  // Return NULL if input string is not well formed CSV string.
  if (!reValid.test(csv)) {
    return null;
  }

  // "Walk" the string using replace with callback.
  var output = [];
  csv.replace(reValue, function(m0, m1, m2) {
    // Remove backslash from any delimiters in the value
    if (m1 !== undefined) {
      var reDelimiterUnescape = /\\D/g;              
      reDelimiterUnescape = RegExp(reDelimiterUnescape.source.replace(/D/, delimiter), 'g');
      output.push(m1.replace(reDelimiterUnescape, delimiter));
    } else if (m2 !== undefined) { 
      output.push(m2);
    }
    return '';
  });

  // Handle special case of empty last value.
  var reEmptyLast = /S\s*$/;
  reEmptyLast = RegExp(reEmptyLast.source.replace(/S/, separator));
  if (reEmptyLast.test(csv)) {
    output.push('');
  }

  return output;
};

注意:我还没有测试过,但我想我可能会将最后一场比赛合并到主拆分/回调中。

这是执行逐行拆分部分的代码:

$.csv2Array = function(csv, meta) {
  var meta = (meta !== undefined ? meta : {});
  var separator = 'separator' in meta ? meta.separator : $.csvDefaults.separator;
  var delimiter = 'delimiter' in meta ? meta.delimiter : $.csvDefaults.delimiter;
  var skip = 'skip' in meta ? meta.skip : $.csvDefaults.skip;

  // process by line
  var lines = csv.split(/\r\n|\r|\n/g);
  var output = [];
  for(var i in lines) {
    if(i < skip) {
      continue;
    }
    // process each value
    var line = $.csvEntry2Array(lines[i], {
      delimiter: delimiter,
      separator: separator
    });
    output.push(line);
  }

  return output;
};

有关该reges如何工作的详细信息,请查看此答案。我的是稍微改编的版本。我合并了单引号和双引号匹配以仅匹配一个文本分隔符,并使分隔符/分隔符动态化。它在验证实体方面做得很好,但我在顶部添加的线分割解决方案非常脆弱,并且在我上面描述的边缘情况下中断。

我只是在寻找一种解决方案,该解决方案可以遍历提取有效条目的字符串(传递给条目解析器),或者在错误数据上失败,返回一个错误,指示解析失败的行。

更新:

splitLines: function(csv, delimiter) {
  var state = 0;
  var value = "";
  var line = "";
  var lines = [];
  function endOfRow() {
    lines.push(value);
    value = "";
    state = 0;
  };
  csv.replace(/(\"|,|\n|\r|[^\",\r\n]+)/gm, function (m0){
    switch (state) {
      // the start of an entry
      case 0:
        if (m0 === "\"") {
          state = 1;
        } else if (m0 === "\n") {
          endOfRow();
        } else if (/^\r$/.test(m0)) {
          // carriage returns are ignored
        } else {
          value += m0;
          state = 3;
        }
        break;
      // delimited input  
      case 1:
        if (m0 === "\"") {
          state = 2;
        } else {
          value += m0;
          state = 1;
        }
        break;
      // delimiter found in delimited input
      case 2:
        // is the delimiter escaped?
        if (m0 === "\"" && value.substr(value.length - 1) === "\"") {
          value += m0;
          state = 1;
        } else if (m0 === ",") {
          value += m0;
          state = 0;
        } else if (m0 === "\n") {
          endOfRow();
        } else if (m0 === "\r") {
          // Ignore
        } else {
          throw new Error("Illegal state");
        }
        break;
      // un-delimited input
      case 3:
        if (m0 === ",") {
          value += m0;
          state = 0;
        } else if (m0 === "\"") {
          throw new Error("Unquoted delimiter found");
        } else if (m0 === "\n") {
          endOfRow();
        } else if (m0 === "\r") {
          // Ignore
        } else {
          throw new Error("Illegal data");
        }
          break;
      default:
        throw new Error("Unknown state");
    }
    return "";
  });
  if (state != 0) {
    endOfRow();
  }
  return lines;
}

一个分线器只需要 4 个状态:

  • 0:条目的开始
  • 1:以下引用
  • 2:遇到第二个报价
  • 3:以下未引用

它几乎是一个完整的解析器。对于我的用例,我只想要一个分行器,这样我就可以提供一种更精细的方法来处理 CSV 数据。

注意:这种方法的功劳归于另一个未经他许可我不会公开命名的开发者。我所做的只是将它从一个完整的解析器调整为一个分行器。

更新:

在之前的 lineSplitter 实现中发现了一些破碎的边缘情况。所提供的应该完全符合RFC 4180

4

3 回答 3

2

正如我在评论中指出的那样,仅使用单个正则表达式没有完整的解决方案。

这里描述了一种使用多个正则表达式的新方法,方法是在逗号上拆分并用嵌入的逗号连接回字符串 -

就个人而言,我会使用一个简单的有限状态机,如此处所述

状态机有更多的代码,但代码更干净,每段代码在做什么也很清楚。从长远来看,这将更加可靠和可维护。

于 2012-05-02T04:49:15.350 回答
1

使用正则表达式进行解析不是一个好主意。最好用它来检测“坏”分裂,然后将它们合并回来:

var lines = csv.split(/\r?\n/g);
var bad = [];

for(var i=lines.length-1; i> 0; i--) {
    // find all the unescaped quotes on the line:
    var m = lines[i].match(/[^\\]?\"/g);

    // if there are an odd number of them, this line, and the line after it is bad:
    if((m ? m.length : 0) % 2 == 1) { bad.push(i--); }
}

// starting at the bottom of the list, merge lines back, using \r\n
for(var b=0,len=bad.length; b < len; b++) {
    lines.splice(bad[b]-1, 2, lines[bad[b]-1]+"\r\n"+lines[bad[b]]);
}

(此答案已在 CC0 和 WTFPL 下获得许可。)

于 2012-05-02T05:08:38.340 回答
0

小心-该换行符是该值的一部分。不是PCB,是P\nCB

但是,为什么不能只使用string.split(',')?如果需要,您可以遍历列表并强制转换为整数或删除填充引号。

于 2012-05-02T04:57:40.670 回答