O formato csv pode ser definido por uma regex?


19

Recentemente, um colega e eu discutimos se um regex puro é capaz de encapsular completamente o formato csv, de forma que ele é capaz de analisar todos os arquivos com qualquer caractere de escape, caractere de citação e caractere separador.

O regex não precisa ser capaz de alterar esses caracteres após a criação, mas não deve falhar em nenhum outro caso de borda.

Argumentei que isso é impossível apenas para um tokenizador. O único regex que pode fazer isso é um estilo PCRE muito complexo que vai além da tokenização.

Eu estou procurando algo ao longo das linhas de:

... o formato csv é uma gramática livre de contexto e, como tal, é impossível analisar apenas regex ...

Ou eu estou errado? É possível analisar o csv apenas com um regex POSIX?

Por exemplo, se o caractere de escape e o caractere de cotação são ", essas duas linhas são csv válidas:

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

não é um CSV como não há em qualquer lugar de nidificação (IIRC)
catraca aberração

1
mas quais são os casos extremos? talvez haja mais em CSV, do que eu jamais pensei?
C69

1
@ c69 Que tal escape e quote? são ambos ". Então, o seguinte é válido:"""this is a test.""",""
Spencer Rathbun

Você tentou regexp a partir daqui ?
dasblinkenlight

1
Você precisa estar atento a casos extremos, mas uma regex deve poder tokenizar csv conforme a descrição. O regex não precisa contar um número arbitrário de aspas - ele só precisa contar até 3, o que expressões regulares podem fazer. Como outros já mencionaram, você deve tentar escrever uma representação bem definida do que você espera que um token csv seja ...
comingstorm

Respostas:


20

Bom em teoria, terrível na prática

Por CSV , vou assumir que você quer dizer a convenção conforme descrito na RFC 4180 .

Embora a correspondência de dados CSV básicos seja trivial:

"data", "more data"

Nota: BTW, é muito mais eficiente usar uma função .split ('/ n'). Split ('"') para dados muito simples e bem estruturados como este. Expressões regulares funcionam como um NDFSM (finito não determinístico) State Machine) que desperdiça muito tempo retornando quando você começa a adicionar casos extremos, como caracteres de escape.

Por exemplo, aqui está a string de correspondência de expressão regular mais abrangente que eu encontrei:

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

Ele lida razoavelmente com valores entre aspas simples e duplas, mas não com novas linhas de valores, aspas escapadas, etc.

Origem: Estouro de Pilha - Como posso analisar uma string com JavaScript

Torna-se um pesadelo quando os casos comuns são apresentados como ...

"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

Somente o caso de nova linha como valor é suficiente para quebrar 99,9999% dos analisadores baseados em RegEx encontrados na natureza. A única alternativa "razoável" é usar a correspondência RegEx para tokenização básica de caracteres de controle / não controle (ou seja, terminal x não terminal) emparelhada com uma máquina de estado usada para análises de nível superior.

Fonte: Experiência conhecida como dor e sofrimento extensos.

Sou o autor do jquery-CSV , o único analisador de CSV baseado em javascript e totalmente compatível com RFC do mundo. Passei meses enfrentando esse problema, conversando com muitas pessoas inteligentes e tentando várias implementações diferentes, incluindo três reescritas completas do mecanismo do analisador de núcleo.

tl; dr - Moral da história, somente o PCRE é péssimo por analisar qualquer coisa, exceto as gramáticas regulares mais simples e estritas (Ie Tipo III). Embora seja útil para tokenizar seqüências de caracteres terminais e não terminais.


1
Sim, essa tem sido a minha experiência também. Qualquer tentativa de encapsular completamente mais do que um padrão CSV muito simples se depara com essas coisas e você se depara com os problemas de eficiência e os problemas de complexidade de uma regex massiva. Você já olhou para a biblioteca node-csv ? Parece validar essa teoria também. Toda implementação não trivial usa um analisador internamente.
Spencer Rathbun

@SpencerRathbun Yep. Tenho certeza de que já dei uma olhada na fonte node-csv antes. Parece usar uma máquina de estado de tokenização de caracteres típica para processamento. O analisador jquery-csv funciona no mesmo conceito fundamental, exceto que eu uso regex para tokenização de terminal / não-terminal. Em vez de avaliar e concatenar em uma base char a char, o regex pode combinar vários caracteres não terminais de cada vez e retorná-los como um grupo (ou seja, string). Isso minimiza a concatenação desnecessária e 'deveria' aumentar a eficiência.
precisa

20

O Regex pode analisar qualquer idioma comum e não pode analisar coisas sofisticadas, como gramáticas recursivas. Mas o CSV parece ser bastante regular, tão parseable com uma regex.

Vamos trabalhar a partir da definição : permitidas são sequência, alternativas de forma de escolha ( |) e repetição (estrela de Kleene, the *).

  • Um valor não citado é regular: [^,]*# qualquer caractere, exceto vírgula
  • Um valor entre aspas é regular: "([^\"]|\\\\|\\")*"# sequência de qualquer coisa, exceto aspas "ou aspas \"escapadas ou escapes escapadas\\
    • Alguns formulários podem incluir aspas vazias com aspas, o que adiciona uma variante ("")*"à expressão acima.
  • Um valor permitido é regular: <unquoted-value> |<quoted-value>
  • Uma única linha CSV é regular: <valor> (,<valor>)*
  • Uma sequência de linhas separadas por \ntambém é obviamente regular.

Não testei meticulosamente cada uma dessas expressões e nunca defini grupos de captura. Eu também anotado sobre alguns aspectos técnicos, como as variantes de caracteres que podem ser utilizados em vez de ,, "ou linha de separadores: estes não quebrar a regularidade, você é só pegar várias línguas ligeiramente diferentes.

Se você encontrar um problema nessa prova, comente! :)

Mas, apesar disso, a análise prática de arquivos CSV por expressões regulares puras pode ser problemática. Você precisa saber qual das variantes está sendo alimentada no analisador e não há um padrão para isso. Você pode tentar vários analisadores em cada linha até que um seja bem-sucedido ou, de alguma forma, adivinhar os comentários do formato. Mas isso pode exigir outros meios além das expressões regulares para fazer com eficiência, ou de todo.


4
Absolutamente um +1 para o ponto prático. Há algo que tenho certeza, em algum lugar profundo há um exemplo de um valor (artificial) que quebraria a versão do valor citado. Eu simplesmente não sei o que é. O 'diversão' com vários analisadores seria "estes dois trabalhos, mas dão respostas diferentes"

1
Obviamente, você precisará de diferentes expressões regulares para aspas com barra invertida versus aspas com escape com aspas duplas. Uma regex para o primeiro tipo de campo csv deve ser algo como [^,"]*|"(\\(\\|")|[^\\"])*", e o último deve ser algo como [^,"]*|"(""|[^"])*". (Cuidado, pois eu não testei um desses!)
comingstorm

Procurando por algo que pode ser um padrão, há um caso que está faltando - um valor com um delimitador de registro incluído. Isso também faz a análise prática ainda mais divertido quando há várias maneiras diferentes de lidar com isso

Boa resposta, mas se eu correr perl -pi -e 's/"([^\"]|\\\\|\\")*"/yay/'e canalizá-lo "I have here an item,\" that is a test\"", o resultado é `sim, isso é um teste \" ". Acho que seu regex é falho.
Spencer Rathbun

@ SpencerRathbun: quando tiver mais tempo, testarei as regexes e provavelmente até colarei algum código de prova de conceito que passa nos testes. Desculpe, o dia de trabalho está acontecendo.
9000

5

Resposta simples - provavelmente não.

O primeiro problema é a falta de um padrão. Embora se possa descrever seu csv de maneira estritamente definida, não se pode esperar obter arquivos csv estritamente definidos. "Seja conservador no que faz, seja liberal no que aceita dos outros" - Jon Postal

Supondo que se tenha um padrão padrão aceitável, há a questão dos caracteres de escape e se esses precisam ser equilibrados.

Uma sequência em muitos formatos csv é definida como string value 1,string value 2. No entanto, se essa sequência contiver uma vírgula, será agora "string, value 1",string value 2. Se ele contiver uma cotação, ele se tornará "string, ""value 1""",string value 2.

Neste ponto, acredito que é impossível. O problema é que você precisa determinar quantas aspas você leu e se uma vírgula está dentro ou fora do modo de aspas duplas do valor. Equilibrar parênteses é um problema impossível de regex. Alguns mecanismos de expressão regular estendida (PCRE) podem lidar com isso, mas não é uma expressão regular então.

Você pode achar /programming/8629763/csv-parsing-with-a-context-free-grammar útil.


Alteradas:

Eu tenho procurado formatos para caracteres de escape e não encontrei nenhum que precise de contagem arbitrária - então esse provavelmente não é o problema.

No entanto, existem questões sobre qual é o caractere de escape e o delimitador de registro (para começar). http://www.csvreader.com/csv_format.php é uma boa leitura sobre os diferentes formatos na natureza.

  • As regras para a sequência entre aspas (se for uma sequência entre aspas simples ou uma sequência entre aspas duplas) diferem.
    • 'This, is a value' vs "This, is a value"
  • As regras para caracteres de escape
    • "This ""is a value""" vs "This \"is a value\""
  • A manipulação do delimitador de registro incorporado ({rd})
    • (incorporada não processada) "This {rd}is a value"vs (escapado) "This \{rd}is a value"vs (traduzido)"This {0x1C}is a value"

O principal aqui é que é possível ter uma string que sempre terá várias interpretações válidas.

A pergunta relacionada (para casos extremos) "é possível ter uma sequência inválida aceita?"

Eu ainda duvido muito que exista uma expressão regular que possa corresponder a todos os CSV válidos criados por algum aplicativo e rejeitar todos os CSVs que não podem ser analisados.


1
Citações dentro de aspas não precisam ser balanceadas. Em vez disso, deve haver um número par de aspas antes de uma cotação incorporado, que é obviamente regulares: ("")*". Se as cotações dentro do valor estiverem desequilibradas, já não é da nossa conta.
9000

Esta é a minha posição, tendo encontrado essas desculpas horríveis para "transferência de dados" no passado. A única coisa que os tratava adequadamente era um analisador, o regex puro era interrompido a cada poucas semanas.
Spencer Rathbun

2

Primeiro defina a gramática do seu CSV (os delimitadores de campo são escapados ou codificados de alguma forma se aparecerem em texto?) E, em seguida, é possível determinar se é analisável com regex. Gramática primeiro: analisador segundo: http://www.boyet.com/articles/csvparser.html Deve-se observar que esse método usa um tokenizer - mas não consigo criar um regex POSIX que corresponda a todos os casos extremos. Se o uso de formatos CSV não for regular e sem contexto ... sua resposta estará na sua pergunta. Boa visão geral aqui: http://nikic.github.com/2012/06/15/The-true-power-of-regular-expressions.html


2

Este regexp pode tokenizar o CSV normal, conforme descrito no RFC:

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

Explicação:

  • ("(?:[^"]|"")*"|[^,"\n\r]*) - um campo CSV, citado ou não
    • "(?:[^"]|"")*" - um campo citado;
      • [^"]|""- cada personagem é ou não ", ou "escapou como""
    • [^,"\n\r]* - um campo não citado, que não pode conter , " \n \r
  • (,|\r?\n|\r)- o seguinte separador, ,ou uma nova linha
    • \r?\n|\r - uma nova linha, uma das \r\n \n \r

Um arquivo CSV inteiro pode ser correspondido e validado usando esse regexp repetidamente. Em seguida, é necessário corrigir os campos entre aspas e dividi-los em linhas com base nos separadores.

Aqui está o código para um analisador CSV em Javascript, com base no 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 essa resposta ajuda a resolver seu argumento é para você decidir; Estou feliz por ter um analisador de CSV pequeno, simples e correto.

Na minha opinião, um lexprograma é mais ou menos uma expressão regular grande, e esses podem simbolizar formatos muito mais complexos, como a linguagem de programação C.

Com referência às definições da RFC 4180 :

  1. quebra de linha (CRLF) - O regexp é mais flexível, permitindo CRLF, LF ou CR.
  2. O último registro no arquivo pode ou não ter uma quebra de linha final - a regexp atual requer uma quebra de linha final, mas o analisador se ajusta a isso.
  3. Talvez haja uma linha de cabeçalho opcional - Isso não afeta o analisador.
  4. Cada linha deve conter o mesmo número de campos em todo o arquivo - não imposto Os
    espaços são considerados parte de um campo e não devem ser ignorados - ok
    O último campo no registro não deve ser seguido por vírgula - não imposto
  5. Cada campo pode ou não estar entre aspas duplas ... - ok
  6. Os campos que contêm quebras de linha (CRLF), aspas duplas e vírgulas devem ser colocados entre aspas duplas - ok
  7. as aspas duplas que aparecem dentro de um campo devem ser escapadas precedendo-as com outra aspas duplas - ok

O próprio regexp satisfaz a maioria dos requisitos da RFC 4180. Não concordo com os outros, mas é fácil ajustar o analisador para implementá-los.


1
Isso parece mais como auto-promoção de abordar a pergunta, consulte Como responder
mosquito

1
@gnat, editei minha resposta para dar mais explicações, verificar o regexp no RFC 4180 e torná-lo menos autopromocional. Acredito que essa resposta tenha valor, pois contém um regexp testado que pode tokenizar a forma mais comum de CSV usada pelo Excel e outras planilhas. Eu acho que isso resolve a questão. O pequeno analisador de CSV demonstra que é fácil analisar o CSV usando esse regexp.
Sam Watkins

Sem querer me promover excessivamente, aqui estão minhas bibliotecas completas de csv e tsv que estou usando como parte de um pequeno aplicativo de planilha (as planilhas do Google parecem pesadas demais para mim). Este é um código-fonte aberto / domínio público / CC0, como todas as coisas que publico. Espero que isso possa ser útil para outra pessoa. sam.aiki.info/code/js
Sam Watkins
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.