Pergunta

Como você analisar um arquivo CSV usando gawk? Simplesmente definindo FS="," não é suficiente, como um campo citado com um interior vírgula serão tratados como múltiplos campos.

Exemplo usando FS="," que não funciona:

conteúdo do arquivo:

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

roteiro gawk:

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

má saída:

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"
---------------------------

saída desejada:

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

Solução

A resposta curta é "Eu não usaria gawk para CSV parse se o CSV contém dados constrangedores", onde os meios 'estranhas' coisas como vírgulas nos dados de campo CSV.

A próxima pergunta é "O que outro processamento que você vai estar fazendo", uma vez que irá influenciar o que as alternativas que você usa.

Eu provavelmente usar Perl eo Text :: CSV ou módulos Text :: CSV_XS para ler e processar os dados. Lembre-se, Perl foi originalmente escrito em parte como um assassino awk e sed -., Portanto, os programas a2p e s2p ainda distribuídos com Perl que awk converter e scripts de sed (respectivamente) em Perl

Outras dicas

A versão gawk 4 Manual diz ao uso FPAT = "([^,]*)|(\"[^\"]+\")"

Quando FPAT é definido, ele desabilita FS e especifica campos pelo conteúdo e não pelo separador.

Você pode usar uma função wrapper simples chamado csvquote para higienizar a entrada e restaurá-lo depois de awk é feito processá-lo. Tubo de seus dados através dele no início e no fim, e tudo deve funcionar ok:

Antes:

gawk -f mypgoram.awk input.csv

depois:

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

Consulte https://github.com/dbro/csvquote para o código e documentação.

Se permitido, gostaria de usar o Python csv módulo, especial pagar atenção para a dialeto usado e os parâmetros de formatação necessário , para analisar o arquivo CSV que você tem.

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

Eu não sou exatamente certo se esta é a maneira certa de fazer as coisas. Eu prefiro trabalhar em um arquivo CSV em que quer todos os valores são cotados ou nenhum. Btw, awk permite expressões regulares para ser campo Separadores. Verifique se isso é ú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
  }
}

Patterns que seguem este pode acessar os campos na coluna []. NúmeroDeColunas indica o número de elementos em coluna [] que foram encontrados. Se nem todas as linhas contêm o mesmo número de colunas, Coluna [] contém dados adicionais após coluna [ColumnCount] ao processar as linhas mais curtas.

Esta implementação é lenta, mas parece emular o recurso FPAT / patsplit() encontrado em gawk> = 4.0.0 mencionado em uma resposta anterior.

Referência

Aqui está o que eu vim acima com. Quaisquer comentários e / ou melhores soluções seria apreciada.

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"
}

A idéia básica é que ciclo I pelos campos, e qualquer campo que começa com uma citação, mas não termina com uma citação recebe o próximo campo anexado a ele.

Perl tem o Text :: módulo CSV_XS que é lidar com a estranheza por vírgula citado construído para o efeito.
Alternadamente tentar o 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

Produz 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
---

Aqui está uma versão legível.
Guardá-lo como parsecsv, chmod + x, e executá-lo 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";
    }
}

Você pode precisar de ponto para uma versão diferente de perl em sua máquina, uma vez que o texto :: módulo CSV_XS não pode ser instalado em sua versão padrão do 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.

Se nenhuma das suas versões do Perl têm Text :: CSV_XS instalado, você precisará:
sudo apt-get install cpanminus
sudo cpanm Text::CSV_XS

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top