Как я могу проанализировать строку CSV с Javascript, которая содержит запятую в данных?

У меня есть следующий тип строки

var string = "'string, duppi, du', 23, lala"

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

Я не могу определить правильное регулярное выражение для разделения...

string.split(/,/)

даст мне

["'string", " duppi", " du'", " 23", " lala"]

но результат должен быть:

["string, duppi, du", "23", "lala"]

есть ли какое-либо кросс-браузерное решение?

Ответ 1

Отказ

2014-12-01 Обновление: нижеприведенный ответ работает только для одного особого формата CSV. Как правильно указал DG в комментариях, это решение НЕ соответствует определению CSV RFC 4180, а также не соответствует формату MS Excel. Это решение просто демонстрирует, как можно разобрать одну (нестандартную) строку ввода CSV, содержащую сочетание типов строк, где строки могут содержать экранированные кавычки и запятые.

Нестандартное решение CSV

Как правильно указывает austincheney, вам действительно нужно проанализировать строку от начала до конца, если вы хотите правильно обрабатывать строки с кавычками, которые могут содержать экранированные символы. Кроме того, ОП четко не определяет, что такое "строка CSV". Сначала мы должны определить, что представляет собой действительную строку CSV и ее отдельные значения.

Указано: "Определение CSV String"

В целях этого обсуждения "строка CSV" состоит из нуля или более значений, где несколько значений разделяются запятой. Каждое значение может состоять из:

  • Строка с двойными кавычками. (может содержать неизолированные одинарные кавычки.)
  • Одиночная кавычка. (может содержать невыпадающие двойные кавычки.)
  • Строка без кавычек. (не может содержать кавычки, запятые или обратную косую черту).
  • Пустое значение. (Все пробелы считаются пустыми.)

Правила/Примечания:

  • Цитируемые значения могут содержать запятые.
  • Цитируемые значения могут содержать escape--что, например. 'that\ cool'.
  • Цифры, содержащие кавычки, запятые или обратную косую черту, должны быть указаны.
  • Должны быть указаны значения, содержащие ведущие или конечные пробелы.
  • Обратная косая черта удаляется из всех: \' в одинарных кавычках.
  • Обратная косая черта удаляется из всех: \" в двойных кавычках.
  • Строки, не содержащие кавычки, обрезаны из любого верхнего и конечного пробелов.
  • Разделитель запятой может иметь смежные пробелы (которые игнорируются).

Найти:

Функция JavaScript, которая преобразует допустимую строку CSV (как определено выше) в массив строковых значений.

Решение:

Регулярные выражения, используемые этим решением, являются сложными. И (IMHO) все нетривиальные регулярные выражения должны быть представлены в режиме свободного пробела с большим количеством комментариев и отступов. К сожалению, JavaScript не разрешает режим свободного пробела. Таким образом, регулярные выражения, реализованные этим решением, сначала представлены в собственном синтаксисе regex (выражаемом с помощью Python handy: r'''...''' синтаксис raw-multi-line-string).

Сначала это регулярное выражение, которое подтверждает, что строка CVS удовлетворяет вышеуказанным требованиям:

Regex для проверки строки CSV:

re_valid = r"""
# Validate a CSV string having single, double or un-quoted values.
^                                   # Anchor to start of string.
\s*                                 # Allow whitespace before value.
(?:                                 # Group for value alternatives.
  '[^'\\]*(?:\\[\S\s][^'\\]*)*'     # Either Single quoted string,
| "[^"\\]*(?:\\[\S\s][^"\\]*)*"     # or Double quoted string,
| [^,'"\s\\]*(?:\s+[^,'"\s\\]+)*    # or Non-comma, non-quote stuff.
)                                   # End group of value alternatives.
\s*                                 # Allow whitespace after value.
(?:                                 # Zero or more additional values
  ,                                 # Values separated by a comma.
  \s*                               # Allow whitespace before value.
  (?:                               # Group for value alternatives.
    '[^'\\]*(?:\\[\S\s][^'\\]*)*'   # Either Single quoted string,
  | "[^"\\]*(?:\\[\S\s][^"\\]*)*"   # or Double quoted string,
  | [^,'"\s\\]*(?:\s+[^,'"\s\\]+)*  # or Non-comma, non-quote stuff.
  )                                 # End group of value alternatives.
  \s*                               # Allow whitespace after value.
)*                                  # Zero or more additional values
$                                   # Anchor to end of string.
"""

Если строка соответствует указанному выше выражению, то эта строка является допустимой строкой CSV (в соответствии с ранее описанными правилами) и может быть проанализирована с использованием следующего регулярного выражения. Следующее regex затем используется для сопоставления одного значения из строки CSV. Он применяется повторно до тех пор, пока не будет найдено больше совпадений (и все значения были проанализированы).

Regex для синтаксического анализа одного значения из допустимой строки CSV:

re_value = r"""
# Match one value in valid CSV string.
(?!\s*$)                            # Don't match empty last value.
\s*                                 # Strip whitespace before value.
(?:                                 # Group for value alternatives.
  '([^'\\]*(?:\\[\S\s][^'\\]*)*)'   # Either $1: Single quoted string,
| "([^"\\]*(?:\\[\S\s][^"\\]*)*)"   # or $2: Double quoted string,
| ([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)  # or $3: Non-comma, non-quote stuff.
)                                   # End group of value alternatives.
\s*                                 # Strip whitespace after value.
(?:,|$)                             # Field ends on comma or EOS.
"""

Обратите внимание, что есть одно специальное значение, которое это регулярное выражение не совпадает - последнее значение, когда это значение пусто. Этот специальный случай "пустое последнее значение" проверяется и обрабатывается следующей функцией js.

Функция JavaScript для разбора строки CSV:

// Return array of string values, or NULL if CSV string not well formed.
function CSVtoArray(text) {
    var re_valid = /^\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*(?:,\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*)*$/;
    var re_value = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;
    // Return NULL if input string is not well formed CSV string.
    if (!re_valid.test(text)) return null;
    var a = [];                     // Initialize array to receive values.
    text.replace(re_value, // "Walk" the string using replace with callback.
        function(m0, m1, m2, m3) {
            // Remove backslash from \' in single quoted values.
            if      (m1 !== undefined) a.push(m1.replace(/\\'/g, "'"));
            // Remove backslash from \" in double quoted values.
            else if (m2 !== undefined) a.push(m2.replace(/\\"/g, '"'));
            else if (m3 !== undefined) a.push(m3);
            return ''; // Return empty string.
        });
    // Handle special case of empty last value.
    if (/,\s*$/.test(text)) a.push('');
    return a;
};

Пример ввода и вывода:

В следующих примерах фигурные скобки используются для разграничения {result strings}. (Это поможет визуализировать ведущие/конечные пробелы и строки нулевой длины.)

// Test 1: Test string from original question.
var test = "'string, duppi, du', 23, lala";
var a = CSVtoArray(test);
/* Array hes 3 elements:
    a[0] = {string, duppi, du}
    a[1] = {23}
    a[2] = {lala} */
// Test 2: Empty CSV string.
var test = "";
var a = CSVtoArray(test);
/* Array hes 0 elements: */
// Test 3: CSV string with two empty values.
var test = ",";
var a = CSVtoArray(test);
/* Array hes 2 elements:
    a[0] = {}
    a[1] = {} */
// Test 4: Double quoted CSV string having single quoted values.
var test = "'one','two with escaped \' single quote', 'three, with, commas'";
var a = CSVtoArray(test);
/* Array hes 3 elements:
    a[0] = {one}
    a[1] = {two with escaped ' single quote}
    a[2] = {three, with, commas} */
// Test 5: Single quoted CSV string having double quoted values.
var test = '"one","two with escaped \" double quote", "three, with, commas"';
var a = CSVtoArray(test);
/* Array hes 3 elements:
    a[0] = {one}
    a[1] = {two with escaped " double quote}
    a[2] = {three, with, commas} */
// Test 6: CSV string with whitespace in and around empty and non-empty values.
var test = "   one  ,  'two'  ,  , ' four' ,, 'six ', ' seven ' ,  ";
var a = CSVtoArray(test);
/* Array hes 8 elements:
    a[0] = {one}
    a[1] = {two}
    a[2] = {}
    a[3] = { four}
    a[4] = {}
    a[5] = {six }
    a[6] = { seven }
    a[7] = {} */

Дополнительные примечания:

Это решение требует, чтобы строка CSV была "действительной". Например, некотируемые значения могут не содержать обратных косых черт или кавычек, например. следующая строка CSV недействительна:

var invalid1 = "one, that me!, escaped \, comma"

Это не ограничение, потому что любая подстрока может быть представлена ​​как одно или двойное кавычки. Также обратите внимание, что это решение представляет собой только одно возможное определение для: "Comma Separated Values".

Изменить: 2014-05-19: Добавлен отказ от ответственности. Изменить: 2014-12-01: Перемещено выражение об отказе от ответственности.

Ответ 2

Решение RFC 4180

Это не решает строку в вопросе, так как ее формат не соответствует RFC 4180; допустимая кодировка - это двойная кавычка с двойной кавычкой. Нижеприведенное решение работает правильно с CSV файлами d/l из электронных таблиц Google.

ОБНОВЛЕНИЕ (3/2017)

Разбор одной строки был бы неправильным. Согласно RFC 4180 поля могут содержать CRLF, что приведет к тому, что любой считыватель строк разорвет файл CSV. Вот обновленная версия, которая анализирует строку CSV:

'use strict';

function csvToArray(text) {
    let p = '', row = [''], ret = [row], i = 0, r = 0, s = !0, l;
    for (l of text) {
        if ('"' === l) {
            if (s && l === p) row[i] += l;
            s = !s;
        } else if (',' === l && s) l = row[++i] = '';
        else if ('\n' === l && s) {
            if ('\r' === p) row[i] = row[i].slice(0, -1);
            row = ret[++r] = [l = '']; i = 0;
        } else row[i] += l;
        p = l;
    }
    return ret;
};

let test = '"one","two with escaped """" double quotes""","three, with, commas",four with no quotes,"five with CRLF\r\n"\r\n"2nd line one","two with escaped """" double quotes""","three, with, commas",four with no quotes,"five with CRLF\r\n"';
console.log(csvToArray(test));

Ответ 3

PEG (.js), которая обрабатывает примеры RFC 4180 по адресу http://en.wikipedia.org/wiki/Comma-separated_values:

start
  = [\n\r]* first:line rest:([\n\r]+ data:line { return data; })* [\n\r]* { rest.unshift(first); return rest; }

line
  = first:field rest:("," text:field { return text; })*
    & { return !!first || rest.length; } // ignore blank lines
    { rest.unshift(first); return rest; }

field
  = '"' text:char* '"' { return text.join(''); }
  / text:[^\n\r,]* { return text.join(''); }

char
  = '"' '"' { return '"'; }
  / [^"]

Тестируйте по адресу http://jsfiddle.net/knvzk/10 или https://pegjs.org/online.

Загрузите созданный парсер на странице https://gist.github.com/3362830.

Ответ 4

У меня был очень специфический вариант использования, когда я хотел скопировать ячейки из Google Таблиц в мое веб-приложение. Клетки могут включать двойные кавычки и символы новой строки. Используя копирование и вставку, ячейки разделяются символами табуляции, а ячейки с нечетными данными дублируются. Я пробовал это основное решение, связанную статью с помощью regexp, Jquery-CSV и CSVToArray. http://papaparse.com/ Это единственное, что сработало из коробки. Копирование и вставка осуществляется с помощью Google Таблиц с параметрами автоматического обнаружения по умолчанию.

Ответ 5

Мне понравился ответ FakeRainBrigand, однако он содержит несколько проблем: он не может обрабатывать пробелы между цитатой и запятой и не поддерживает две последовательные запятые. Я попытался отредактировать его ответ, но мое редактирование было отклонено рецензентами, которые, по-видимому, не поняли мой код. Вот моя версия кода FakeRainBrigand. Существует также скрипка: http://jsfiddle.net/xTezm/46/

String.prototype.splitCSV = function() {
        var matches = this.match(/(\s*"[^"]+"\s*|\s*[^,]+|,)(?=,|$)/g);
        for (var n = 0; n < matches.length; ++n) {
            matches[n] = matches[n].trim();
            if (matches[n] == ',') matches[n] = '';
        }
        if (this[0] == ',') matches.unshift("");
        return matches;
}

var string = ',"string, duppi, du" , 23 ,,, "string, duppi, du",dup,"", , lala';
var parsed = string.splitCSV();
alert(parsed.join('|'));

Ответ 6

Добавление еще одного в список, потому что я считаю, что все вышеперечисленное не совсем "поцелуй" достаточно.

Этот использует регулярное выражение, чтобы найти запятые или новые строки при пропуске цитируемых элементов. Надеюсь, это то, что новички могут прочитать самостоятельно. У регулярного выражения splitFinder есть три вещи, которые он делает (разделенные на |):

  1. , - считает запятые
  2. \r?\n - находит новые строки (потенциально с возвратом каретки, если экспортер был хорош)
  3. "(\\"|[^"])*?" - пропускает все, что заключено в кавычки, потому что запятые и переводы строк там не имеют значения. Если в указанном элементе есть экранированная цитата \\", она будет захвачена до того, как будет найдена конечная цитата.

const splitFinder = /,|\r?\n|"(\\"|[^"])*?"/g;

function csvTo2dArray(parseMe) {
  let currentRow = [];
  const rowsOut = [currentRow];
  let lastIndex = splitFinder.lastIndex = 0;
  
  // add text from lastIndex to before a found newline or comma
  const pushCell = (endIndex) => {
    endIndex = endIndex || parseMe.length;
    const addMe = parseMe.substring(lastIndex, endIndex);
    // remove quotes around the item
    currentRow.push(addMe.replace(/^"|"$/g, ""));
    lastIndex = splitFinder.lastIndex;
  }


  let regexResp;
  // for each regexp match (either comma, newline, or quoted item)
  while (regexResp = splitFinder.exec(parseMe)) {
    const split = regexResp[0];

    // if it not a quote capture, add an item to the current row
    // (quote captures will be pushed by the newline or comma following)
    if (split.startsWith('"') === false) {
      const splitStartIndex = splitFinder.lastIndex - split.length;
      pushCell(splitStartIndex);

      // then start a new row if newline
      const isNewLine = /^\r?\n$/.test(split);
      if (isNewLine) { rowsOut.push(currentRow = []); }
    }
  }
  // make sure to add the trailing text (no commas or newlines after)
  pushCell();
  return rowsOut;
}

const rawCsv = 'a,b,c\n"test\r\n","comma, test","\r\n",",",\nsecond,row,ends,with,empty\n"quote\"test"'
const rows = csvTo2dArray(rawCsv);
console.log(rows);

Ответ 7

Если у вас есть разделитель цитат, это двойные кавычки, то это дубликат кода JavaScript для анализа данных CSV.

Вы можете сначала перевести все одиночные кавычки в двойные кавычки:

string = string.replace( /'/g, '"' );

... или вы можете редактировать регулярное выражение в этом вопросе для распознавания одиночных кавычек вместо двойных кавычек:

// Quoted fields.
"(?:'([^']*(?:''[^']*)*)'|" +

Однако это предполагает определенную разметку, которая непонятна из вашего вопроса. Просьба уточнить, каковы могут быть все различные возможности разметки в соответствии с моим комментарием по вашему вопросу.

Ответ 8

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

Вы не можете использовать regex для этого. Фактически вам нужно написать микроанализатор для анализа строки, которую вы хотите разделить. Я, ради этого ответа, вызову цитированные части ваших строк в виде подстрок. Вам нужно специально пройти через строку. Рассмотрим следующий случай:

var a = "some sample string with \"double quotes\" and 'single quotes' and some craziness like this: \\\" or \\'",
    b = "sample of code from JavaScript with a regex containing a comma /\,/ that should probably be ignored.";

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

Я не собираюсь писать этот уровень сложности кода для вас, но вы можете посмотреть на то, что я недавно написал, который имеет шаблон, который вам нужен. Этот код не имеет ничего общего с запятыми, но в остальном это достаточно правильный микропарсер, чтобы вы могли писать свой собственный код. Посмотрите на функцию asifix следующего приложения:

https://github.com/austincheney/Pretty-Diff/blob/master/fulljsmin.js

Ответ 9

Люди, похоже, против RegEx для этого. Почему?

(\s*'[^']+'|\s*[^,]+)(?=,|$)

Вот код. Я также сделал fiddle.

String.prototype.splitCSV = function(sep) {
  var regex = /(\s*'[^']+'|\s*[^,]+)(?=,|$)/g;
  return matches = this.match(regex);    
}

var string = "'string, duppi, du', 23, 'string, duppi, du', lala";
var parsed = string.splitCSV();
alert(parsed.join('|'));

Ответ 10

При чтении csv для строки он содержит нулевое значение между строкой, поэтому попробуйте \0. По строкам он работает.

stringLine = stringLine.replace( /\0/g, "" );

Ответ 11

Чтобы дополнить этот ответ

Если вам нужно разобрать цитаты, экранированные другой цитатой, например:

"some ""value"" that is on xlsx file",123

Вы можете использовать

function parse(text) {
  const csvExp = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|"([^""]*(?:"[\S\s][^""]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;

  const values = [];

  text.replace(csvExp, (m0, m1, m2, m3, m4) => {
    if (m1 !== undefined) {
      values.push(m1.replace(/\\'/g, "'"));
    }
    else if (m2 !== undefined) {
      values.push(m2.replace(/\\"/g, '"'));
    }
    else if (m3 !== undefined) {
      values.push(m3.replace(/""/g, '"'));
    }
    else if (m4 !== undefined) {
      values.push(m4);
    }
    return '';
  });

  if (/,\s*$/.test(text)) {
    values.push('');
  }

  return values;
}

Ответ 12

Я также сталкивался с тем же типом проблемы, когда мне приходится разбирать файл CSV. Файл содержит адрес столбца, который содержит ",".
После разбора этого CSV на JSON я получаю несогласованное отображение ключей при преобразовании его в файл JSON.
Я использовал node для разбора файла и библиотеки, например детский анализ и csvtojson
Пример файла -

address,pincode
foo,baar , 123456

Пока я разбирался напрямую без использования детского анализа в JSON, я получал

[{
 address: 'foo',
 pincode: 'baar',
 'field3': '123456'
}]

Итак, я написал код, который удаляет запятую (,) с любым другим разделителем с каждым полем

/*
 csvString(input) = "address, pincode\\nfoo, bar, 123456\\n"
 output = "address, pincode\\nfoo {YOUR DELIMITER} bar, 123455\\n"
*/
const removeComma = function(csvString){
    let delimiter = '|'
    let Baby = require('babyparse')
    let arrRow = Baby.parse(csvString).data;
    /*
      arrRow = [ 
      [ 'address', 'pincode' ],
      [ 'foo, bar', '123456']
      ]
    */
    return arrRow.map((singleRow, index) => {
        //the data will include 
        /* 
        singleRow = [ 'address', 'pincode' ]
        */
        return singleRow.map(singleField => {
            //for removing the comma in the feild
            return singleField.split(',').join(delimiter)
        })
    }).reduce((acc, value, key) => {
        acc = acc +(Array.isArray(value) ?
         value.reduce((acc1, val)=> {
            acc1 = acc1+ val + ','
            return acc1
        }, '') : '') + '\n';
        return acc;
    },'')
}

Ответ 13

Согласно этот пост в блоге, эта функция должна это сделать:

String.prototype.splitCSV = function(sep) {
  for (var foo = this.split(sep = sep || ","), x = foo.length - 1, tl; x >= 0; x--) {
    if (foo[x].replace(/'\s+$/, "'").charAt(foo[x].length - 1) == "'") {
      if ((tl = foo[x].replace(/^\s+'/, "'")).length > 1 && tl.charAt(0) == "'") {
        foo[x] = foo[x].replace(/^\s*'|'\s*$/g, '').replace(/''/g, "'");
      } else if (x) {
        foo.splice(x - 1, 2, [foo[x - 1], foo[x]].join(sep));
      } else foo = foo.shift().split(sep).concat(foo);
    } else foo[x].replace(/''/g, "'");
  } return foo;
};

Вы бы назвали это так:

var string = "'string, duppi, du', 23, lala";
var parsed = string.splitCSV();
alert(parsed.join("|"));

Этот jsfiddle работает, но похоже, что некоторые из элементов имеют перед ними пробелы.

Ответ 14

Помимо отличного и полного ответа от ridgerunner, я подумал о очень простом обходном пути, когда ваш бэкэнд запускает php.

Добавьте этот php файл в ваш доменный сервер (скажем: csv.php)

<?php
session_start(); //optional
header("content-type: text/xml");
header("charset=UTF-8");
//set the delimiter and the End of Line character of your csv content:
echo json_encode(array_map('str_getcsv',str_getcsv($_POST["csv"],"\n")));
?>

Теперь добавьте эту функцию в свой инструментарий javascript (нужно немного переделать, чтобы сделать crossbrowser, на мой взгляд.)

function csvToArray(csv) {
    var oXhr = new XMLHttpRequest;
    oXhr.addEventListener("readystatechange",
            function () {
                if (this.readyState == 4 && this.status == 200) {
                    console.log(this.responseText);
                    console.log(JSON.parse(this.responseText));
                }
            }
    );
    oXhr.open("POST","path/to/csv.php",true);
    oXhr.setRequestHeader("Content-type","application/x-www-form-urlencoded; charset=utf-8");
    oXhr.send("csv=" + encodeURIComponent(csv));
}

Будет стоить вам 1 вызов ajax, но, по крайней мере, вы не будете дублировать код и не включать внешнюю библиотеку.

Ссылка: http://php.net/manual/en/function.str-getcsv.php

Ответ 15

Вы можете использовать papaparse.js как пример ниже:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>CSV</title>
</head>
<body>

    <input type="file" id="files" multiple="">
    <button onclick="csvGetter()">CSV Getter</button>
    <h3>The Result will be in the Console.</h3>


<script src="papaparse.min.js"></script>
<script>
     function csvGetter() {

        var file = document.getElementById('files').files[0];
        Papa.parse(file, {
            complete: function(results) {
                console.log(results.data);
                }
           });
        }

  </script>

Не забудьте включить файл papaparse.js в ту же папку.

Ответ 16

нет регулярных выражений, читается согласно https://en.wikipedia.org/wiki/Comma-separated_values#Basic_rules

function csv2arr(str: string) {
    let line = ["",];
    const ret = [line,];
    let quote = false;

    for (let i = 0; i < str.length; i++) {
        const cur = str[i];
        const next = str[i + 1];

        if (!quote) {
            const cellIsEmpty = line[line.length - 1].length === 0;
            if (cur === '"' && cellIsEmpty) quote = true;
            else if (cur === ",") line.push("");
            else if (cur === "\r" && next === "\n") { line = ["",]; ret.push(line); i++; }
            else if (cur === "\n" || cur === "\r") { line = ["",]; ret.push(line); }
            else line[line.length - 1] += cur;
        } else {
            if (cur === '"' && next === '"') { line[line.length - 1] += cur; i++; }
            else if (cur === '"') quote = false;
            else line[line.length - 1] += cur;
        }
    }
    return ret;
}