O analisador CSV usado no plug-in jquery-csv
É um analisador gramatical básico de Chomsky Tipo III .
Um tokenizador de regex é usado para avaliar os dados char a char. Quando um caractere de controle é encontrado, o código é passado para uma instrução switch para posterior avaliação com base no estado inicial. Caracteres que não são de controle são agrupados e copiados em massa para reduzir o número de operações de cópia de seqüência necessárias.
O tokenizador:
var tokenizer = /("|,|\n|\r|[^",\r\n]+)/;
O primeiro conjunto de correspondências são os caracteres de controle: delimitador de valor (") separador de valor (,) e separador de entrada (todas as variações de nova linha). A última correspondência lida com o agrupamento de caracteres sem controle.
Existem 10 regras que o analisador deve atender:
- Regra # 1 - Uma entrada por linha, cada linha termina com uma nova linha
- Regra # 2 - Nova linha final no final do arquivo omitida
- Regra # 3 - Primeira linha contém dados do cabeçalho
- Regra # 4 - Os espaços são considerados dados e as entradas não devem conter vírgula à direita
- Regra # 5 - As linhas podem ou não ser delimitadas por aspas duplas
- Regra # 6 - Os campos contendo quebras de linha, aspas duplas e vírgulas devem ser colocados entre aspas duplas
- Regra nº 7 - Se aspas duplas forem usadas para colocar campos, uma aspas dupla aparecendo dentro de um campo deve ser escapada precedendo-a com outra aspas
- Alteração 1 - Um campo não citado pode ou pode
- Alteração 2 - Um campo citado pode ou não
- Alteração 3 - O último campo de uma entrada pode ou não conter um valor nulo
Nota: As 7 principais regras são derivadas diretamente do IETF RFC 4180 . Os três últimos foram adicionados para cobrir casos avançados introduzidos por aplicativos modernos de planilhas (ex Excel, Google Spreadsheet) que não delimitam (ou seja, citam) todos os valores por padrão. Tentei contribuir com as alterações na RFC, mas ainda não recebi uma resposta à minha pergunta.
Chega de conclusão, aqui está o diagrama:
Estados:
- estado inicial de uma entrada e / ou um valor
- uma cotação de abertura foi encontrada
- uma segunda cotação foi encontrada
- um valor não cotado foi encontrado
Transições:
- uma. verifica os valores entre aspas (1), valores não citados (3), valores nulos (0), entradas nulas (0) e novas entradas (0)
- b. verifica se há um segundo caractere de cotação (2)
- c. verifica se há uma cotação de escape (1), final do valor (0) e final da entrada (0)
- d. verifica o final do valor (0) e o final da entrada (0)
Nota: Na verdade, está faltando um estado. Deve haver uma linha de 'c' -> 'b' marcada com o estado '1' porque um segundo delimitador com escape significa que o primeiro delimitador ainda está aberto. De fato, provavelmente seria melhor representá-lo como outra transição. Criando isso é uma arte, não existe uma maneira correta.
Nota: Também está faltando um estado de saída, mas em dados válidos o analisador sempre termina na transição 'a' e nenhum dos estados é possível porque não há mais nada a analisar.
A diferença entre estados e transições:
Um estado é finito, o que significa que só pode ser inferido como significando uma coisa.
Uma transição representa o fluxo entre estados, portanto pode significar muitas coisas.
Basicamente, o relacionamento estado-> transição é 1 -> * (isto é, um para muitos). O estado define 'o que é' e a transição define 'como é tratado'.
Nota: Não se preocupe se a aplicação de estados / transições não parecer intuitiva, não é intuitiva. Foi preciso uma correspondência extensa com alguém muito mais esperto do que eu antes de finalmente entender o conceito.
O pseudo-código:
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: Esta é a essência, na prática, há muito mais a considerar. Por exemplo, verificação de erros, valores nulos, uma linha em branco à direita (ou seja, qual é válida), etc.
Nesse caso, o estado é a condição das coisas quando o bloco de correspondência de expressão regular termina uma iteração. A transição é representada como as instruções de caso.
Como seres humanos, temos a tendência de simplificar operações de baixo nível em resumos de nível superior, mas trabalhar com um FSM está trabalhando com operações de baixo nível. Embora seja muito fácil trabalhar com estados e transições individualmente, é inerentemente difícil visualizar o todo de uma só vez. Achei mais fácil seguir os caminhos individuais de execução repetidamente, até que eu pudesse intuir como as transições acontecem. É como aprender matemática básica, você não será capaz de avaliar o código de um nível superior até que os detalhes de baixo nível se tornem automáticos.
Além: Se você observar a implementação real, há muitos detalhes ausentes. Primeiro, todos os caminhos impossíveis lançam exceções específicas. Deveria ser impossível atingi-los, mas se algo quebrar, eles absolutamente acionarão exceções no corredor de teste. Segundo, as regras do analisador para o que é permitido em uma sequência de dados CSV 'legal' são bastante soltas, portanto o código necessário para lidar com muitos casos específicos de borda. Independentemente disso, esse foi o processo usado para zombar do FSM antes de todas as correções, extensões e ajustes finos.
Como na maioria dos projetos, não é uma representação exata da implementação, mas descreve as partes importantes. Na prática, existem três funções diferentes do analisador derivadas desse design: um divisor de linha específico para csv, um analisador de linha única e um analisador de várias linhas completo. Todos eles operam de maneira semelhante, diferem na maneira como lidam com os caracteres de nova linha.