2017-07-31 68 views
14

Celem tego pytania jest udzielenie odpowiedzi kanonicznej.Jaki jest najskuteczniejszy sposób wydajnego analizowania pliku CSV za pomocą awk?

Biorąc CSV, jak mogłoby być generowane przez Excel lub innych narzędzi z osadzonymi znakami nowej linii, osadzone cudzysłowy i pustych pól, takich jak:

$ 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 

Jaka jest najbardziej solidną drogę efektywne wykorzystanie awk zidentyfikować poszczególne rekordy i pola :

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

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

, więc może być używany jako te rekordy i pola wewnętrznie przez resztę skryptu awk.

Prawidłowy plik CSV to taki, który jest zgodny z RFC 4180 lub może być generowany przez MS-Excel.

Rozwiązanie musi tolerować koniec rekordu będącego po prostu LF (\n), co jest typowe dla plików systemu UNIX, a nie CRLF (\r\n), zgodnie z tym standardem, a wygenerowałby go Excel lub inne narzędzia systemu Windows. Będzie także tolerował niecytowane pola pomieszane z cytowanymi polami. Nie trzeba będzie specjalnie tolerować ucieczki " s z poprzedzającym ukośnikiem odwrotnym (tj. \" zamiast ""), jak pozwalają na to inne formaty CSV - jeśli to masz, dodanie z wyprzedzeniem gsub(/\\"/,"\"\"") poradzi sobie z tym i spróbuje automatycznie obsłużyć oba mechanizmy ucieczki jeden skrypt spowodowałby, że skrypt byłby niepotrzebnie delikatny i skomplikowany.

Odpowiedz

13

Jeśli CSV nie może zawierać znaków nowej linii lub uciekły cudzysłów to wszystko, czego potrzebujesz, to (z awk GNU dla 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> 

W przeciwnym razie, choć bardziej ogólne, solidne, przenośne rozwiązanie, które będzie pracować z dowolne nowoczesne awk to:

$ 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"> -> <[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=<rec1, fld1> 
    $2=<> 
    $3=<rec1","fld3.1 
", 
fld3.2> 
    $4=<rec1 
fld4> 
---- 
Record 2: 
    $1=<rec2, fld1.1 

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

Powyższe zakłada zakończenia linii UNIX \n. W przypadku zakończeń linii Windows \r\n jest to znacznie prostsze, ponieważ "znaki nowej linii" w każdym polu będą po prostu liniami (tzn. \n s), więc można ustawić RS="\r\n", a następnie \n s w polach nie będą traktowane jako zakończenia linii.

Działa poprzez zliczenie ile " s są obecne do tej pory w bieżącym rekordzie gdy napotka RS - jeśli jest to liczba nieparzysta wtedy RS (przypuszczalnie \n ale nie musi być) jest mid-field i tak budujemy bieżący rekord, ale jeśli nawet jest, to jest to koniec aktualnego rekordu, więc możemy kontynuować z resztą skryptu przetwarzającego teraz kompletny rekord.

gsub(/@/,"@A"); gsub(/""/,"@B") przekształca każdą parę cudzysłów axcross cały rekord (pamiętać te "" pary można stosować tylko w obszarach cytowanych) na sznurku @B który nie zawiera podwójny cudzysłów tak, że kiedy podzielić rekord na pola funkcja match() nie zostanie wywołana przez cytaty pojawiające się wewnątrz pól. gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i) przywraca cytaty w każdym polu osobno, a także przekształca je w s w rzeczywistą reprezentację.