Il parser CSV utilizzato nel plug-in jquery-csv
È un parser grammaticale di base di tipo III Chomsky .
Un tokenizer regex viene utilizzato per valutare i dati su base char-by-char. Quando viene rilevato un carattere di controllo, il codice viene passato a un'istruzione switch per un'ulteriore valutazione basata sullo stato iniziale. I caratteri non di controllo sono raggruppati e copiati in massa per ridurre il numero di operazioni di copia di stringhe necessarie.
Il tokenizer:
var tokenizer = /("|,|\n|\r|[^",\r\n]+)/;
Il primo set di corrispondenze sono i caratteri di controllo: delimitatore di valore (") separatore di valore (,) e separatore di voci (tutte le varianti di newline). L'ultima corrispondenza gestisce il raggruppamento di caratteri non di controllo.
Esistono 10 regole che il parser deve soddisfare:
- Regola n. 1: una voce per riga, ogni riga termina con una nuova riga
- Regola n. 2: trascinamento della nuova riga alla fine del file omesso
- Regola n. 3: la prima riga contiene i dati dell'intestazione
- Regola n. 4: gli spazi sono considerati dati e le voci non devono contenere una virgola finale
- Regola n. 5: le righe possono essere o meno delimitate da virgolette doppie
- Regola n. 6: i campi che contengono interruzioni di riga, virgolette doppie e virgole devono essere racchiusi tra virgolette doppie
- Regola n. 7 - Se per racchiudere i campi vengono utilizzate le virgolette doppie, è necessario evitare una virgoletta doppia all'interno di un campo precedendola con un'altra virgoletta doppia
- Emendamento n. 1 - Un campo non quotato può o può
- Emendamento n. 2 - Un campo tra virgolette può o meno
- Emendamento n. 3 - L'ultimo campo di una voce può contenere o meno un valore nullo
Nota: le prime 7 regole sono derivate direttamente da IETF RFC 4180 . Gli ultimi 3 sono stati aggiunti ai casi limite introdotti dalle moderne app per fogli di calcolo (ex Excel, Google Spreadsheet) che non delimitano (ovvero citano) tutti i valori per impostazione predefinita. Ho provato a contribuire con le modifiche alla RFC ma non ho ancora ricevuto risposta alla mia richiesta.
Basta con il wind-up, ecco il diagramma:
Stati:
- stato iniziale per una voce e / o un valore
- è stata trovata una citazione di apertura
- è stata rilevata una seconda citazione
- è stato rilevato un valore non quotato
transizioni:
- un. controlla sia i valori tra virgolette (1), i valori non quotati (3), i valori null (0), le voci null (0) e le nuove voci (0)
- b. controlla un secondo preventivo char (2)
- c. verifica la quotazione di escape (1), la fine del valore (0) e la fine della registrazione (0)
- d. controlla la fine del valore (0) e la fine della voce (0)
Nota: in realtà manca uno stato. Dovrebbe esserci una linea da 'c' -> 'b' contrassegnata con lo stato '1' perché un secondo delimitatore con escape indica che il primo delimitatore è ancora aperto. In effetti, probabilmente sarebbe meglio rappresentarlo come un'altra transizione. La creazione di questi è un'arte, non esiste un solo modo corretto.
Nota: manca anche uno stato di uscita ma su dati validi il parser termina sempre sulla transizione 'a' e nessuno degli stati è possibile perché non è rimasto nulla da analizzare.
La differenza tra stati e transizioni:
Uno stato è finito, nel senso che può essere dedotto solo per significare una cosa.
Una transizione rappresenta il flusso tra gli stati, quindi può significare molte cose.
Fondamentalmente, la relazione stato-> transizione è 1 -> * (ovvero da uno a molti). Lo stato definisce "che cos'è" e la transizione definisce "come viene gestita".
Nota: non preoccuparti se l'applicazione di stati / transizioni non sembra intuitiva, non è intuitiva. Ci è voluto un po 'di corrispondenza con qualcuno molto più intelligente di me prima che finalmente riuscissi a mantenere il concetto.
Lo pseudo-codice:
csv = // csv input string
// init all state & data
state = 0
value = ""
entry = []
output = []
endOfValue() {
entry.push(value)
value = ""
}
endOfEntry() {
endOfValue()
output.push(entry)
entry = []
}
tokenizer = /("|,|\n|\r|[^",\r\n]+)/gm
// using the match extension of string.replace. string.exec can also be used in a similar manner
csv.replace(tokenizer, function (match) {
switch(state) {
case 0:
if(opening delimiter)
state = 1
break
if(new-line)
endOfEntry()
state = 0
break
if(un-delimited data)
value += match
state = 3
break
case 1:
if(second delimiter encountered)
state = 2
break
if(non-control char data)
value += match
state = 1
break
case 2:
if(escaped delimiter)
state = 1
break
if(separator)
endOfValue()
state = 0
break
if(newline)
endOfEntry()
state = 0
break
case 3:
if(separator)
endOfValue()
state = 0
break
if(newline)
endOfEntry()
state = 0
break
}
}
Nota: questo è l'essenza, in pratica c'è molto altro da considerare. Ad esempio, controllo degli errori, valori null, una riga vuota finale (vale a dire che è valida), ecc.
In questo caso, lo stato è la condizione delle cose quando il blocco di corrispondenza regex termina un'iterazione. La transizione è rappresentata come le dichiarazioni del caso.
Come umani, abbiamo la tendenza a semplificare le operazioni di basso livello in abstract di livello superiore, ma lavorare con un FSM sta funzionando con operazioni di basso livello. Mentre gli stati e le transizioni sono molto facili da lavorare individualmente, è intrinsecamente difficile visualizzare il tutto in una volta. Ho trovato più facile seguire ripetutamente i singoli percorsi di esecuzione fino a quando non ho potuto intuire come si svolgono le transizioni. È il re dell'apprendimento della matematica di base, non sarai in grado di valutare il codice da un livello superiore fino a quando i dettagli di basso livello iniziano a diventare automatici.
A parte: se guardi l'implementazione effettiva, mancano molti dettagli. Innanzitutto, tutti i percorsi impossibili genereranno specifiche eccezioni. Dovrebbe essere impossibile colpirli, ma se qualcosa dovesse rompersi scateneranno assolutamente delle eccezioni nel test runner. In secondo luogo, le regole del parser per ciò che è consentito in una stringa di dati CSV "legale" sono piuttosto vaghe, quindi il codice necessario per gestire molti casi limite specifici. Indipendentemente da ciò, questo era il processo utilizzato per deridere l'FSM prima di tutte le correzioni di bug, estensioni e messa a punto.
Come con la maggior parte dei progetti, non è una rappresentazione esatta dell'implementazione ma delinea le parti importanti. In pratica, ci sono in realtà 3 diverse funzioni del parser derivate da questo progetto: uno splitter di linea specifico per CSV, un parser a linea singola e un parser a più righe completo. Operano tutti in modo simile, si differenziano per il modo in cui gestiscono i caratteri newline.