Как я могу проанализировать строку CSV с Javascript, который содержит запятую в данных? - PullRequest
78 голосов
/ 13 декабря 2011

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

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

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

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

string.split(/,/)

даст мне

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

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

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

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

Ответы [ 15 ]

188 голосов
/ 14 декабря 2011

Отказ

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

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

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

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

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

  1. Строка в двойных кавычках. (может содержать неэкранированные одинарные кавычки.)
  2. Строка в одинарных кавычках. (может содержать неэкранированные двойные кавычки.)
  3. Строка без кавычек. (НЕ МОЖЕТ содержать кавычки, запятые или обратную косую черту.)
  4. Пустое значение. (Все пустые значения считаются пустыми.)

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

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

Найти:

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

Решение:

Регулярные выражения, используемые этим решением, являются сложными. И (IMHO) все нетривиальных регулярных выражений должны быть представлены в свободном интервале с большим количеством комментариев и отступов. К сожалению, JavaScript не поддерживает режим свободного пробела. Таким образом, регулярные выражения, реализованные этим решением, сначала представлены в собственном синтаксисе регулярных выражений (выражается с помощью удобного Python: 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 (в соответствии с ранее указанными правилами) и может быть проанализирована с использованием следующего регулярного выражения. Затем используется следующее регулярное выражение для сопоставления одного значения из строки 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's me!, escaped \, comma"

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

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

31 голосов
/ 10 января 2017

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);
6 голосов
/ 15 августа 2012

грамматика 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.

3 голосов
/ 16 апреля 2019

Добавление еще одного в список, потому что я нахожу все вышеперечисленное не совсем "ПОЦЕЛУЙ" достаточно.

Этот метод использует регулярные выражения, чтобы найти запятые или символы новой строки, пропуская цитируемые элементы. Надеюсь, это то, что новички могут прочитать самостоятельно. 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's 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);
3 голосов
/ 18 ноября 2016

Мне понравился ответ 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('|'));
3 голосов
/ 25 апреля 2016

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

2 голосов
/ 13 декабря 2011

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

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

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

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

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

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

1 голос
/ 09 июня 2017

Я также столкнулся с такой же проблемой, когда мне нужно проанализировать файл CSV. Файл содержит столбец Address, который содержит ','.
После анализа этого CSV в JSON я получаю несоответствующее отображение ключей при преобразовании его в файл JSON.
Я использовал узел для разбора файла и библиотеки как baby parse и 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;
    },'')
}

Возвращенная функция может быть передана в библиотеку 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
}]
1 голос
/ 20 мая 2016

В дополнение к этому ответу

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

"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;
}
1 голос
/ 16 июня 2015

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

stringLine = stringLine.replace( /\0/g, "" );
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...