Анализ файла значений, разделенных запятыми, то есть CSV-файла, из командной оболочки bash может быть сложной задачей и приводить к ошибкам в зависимости от сложности CSV-файла. Однако это частая задача во многих сценариях оболочки автоматизации или для быстрой обработки и переформатирования данных из файла, загруженного в bash.
В этом посте рассказывается о том, как разобрать CSV-файл с помощью встроенных команд Bash или с помощью команды awk для разбора более сложного формата. Решения, представленные ниже, могут быть легко адаптированы к другим форматам файлов, например, к файлу значений, разделенных табуляцией, т.е. к TSV-файлу.
Для примеров в этой статье мы используем CSV-файл из datahub.io со списком стран и их двухзначным кодом (ISO 3166-1). CSV-файл содержит два поля Name и Code с 249 записями + 1 строка заголовка, что делает его файлом в 250 строк.
$ head -n5 countries.csv Name,Code Afghanistan,AF Åland Islands,AX Albania,AL Algeria,DZ ...
CSV-файл – это файл, C содержащий S отдельные V значения. Несмотря на то, что этот формат используется десятилетиями и в широком спектре программного обеспечения, на самом деле он не является стандартным. Для CSV-файлов нет официальной спецификации, что приводит к сложности их разбора.
Некоторые программы могут допускать различные сложные варианты использования, такие как поддержка многострочных полей или некоторых пользовательских разделителей. Наиболее близкими к спецификации являются IETF RFC 4180 и IETF RFC 7111, которые определяют mime-тип IANA для CSV как text/csv.
Вы можете найти хороший обзор предварительного общего определения формата CSV в разделе 2 RFC 4180. Краткое описание высокого уровня может быть следующим.
Представление такого файла с учетом вышеуказанных критериев может выглядеть следующим образом. Обозначение CRLF указывает на разрыв строки в CSV-файле. Строка записи row3 представляет поля с экранированными двойными кавычками, пробелом и разрывом строки.
header_column_1,header_column_2,header_column_3 CRLF row1_field1,row1_field2,row1_field3 CRLF row2_field1,row2_field2,row2_field3 CRLF "row3""field1","row3 field2","row3 CRLF field3" CRLF row4_field1,row4_field2,row4_field3
При попытке проанализировать CSV-файл в bash важно понимать источник происхождения данных и следует ли ожидать поддержки сложных форматов. В некоторых случаях у вас может не быть другого выбора, кроме как использовать внешнюю библиотеку для поддержки некоторых сложных форматов.
Для перебора наших выборочных данных самый простой способ – прочитать файл и использовать внутренний разделитель полей (IFS). Для чтения каждой строки CSV-файла вы можете использовать встроенную команду read, которая считывает строку из стандартного ввода и разбивает ее на поля, присваивая каждое слово переменной. Опция -r запрещает использование обратной косой черты \ для экранирования любых символов. Без опции -r неэкранированная обратная косая черта была бы удалена вместо того, чтобы быть представленной в виде символа.
Обратите внимание, что read потребуется имя переменной для каждого поля, которое вы хотите записать, и последнее указанное значение будет просто общим для всех остальных полей.
👉 Этот метод рекомендуется только для простых CSV-файлов, в которых нет текстовых полей, содержащих лишние разделители-запятые
,
или возвращаемые строки.
Ниже приведен простой пример команды IFS с разделителем полей запятой (,) формата CSV и команды read с двумя ожидаемыми полями name и code, которые были бы доступны внутри цикла while в качестве переменных $name и $code.
while IFS=, read -r name code; do # сделай что-нибудь... Не забудьте пропустить строку заголовка! [[ "$name" != "Name" ]] && echo "$name" done < countries.csv
⚠️ Однако в этой методологии есть загвоздка. Она не поддерживает полную спецификацию CSV и не будет работать так, как вы ожидаете, с данным набором данных. Если вы внимательно посмотрите на выходные данные, некоторые из них возвращают неполные значения, поскольку некоторые поля в CSV-файле являются текстовыми полями, которые содержат разделитель через запятую , и заключены в двойные кавычки “.
... United States United States Minor Outlying Islands Uruguay Uzbekistan Vanuatu "Venezuela Viet Nam "Virgin Islands "Virgin Islands Wallis and Futuna Western Sahara ...
Вы можете подсчитать, сколько у нас неверных записей, с помощью другого цикла while, простого регулярного выражения и счетчика, используя арифметическое расширение.
count=0
while IFS=, read -r name code; do
# сделай что-нибудь...
[[ "$code" == *","* ]] && echo "$name $code" && ((++count))
done < countries.csv; \
echo ">> мы нашли ${count} плохих записей"
"Bolivia Plurinational State of",BO "Bonaire Sint Eustatius and Saba",BQ "Congo the Democratic Republic of the",CD "Iran Islamic Republic of",IR "Korea Democratic People's Republic of",KP "Korea Republic of",KR "Macedonia the Former Yugoslav Republic of",MK "Micronesia Federated States of",FM "Moldova Republic of",MD "Palestine State of",PS "Saint Helena Ascension and Tristan da Cunha",SH "Taiwan Province of China",TW "Tanzania United Republic of",TZ "Venezuela Bolivarian Republic of",VE "Virgin Islands British",VG "Virgin Islands U.S.",VI >> мы нашли 16 плохих записей
Более 6% записей вернут неполные данные. Итак, если вы не уверены, что у вас нет таких текстовых полей, мы бы не рекомендовали использовать этот первый метод.
В этом примере использовалась конструкция оператора If в Bash.
Awk – это специализированный язык, предназначенный для обработки текста. Он доступен в большинстве Unix-подобных систем, к сожалению, между реализациями и версиями может быть много различий. В нашем примере мы будем использовать мощный GNU awk, который, вероятно, является наиболее полной реализацией awk.
👉 Этот метод рекомендуется для сложных CSV-файлов, в которых нет текстовых полей, содержащих разделители новой строки, такие как символы \n или \r.
Используя тот же набор данных countries.csv, что и в нашем первом примере, теперь мы собираемся проанализировать наш CSV с помощью реализации, использующей шаблоны полей (FPAT). Мы будем внимательно следить за тем, чтобы поля разделялись запятыми (,), игнорируя те, которые находятся в полях, заключенных в кавычки “. FPAT = “([^,]+)|(\”[^\”]+\”)” Определение можно разбить следующим образом:
или
Ниже приведен пример реализации с использованием awk для разбора CSV-файла с помощью FPAT.
gawk ' BEGIN { FPAT = "([^,]+)|(\"[^\"]+\")" count=0 } { if ($1 != "Name") { # Не забудьте пропустить строку заголовка! printf("%s\n", $1) ++count } } END { printf("Количество записей: %s\n", count) } ' countries.csv
Теперь мы правильно заполняем названия всех стран.
⚠️ Этот подход по-прежнему не поддерживает полную спецификацию CSV. Если ваши текстовые поля содержат возвращаемые строки или другие странности, то этот синтаксический анализ завершится неудачей. В большинстве случаев это может быть нормально, если формат содержимого известен заранее. Если некоторые поля содержат ручные пользовательские записи, вы рискуете допустить ошибки.
Не существует простого способа поддерживать полные реализации CSV с помощью только встроенных функций bash или awk, учитывая многочисленные спецификации и реализации CSV. Несмотря на широкое распространение этого формата, сложно должным образом поддерживать многие граничные случаи. Для поддержки полной реализации CSV из вашего сценария оболочки вам нужно будет использовать более продвинутое решение.
Первой альтернативой разбору сложного CSV-файла из сценария оболочки является использование csvkit. Например, вы можете обработать данные с помощью csvkit, чтобы преобразовать их в формат JSON, а затем выполнить более сложную работу с помощью такого инструмента, как jq легкого и гибкого JSON-процессора командной строки. csvkit предоставляет множество утилит командной строки для импорта, экспорта, анализа, сортировки, объединения, очистки и форматирования CSV-файлов. Официальное руководство довольно полное.
Другой вариант – использовать стандартный csv-модуль в python. Это довольно просто реализовать. Вы можете использовать класс reader или DictReader из csv модуля python. Возможно, если вы не хотите реализовывать все на python, вы можете просто предварительно обработать свои CSV-файлы и очистить поля, чтобы убедиться, что они отформатированы так, как вы ожидаете. Затем вы все равно можете обработать чистый вывод CSV с помощью bash или awk, как в наших предыдущих примерах.
#!/usr/local/bin/python # csv-reader.py: Пример синтаксического анализа CSV в python import csv with open('countries.csv', 'r') as csvfile: reader = csv.DictReader(csvfile) for row in reader: print('Код страны', row['Code'], 'is for', row['Name'])
Ниже приведен пример вывода csv-reader.py скрипта на примере CSV-файла с названием страны и кодом.
[me@linux: ~]$ python3 csv-reader.py | head -n 5 Код страны AF is for Afghanistan Код страны AX is for Åland Islands Код страны AL is for Albania Код страны DZ is for Algeria Код страны AS is for American Samoa