Quel est le moyen le plus robuste pour analyser efficacement CSV en utilisant awk?

Le but de cette question est de fournir une réponse canonique.

donné un CSV comme pourrait être généré par Excel ou d'autres outils avec newlines embedded, embedded double guillemets et des champs vides comme:

$ cat file.csv
"rec1, fld1",,"rec1"",""fld3.1
"",
fld3.2","rec1
fld4"
"rec2, fld1.1

fld1.2","rec2 fld2.1""fld2.2""fld2.3","",rec2 fld4

Quelle est la façon la plus robuste d'utiliser efficacement awk pour identifier les enregistrements et les champs distincts:

Record 1:
    =<rec1, fld1>
    =<>
    =<rec1","fld3.1
",
fld3.2>
    =<rec1
fld4>
----
Record 2:
    =<rec2, fld1.1

fld1.2>
    =<rec2 fld2.1"fld2.2"fld2.3>
    =<>
    =<rec2 fld4>
----

donc il peut être utilisé comme ces enregistrements et champs en interne par le reste du script awk.

un CSV valide serait celui qui est conforme à RFC 4180 ou peut être généré par MS-Excel.

la solution doit tolérer la fin de l'enregistrement juste en étant LF ( n ) comme est typique pour les fichiers UNIX plutôt que CRLF ( rn ) comme cette norme exige et Excel ou d'autres outils de Windows générerait. Il tolérera également les champs non cotés mélangés avec des champs cotés. Il doit surtout pas besoin de tolérer échapper " s avec un antislash précédent (i.e. " au lieu de "" ) comme certains autres formats CSV le permettent - Si vous avez cela, alors ajouter un gsub(/"/,"""") à l'avant le manipulerait et essayer de gérer les deux mécanismes d'échappement automatiquement dans un script rendrait le script inutilement fragile et compliqué.

17
demandé sur Ed Morton 2017-07-31 19:02:39

1 réponses

si votre CSV ne peut pas contenir de nouvelles lignes ou de doubles guillemets échappés, alors tout ce dont vous avez besoin est (avec GNU awk pour FPAT ):

$ echo 'foo,"field,with,commas",bar' |
    awk -v FPAT='[^,]*|"[^"]+"' '{for (i=1; i<=NF;i++) print i, "<" $i ">"}'
1 <foo>
2 <"field,with,commas">
3 <bar>

autrement, bien que, la solution plus générale, robuste, portable qui fonctionnera avec n'importe quel awk moderne est:

$ cat decsv.awk
function buildRec(      i,orig,fpat,done) {
    "151910920" = PrevSeg "151910920"
    if ( gsub(/"/,"&") % 2 ) {
        PrevSeg = "151910920" RS
        done = 0
    }
    else {
        PrevSeg = ""
        gsub(/@/,"@A"); gsub(/""/,"@B")            # <"x@foo""bar"> -> <"x@Afoo@Bbar">
        orig = "151910920"; "151910920" = ""                         # Save "151910920" and empty it
        fpat = "([^" FS "]*)|(\"[^\"]+\")"         # Mimic GNU awk FPAT meaning
        while ( (orig!="") && match(orig,fpat) ) { # Find the next string matching fpat
            $(++i) = substr(orig,RSTART,RLENGTH)   # Create a field in new "151910920"
            gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i)  # <"x@Afoo@Bbar"> -> <"x@foo"bar">
            gsub(/^"|"$/,"",$i)                    # <"x@foo"bar">   -> <x@foo"bar>
            orig = substr(orig,RSTART+RLENGTH+1)   # Move past fpat+sep in orig "151910920"
        }
        done = 1
    }
    return done
}

BEGIN { FS=OFS="," }
!buildRec() { next }
{
    printf "Record %d:\n", ++recNr
    for (i=1;i<=NF;i++) {
        # To replace newlines with blanks add gsub(/\n/," ",$i) here
        printf "    $%d=<%s>\n", i, $i
    }
    print "----"
}

.

$ awk -f decsv.awk file.csv
Record 1:
    =<rec1, fld1>
    =<>
    =<rec1","fld3.1
",
fld3.2>
    =<rec1
fld4>
----
Record 2:
    =<rec2, fld1.1

fld1.2>
    =<rec2 fld2.1"fld2.2"fld2.3>
    =<>
    =<rec2 fld4>
----

ce qui précède suppose des fins de ligne UNIX de \n . Avec Windows \r\n fin de ligne il est beaucoup plus simple que le" newlines" dans chaque champ il y aura en fait juste des flux de ligne (i.e. \n s) et donc vous pouvez définir RS="\r\n" et ensuite les \n s dans les champs ne seront pas traités comme des fins de ligne.

cela fonctionne simplement en comptant combien de " s sont présents jusqu'à présent dans le dossier actuel chaque fois qu'il rencontre le RS - si c'est un nombre impair alors le RS (probablement \n mais ne doit pas être) est milieu de champ et donc nous continuons à construire le courant mais si c'est ça la fin de l'enregistrement en cours et si nous pouvons continuer avec le reste du script de traitement de l'désormais dossier complet.

le gsub(/@/,"@A"); gsub(/""/,"@B") convertit chaque paire de guillemets doubles axcross le dossier entier (gardez à l'Esprit ces "" paires ne peuvent s'appliquer qu'à l'intérieur des champs cités) en une chaîne de caractères @B qui ne contient pas de guillemets doubles de sorte que lorsque nous divisons le dossier en champs le match () ne se fait pas trébucher par des guillemets apparaissant à l'intérieur des champs. Le gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i) restaure les citations à l'intérieur de chaque champ individuellement et convertit aussi le "" s en " s qu'ils représentent vraiment.

19
répondu Ed Morton 2017-08-26 15:05:02