Вопрос по regex, parsing, javascript – Разделить строку CSV по строке, пропуская символы новой строки, заключенные в кавычки

3

Если следующее регулярное выражение может разбить строку 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"; запись.

Update:

Я, вероятно, должен был упомянуть об этом раньше, но это часть выделенной библиотеки синтаксического анализа CSV, которая называетсяJQuery-CSV, Чтобы обеспечить лучший контекст, я добавил текущую реализацию синтаксического анализатора ниже.

Here's the code for validating and parsing an entry (ie one line):

$.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;
};

Note: I haven't tested yet but I think I could probably incorporate the last match into the main split/callback.

This is the code that does the split-by-line part:

$.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;
};

Чтобы понять, как это работает, взгляните наэтот ответ, Моя версия слегка адаптирована. Я объединил одинарные и двойные кавычки, чтобы сопоставить только один текстовый разделитель, и сделал разделитель / разделитель динамическим. Он отлично справляется с проверкой записей, но решение о разделении строк, которое я добавил сверху, довольно хрупкое и ломает случай с краями, который я описал выше.

Я просто ищу решение, которое просматривает строку, извлекающую допустимые записи (для передачи анализатору записей), или приводит к сбою неверных данных, возвращая ошибку, указывающую строку, на которой произошел сбой анализа

Update:

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;
}

All it took is 4 states for a line splitter:

0: the start of an entry 1: the following is quoted 2: a second quote has been encountered 3: the following isn't quoted

Это почти полный анализатор. Для моего случая использования я просто хотел разделитель строк, чтобы я мог предоставить более грандиозный подход к обработке данных CSV.

Note: Credit for this approach goes to another dev whom I won't name publicly without his permission. All I did was adapt it from a complete parser to a line-splitter.

Update:

Обнаружено несколько прерванных случаев в предыдущей реализации lineSplitter. Предоставленный должен быть полностьюRFC 4180 совместимый.

Regex для обнаружения, а не для анализа. Если вы сканируете текст, если вам нужно «запомнить» что угодно, кроме символов (то есть я "m" внутри "цитируемого литерала), вы анализируете его. Тонкая разница, поэтому каждый хочет использовать его для разбора. Jeff Meatball Yang
Вы просто не можете сделать это с помощью регулярных выражений. Вы можете создать регулярное выражение, которое обрабатывает некоторые или даже большинство условий. Но всегда будет какой-то действительный CSV, который не будет работать с регулярным выражением. James Anderson

Ваш Ответ

3   ответа
2

Как я отметил в комментарии, нет полного решения, использующего только одно регулярное выражение.

Описан новый метод, использующий несколько регулярных выражений путем разделения на запятую и объединения обратных строк со встроенными запятымиВот:-

Лично я бы использовал простой конечный автомат, как описаноВот

Конечный автомат имеет больше кода, но код чище и понятно, что делает каждый фрагмент кода. В долгосрочной перспективе это будет гораздо надежнее и ремонтопригоднее.

Error: User Rate Limit Exceeded Evan Plaice
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]]);
}

(This answer is licensed under both CC0 and WTFPL.)

Error: User Rate Limit Exceeded Evan Plaice
0

Будьте осторожны - эта новая строка является ЧАСТЬЮ этого значения. Это неPCBэтоP\nCB.

Однако почему вы не можете просто использоватьstring.split(',')? При необходимости вы можете просмотреть список и привести к целочисленным значениям или удалить дополненные кавычки.

Error: User Rate Limit Exceeded Evan Plaice

Похожие вопросы