Qual è il modo più efficace per analizzare in modo efficiente CSV utilizzando awk?

L’intento di questa domanda è di fornire una risposta canonica.

Dato un CSV come potrebbe essere generato da Excel o altri strumenti con nuove linee incorporate, doppie virgolette incorporate e campi vuoti come:

$ 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 

Qual è il modo più efficace per utilizzare efficientemente awk per identificare i record e i campi separati:

 Record 1: $1= $2= $3= $4= ---- Record 2: $1= $2= $3= $4= ---- 

quindi può essere usato come quei record e campi internamente dal resto dello script awk.

Un CSV valido sarebbe uno conforms a RFC 4180 o può essere generato da MS-Excel.

La soluzione deve tollerare che la fine del record sia solo LF ( \n ) come è tipico per i file UNIX piuttosto che CRLF ( \r\n ) come richiesto da tale standard e che Excel o altri strumenti Windows genererebbero. Tollera anche campi non quotati mescolati con campi quotati. In particolare, non è necessario tollerare l’escape di " s con una barra retroversa precedente (cioè \" invece di "" ) come consentito da alcuni altri formati CSV – se si dispone di quello quindi aggiungere un gsub(/\\"/,"\"\"") in primo piano lo gestiva e cercando di gestire entrambi i meccanismi di escape automaticamente in uno script renderebbe la sceneggiatura inutilmente fragile e complicata.

Se il tuo CSV non può contenere righe nuove o virgolette doppie di escape allora tutto ciò che ti serve è (con GNU awk per FPAT ):

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

Altrimenti, tuttavia, la soluzione più generale, robusta e portatile che funzionerà con qualsiasi moderno awk è:

 $ cat decsv.awk function buildRec( i,orig,fpat,done) { $0 = PrevSeg $0 if ( gsub(/"/,"&") % 2 ) { PrevSeg = $0 RS done = 0 } else { PrevSeg = "" gsub(/@/,"@A"); gsub(/""/,"@B") # <"[email protected]""bar"> -> <"[email protected]@Bbar"> orig = $0; $0 = "" # Save $0 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 $0 gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i) # <"[email protected]@Bbar"> -> <"[email protected]"bar"> gsub(/^"|"$/,"",$i) # <"[email protected]"bar"> ->  orig = substr(orig,RSTART+RLENGTH+1) # Move past fpat+sep in orig $0 } 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: $1= $2=<> $3= $4= ---- Record 2: $1= $2= $3=<> $4= ---- 

Quanto sopra presuppone le terminazioni di riga UNIX di \n . Con Windows \r\n terminazioni di riga è molto più semplice in quanto le “nuove linee” all’interno di ogni campo saranno in realtà solo line feed (cioè \n s) e quindi puoi impostare RS="\r\n" e poi \n s all’interno dei campi non saranno trattati come finali di linea.

Funziona semplicemente contando quanti " s sono presenti finora nel record corrente ogni volta che incontra la RS – se è un numero dispari allora la RS (presumibilmente \n ma non deve essere) è a metà campo e quindi noi continua a build il record corrente, ma se è ancora allora è la fine del record corrente e quindi possiamo continuare con il resto dello script che elabora il record ora completo.

gsub(/@/,"@A"); gsub(/""/,"@B") gsub(/@/,"@A"); gsub(/""/,"@B") converte ogni coppia di virgolette axcross in tutto il record (ricorda che queste coppie "" possono essere applicate solo all’interno di campi quotati) a una stringa @B che non contenga una @B doppia così che quando dividiamo il record in campi il match () non viene intaccato dalle virgolette che appaiono all’interno dei campi. gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i) ripristina le virgolette all’interno di ogni campo singolarmente e converte anche le "" s nella " loro ” rappresentare davvero.