bash - awk csv
Analizando un archivo CSV usando gawk (9)
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 vas a hacer?", Ya que eso influirá en las alternativas que uses.
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 programas a2p
y s2p
aún se distribuyan con Perl que convierte scripts awk
y sed
(respectivamente) en Perl.
¿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"
---------------------------
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.
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.
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.
{
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.
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 ~
# e.g. 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 v.m 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
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.
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 puede no estar instalado 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--compilation aborted.
Si ninguna de sus versiones de Perl tiene instalado Text :: CSV_XS, deberá:
sudo apt-get install cpanminus
sudo cpanm Text::CSV_XS