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


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

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

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

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

string.split(/,/)

даст мне

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

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

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

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

13 68

13 ответов:

отказ от ответственности

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

нестандартный CSV решение

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

дано: определение "CSV String"

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

  1. строка в двойных кавычках. (может содержать одинарные кавычки не экранированные.)
  2. одна строка в кавычках. (может содержать экранированные двойные кавычки.)
  3. строка без кавычек. (не может содержать кавычек, запятых или обратных косых черт.)
  4. пустое значение. (Учитывается значение всех пробелов пустой.)

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

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

найти:

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

устранение:

регулярные выражения, используемые этим решением, являются сложными. И (ИМХО) все нетривиальные регулярные выражения должны быть представлены в режиме свободного интервала с большим количеством комментариев и отступов. К сожалению, JavaScript не позволяет режим свободного интервала. Таким образом, регулярные выражения, реализованные этим решением, сначала представлены в собственном синтаксисе regex (выраженном с помощью удобного Python:r'''...''' сырье-многострочные строки Синтаксис).

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

регулярное выражение для проверки "строки 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 (в соответствии с ранее указанными правилами) и может быть проанализирована с помощью следующего регулярного выражения. Затем для сопоставления одного значения из строки CSV используется следующее регулярное выражение. Он применяется повторно до тех пор, пока не будет найдено больше совпадений (и все значения были проанализированы).

регулярное выражение для анализа одного значения из допустимого 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 : Single quoted string,
| "([^"\]*(?:\[\S\s][^"\]*)*)"   # or : Double quoted string,
| ([^,'"\s\]*(?:\s+[^,'"\s\]+)*)  # or : Non-comma, non-quote stuff.
)                                   # End group of value alternatives.
\s*                                 # Strip whitespace after value.
(?:,|$)                             # Field ends on comma or EOS.
"""

обратите внимание, что существует одно специальное значение case, которому это регулярное выражение не соответствует - самое последнее значение, когда это значение пусто. Это особенное "пустое значение" case проверяется и обрабатывается функцией 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's me!, escaped \, comma"

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

Edit: 2014-05-19: добавил дисклеймер. Edit: 2014-12-01: переместил отказ от ответственности на вершину.

решение 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));

ОТВЕТ

(единое решение)

function CSVtoArray(text) {
    let ret = [''], i = 0, p = '', s = true;
    for (let l in text) {
        l = text[l];
        if ('"' === l) {
            s = !s;
            if ('"' === p) {
                ret[i] += '"';
                l = '-';
            } else if ('' === p)
                l = '-';
        } else if (s && ',' === l)
            l = ret[++i] = '';
        else
            ret[i] += l;
        p = l;
    }
    return ret;
}
let test = '"one","two with escaped """" double quotes""","three, with, commas",four with no quotes,five for fun';
console.log(CSVtoArray(test));

и для удовольствия, вот как вы создаете CSV из массива:

function arrayToCSV(row) {
    for (let i in row) {
        row[i] = row[i].replace(/"/g, '""');
    }
    return '"' + row.join('","') + '"';
}

let row = [
  "one",
  "two with escaped \" double quote",
  "three, with, commas",
  "four with no quotes (now has)",
  "five for fun"
];
let text = arrayToCSV(row);
console.log(text);

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.

Мне понравился ответ FakeRainBrigand, однако он содержит несколько проблем: он не может обрабатывать пробелы между кавычкой и запятой и не поддерживает 2 последовательные запятые. Я попытался отредактировать его ответ, но мое редактирование было отклонено рецензентами, которые, по-видимому, не понимали мой код. Вот моя версия кода 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('|'));

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

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

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

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

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

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

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

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

вы не можете использовать 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

люди, казалось, были против RegEx для этого. Зачем?

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

вот код. Я также сделал скрипка.

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('|'));

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

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

дополнить ответ

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

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

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

address,pincode
foo,baar , 123456

в то время как я разбирал непосредственно без использования baby parse в 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;
    },'')
}

возвращенная функция может быть передана в библиотеку csvtojson и, таким образом, результат может быть использован.

const csv = require('csvtojson')

let csvString = "address, pincode\nfoo, bar, 123456\n"
let jsonArray = []
modifiedCsvString = removeComma(csvString)
csv()
  .fromString(modifiedCsvString)
  .on('json', json => jsonArray.push(json))
  .on('end', () => {
    /* do any thing with the json Array */
  })
Теперь вы можете получить выход, как
[{
  address: 'foo, bar',
  pincode: 123456
}]

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

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 вроде работает, но похоже, что некоторые элементы имеют пробелы перед ними.

помимо отличного и полного ответа от 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 вызов, но по крайней мере вы не будете дублировать код и не включать какие-либо внешние библиотека.

Ref:http://php.net/manual/en/function.str-getcsv.php