ฉันจะแยกวิเคราะห์สตริง CSV ด้วย JavaScript ซึ่งมีลูกน้ำในข้อมูลได้อย่างไร


99

ฉันมีสตริงประเภทต่อไปนี้

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

ฉันต้องการแยกสตริงออกเป็นอาร์เรย์ในแต่ละลูกน้ำ แต่มีเพียงเครื่องหมายจุลภาคที่อยู่นอกเครื่องหมายคำพูดเดียว

ฉันไม่สามารถหานิพจน์ทั่วไปที่ถูกต้องสำหรับการแยก ...

string.split(/,/)

จะให้ฉัน

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

แต่ผลลัพธ์ควรเป็น:

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

มีโซลูชันข้ามเบราว์เซอร์หรือไม่?


มันเป็นคำพูดเดี่ยวเสมอหรือไม่? เคยมีคำพูดเดียวในสตริงที่ยกมาหรือไม่? ถ้าเป็นเช่นนั้นมันจะหนีอย่างไร (แบ็กสแลช, เพิ่มเป็นสองเท่า)?
Phrogz

จะเกิดอะไรขึ้นถ้าอักขระอัญประกาศสามารถใช้แทนกันได้อย่างสมบูรณ์ระหว่างอักขระอัญประกาศคู่และเดี่ยวในโค้ด JavaScript และ HTML / XML หากเป็นเช่นนั้นสิ่งนี้จำเป็นต้องมีการดำเนินการแยกวิเคราะห์ CSV ที่ครอบคลุมมากขึ้น
austincheney

ใช่จริงๆแล้วอาจมีเครื่องหมายคำพูดเดียวอยู่ข้างในการหลีกเลี่ยงด้วยแบ็กสแลชก็ใช้ได้
Hans

ค่าสามารถเป็นสตริงที่ยกมาสองครั้งได้หรือไม่?
ridgerunner

1
Papa Parse ทำได้ดี การแยกวิเคราะห์ไฟล์ CSV ในเครื่องด้วย JavaScript และ Papa Parse: joyofdata.de/blog/…
Raffael

คำตอบ:


217

ข้อจำกัดความรับผิดชอบ

อัปเดต 2014-12-01: คำตอบด้านล่างใช้ได้กับรูปแบบ CSV ที่เฉพาะเจาะจงรูปแบบเดียวเท่านั้น ตามที่ DGระบุไว้อย่างถูกต้องในความคิดเห็นโซลูชันนี้ไม่สอดคล้องกับคำจำกัดความ RFC 4180 ของ CSV และยังไม่พอดีกับรูปแบบ Microsoft Excel โซลูชันนี้แสดงให้เห็นว่าเราสามารถแยกวิเคราะห์บรรทัด CSV หนึ่งบรรทัด (ที่ไม่ได้มาตรฐาน) ซึ่งมีการผสมของประเภทสตริงโดยที่สตริงอาจมีเครื่องหมายอัญประกาศและเครื่องหมายจุลภาคที่ใช้ Escape

โซลูชัน CSV ที่ไม่ได้มาตรฐาน

ดังที่austincheney ชี้ให้เห็นอย่างถูกต้องคุณจำเป็นต้องแยกวิเคราะห์สตริงตั้งแต่ต้นจนจบหากคุณต้องการจัดการสตริงที่ยกมาอย่างถูกต้องซึ่งอาจมีอักขระที่ใช้ Escape นอกจากนี้ OP ไม่ได้กำหนดอย่างชัดเจนว่า "สตริง CSV" คืออะไร ก่อนอื่นเราต้องกำหนดสิ่งที่เป็นสตริง CSV ที่ถูกต้องและแต่ละค่า

ระบุ: คำจำกัดความ "สตริง CSV"

เพื่อจุดประสงค์ของการสนทนานี้ "สตริง CSV" ประกอบด้วยค่าศูนย์หรือมากกว่าโดยที่ค่าหลายค่าจะถูกคั่นด้วยลูกน้ำ แต่ละค่าอาจประกอบด้วย:

  1. สตริงที่มีเครื่องหมายอัญประกาศคู่ (อาจมีเครื่องหมายคำพูดเดี่ยวที่ไม่ใช้ Escape)
  2. สตริงคำพูดเดี่ยว (อาจมีเครื่องหมายคำพูดคู่ที่ไม่ใช้ Escape)
  3. สตริงที่ไม่มีเครื่องหมายคำพูด(ต้องไม่มีเครื่องหมายคำพูดลูกน้ำหรือแบ็กสแลช)
  4. ค่าว่าง (ค่าช่องว่างทั้งหมดถือเป็นค่าว่าง)

กฎ / หมายเหตุ:

  • ค่าที่ยกมาอาจมีเครื่องหมายจุลภาค
  • 'that\'s cool'ค่าที่ยกมาอาจมีหนีอะไรเช่น
  • ต้องใส่เครื่องหมายอัญประกาศจุลภาคหรือแบ็กสแลช
  • ค่าที่มีช่องว่างนำหน้าหรือต่อท้ายต้องถูกยกมา
  • แบ็กสแลชจะถูกลบออกจากทั้งหมด: \'ในค่าที่ยกมาเดียว
  • แบ็กสแลชจะถูกลบออกจากทั้งหมด: \"ในค่าที่ยกมาสองครั้ง
  • สตริงที่ไม่มีเครื่องหมายคำพูดจะถูกตัดออกจากช่องว่างที่นำหน้าและต่อท้าย
  • ตัวคั่นลูกน้ำอาจมีช่องว่างที่อยู่ติดกัน (ซึ่งถูกละเว้น)

หา:

ฟังก์ชัน JavaScript ที่แปลงสตริง CSV ที่ถูกต้อง (ตามที่กำหนดไว้ด้านบน) เป็นอาร์เรย์ของค่าสตริง

วิธีการแก้:

นิพจน์ทั่วไปที่ใช้โดยโซลูชันนี้มีความซับซ้อน และ (IMHO) ทั้งหมดไม่น่ารำคาญแสดงออกปกติควรจะนำเสนอในโหมดฟรีระยะห่างที่มีจำนวนมากแสดงความคิดเห็นและการเยื้อง น่าเสียดายที่ JavaScript ไม่อนุญาตให้ใช้โหมดเว้นระยะห่าง ดังนั้นนิพจน์ทั่วไปที่ใช้โดยโซลูชันนี้จึงถูกนำเสนอครั้งแรกในไวยากรณ์ของนิพจน์ทั่วไปแบบเนทีฟ (แสดงโดยใช้r'''...'''ไวยากรณ์สตริงดิบหลายบรรทัดที่มีประโยชน์ของ Python )

อันดับแรกนี่คือนิพจน์ทั่วไปที่ตรวจสอบว่าสตริง 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 $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.
"""

โปรดสังเกตว่ามีค่ากรณีพิเศษค่าหนึ่งที่นิพจน์ทั่วไปนี้ไม่ตรงกันนั่นคือค่าสุดท้ายเมื่อค่านั้นว่างเปล่า พิเศษนี้"ว่างค่าสุดท้าย"กรณีที่มีการทดสอบและการจัดการโดยใช้ฟังก์ชัน JavaScript ซึ่งต่อไปนี้

ฟังก์ชัน 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 has three elements:
    a[0] = {string, duppi, du}
    a[1] = {23}
    a[2] = {lala} */
// Test 2: Empty CSV string.
var test = "";
var a = CSVtoArray(test);
/* Array has zero elements: */
// Test 3: CSV string with two empty values.
var test = ",";
var a = CSVtoArray(test);
/* Array has two 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 has three 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 has three 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 has eight 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:ย้ายข้อจำกัดความรับผิดชอบไปด้านบน

1
@Evan Plaice - ขอบคุณสำหรับคำพูดที่ดี แน่ใจว่าคุณสามารถใช้ตัวคั่นใดก็ได้ เพียงแทนที่เครื่องหมายจุลภาคทุกตัวใน regex ของฉันด้วยตัวคั่นที่เลือก (แต่ตัวคั่นไม่สามารถเว้นวรรคได้) ไชโย
ridgerunner

2
@Evan Plaice - คุณสามารถใช้ regexes ของฉันเพื่อจุดประสงค์ใดก็ได้ที่คุณต้องการ การจดบันทึกเป็นสิ่งที่ดี แต่ไม่จำเป็น ขอให้โชคดีกับปลั๊กอินของคุณ ไชโย!
ridgerunner

1
เจ๋งดีนี่คือรหัสโครงการ. google.com/p/jquery-csv ในที่สุดฉันต้องการเพิ่มรูปแบบส่วนขยายใน CSV ที่เรียกว่า SSV (ค่าที่แยกจากโครงสร้าง) ซึ่งเป็นเพียง CSV ที่มีข้อมูลเมตา (เช่นตัวคั่นตัวคั่นการสิ้นสุดบรรทัด ฯลฯ )
Evan Plaice

1
ขอบคุณมากสำหรับการใช้งานที่ยอดเยี่ยมนี้ - ฉันใช้มันเป็นพื้นฐานสำหรับโมดูล Node.js ( csv-iterator )
mirkokiefer

3
ฉันขอปรบมือให้กับรายละเอียดและชี้แจงคำตอบของคุณ แต่ควรสังเกตบางแห่งว่าความหมายของ CSV ของคุณไม่ตรงกับ RFC 4180 ซึ่งเป็นสิ่งที่ปิดตามมาตรฐานสำหรับ CSV และที่ฉันสามารถพูดได้โดยทั่วไปมักใช้ โดยเฉพาะอย่างยิ่งนี่เป็นวิธีปกติในการ "หลบหนี" อักขระเครื่องหมายคำพูดคู่ภายในฟิลด์สตริง: "field one", "field two", "a ""final"" field containing two double quote marks"ฉันไม่ได้ทดสอบคำตอบของ Trevor Dixon ในหน้านี้ แต่เป็นคำตอบที่ระบุถึงนิยาม RFC 4180 ของ CSV
DG.

55

โซลูชัน RFC 4180

สิ่งนี้ไม่ได้แก้สตริงในคำถามเนื่องจากรูปแบบไม่เป็นไปตาม RFC 4180 การเข้ารหัสที่ยอมรับได้คือการหลีกเลี่ยงเครื่องหมายอัญประกาศคู่ด้วยเครื่องหมายคำพูดคู่ โซลูชันด้านล่างทำงานได้อย่างถูกต้องกับไฟล์ CSV d / l จาก Google สเปรดชีต

อัพเดท (3/2560)

การแยกวิเคราะห์บรรทัดเดียวจะผิด ตามฟิลด์ 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);


1
คนนี้ทำงานให้ฉันไม่ใช่คนอื่น
WtFudgE

7

ฉันชอบคำตอบของ 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('|'));

7

ไวยากรณ์ 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

ดาวน์โหลด parser ที่สร้างขึ้นที่https://gist.github.com/3362830


6

ฉันมีกรณีการใช้งานที่เฉพาะเจาะจงมากซึ่งฉันต้องการคัดลอกเซลล์จาก Google ชีตไปยังเว็บแอปของฉัน เซลล์อาจมีเครื่องหมายอัญประกาศคู่และอักขระขึ้นบรรทัดใหม่ การใช้การคัดลอกและวางเซลล์จะถูกคั่นด้วยอักขระแท็บและเซลล์ที่มีข้อมูลคี่จะถูกยกมาสองครั้ง ฉันลองใช้วิธีแก้ปัญหาหลักบทความที่เชื่อมโยงโดยใช้ regexp และ Jquery-CSV และ CSVToArray http://papaparse.com/ เป็นหนึ่งเดียวที่ทำงานนอกกรอบ การคัดลอกและวางทำได้อย่างราบรื่นด้วย Google ชีตพร้อมตัวเลือกการตรวจหาอัตโนมัติเริ่มต้น


1
สิ่งนี้ควรได้รับการจัดอันดับให้สูงขึ้นมากอย่าพยายามม้วนตัวแยกวิเคราะห์ CSV ของคุณเองเพราะจะทำงานไม่ถูกต้องโดยเฉพาะเมื่อใช้ regexes Papaparse ยอดเยี่ยมมาก - ใช้มัน!
cbley

4

ผู้คนดูเหมือนจะต่อต้าน 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('|'));

3
อืม regexp ของคุณมีปัญหาบางประการ: ไม่สามารถจัดการช่องว่างระหว่างเครื่องหมายคำพูดและเครื่องหมายจุลภาคและไม่รองรับเครื่องหมายจุลภาค 2 รายการติดต่อกัน ฉันได้อัปเดตคำตอบของคุณด้วยรหัสที่แก้ไขปัญหาทั้งสองและสร้างซอใหม่: jsfiddle.net/xTezm/43
HammerNL

ด้วยเหตุผลบางประการที่ฉันแก้ไขโค้ดของคุณถูกปฏิเสธเนื่องจากจะ "เบี่ยงเบนไปจากจุดประสงค์เดิมของโพสต์" ที่แปลกมาก!? ฉันเพิ่งเอารหัสของคุณและแก้ไขปัญหาสองอย่างกับมัน จะเปลี่ยนความตั้งใจของผู้โพสต์ได้อย่างไร!? อย่างไรก็ตาม ... ฉันเพิ่งเพิ่มคำตอบใหม่สำหรับคำถามนี้
HammerNL

คำถามที่ดีสำหรับคำตอบของคุณ @FakeRainBrigand ฉันสำหรับ regex และด้วยเหตุนี้ฉันจึงยอมรับว่ามันเป็นเครื่องมือที่ไม่ถูกต้องสำหรับงาน
niry

2
@niry รหัสของฉันที่นี่แย่มาก ฉันสัญญาว่าฉันจะดีขึ้นในช่วง 6 ปีที่ผ่านมา :-p
Brigand

4

เพิ่มอีกหนึ่งรายการเพราะฉันพบว่าทั้งหมดข้างต้นไม่ค่อย "จูบ" เพียงพอ

อันนี้ใช้ regex เพื่อค้นหาจุลภาคหรือขึ้นบรรทัดใหม่ในขณะที่ข้ามรายการที่ยกมา หวังว่านี่จะเป็นสิ่งที่ noobies สามารถอ่านได้ด้วยตัวเอง splitFinderregexp มีสามสิ่งที่มันไม่ (แยกด้วย|):

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


หากฉันอ่านไฟล์ผ่าน fileReader และผลลัพธ์ของฉัน: Id, Name, Age 1, John Smith, 65 2, Jane Doe, 30 ฉันจะแยกวิเคราะห์ตามคอลัมน์ที่ฉันระบุได้อย่างไร
bluePearl

หลังจากที่คุณได้รับอาร์เรย์ 2d แล้วให้ลบดัชนีแรกออก (ซึ่งเป็นชื่อเสาของคุณ) จากนั้นทำซ้ำเหนือส่วนที่เหลือของอาร์เรย์สร้างวัตถุที่มีค่าแต่ละค่าเป็นคุณสมบัติ จะมีลักษณะดังนี้[{Id: 1, Name: "John Smith", Age: 65}, {Id: 2, Name: "Jane Doe", Age: 30}]
Seph Reed

3

ถ้าคุณสามารถมีตัวคั่นคำพูดของคุณจะเป็นคำพูดคู่แล้วนี้ซ้ำกับรหัสตัวอย่าง JavaScript เพื่อให้ข้อมูล

คุณสามารถแปล single-quotes ทั้งหมดเป็น double-quotes ก่อน:

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

... หรือคุณสามารถแก้ไขนิพจน์ทั่วไปในคำถามนั้นเพื่อจดจำเครื่องหมายคำพูดเดี่ยวแทนที่จะเป็นเครื่องหมายคำพูดคู่:

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

อย่างไรก็ตามสิ่งนี้ถือว่ามาร์กอัปบางอย่างที่ไม่ชัดเจนจากคำถามของคุณ โปรดชี้แจงว่าความเป็นไปได้ที่หลากหลายของมาร์กอัปเป็นไปได้อย่างไรต่อความคิดเห็นของฉันเกี่ยวกับคำถามของคุณ


2

คำตอบของฉันถือว่าข้อมูลที่คุณป้อนเป็นการสะท้อนรหัส / เนื้อหาจากแหล่งที่มาของเว็บซึ่งอักขระเครื่องหมายคำพูดเดี่ยวและคู่สามารถใช้แทนกันได้อย่างสมบูรณ์หากเกิดขึ้นเป็นชุดการจับคู่ที่ไม่ใช้ Escape

คุณไม่สามารถใช้ regex สำหรับสิ่งนี้ คุณต้องเขียน micro parser เพื่อวิเคราะห์สตริงที่คุณต้องการแยก ฉันจะเรียกส่วนที่ยกมาของสตริงของคุณว่าสตริงย่อยเพื่อประโยชน์ของคำตอบนี้ คุณต้องเดินข้ามเชือกโดยเฉพาะ พิจารณากรณีต่อไปนี้:

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


2

เพื่อเสริมคำตอบนี้

หากคุณต้องการแยกวิเคราะห์เครื่องหมายคำพูดที่ใช้เครื่องหมายคำพูดอื่นตัวอย่าง:

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

ฉันพบว่ายังไม่สามารถแยกวิเคราะห์ได้"jjj "" kkk""","123"
niry

2

ในขณะที่อ่านไฟล์ CSV เป็นสตริงจะมีค่าว่างระหว่างสตริงดังนั้นให้ลองใช้\ 0 ทีละบรรทัด มันใช้ได้กับฉัน

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

2

ฉันยังประสบปัญหาประเภทเดียวกันเมื่อต้องแยกวิเคราะห์ไฟล์ CSV

ไฟล์นี้มีที่อยู่ของคอลัมน์ซึ่งมีเครื่องหมาย ","

หลังจากแยกวิเคราะห์ไฟล์ CSV เป็น JSON ฉันได้รับการแมปคีย์ที่ไม่ตรงกันในขณะที่แปลงเป็นไฟล์ JSON

ผมใช้Node.jsสำหรับการแยกไฟล์และห้องสมุดเช่นแยกทารกและ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
}]

2

ไม่มี regexp อ่านได้และอ้างอิงจาก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;
}

1

ตามโพสต์บล็อกนี้ฟังก์ชั่นนี้ควรทำ:

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 นี้แต่ดูเหมือนว่าองค์ประกอบบางส่วนจะมีช่องว่างอยู่ข้างหน้า


ลองนึกภาพว่าต้องทำทุกอย่างในนิพจน์ทั่วไป นี่คือสาเหตุที่ regexes ไม่เหมาะสำหรับการแยกวิเคราะห์ในบางครั้ง
CanSpice

วิธีนี้ไม่ได้ผล กำหนดสตริงการทดสอบดั้งเดิม: "'string, duppi, du', 23, lala"ฟังก์ชันนี้จะส่งคืน:["'string"," duppi"," du'"," 23"," lala"]
ridgerunner

@ridgerunner: ใช่คุณ ฉันได้แก้ไขคำตอบและ jsfiddle เพื่อแก้ไขฟังก์ชันแล้ว โดยทั่วไปฉันเปลี่ยน"'"ไปใช้'"'และในทางกลับกัน
CanSpice

นั่นช่วยได้ แต่ตอนนี้ฟังก์ชันจัดการกับสตริง CSV ที่มีเครื่องหมายอัญประกาศอย่างไม่ถูกต้องที่มีค่าที่ยกมาสองครั้ง เช่นการย้อนกลับประเภทใบเสนอราคาของสตริงการทดสอบดั้งเดิมดังนี้: '"string, duppi, du", 23, lala'ผลลัพธ์ใน:['"string',' duppi'.' du"',' 23',' lala']
ridgerunner

@ CanSpice ความคิดเห็นของคุณเป็นแรงบันดาลใจให้ฉันลองใช้ RegEx ไม่มีคุณสมบัติมากนัก แต่สามารถเพิ่มได้อย่างง่ายดาย (คำตอบของฉันอยู่ในหน้านี้หากคุณสนใจ)
Brigand

0

สำนวนการช่วยเหลือเป็นประจำ! โค้ดสองสามบรรทัดเหล่านี้จัดการฟิลด์ที่อ้างถึงอย่างถูกต้องพร้อมด้วยเครื่องหมายจุลภาคอัญประกาศและขึ้นบรรทัดใหม่ตามมาตรฐาน RFC 4180

function parseCsv(data, fieldSep, newLine) {
    fieldSep = fieldSep || ',';
    newLine = newLine || '\n';
    var nSep = '\x1D';
    var qSep = '\x1E';
    var cSep = '\x1F';
    var nSepRe = new RegExp(nSep, 'g');
    var qSepRe = new RegExp(qSep, 'g');
    var cSepRe = new RegExp(cSep, 'g');
    var fieldRe = new RegExp('(?<=(^|[' + fieldSep + '\\n]))"(|[\\s\\S]+?(?<![^"]"))"(?=($|[' + fieldSep + '\\n]))', 'g');
    var grid = [];
    data.replace(/\r/g, '').replace(/\n+$/, '').replace(fieldRe, function(match, p1, p2) {
        return p2.replace(/\n/g, nSep).replace(/""/g, qSep).replace(/,/g, cSep);
    }).split(/\n/).forEach(function(line) {
        var row = line.split(fieldSep).map(function(cell) {
            return cell.replace(nSepRe, newLine).replace(qSepRe, '"').replace(cSepRe, ',');
        });
        grid.push(row);
    });
    return grid;
}

const csv = 'A1,B1,C1\n"A ""2""","B, 2","C\n2"';
const separator = ',';      // field separator, default: ','
const newline = ' <br /> '; // newline representation in case a field contains newlines, default: '\n' 
var grid = parseCsv(csv, separator, newline);
// expected: [ [ 'A1', 'B1', 'C1' ], [ 'A "2"', 'B, 2', 'C <br /> 2' ] ]

คุณไม่จำเป็นต้องใช้เครื่องจักรที่มีสถานะ จำกัด นิพจน์ทั่วไปจัดการกับ RFC 4180 ได้อย่างถูกต้องด้วยการมองเชิงบวกเบื้องหลังการมองเชิงลบและการมองเชิงบวก

โคลน / ดาวน์โหลดโค้ดได้ที่https://github.com/peterthoeny/parse-csv-js


0

นอกเหนือจากคำตอบที่ยอดเยี่ยมและสมบูรณ์จาก 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));
}

จะเสียค่าใช้จ่ายในการโทร Ajax หนึ่งครั้ง แต่อย่างน้อยคุณจะไม่ทำซ้ำรหัสหรือรวมไลบรารีภายนอกใด ๆ

อ้างอิง: http://php.net/manual/en/function.str-getcsv.php


0

คุณสามารถใช้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>
    </body>

</html>

อย่าลืมใส่ papaparse.js ไว้ในโฟลเดอร์เดียวกัน


0

ฉันใช้ regex หลายครั้ง แต่ฉันมักจะต้องเรียนรู้ใหม่ทุกครั้งซึ่งน่าผิดหวัง :-)

นี่คือโซลูชันที่ไม่ใช่ regex:

function csvRowToArray(row, delimiter = ',', quoteChar = '"'){
    let nStart = 0, nEnd = 0, a=[], nRowLen=row.length, bQuotedValue;
    while (nStart <= nRowLen) {
        bQuotedValue = (row.charAt(nStart) === quoteChar);
        if (bQuotedValue) {
            nStart++;
            nEnd = row.indexOf(quoteChar + delimiter, nStart)
        } else {
            nEnd = row.indexOf(delimiter, nStart)
        }
        if (nEnd < 0) nEnd = nRowLen;
        a.push(row.substring(nStart,nEnd));
        nStart = nEnd + delimiter.length + (bQuotedValue ? 1 : 0)
    }
    return a;
}

มันทำงานอย่างไร:

  1. ส่งผ่านสตริง csv ในrow.
  2. ในขณะที่ตำแหน่งเริ่มต้นของค่าถัดไปอยู่ในแถวให้ทำดังต่อไปนี้:
    • หากมีการอ้างอิงค่านี้nEndให้ตั้งค่าเป็นเครื่องหมายคำพูดปิด
    • หากไม่ได้ยกราคาให้ตั้งค่าnEndเป็นตัวคั่นถัดไป
    • เพิ่มค่าให้กับอาร์เรย์
    • ตั้งค่าnStartเป็นnEndบวกความยาวของเส้นคั่น

บางครั้งคุณควรเขียนฟังก์ชันเล็ก ๆ ของคุณเองแทนที่จะใช้ไลบรารี โค้ดของคุณจะทำงานได้ดีและใช้เพียงรอยเล็ก ๆ นอกจากนี้คุณสามารถปรับแต่งให้เหมาะกับความต้องการของคุณเองได้อย่างง่ายดาย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.