Podziel ciąg CSV, pomijając wiersze zawarte w cudzysłowie

Jeśli poniższe wyrażenie regularne może podzielić ciąg csv według linii.

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

Jak można to dostosować do pominięcia znaków nowej linii zawartych w wartości CSV (tj. Między cudzysłowami / cudzysłowami)?

Przykład:

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

Jeśli go nie widzisz, mamy wersję z widocznymi znakami nowej linii:

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

Część, którą próbuję pominąć, to nowa linia zawarta w środku wpisu „PCB”.

Aktualizacja:

Prawdopodobnie powinienem o tym wcześniej wspomnieć, ale jest to część dedykowanej biblioteki analizującej CSV o nazwiejquery-csv. Aby zapewnić lepszy kontekst, dodałem poniżej bieżącą implementację parsera.

Oto kod do sprawdzania poprawności i analizowania wpisu (tj. Jeden wiersz):

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

Uwaga: Jeszcze nie testowałem, ale myślę, że prawdopodobnie mógłbym dołączyć ostatni mecz do głównego podziału / wywołania zwrotnego.

To jest kod, który wykonuje część podzieloną według linii:

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

Aby dowiedzieć się, w jaki sposób działa regesta odpowiedź. Moja jest nieco dostosowana wersja. Skonsolidowałem dopasowanie pojedynczej i podwójnej wyceny tak, aby pasowało tylko do jednego ogranicznika tekstu i sprawiło, że separator / separatory były dynamiczne. Świetnie sprawdza się w zatwierdzaniu zgłoszeń, ale rozwiązanie dzielenia linii, które dodałem na górze, jest dość słabe i załamuje się w przypadku krawędzi opisanym powyżej.

Po prostu szukam rozwiązania, które przenosi ciąg wyodrębniający poprawne wpisy (aby przekazać do parsera wejścia) lub nie działa na złych danych, zwracając błąd wskazujący linię, w której przetwarzanie nie powiodło się.

Aktualizacja:

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

Wystarczył 4 stany dla rozdzielacza linii:

0: początek wpisu1: cytowany jest następujący tekst2: napotkano drugi cytat3: nie cytuje się poniższego

To prawie kompletny parser. W moim przypadku użycia chciałem tylko rozgałęźnika linii, dzięki czemu mogłem zapewnić bardziej granularne podejście do przetwarzania danych CSV.

Uwaga: Kredyt dla tego podejścia trafia do innego deva, którego nie podam publicznie bez jego zgody. Wszystko, co zrobiłem, to dostosowanie go z kompletnego parsera do rozdzielacza linii.

Aktualizacja:

Odkryłem kilka przypadków złamania krawędzi w poprzedniej implementacji lineSplitter. Podany powinien być w pełniRFC 4180 zgodny.

questionAnswers(3)

yourAnswerToTheQuestion