Может ли awk работать с CSV-файлом, содержащим запятую внутри заключенного в кавычки поля?
Я использую awk для подсчета суммы одного столбца в csv-файле. Формат данных примерно такой:
id, name, value
1, foo, 17
2, bar, 76
3, "I am the, question", 99
Я использовал этот скрипт awk для подсчета суммы:
awk -F, '{sum+=$3} END {print sum}'
Часть значения в поле name содержит запятую, и это нарушает мой сценарий awk. Мой вопрос: Может ли awk решить эту проблему? Если да, то как я могу это сделать?
Спасибо.
11 ответов:
Вы пишете функцию в awk, как показано ниже:
$ awk 'func isnum(x){return(x==x+0)}BEGIN{print isnum("hello"),isnum("-42")}' 0 1
Вы можете включить в свой скрипт эту функцию и проверить, является ли третье поле числовым или нет.если не числовое, то переходите к 4-му полю, а если 4-е поле inturn не числовое, переходите к 5-му ...пока вы не достигнете числового значения.вероятно, здесь поможет петля, и добавьте ее к сумме.
Один из способов использования
GNU awk
и FPATawk 'BEGIN { FPAT = "([^, ]+)|(\"[^\"]+\")" } { sum+=$3 } END { print sum }' file.txt
Результат:
192
Вы можете помочь awk работать с полями данных, содержащими запятые (или новые строки), используя небольшой скрипт, который я написал под названием csvquote. Он заменяет оскорбительные запятые внутри закавыченных полей непечатными символами. Если вам нужно, вы можете позже восстановить эти запятые - но в этом случае вам это не нужно.
Вот команда:
csvquote inputfile.csv | awk -F, '{sum+=$3} END {print sum}'
См. https://github.com/dbro/csvquote для кода
Для такого простого входного файла, как этот, вы можете просто написать небольшую функцию для преобразования всех реальных FSs вне кавычек в какое-то другое значение (я выбрал RS, так как разделитель записей не может быть частью записи), а затем использовать его в качестве FS, например:
$ cat decsv.awk BEGIN{ fs=FS; FS=RS } { decsv() for (i=1;i<=NF;i++) { printf "Record %d, Field %d is <%s>\n" ,NR,i,$i } print "" } function decsv( curr,head,tail) { tail = $0 while ( match(tail,/"[^"]+"/) ) { head = substr(tail, 1, RSTART-1); gsub(fs,RS,head) curr = curr head substr(tail, RSTART, RLENGTH) tail = substr(tail, RSTART + RLENGTH) } gsub(fs,RS,tail) $0 = curr tail } $ cat file id, name, value 1, foo, 17 2, bar, 76 3, "I am the, question", 99 $ awk -F", " -f decsv.awk file Record 1, Field 1 is <id> Record 1, Field 2 is <name> Record 1, Field 3 is <value> Record 2, Field 1 is <1> Record 2, Field 2 is <foo> Record 2, Field 3 is <17> Record 3, Field 1 is <2> Record 3, Field 2 is <bar> Record 3, Field 3 is <76> Record 4, Field 1 is <3> Record 4, Field 2 is <"I am the, question"> Record 4, Field 3 is <99>
Это становится сложным только тогда, когда вам приходится иметь дело с внедренными новыми линиями и внедренными экранированными кавычками внутри котировок, и даже тогда это не слишком сложно, и все это было сделано раньше...
Смотрите Что самое главное надежный способ эффективного разбора CSV с помощью awk? для получения дополнительной информации.
Вы всегда можете решить проблему из первоисточника. Поместите кавычки вокруг поля имени, так же, как поле "я-вопрос". Это гораздо проще, чем тратить свое время на разработку обходных путей для этого.
Обновить (как просил Деннис). Простой пример
$ s='id, "name1,name2", value 1, foo, 17 2, bar, 76 3, "I am the, question", 99' $ echo $s|awk -F'"' '{ for(i=1;i<=NF;i+=2) print $i}' id, , value 1, foo, 17 2, bar, 76 3, , 99 $ echo $s|awk -F'"' '{ for(i=2;i<=NF;i+=2) print $i}' name1,name2 I am the, question
Как вы можете видеть, установив разделитель в двойную кавычку, поля, принадлежащие к "кавычкам", всегда имеют четное число. Поскольку OP не может позволить себе роскошь изменять исходные данные, этот метод не будет к нему подходить.
Если вы точно знаете, что столбец 'value' всегда является последним столбцом:
awk -F, '{sum+=$NF} END {print sum}'
NF представляет число полей, поэтому $NF-последний столбец
Эта статья помогла мне решить эту же проблему с полем данных. Большинство CSV помещают кавычки вокруг полей с пробелами или запятыми внутри них. Это портит подсчет полей для awk, если вы не отфильтруете их.
Если вам нужны данные в тех полях, которые содержат мусор, это не для вас.
ghostdog74
предоставил ответ, который опустошает это поле, но сохраняет общее количество полей в конце, что является ключом к обеспечению согласованности вывода данных. Мне не понравилось, как такое решение ввели новые линии. Это версия этого решения, которое я использовал. У первых трех полей никогда не было этой проблемы в данных. Четвертое поле, содержащее имя клиента, часто так и делалось, но мне нужны были эти данные. Остальные поля, которые показывают проблему, я мог бы выбросить без проблем, потому что они не были нужны в выводе моего отчета. Поэтому я сначала отсеял мусор 4-го поля очень конкретно и удалил первые два экземпляра кавычек. Затем я применяю то, чтоghostdog74
дал, чтобы очистить оставшиеся поля, которые есть запятые внутри них - это также удаляет кавычки, но я используюprintf
для сохранения данных в одной записи. Я начинаю с 85 полей и заканчиваю с 85 полей во всех случаях из моих 8000+ строк грязных данных. Отличный результат!grep -i $1 $dbfile | sed 's/\, Inc.//;s/, LLC.//;s/, LLC//;s/, Ltd.//;s/\"//;s/\"//' | awk -F'"' '{ for(i=1;i<=NF;i+=2) printf ($i);printf ("\n")}' > $tmpfile
Решение, которое опустошает поля с запятыми внутри них, но также поддерживает запись, конечно, является:
awk -F'"' '{ for(i=1;i<=NF;i+=2) printf ($i);printf ("\n")}
Огромное спасибо ghostdog74 за отличное решение!
NetsGuy256/
Я использую
`FPAT="([^,]+)|(\"[^\"]+\")" `
Определяют с полей поглазеть. Я обнаружил, что когда поле равно null, это не распознает правильное количество полей. Потому что " + " требует по крайней мере 1 символ в поле. Я изменил его на:
`FPAT="([^,]*)|(\"[^\"]*\")"`
И заменить
"+"
на"*"
. Он работает правильно.Я также нахожу, что руководство пользователя GNU Awk также имеет эту проблему. https://www.gnu.org/software/gawk/manual/html_node/Splitting-By-Content.html
FPAT является элегантным решением, потому что он может обрабатывать страшные запятые в задаче кавычек, но суммировать столбец чисел в последнем столбце независимо от числа предыдущих разделителей, $NF работает хорошо:
awk -F"," '{sum+=$NF} END {print sum}'
Для доступа к предпоследнему столбцу вы можете использовать следующее:
awk -F"," '{sum+=$(NF-1)} END {print sum}'
Полноценные CSV-Парсеры, такие как Perl
Text::CSV_XS
, специально созданы для обработки такого рода странностей.
perl -MText::CSV_XS -lne 'BEGIN{$csv=Text::CSV_XS->new({allow_whitespace => 1})} if($csv->parse($_)){@f=$csv->fields();$sum+=$f[2]} END{print $sum}' file
allow_whitespace
необходимо, так как входные данные имеют пробелы, окружающие разделители запятых. Очень старые версииText::CSV_XS
могут не поддерживать этот параметр.Я дал более подробное объяснение
Text::CSV_XS
в моем ответе здесь: разбирать csv-файл с помощью gawk