Analizando un archivo CSV usando gawk

¿Cómo se analiza un archivo CSV usando gawk? Simplemente configurar FS="," no es suficiente, ya que un campo citado con una coma dentro se tratará como campos múltiples.

Ejemplo que usa FS="," que no funciona:

contenido del archivo:

 one,two,"three, four",five "six, seven",eight,"nine" 

guión de gawk:

 BEGIN { FS="," } { for (i=1; i<=NF; i++) printf "field #%d: %s\n", i, $(i) printf "---------------------------\n" } 

mal resultado:

 field #1: one field #2: two field #3: "three field #4: four" field #5: five --------------------------- field #1: "six field #2: seven" field #3: eight field #4: "nine" --------------------------- 

salida deseada:

 field #1: one field #2: two field #3: "three, four" field #4: five --------------------------- field #1: "six, seven" field #2: eight field #3: "nine" --------------------------- 

La respuesta corta es “No usaría gawk para analizar CSV si el CSV contiene datos incómodos”, donde “torpe” significa cosas como comas en los datos de campo CSV.

La siguiente pregunta es “¿Qué otro procesamiento va a hacer?”, Ya que eso influirá en las alternativas que use.

Probablemente usaría Perl y los módulos Text :: CSV o Text :: CSV_XS para leer y procesar los datos. Recuerde, Perl fue originalmente escrito en parte como un asesino de awk y sed – de ahí que los progtwigs a2p y s2p aún se distribuyan con Perl que convierte scripts awk y sed (respectivamente) en Perl.

El manual de la versión 4 de gawk dice que use FPAT = "([^,]*)|(\"[^\"]+\")"

Cuando se define FPAT , deshabilita FS y especifica campos por contenido en lugar de por separador.

Si es permisible, usaría el módulo csv de Python, prestando especial atención al dialecto utilizado y los parámetros de formato requeridos , para analizar el archivo CSV que tiene.

Puede usar una función de envoltura simple llamada csvquote para desinfectar la entrada y restaurarla después de que awk termine de procesarla. Transfiere tus datos al principio y al final, y todo debería funcionar bien:

antes de:

 gawk -f mypgoram.awk input.csv 

después:

 csvquote input.csv | gawk -f mypgoram.awk | csvquote -u 

Consulte https://github.com/dbro/csvquote para obtener el código y la documentación.

csv2delim.awk

 # csv2delim.awk converts comma delimited files with optional quotes to delim separated file # delim can be any character, defaults to tab # assumes no repl characters in text, any delim in line converts to repl # repl can be any character, defaults to ~ # changes two consecutive quotes within quotes to ' # usage: gawk -f csv2delim.awk [-v delim=d] [-v repl=`"] input-file > output-file # -v delim delimiter, defaults to tab # -v repl replacement char, defaults to ~ # eg gawk -v delim=; -v repl=` -f csv2delim.awk test.csv > test.txt # abe 2-28-7 # abe 8-8-8 1.0 fixed empty fields, added replacement option # abe 8-27-8 1.1 used split # abe 8-27-8 1.2 inline rpl and "" = ' # abe 8-27-8 1.3 revert to 1.0 as it is much faster, split most of the time # abe 8-29-8 1.4 better message if delim present BEGIN { if (delim == "") delim = "\t" if (repl == "") repl = "~" print "csv2delim.awk vm 1.4 run at " strftime() > "/dev/stderr" ########################################### } { #if ($0 ~ repl) { # print "Replacement character " repl " is on line " FNR ":" lineIn ";" > "/dev/stderr" #} if ($0 ~ delim) { print "Temp delimiter character " delim " is on line " FNR ":" lineIn ";" > "/dev/stderr" print " replaced by " repl > "/dev/stderr" } gsub(delim, repl) $0 = gensub(/([^,])\"\"/, "\\1'", "g") # $0 = gensub(/\"\"([^,])/, "'\\1", "g") # not needed above covers all cases out = "" #for (i = 1; i <= length($0); i++) n = length($0) for (i = 1; i <= n; i++) if ((ch = substr($0, i, 1)) == "\"") inString = (inString) ? 0 : 1 # toggle inString else out = out ((ch == "," && ! inString) ? delim : ch) print out } END { print NR " records processed from " FILENAME " at " strftime() > "/dev/stderr" } 

test.csv

 "first","second","third" "fir,st","second","third" "first","sec""ond","third" " first ",sec ond,"third" "first" , "second","th ird" "first","sec;ond","third" "first","second","th;ird" 1,2,3 ,2,3 1,2, ,2, 1,,2 1,"2",3 "1",2,"3" "1",,"3" 1,"",3 "","","" "","""aiyn","oh""" """","""","""" 11,2~2,3 

test.bat

 rem test csv2delim rem default is: -v delim={tab} -v repl=~ gawk -f csv2delim.awk test.csv > test.txt gawk -v delim=; -f csv2delim.awk test.csv > testd.txt gawk -v delim=; -v repl=` -f csv2delim.awk test.csv > testdr.txt gawk -v repl=` -f csv2delim.awk test.csv > testr.txt 

No estoy exactamente seguro de si esta es la forma correcta de hacer las cosas. Preferiría trabajar en un archivo csv en el que se hayan citado todos los valores o ninguno. Por cierto, awk permite que las expresiones regulares sean Separadores de campo. Verifica si eso es útil.

 { ColumnCount = 0 $0 = $0 "," # Assures all fields end with comma while($0) # Get fields by pattern, not by delimiter { match($0, / *"[^"]*" *,|[^,]*,/) # Find a field with its delimiter suffix Field = substr($0, RSTART, RLENGTH) # Get the located field with its delimiter gsub(/^ *"?|"? *,$/, "", Field) # Strip delimiter text: comma/space/quote Column[++ColumnCount] = Field # Save field without delimiter in an array $0 = substr($0, RLENGTH + 1) # Remove processed text from the raw data } } 

Los patrones que siguen a este pueden acceder a los campos en Columna []. ColumnCount indica la cantidad de elementos en la Columna [] que se encontraron. Si no todas las filas contienen el mismo número de columnas, la columna [] contiene datos adicionales después de la columna [ColumnCount] al procesar las filas más cortas.

Esta implementación es lenta, pero parece emular la característica FPAT / patsplit() encuentra en gawk> = 4.0.0 mencionado en una respuesta anterior.

Referencia

Esto es lo que se me ocurrió. Cualquier comentario y / o mejores soluciones serían apreciadas.

 BEGIN { FS="," } { for (i=1; i<=NF; i++) { f[++n] = $i if (substr(f[n],1,1)=="\"") { while (substr(f[n], length(f[n]))!="\"" || substr(f[n], length(f[n])-1, 1)=="\\") { f[n] = sprintf("%s,%s", f[n], $(++i)) } } } for (i=1; i<=n; i++) printf "field #%d: %s\n", i, f[i] print "----------------------------------\n" } 

La idea básica es que recorro los campos, y cualquier campo que comience con una cita pero no termine con una cita obtiene el siguiente campo adjunto.

Perl tiene el módulo Text :: CSV_XS que está especialmente diseñado para manejar la rareza de comas citadas.
Alternativamente pruebe el módulo Text :: CSV.

perl -MText::CSV_XS -ne 'BEGIN{$csv=Text::CSV_XS->new()} if($csv->parse($_)){@f=$csv->fields();for $n (0..$#f) {print "field #$n: $f[$n]\n"};print "---\n"}' file.csv

Produce este resultado:

 field #0: one field #1: two field #2: three, four field #3: five --- field #0: six, seven field #1: eight field #2: nine --- 

Aquí hay una versión legible para los humanos.
Guárdelo como parsecsv, chmod + x, y ejecútelo como “parsecsv file.csv”

 #!/usr/bin/perl use warnings; use strict; use Text::CSV_XS; my $csv = Text::CSV_XS->new(); open(my $data, '<', $ARGV[0]) or die "Could not open '$ARGV[0]' $!\n"; while (my $line = <$data>) { if ($csv->parse($line)) { my @f = $csv->fields(); for my $n (0..$#f) { print "field #$n: $f[$n]\n"; } print "---\n"; } } 

Es posible que deba indicar una versión diferente de Perl en su máquina, ya que el módulo Text :: CSV_XS no puede instalarse en su versión predeterminada de perl.

 Can't locate Text/CSV_XS.pm in @INC (@INC contains: /home/gnu/lib/perl5/5.6.1/i686-linux /home/gnu/lib/perl5/5.6.1 /home/gnu/lib/perl5/site_perl/5.6.1/i686-linux /home/gnu/lib/perl5/site_perl/5.6.1 /home/gnu/lib/perl5/site_perl .). BEGIN failed--comstacktion aborted. 

Si ninguna de sus versiones de Perl tiene instalado Text :: CSV_XS, deberá:
sudo apt-get install cpanminus
sudo cpanm Text::CSV_XS