Il formato CSV può essere definito da una regex?


19

Un collega e io abbiamo recentemente discusso se una regex pura sia in grado di incapsulare completamente il formato CSV, in modo che sia in grado di analizzare tutti i file con un dato carattere di escape, carattere di virgolette e carattere di separazione.

Il regex non deve essere in grado di modificare questi caratteri dopo la creazione, ma non deve fallire in nessun altro caso limite.

Ho sostenuto che questo è impossibile solo per un tokenizer. L'unica regex che potrebbe essere in grado di farlo è uno stile PCRE molto complesso che va oltre la semplice tokenizzazione.

Sto cercando qualcosa sulla falsariga di:

... il formato CSV è una grammatica libera dal contesto e come tale, è impossibile analizzare solo con regex ...

O mi sbaglio? È possibile analizzare CSV con solo una regex POSIX?

Ad esempio, se sono sia il carattere di escape che il carattere di virgolette ", allora queste due righe sono csv valide:

"""this is a test.""",""
"and he said,""What will be, will be."", to which I replied, ""Surely not!""","moving on to the next field here..."

non è un CSV perché non c'è nidificazione da nessuna parte (IIRC)
maniaco del cricchetto

1
ma quali sono i casi limite? forse c'è di più in CSV, di quanto avessi mai pensato?
c69,

1
@ c69 Che ne dici di escape e quote char sono entrambi ". Quindi vale quanto segue:"""this is a test.""",""
Spencer Rathbun,

Hai provato regexp da qui ?
dasblinkenlight,

1
È necessario fare attenzione ai casi limite, ma un regex dovrebbe essere in grado di tokenizzare CSV come è stato descritto. Il regex non ha bisogno di contare un numero arbitrario di virgolette - deve solo contare fino a 3, cosa che le espressioni regolari possono fare. Come altri hanno già detto, dovresti provare a scrivere una rappresentazione ben definita di ciò che ti aspetti che sia un token CSV ...
comingstorm

Risposte:


20

Bello in teoria, terribile in pratica

Per CSV suppongo che intendi la convenzione come descritto in RFC 4180 .

Mentre la corrispondenza dei dati CSV di base è banale:

"data", "more data"

Nota: A proposito, è molto più efficiente utilizzare una funzione .split ('/ n'). Split ('"') per dati molto semplici e ben strutturati come questo. Le espressioni regolari funzionano come NDFSM (non deterministico finito State Machine) che fa perdere molto tempo indietro quando si inizia ad aggiungere casi limite come i caratteri di escape.

Ad esempio, ecco la stringa di corrispondenza delle espressioni regolari più completa che ho trovato:

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.
"""

Gestisce ragionevolmente valori tra virgolette singole e doppie, ma non newline in valori, virgolette di escape, ecc.

Fonte: StackTranslate.it - ​​Come posso analizzare una stringa con JavaScript

Diventa un incubo una volta introdotti i casi limite comuni come ...

"such as ""escaped""","data"
"values that contain /n newline chars",""
"escaped, commas, like",",these"
"un-delimited data like", this
"","empty values"
"empty trailing values",        // <- this is completely valid
                                // <- trailing newline, may or may not be included

Il solo caso limite come valore di riga da solo è sufficiente per interrompere il 99,9999% dei parser basati su RegEx trovati in natura. L'unica alternativa "ragionevole" è quella di utilizzare la corrispondenza RegEx per la tokenizzazione di base di controllo / non controllo (ovvero terminale vs non terminale) accoppiata con una macchina a stati utilizzata per analisi di livello superiore.

Fonte: esperienza altrimenti nota come dolore e sofferenza estesi.

Sono l'autore di jquery-CSV , l'unico parser CSV basato su javascript, completamente conforme a RFC, al mondo. Ho trascorso mesi ad affrontare questo problema, parlando con molte persone intelligenti e provando moltissime implementazioni diverse tra cui 3 riscritture complete del motore di analisi principale.

tl; dr - Morale della storia, PCRE da solo fa schifo per l'analisi di qualsiasi cosa tranne le grammatiche regolari più semplici e rigorose (Ie Tipo III). Sebbene sia utile per tokenizzare stringhe terminali e non terminali.


1
Sì, è stata anche la mia esperienza. Ogni tentativo di incapsulare completamente più di un modello CSV molto semplice si imbatte in queste cose, e quindi ti scontri sia con i problemi di efficienza che con quelli di complessità di una regex massiccia. Hai guardato la libreria node-csv ? Sembra confermare anche questa teoria. Ogni implementazione non banale utilizza internamente un parser.
Spencer Rathbun,

@SpencerRathbun Yep. Sono sicuro di aver dato un'occhiata alla fonte node-csv prima. Sembra utilizzare una tipica macchina a stati di tokenizzazione dei caratteri per l'elaborazione. Il parser jquery-csv funziona sullo stesso concetto fondamentale tranne che uso regex per la tokenizzazione terminale / non terminale. Invece di valutare e concatenare su base char-by-char, regex è in grado di abbinare più caratteri non terminali contemporaneamente e restituirli come gruppo (cioè stringa). Ciò riduce al minimo la concatenazione non necessaria e "dovrebbe" aumentare l'efficienza.
Evan Plaice,

20

Regex può analizzare qualsiasi linguaggio normale e non può analizzare cose fantasiose come grammatiche ricorsive. Ma CSV sembra essere abbastanza regolare, così analizzabile con una regex.

Lavoriamo dalla definizione : sono consentite la sequenza, la scelta della forma alternativa ( |) e la ripetizione (stella di Kleene, la *).

  • Un valore non quotato è regolare: [^,]*# qualsiasi carattere tranne virgola
  • Un valore tra virgolette è regolare: "([^\"]|\\\\|\\")*"# sequenza di qualsiasi cosa tranne virgoletta "o citazione \"sfuggita o fuga sfuggita\\
    • Alcuni moduli possono includere virgolette di escape con virgolette, che aggiunge una variante ("")*"all'espressione sopra.
  • Un valore consentito è regolare: <unquoted-value> |<quoted-value>
  • Una singola riga CSV è regolare: <valore> (,<valore>)*
  • Anche una sequenza di linee separate da \nè ovviamente regolare.

Non ho testato meticolosamente ciascuna di queste espressioni e non ho mai definito i gruppi di cattura. Ho anche sorvolato alcuni aspetti tecnici, come le varianti di caratteri che possono essere utilizzati al posto di ,, "o linea separatori: questi non rompere la regolarità, basta avere diverse lingue leggermente diverse.

Se riesci a individuare un problema in questa prova, ti preghiamo di commentare! :)

Ma nonostante ciò, l' analisi pratica dei file CSV con espressioni regolari pure può essere problematica. È necessario sapere quale delle varianti viene inviata al parser e non esiste uno standard per questo. Puoi provare diversi parser su ogni riga fino a quando uno non riesce, o in qualche modo dividere il formato dei commenti del modulo. Ma ciò può richiedere mezzi diversi dalle espressioni regolari per fare in modo efficiente o per niente.


4
Assolutamente un +1 per il punto pratico. C'è qualcosa che sono sicuro, da qualche parte in profondità è un esempio di un valore (inventato) che spezzerebbe la versione del valore citato, ma non so di cosa si tratti. Il "divertimento" con più parser sarebbe "questi due funzionano, ma danno risposte diverse"

1
Avrai ovviamente bisogno di regex differenti per le virgolette con il backslash rispetto alle virgolette con il doppio delle virgolette. Una regex per il primo tipo di campo CSV dovrebbe essere qualcosa di simile [^,"]*|"(\\(\\|")|[^\\"])*", e il secondo dovrebbe essere qualcosa di simile [^,"]*|"(""|[^"])*". (Attenzione, poiché non ho provato nessuno di questi!)
comingstorm

A caccia di qualcosa che potrebbe essere uno standard, c'è un caso mancato: un valore con un delimitatore di record incluso. Questo rende anche l'analisi pratica ancora più divertente quando ci sono diversi modi per gestirlo

Bella risposta, ma se corro perl -pi -e 's/"([^\"]|\\\\|\\")*"/yay/'e installo "I have here an item,\" that is a test\""poi il risultato è `yay che è un test \" ". Pensa che il tuo regex sia difettoso.
Spencer Rathbun,

@SpencerRathbun: quando avrò più tempo in realtà testerò le regex e probabilmente incolerò anche un codice di prova che supera i test. Siamo spiacenti, la giornata lavorativa sta succedendo.
9000

5

Risposta semplice - probabilmente no.

Il primo problema è la mancanza di uno standard. Mentre uno può descrivere il suo CSV in un modo che è strettamente definito, non ci si può aspettare di ottenere file CSV rigorosamente definiti. "Sii conservatore in ciò che fai, sii liberale in ciò che accetti dagli altri" -Jon Postal

Supponendo che si abbia uno stile standard accettabile, c'è la questione dei caratteri di escape e se questi devono essere bilanciati.

Una stringa in molti formati CSV è definita come string value 1,string value 2. Tuttavia, se quella stringa contiene una virgola, lo è ora "string, value 1",string value 2. Se contiene una citazione diventa "string, ""value 1""",string value 2.

A questo punto credo sia impossibile. Il problema è che devi determinare quante virgolette hai letto e se una virgola si trova all'interno o all'esterno della modalità a virgolette doppie del valore. Il bilanciamento delle parentesi è un problema regex impossibile. Alcuni motori di espressione regolare estesa (PCRE) possono occuparsene, ma allora non è un'espressione regolare.

Potresti trovare utile /programming/8629763/csv-parsing-with-a-context-free-grammar .


modificato:

Ho cercato i formati per i caratteri di escape e non ho trovato nessuno che abbia bisogno di un conteggio arbitrario, quindi probabilmente non è questo il problema.

Tuttavia, ci sono problemi di qual è il carattere di escape e delimitatore di record (per cominciare). http://www.csvreader.com/csv_format.php è una buona lettura dei diversi formati in natura.

  • Le regole per la stringa tra virgolette (se è una stringa tra virgolette singole o una stringa tra virgolette doppie) differiscono.
    • 'This, is a value' vs "This, is a value"
  • Le regole per i personaggi di fuga
    • "This ""is a value""" vs "This \"is a value\""
  • La gestione del delimitatore di record incorporato ({rd})
    • (incorporato grezzo) "This {rd}is a value"vs (scappato) "This \{rd}is a value"vs (tradotto)"This {0x1C}is a value"

La cosa chiave qui è che è possibile avere una stringa che avrà sempre più interpretazioni valide.

La domanda correlata (per i casi limite) "è possibile avere una stringa non valida accettata?"

Dubito ancora fortemente che esista un'espressione regolare che può corrispondere a ogni CSV valido creato da un'applicazione e che rifiuta ogni CSV che non può essere analizzato.


1
Le citazioni all'interno delle virgolette non devono essere bilanciate. Invece, ci deve essere un numero pari di citazioni prima di un preventivo integrato, che è ovviamente regolare: ("")*". Se le quotazioni all'interno del valore sono sbilanciate, non sono già affari nostri.
9000

Questa è la mia posizione, avendo incontrato queste orribili scuse per il "trasferimento di dati" in passato. L'unica cosa che li gestiva correttamente era un parser, il regex puro si spezzava ogni poche settimane.
Spencer Rathbun,

2

Definisci prima la grammatica per il tuo CSV (i delimitatori di campo sono scappati o codificati in qualche modo se appaiono nel testo?) E quindi può essere determinato se è analizzabile con regex. Prima grammatica: secondo parser: http://www.boyet.com/articles/csvparser.html Va notato che questo metodo utilizza un tokenizer, ma non riesco a costruire un regex POSIX che corrisponda a tutti i casi limite. Se il tuo utilizzo dei formati CSV è non regolare e privo di contesto ... allora la tua risposta è nella tua domanda. Buona panoramica qui: http://nikic.github.com/2012/06/15/The-true-power-of-regular-expressions.html


2

Questo regexp può tokenizzare CSV normale, come descritto nella RFC:

/("(?:[^"]|"")*"|[^,"\n\r]*)(,|\r?\n|\r)/

Spiegazione:

  • ("(?:[^"]|"")*"|[^,"\n\r]*) - un campo CSV, citato o meno
    • "(?:[^"]|"")*" - un campo tra virgolette;
      • [^"]|""- ogni personaggio non è ", o è "sfuggito a""
    • [^,"\n\r]* - un campo non quotato, che potrebbe non contenere , " \n \r
  • (,|\r?\n|\r)- il seguente separatore, uno ,o una nuova riga
    • \r?\n|\r - una nuova riga, una delle \r\n \n \r

È possibile abbinare e convalidare un intero file CSV utilizzando questo regexp ripetutamente. È quindi necessario correggere i campi tra virgolette e dividerli in righe in base ai separatori.

Ecco il codice per un parser CSV in Javascript, basato su regexp:

var csv_tokens_rx = /("(?:[^"]|"")*"|[^,"\n\r]*)(,|\r?\n|\r)/y;
var csv_unescape_quote_rx = /""/g;
function csv_parse(s) {
    if (s && s.slice(-1) != '\n')
        s += '\n';
    var ok;
    var rows = [];
    var row = [];
    csv_tokens_rx.lastIndex = 0;
    while (true) {
        ok = csv_tokens_rx.lastIndex == s.length;
        var m = s.match(csv_tokens_rx);
        if (!m)
            break;
        var v = m[1], d = m[2];
        if (v[0] == '"') {
            v = v.slice(1, -1);
            v = v.replace(csv_unescape_quote_rx, '"');
        }
        if (d == ',' || v)
            row.push(v);
        if (d != ',') {
            rows.push(row)
            row = [];
        }
    }
    return ok ? rows : null;
}

Se questa risposta ti aiuti a risolvere il tuo argomento, devi decidere tu; Sono solo felice di avere un parser CSV piccolo, semplice e corretto.

A mio avviso, un lexprogramma è più o meno una grande espressione regolare e questi possono tokenizzare formati molto più complessi, come il linguaggio di programmazione C.

Con riferimento alle definizioni RFC 4180 :

  1. line break (CRLF) - Il regexp è più flessibile, consentendo CRLF, LF o CR.
  2. L'ultimo record nel file può avere o meno un'interruzione di riga finale. La regexp così com'è richiede un'interruzione di riga finale, ma il parser si adatta a quello.
  3. C'è forse una riga di intestazione opzionale - Ciò non influisce sul parser.
  4. Ogni riga deve contenere lo stesso numero di campi in tutto il file - non forzato Gli
    spazi sono considerati parte di un campo e non devono essere ignorati - okay
    L'ultimo campo nel record non deve essere seguito da una virgola - non forzato
  5. Ogni campo può essere racchiuso tra virgolette doppie o meno ... - okay
  6. I campi che contengono interruzioni di riga (CRLF), virgolette doppie e virgole devono essere racchiusi tra virgolette doppie, va bene
  7. una virgoletta doppia che appare all'interno di un campo deve essere sfuggita precedendola con un'altra virgoletta doppia - okay

Lo stesso regexp soddisfa la maggior parte dei requisiti RFC 4180. Non sono d'accordo con gli altri, ma è facile regolare il parser per implementarli.


1
sembra più un'autopromozione che rispondere alla domanda posta, vedi Come rispondere
moscerino

1
@gnat, ho modificato la mia risposta per fornire ulteriori spiegazioni, controllare la regexp rispetto a RFC 4180 e per renderla meno autopromozione. Credo che questa risposta abbia valore, in quanto contiene una regexp testata che può tokenizzare la forma più comune di CSV utilizzata da Excel e altri fogli di calcolo. Penso che questo risolva la domanda. Il piccolo parser CSV dimostra che è facile analizzare CSV usando questo regexp.
Sam Watkins,

Senza voler promuovermi eccessivamente, ecco le mie librerie complete di csv e tsv che sto usando come parte di una piccola app per fogli di calcolo (i fogli di Google sono troppo pesanti per me). Questo è codice open source / dominio pubblico / CC0 come tutte le cose che pubblico. Spero che questo possa essere utile per qualcun altro. sam.aiki.info/code/js
Sam Watkins
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.