Le format csv peut-il être défini par une expression régulière?

19

Un collègue et moi avons récemment discuté de la question de savoir si une expression rationnelle pure est capable d'encapsuler entièrement le format csv, de telle sorte qu'elle soit capable d'analyser tous les fichiers avec n'importe quel caractère d'échappement, de guillemet et de séparateur.

La regex n'a pas besoin d'être capable de changer ces caractères après la création, mais elle ne doit échouer sur aucun autre cas de bord.

J'ai soutenu que cela est impossible pour un simple tokenizer. Le seul regex qui pourrait être en mesure de le faire est un style PCRE très complexe qui va au-delà de la simple symbolisation.

Je cherche quelque chose dans le sens de:

... le format csv est une grammaire sans contexte et en tant que tel, il est impossible d'analyser avec regex seul ...

Ou ai-je tort? Est-il possible d'analyser csv avec juste une expression rationnelle POSIX?

Par exemple, si le caractère d'échappement et le caractère de citation le sont ", ces deux lignes sont des csv valides:

"""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..."
Spencer Rathbun
la source
ce n'est pas un CSV car il n'y a pas d'imbrication nulle part (IIRC)
ratchet freak
1
mais quels sont les cas de bord? peut-être y a-t-il plus dans CSV que je ne le pensais?
c69
1
@ c69 Qu'en est-il des caractères d'échappement et de citation "? Ensuite, ce qui suit est valide:"""this is a test.""",""
Spencer Rathbun
Avez-vous essayé regexp d' ici ?
dasblinkenlight
1
Vous devez faire attention aux cas marginaux, mais une expression régulière devrait être en mesure de symboliser le csv comme vous l'avez décrit. Le regex n'a pas besoin de compter un nombre arbitraire de guillemets - il a seulement besoin de compter jusqu'à 3, ce que les expressions régulières peuvent faire. Comme d'autres l'ont mentionné, vous devriez essayer d'écrire une représentation bien définie de ce que vous attendez d'un jeton csv ...
comingstorm

Réponses:

20

Agréable en théorie, terrible en pratique

Par CSV, je vais supposer que vous entendez la convention décrite dans la RFC 4180 .

Bien que la correspondance des données CSV de base soit triviale:

"data", "more data"

Remarque: BTW, il est beaucoup plus efficace d'utiliser une fonction .split ('/ n'). Split ('"') pour des données très simples et bien structurées comme celle-ci. Les expressions régulières fonctionnent comme un NDFSM (Non-Deterministic Finite State Machine) qui gaspille beaucoup de temps à revenir en arrière une fois que vous commencez à ajouter des cas marginaux comme des caractères d'échappement.

Par exemple, voici la chaîne de correspondance d'expressions régulières la plus complète que j'ai trouvée:

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

Il gère raisonnablement les valeurs entre guillemets simples et doubles, mais pas les retours à la ligne des valeurs, les guillemets échappés, etc.

Source: Stack Overflow - Comment puis-je analyser une chaîne avec JavaScript

Cela devient un cauchemar une fois que les cas de bord communs sont introduits comme ...

"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

Le cas de bord de nouvelle ligne en tant que valeur suffit à lui seul à casser 99,9999% des analyseurs basés sur RegEx trouvés dans la nature. La seule alternative «raisonnable» consiste à utiliser la correspondance RegEx pour la tokenisation de caractère de contrôle / non-contrôle de base (c'est-à-dire terminal vs non-terminal) couplée à une machine d'état utilisée pour une analyse de niveau supérieur.

Source: Expérience autrement connue sous le nom de douleur et de souffrance étendues.

Je suis l'auteur de jquery-CSV , le seul analyseur CSV basé sur javascript et entièrement compatible RFC au monde. J'ai passé des mois à résoudre ce problème, à parler à de nombreuses personnes intelligentes et à essayer une tonne d'implémentations différentes, y compris 3 réécritures complètes du moteur de l'analyseur principal.

tl; dr - Morale de l'histoire, PCRE est nul à lui seul pour analyser tout sauf les grammaires régulières les plus simples et les plus strictes (c'est-à-dire de type III). Bien qu'il soit utile pour la tokenisation des chaînes terminales et non terminales.

Plie d'Evan
la source
1
Oui, c'est aussi mon expérience. Toute tentative d'encapsuler pleinement plus qu'un modèle CSV très simple se heurte à ces choses, puis vous vous heurtez à la fois aux problèmes d'efficacité et aux problèmes de complexité d'une expression régulière massive. Avez-vous regardé la bibliothèque node-csv ? Il semble également valider cette théorie. Chaque implémentation non triviale utilise un analyseur en interne.
Spencer Rathbun
@SpencerRathbun Yep. Je suis sûr que j'ai jeté un œil à la source node-csv auparavant. Il semble utiliser une machine d'état de tokenisation de caractères typique pour le traitement. L'analyseur jquery-csv fonctionne sur le même concept fondamental, sauf que j'utilise regex pour la tokenisation terminale / non terminale. Au lieu d'évaluer et de concaténer sur une base char par char, regex est capable de faire correspondre plusieurs caractères non terminaux à la fois et de les renvoyer en tant que groupe (c'est-à-dire une chaîne). Cela minimise la concaténation inutile et «devrait» augmenter l'efficacité.
Evan Plaice
20

Regex peut analyser n'importe quel langage normal et ne peut pas analyser des choses fantaisistes comme des grammaires récursives. Mais CSV semble être assez régulier, donc analysable avec une expression régulière.

Travaillons à partir de la définition : sont autorisés la séquence, les alternatives de forme de choix ( |) et la répétition (étoile de Kleene, la *).

  • Une valeur sans guillemets est régulière: [^,]*# tout caractère mais virgule
  • Une valeur entre guillemets est régulière: "([^\"]|\\\\|\\")*"# séquence de tout sauf guillemet "ou guillemet \"échappé ou échappement échappé\\
    • Certains formulaires peuvent inclure des guillemets d'échappement avec des guillemets, ce qui ajoute une variante ("")*"à l'expression ci-dessus.
  • Une valeur autorisée est régulière: <valeur-quotée> |<valeur-quotée>
  • Une seule ligne CSV est régulière: <valeur> (,<valeur>)*
  • Une séquence de lignes séparées par \nest également évidemment régulière.

Je n'ai pas testé méticuleusement chacune de ces expressions et je n'ai jamais défini de groupes de captures. J'ai aussi passé sous silence certains aspects techniques, comme les variantes de caractères qui peuvent être utilisés au lieu de ,, "ou séparateurs ligne: ceux - ci ne se cassent pas la régularité, vous obtenez juste plusieurs langues légèrement différentes.

Si vous pouvez détecter un problème dans cette épreuve, veuillez commenter! :)

Mais malgré cela, l' analyse pratique des fichiers CSV par des expressions régulières pures peut être problématique. Vous devez savoir laquelle des variantes est envoyée à l'analyseur, et il n'y a pas de norme pour cela. Vous pouvez essayer plusieurs parseurs contre chaque ligne jusqu'à ce que l'un réussisse, ou diviser en quelque sorte les commentaires du formulaire de format. Mais cela peut nécessiter des moyens autres que les expressions régulières pour faire efficacement, ou pas du tout.

9000
la source
4
Absolument un +1 pour le point pratique. Il y a quelque chose dont je suis sûr, quelque part profond est un exemple d'une valeur (artificielle) qui briserait la version de la valeur citée, je ne sais pas ce que c'est. Le «plaisir» avec plusieurs analyseurs serait «ces deux travaux, mais donnent des réponses différentes»
1
Vous aurez évidemment besoin de différentes expressions rationnelles pour les guillemets échappés par une barre oblique inverse par rapport aux guillemets échappés par des guillemets doubles. Une expression régulière pour le premier type de champ csv devrait être quelque chose comme [^,"]*|"(\\(\\|")|[^\\"])*", et le dernier devrait être quelque chose comme [^,"]*|"(""|[^"])*". (Attention, je n'ai testé aucun de ces deux!)
comingstorm
En cherchant quelque chose qui pourrait être une norme, il y a un cas qui est manqué - une valeur avec un délimiteur d'enregistrement inclus. Cela rend également l'analyse syntaxique pratique encore plus amusante lorsqu'il existe plusieurs façons de gérer cela
Bonne réponse, mais si je lance perl -pi -e 's/"([^\"]|\\\\|\\")*"/yay/'et que je redirige , "I have here an item,\" that is a test\""le résultat est `yay c'est un test \" ". Je pense que votre regex est défectueux.
Spencer Rathbun
@SpencerRathbun: quand j'aurai plus de temps, je testerai les expressions rationnelles et probablement même collerai du code de preuve de concept qui réussira les tests. Désolé, la journée de travail continue.
9000
5

Réponse simple - probablement pas.

Le premier problème est le manque de norme. Bien que l'on puisse décrire leur csv d'une manière strictement définie, on ne peut pas s'attendre à obtenir des fichiers csv strictement définis. "Soyez conservateur dans ce que vous faites, soyez libéral dans ce que vous acceptez des autres" -Jon Postal

En supposant que l'on ait un standard standard acceptable, il y a la question des caractères d'échappement et si ceux-ci doivent être équilibrés.

Une chaîne dans de nombreux formats csv est définie comme string value 1,string value 2. Cependant, si cette chaîne contient une virgule, c'est maintenant "string, value 1",string value 2. S'il contient une citation, il devient "string, ""value 1""",string value 2.

À ce stade, je pense que c'est impossible. Le problème étant que vous devez déterminer le nombre de guillemets que vous avez lus et si une virgule se trouve à l'intérieur ou à l'extérieur du mode entre guillemets doubles de la valeur. L'équilibrage des parenthèses est un problème d'expression rationnelle impossible. Certains moteurs d'expression régulière étendus (PCRE) peuvent y faire face, mais ce n'est pas une expression régulière alors.

Https://stackoverflow.com/questions/8629763/csv-parsing-with-a-context-free-grammar peut vous être utile.


Modifié:

J'ai cherché des formats pour les caractères d'échappement et je n'en ai trouvé aucun qui nécessite un comptage arbitraire - ce n'est probablement pas le problème.

Cependant, il y a des problèmes de caractère d'échappement et de délimiteur d'enregistrement (pour commencer). http://www.csvreader.com/csv_format.php est une bonne lecture sur les différents formats à l'état sauvage.

  • Les règles de la chaîne entre guillemets (s'il s'agit d'une chaîne entre guillemets simple ou d'une chaîne entre guillemets doubles) diffèrent.
    • 'This, is a value' contre "This, is a value"
  • Les règles pour les personnages d'échappement
    • "This ""is a value""" contre "This \"is a value\""
  • La gestion du délimiteur d'enregistrement intégré ({rd})
    • (brut intégré) "This {rd}is a value"vs (échappé) "This \{rd}is a value"vs (traduit)"This {0x1C}is a value"

L'essentiel ici est qu'il est possible d'avoir une chaîne qui aura toujours plusieurs interprétations valides.

La question connexe (pour les cas marginaux) "est-il possible d'avoir une chaîne invalide acceptée?"

Je doute fortement qu'il existe une expression régulière qui puisse correspondre à chaque CSV valide créé par une application et rejeter chaque csv qui ne peut pas être analysé.

Communauté
la source
1
Les citations à l'intérieur des citations n'ont pas besoin d'être équilibrées. Au lieu de cela, il doit y avoir un nombre pair de citations avant un devis intégré, ce qui est évidemment normal: ("")*". Si les devis à l' intérieur de la valeur sont hors d'équilibre, ce n'est déjà pas notre affaire.
9000
C'est ma position, ayant rencontré dans le passé ces horribles excuses pour le "transfert de données". La seule chose qui les traitait correctement était un analyseur syntaxique, pur regex cassé toutes les quelques semaines.
Spencer Rathbun
2

Définissez d'abord la grammaire de votre CSV (les délimiteurs de champ sont-ils échappés ou encodés d'une manière ou d'une autre s'ils apparaissent dans le texte?), Puis il peut être déterminé s'il est analysable avec regex. Première grammaire: deuxième analyseur: http://www.boyet.com/articles/csvparser.html Il convient de noter que cette méthode utilise un tokenizer - mais je ne peux pas constuire une expression régulière POSIX qui correspondrait à tous les cas de bord. Si votre utilisation des formats CSV est non régulière et sans contexte ... alors votre réponse est dans votre question. Bon aperçu ici: http://nikic.github.com/2012/06/15/The-true-power-of-regular-expressions.html

iivel
la source
2

Cette expression régulière peut symboliser le CSV normal, comme décrit dans le RFC:

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

Explication:

  • ("(?:[^"]|"")*"|[^,"\n\r]*) - un champ CSV, cité ou non
    • "(?:[^"]|"")*" - un champ entre guillemets;
      • [^"]|""- chaque caractère est soit non ", soit "échappé comme""
    • [^,"\n\r]* - un champ sans guillemets, qui ne peut pas contenir , " \n \r
  • (,|\r?\n|\r)- le séparateur suivant, soit ,une nouvelle ligne
    • \r?\n|\r - une nouvelle ligne, l'une des \r\n \n \r

Un fichier CSV entier peut être mis en correspondance et validé à l'aide de cette expression régulière à plusieurs reprises. Il est alors nécessaire de corriger les champs entre guillemets et de les diviser en lignes en fonction des séparateurs.

Voici le code d'un analyseur CSV en Javascript, basé sur l'expression rationnelle:

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

Si cette réponse aide à régler votre argument, c'est à vous de décider; Je suis juste heureux d'avoir un petit analyseur CSV simple et correct.

À mon avis, un lexprogramme est plus ou moins une grande expression régulière, et ceux-ci peuvent symboliser des formats beaucoup plus complexes, tels que le langage de programmation C.

En référence aux définitions de la RFC 4180 :

  1. saut de ligne (CRLF) - L'expression rationnelle est plus flexible, permettant CRLF, LF ou CR.
  2. Le dernier enregistrement du fichier peut avoir ou non un saut de ligne de fin - L'expression régulière telle qu'elle est nécessite un saut de ligne final, mais l'analyseur s'ajuste pour cela.
  3. Il peut y avoir une ligne d'en-tête facultative - Cela n'a pas d'impact sur l'analyseur.
  4. Chaque ligne doit contenir le même nombre de champs dans le fichier - non appliqué Les
    espaces sont considérés comme faisant partie d'un champ et ne doivent pas être ignorés - d'accord
    Le dernier champ de l'enregistrement ne doit pas être suivi d'une virgule - non appliqué
  5. Chaque champ peut ou non être mis entre guillemets ... - d'accord
  6. Les champs contenant des sauts de ligne (CRLF), des guillemets doubles et des virgules doivent être placés entre guillemets doubles - d'accord
  7. un guillemet double apparaissant à l'intérieur d'un champ doit être échappé en le précédant d'un autre guillemet double - ok

L'expression régulière elle-même satisfait la plupart des exigences RFC 4180. Je ne suis pas d'accord avec les autres, mais il est facile d'ajuster l'analyseur pour les implémenter.

Sam Watkins
la source
1
cela ressemble plus à une auto-promotion qu'à une réponse à la question posée, voir Comment répondre
gnat
1
@gnat, j'ai modifié ma réponse pour donner plus d'explications, vérifier l'expression rationnelle par rapport à la RFC 4180 et la rendre moins auto-promotionnelle. Je crois que cette réponse a de la valeur, car elle contient une expression rationnelle testée qui peut symboliser la forme de CSV la plus courante utilisée par Excel et d'autres feuilles de calcul. Je pense que cela règle la question. Le petit analyseur CSV démontre qu'il est facile d'analyser CSV en utilisant cette expression régulière.
Sam Watkins
Sans vouloir me promouvoir excessivement, voici mes petites bibliothèques csv et tsv complètes que j'utilise dans le cadre d'une petite application tableur (les feuilles Google me paraissent trop lourdes). C'est du code open source / domaine public / CC0 comme tout ce que je publie. J'espère que cela peut être utile pour quelqu'un d'autre. sam.aiki.info/code/js
Sam Watkins