Разбор файла CSV с помощью gawk

Как вы анализируете файл CSV с помощью gawk? Просто установить FS="," недостаточно, так как поле в кавычках с запятой внутри будет рассматриваться как несколько полей.

Пример использования FS=",", который не работает:

содержимое файла:

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

гавк скрипт:

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

плохой вывод:

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

желаемый результат:

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

person MCS    schedule 24.11.2008    source источник
comment
см. также: stackoverflow.com/questions/45420535/   -  person Sundeep    schedule 28.11.2017
comment
Возможный дубликат Какой самый надежный способ эффективно разобрать CSV с помощью awk?   -  person miken32    schedule 28.12.2018


Ответы (9)


Краткий ответ: «Я бы не стал использовать gawk для анализа CSV, если CSV содержит неудобные данные», где «неудобно» означает такие вещи, как запятые в данных поля CSV.

Следующий вопрос: «Какую еще обработку вы собираетесь выполнять», поскольку это повлияет на то, какие альтернативы вы будете использовать.

Я бы, вероятно, использовал Perl и модули Text::CSV или Text::CSV_XS для чтения и обработки данных. Помните, что Perl изначально был частично написан как убийца awk и sed, поэтому программы a2p и s2p до сих пор распространяются вместе с Perl, которые преобразуют сценарии awk и sed (соответственно) в Perl.

person Jonathan Leffler    schedule 24.11.2008
comment
См. также программу csvfix. Конечно, Python (и Ruby, и Tcl, и большинство других расширяемых языков сценариев) можно использовать вместо Perl; это становится вопросом личного вкуса или выбора компании (Хобсона). - person Jonathan Leffler; 04.07.2012

Руководство по gawk версии 4 говорит использовать FPAT = "([^,]*)|(\"[^\"]+\")"

Когда определено FPAT, оно отключает FS и указывает поля по содержимому, а не по разделителю.

person BCoates    schedule 13.01.2012
comment
Концепция FPAT интересна. Цитата из регулярного выражения не допускает двойных кавычек внутри строки в кавычках. Для этого требуется более сложное регулярное выражение, например: FPAT="([^,]*)|(\"([^\"]|\"\")+\"[^,]*)". Окончательный [^,]* допускает некорректные поля, начинающиеся с кавычек, например "abc"def,; он рассматривает def как часть поля. Внутри двойных кавычек допускаются две последовательные двойные кавычки. Этот материал неприятный, поэтому модули, специфичные для CSV, обычно являются лучшим способом работы с данными CSV, если данные CSV не являются чистыми и простыми. - person Jonathan Leffler; 04.07.2012
comment
FPAT требует gawk 4. Мне потребовалось некоторое время... ;) - person Richard Kiefer; 14.12.2012
comment
Я создал псевдоним для gawk, чтобы упростить запуск gawk в CSVS: alias awkcsv="gawk -v FPAT='([^,]+)|(\"[^\"]+\")'" - person DeegC; 04.01.2016

Вы можете использовать простую функцию-оболочку, называемую csvquote, чтобы очистить ввод и восстановить его после того, как awk завершит его обработку. Передайте данные через него в начале и в конце, и все должно работать нормально:

до:

gawk -f mypgoram.awk input.csv

после:

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

Код и документацию см. на странице https://github.com/dbro/csvquote.

person D Bro    schedule 04.05.2013

Если позволите, я бы использовал модуль Python csv, обращая особое внимание на используемый диалект и требуемые параметры форматирования, чтобы проанализируйте файл CSV, который у вас есть.

person ayaz    schedule 24.11.2008

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

тест.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
person Community    schedule 27.11.2008

Я не совсем уверен, правильно ли это. Я бы предпочел работать с CSV-файлом, в котором либо все значения указаны в кавычках, либо нет. Кстати, awk позволяет регулярным выражениям быть разделителями полей. Проверьте, полезно ли это.

person Vijay Dev    schedule 24.11.2008
comment
Я бы также выбрал подход с регулярным выражением и попытался сделать так, чтобы он соответствовал чему-то вроде этого ^|,|$ (это быстрый снимок, вам, конечно, нужно избегать , я хочу, чтобы это было просто) - person flolo; 24.11.2008

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

Шаблоны, следующие за этим, могут получить доступ к полям в Column[]. ColumnCount указывает количество найденных элементов в Column[]. Если не все строки содержат одинаковое количество столбцов, Column[] содержит дополнительные данные после Column[ColumnCount] при обработке более коротких строк.

Эта реализация медленная, но, похоже, она эмулирует функцию FPAT/patsplit(), найденную в gawk >= 4.0.0, упомянутую в предыдущем ответе.

Справочник

person kbulgrien    schedule 19.03.2012

Вот что я придумал. Буду признателен за любые комментарии и/или лучшие решения.

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

Основная идея заключается в том, что я перебираю поля, и к любому полю, которое начинается с кавычки, но не заканчивается кавычкой, добавляется следующее поле.

person MCS    schedule 24.11.2008
comment
Это больше похоже на C. Используем ли мы правильный инструмент для правильной работы? Я новичок в awk и не могу придумать никаких простых решений. - person Vijay Dev; 24.11.2008
comment
@ Виджай Дев, новичок означает новичок, а не эксперт. - person Robert Gamble; 24.11.2008
comment
Ах, мой английский!! Я хотел сказать: «Я новичок и ‹em›so‹/em› я не могу придумать никаких прямых решений» - person Vijay Dev; 25.11.2008
comment
К вашему сведению, это работает, но вам нужно n=0 в качестве последней строки, чтобы она правильно работала в многострочном файле. - person GoldenBoy; 28.10.2010
comment
Обратите внимание, что допустимым полем может быть: """Jump"", he said!". Это будет разделено запятой, но предшествующий символ будет двойной кавычкой. Скрипт разбивается на запятую, хотя этого не должно быть, поскольку запятая встроена в поле в кавычках. Нечетное количество двойных кавычек перед запятой указывает на конец поля; четное число нет. - person Jonathan Leffler; 11.09.2015

В Perl есть модуль Text::CSV_XS, специально созданный для обработки странностей с запятыми в кавычках.
В качестве альтернативы попробуйте модуль 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

Производит этот вывод:

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

Вот удобочитаемая версия.
Сохраните ее как parsecsv, chmod +x и запустите как "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";
    }
}

Возможно, вам потребуется указать другую версию perl на вашем компьютере, поскольку модуль Text::CSV_XS может не быть установлен в вашей версии 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.

Если ни в одной из ваших версий Perl не установлен Text::CSV_XS, вам потребуется:
sudo apt-get install cpanminus
sudo cpanm Text::CSV_XS

person Chris Koknat    schedule 28.10.2015