Существует ли надежный инструмент командной строки для обработки csv файлов?

Вопрос или проблема

Я работаю с CSV-файлами и иногда мне нужно быстро проверить содержимое строки или столбца из командной строки. В большинстве случаев команды cut, head, tail и подобные выполнят свою задачу; однако cut не может легко справляться с такими ситуациями, как

"это, первая запись", это вторая, 34.5

Здесь первая запятая – часть первого поля, но cut -d, -f1 с этим не согласен. Прежде чем я начну писать решение сам, мне было интересно, знает ли кто-нибудь хороший инструмент, который уже существует для этой работы. Он должен, по крайней мере, уметь обрабатывать приведённый выше пример и возвращать столбец из CSV-файла. Другие желаемые функции включают возможность выбирать столбцы на основе имен столбцов, указанных в первой строке, поддержку других стилей кавычек и поддержку файлов, разделённых табуляцией.

Если вы не знаете такого инструмента, но у вас есть предложения по реализации такой программы на Bash, Perl, Python или других распространённых языках сценариев, я бы не против таких предложений.

Я, вероятно, немного запоздал, но есть ещё один инструмент, который стоит упомянуть: csvkit.

У него много инструментов командной строки, которые могут:

  • реформатировать CSV-файлы,
  • конвертировать в и из CSV из различных форматов (JSON, SQL, XLS),
  • являться эквивалентом cut, grep, sort и других, но с учётом CSV,
  • соединять разные CSV-файлы,
  • делать общие SQL запросы на данных из CSV-файлов.

Вы можете использовать модуль csv в Python.

Простой пример:

import csv
reader = csv.reader(open("test.csv", "r"))
for row in reader:
    for col in row:
        print(col)

Miller – это ещё один отличный инструмент для манипуляции данными, основанными на именах, включая CSV (с заголовками). Чтобы извлечь первый столбец из CSV-файла, не заботясь о его имени, можно использовать что-то вроде

printf '"первый,столбец",второй,третий\n1,2,3\n' |
  mlr --csv -N cut -f 1

Это звучит как работа для Perl с Text::CSV.

perl -MText::CSV -pe '
    BEGIN {$csv = Text::CSV->new();}
    $csv->parse($_) or die;
    @fields = $csv->fields();
    print @fields[1,3];
'

Смотрите документацию для обработки имен столбцов. Разделитель и стиль кавычек можно настроить с помощью параметров для new. Также смотрите Text::CSV::Separator для угадывания разделителей.

Я бы рекомендовал xsv, «быстрый инструмент командной строки для работы с CSV, написанный на Rust».

Написан автором Ripgrep.

Известен в Как мы сделали обработку CSV в 142 раза быстрее (поток Reddit).

Я нашёл csvfix, инструмент командной строки, который хорошо справляется с задачей. Однако вам придётся создать его самостоятельно:

http://neilb.bitbucket.org/csvfix

Он выполняет все ожидаемые функции: упорядочивает/выбирает столбцы, разбивает/объединяет и многие другие, например, генерирует SQL вставки из данных CSV и делает разницу между данными CSV.

Если вы хотите использовать командную строку (и не хотите создавать целую программу для выполнения задачи), вам стоит обратить внимание на rows, проект, над которым я работаю: это интерфейс командной строки для табличных данных, а также библиотека Python для использования в ваших программах. С помощью интерфейса командной строки вы можете красиво выводить любые данные в CSV, XLS, XLSX, HTML или в любой другой табличный формат, поддерживаемый библиотекой, с помощью простой команды:

rows print myfile.csv

Если myfile.csv выглядит так:

state,city,inhabitants,area
RJ,Angra dos Reis,169511,825.09
RJ,Aperibé,10213,94.64
RJ,Araruama,112008,638.02
RJ,Areal,11423,110.92
RJ,Armação dos Búzios,27560,70.28

Тогда rows выведет содержимое в красивом виде, как это:

+-------+-------------------------------+-------------+---------+
| state |              city             | inhabitants |   area  |
+-------+-------------------------------+-------------+---------+
|    RJ |                Angra dos Reis |      169511 |  825.09 |
|    RJ |                       Aperibé |       10213 |   94.64 |
|    RJ |                      Araruama |      112008 |  638.02 |
|    RJ |                         Areal |       11423 |  110.92 |
|    RJ |            Armação dos Búzios |       27560 |   70.28 |
+-------+-------------------------------+-------------+---------+

Установка

Если вы разработчик на Python и уже установили pip на своей машине, просто выполните внутри виртуального окружения или с sudo:

pip install rows

Если вы используете Debian:

sudo apt-get install rows

Другие интересные функции

Конвертация форматов

Вы можете конвертировать между любыми поддерживаемыми форматами:

rows convert myfile.xlsx myfile.csv

Запросы

Да, вы можете использовать SQL в CSV-файле:

$ rows query 'SELECT city, area FROM table1 WHERE inhabitants > 100000' myfile.csv
+----------------+--------+
|      city      |  area  |
+----------------+--------+
| Angra dos Reis | 825.09 |
|       Araruama | 638.02 |
+----------------+--------+

Конвертация результата запроса в файл вместо stdout также возможна с помощью параметра --output.

Как библиотека Python

Вы также можете использовать rows в своих Python-программах:

import rows
table = rows.import_from_csv('myfile.csv')
rows.export_to_txt(table, 'myfile.txt')
# `myfile.txt` будет содержать такой же контент, как вывод `rows print`

Надеюсь, вам это понравится!

Если вам нужен визуальный / интерактивный инструмент в терминале, я искренне рекомендую VisiData.

введите описание изображения здесь

У него есть таблицы частот (показанные выше), сводные таблицы, “плавление”, графики рассеяния, фильтрация / вычисление с использованием Python и многое другое.

Вы можете передавать CSV-файлы следующим образом

vd hello.csv

Есть специфические параметры для CSV: --csv-dialect, --csv-delimiter, --csv-quotechar и --csv-skipinitialspace для тонкой настройки обработки CSV-файлов.

Я использовал csvtool однажды, и он сэкономил мне много времени и усилий. Вызывается из оболочки.

http://caml.inria.fr/cgi-bin/hump.en.cgi?contrib=447

R – не мой любимый язык программирования, но он хорош для таких вещей.
Если ваш CSV-файл выглядит так

***********
foo.csv
***********
 col1, col2, col3
"это, первая запись", это вторая, 34.5
"немного больше", "путаница", вещи

Внутри интерпретатора R введите

> x=read.csv("foo.csv", header=FALSE)

> x
                     col1                col2   col3
1 это, первая запись  это вторая   34.5
2              "немного больше"           путаница  вещи
> x[1]  # первый столбец
                      col1
1 это, первая запись
2              "немного больше"
> x[1,] # первая строка
                      col1                col2  col3
1 это, первая запись  это вторая  34.5

Что касается ваших других запросов, для “возможности выбирать столбцы на основе имен столбцов, указанных в первой строке” смотрите

> x["col1"]
                      col1
1 это, первая запись
2              "немного больше"

Что касается “поддержки других стилей кавычек”, смотрите аргумент quote для read.csv (и связанных функций).
Что касается “поддержки файлов, разделённых табуляцией”, смотрите аргумент sep для read.csv (установите sep на ‘\t’).

Для получения дополнительной информации смотрите онлайн справку.

> help(read.csv)

Одним из лучших инструментов является Miller. Это как awk, sed, cut, join и sort для данных с именными индексами, таких как CSV, TSV и табличный JSON.

Например

echo '"это, первая запись", это вторая, 34.5' | \
mlr --icsv --implicit-csv-header cat

даёт вам

1=это, первая запись,2= это вторая,3= 34.5

Если вы хотите TSV

echo '"это, первая запись", это вторая, 34.5' | \
mlr --c2t --implicit-csv-header cat

даёт вам (возможно убрать заголовок)

1       2       3
это, первая запись         это вторая      34.5

Если вы хотите первый и третий столбцы, меняя их порядок

echo '"это, первая запись", это вторая, 34.5' | \
mlr --csv --implicit-csv-header --headerless-csv-output cut -o -f 3,1

даёт вам

 34.5,"это, первая запись"

cissy также позволяет обрабатывать CSV через командную строку. Он написан на C (маленький/лёгкий) с доступными rpm и deb пакетами для большинства дистрибутивов.

Используя пример:

echo '"это, первая запись", это вторая, 34.5' | cissy -c 1
"это, первая запись"

или

echo '"это, первая запись", это вторая, 34.5' | cissy -c 2
 это вторая

или

echo '"это, первая запись", это вторая, 34.5' | cissy -c 2-
 это вторая, 34.5

Также ознакомьтесь с GNU Recutils и crush-tools.

(через http://www.reddit.com/r/commandline/comments/mfcu9/anyone_using_gnu_recutils_is_it_outdatedsuperceded/)

Чтобы использовать Python из командной строки, вы можете ознакомиться с pythonpy (https://github.com/Russell91/pythonpy):

$ echo $'a,b,c\nd,e,f' | py '[x[1] for x in csv.reader(sys.stdin)]'
b
e

В репозитории GitHub Structured Text Tools есть полезный список соответствующих инструментов командной строки для Linux. В частности, раздел Delimiter Separated Values перечисляет несколько инструментов, способных работать с CSV, которые непосредственно поддерживают запрашиваемые операции.

Существует также библиотека Curry для чтения/записи файлов в формате CSV: CSV.

Используя Raku (ранее известный как Perl_6)

Эти ответы используют язык программирования Raku, в частности в сочетании с модулем Text::CSV. Ответ ниже состоит из двух частей: первая часть читает файл построчно, вторая часть читает файл целиком. Любой из методов можно использовать для обработки текста, создавая файлы, соответствующие RFC-4180 (и, возможно, RFC-4180/RFC-7111).


Построчно:
Чтение файла построчно, как “однострочник”, принимающий ввод командной строки:

~$ raku -MText::CSV -ne 'my $csv = Text::CSV.new; $csv.parse($_); put $csv.strings.raku;'  MS.csv

Пример ввода:

https://www.microsoft.com/en-us/download/details.aspx?id=45485

Пример вывода:

["Имя пользователя", "Имя", "Фамилия", "Отображаемое имя", "Должность", "Отдел", "Номер офиса", "Телефон офиса", "Мобильный телефон", "Факс", "Адрес", "Город", "Штат или провинция", "Почтовый индекс", "Страна или регион"]
["chris@contoso.com", "Chris", "Green", "Chris Green", "IT Manager", "Information Technology", "123451", "123-555-1211", "123-555-6641", "123-555-9821", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]
["ben@contoso.com", "Ben", "Andrews", "Ben Andrews", "IT Manager", "Information Technology", "123452", "123-555-1212", "123-555-6642", "123-555-9822", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]
["david@contoso.com", "David", "Longmuir", "David Longmuir", "IT Manager", "Information Technology", "123453", "123-555-1213", "123-555-6643", "123-555-9823", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]
["cynthia@contoso.com", "Cynthia", "Carey", "Cynthia Carey", "IT Manager", "Information Technology", "123454", "123-555-1214", "123-555-6644", "123-555-9824", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]
["melissa@contoso.com", "Melissa", "MacBeth", "Melissa MacBeth", "IT Manager", "Information Technology", "123455", "123-555-1215", "123-555-6645", "123-555-9825", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]

Выше используется флаг построчного ввода -ne для принятия ввода построчно и вывода разобранных строк. Выше, уберите вызов .raku, чтобы вывести разобранный текст без двойных кавычек/экранирований. Либо код ниже принимает ввод построчно, используя $csv.getline(), который хранит данные внутренне как массив Raku, который затем выводится:

Построчно: Код Raku в виде скрипта (полезный для обработки данных):

use Text::CSV;

my @rows;
my $csv = Text::CSV.new;
my $fh  = open "MS.csv", :r, :!chomp;
while ($csv.getline($fh)) -> $row {
       @rows.push: $row;
       }
$fh.close; 
$_.raku.put for @rows;

Построчно: Код Raku как “однострочник”, принимающий STDIN:

~$ raku -MText::CSV -e 'my @rows; my $csv = Text::CSV.new;  \
                        while ($csv.getline($*IN)) -> $row { @rows.push: $row; };  \
                        $_.raku.put for @rows;' < MS.csv

Пример вывода (последние два приведённых выше примера показывают внутреннее представление данных Raku):

$["Имя пользователя", "Имя", "Фамилия", "Отображаемое имя", "Должность", "Отдел", "Номер офиса", "Телефон офиса", "Мобильный телефон", "Факс", "Адрес", "Город", "Штат или провинция", "Почтовый индекс", "Страна или регион"]
$["chris@contoso.com", "Chris", "Green", "Chris Green", "IT Manager", "Information Technology", "123451", "123-555-1211", "123-555-6641", "123-555-9821", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]
$["ben@contoso.com", "Ben", "Andrews", "Ben Andrews", "IT Manager", "Information Technology", "123452", "123-555-1212", "123-555-6642", "123-555-9822", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]
$["david@contoso.com", "David", "Longmuir", "David Longmuir", "IT Manager", "Information Technology", "123453", "123-555-1213", "123-555-6643", "123-555-9823", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]
$["cynthia@contoso.com", "Cynthia", "Carey", "Cynthia Carey", "IT Manager", "Information Technology", "123454", "123-555-1214", "123-555-6644", "123-555-9824", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]
$["melissa@contoso.com", "Melissa", "MacBeth", "Melissa MacBeth", "IT Manager", "Information Technology", "123455", "123-555-1215", "123-555-6645", "123-555-9825", "1 Microsoft way", "Redmond", "Wa", "98052", "Соединённые Штаты"]

Если вам нужны только первые две строки (например, заголовок и первая строка данных), измените:

while ($csv.getline($*IN)) ...на... for ($csv.getline($*IN) xx 2)

Вывод только одной строки (например, заголовка) вернёт список, разделённый \n (полезно для проверки правильности разбора):

"Имя пользователя"
"Имя"
"Фамилия"
"Отображаемое имя"
"Должность"
"Отдел"
"Номер офиса"
"Телефон офиса"
"Мобильный телефон"
"Факс"
"Адрес"
"Город"
"Штат или провинция"
"Почтовый индекс"
"Страна или регион"

Конечно, вы также можете выводить отдельные столбцы. Просто измените окончательное выражение put на:

.[2].raku.put for @rows;

...чтобы вывести 3-й столбец ("Фамилия", нумерация начинается с 0):

"Фамилия"
"Green"
"Andrews"
"Longmuir"
"Carey"
"MacBeth"


Чтение файла целиком: Модуль Raku Text::CSV предоставляет высокоуровневую функцию csv(…) для изменения/валидации текста CSV/TSV и возвращения файлов, соответствующих RFC-4180 (и, возможно, RFC-4180/RFC-7111).

Конвертация CSV-файла в TSV-файл (читает stdin, выводит столбцы с внутренними пробелами как заключённые в двойные кавычки по умолчанию):

$ cat MS.csv | raku -MText::CSV -e 'my @a = csv(in => $*IN); csv(in => @a, sep => "\t", out => $*OUT);' > MS.tsv

Пример вывода (сохранён и повторно открыт в Vim с модулем CSV):

         "Имя пользователя"     "Имя"    "Фамилия"        "Отображаемое имя"    "Должность"                  Отдел    "Номер офиса"     "Телефон офиса"  "Мобильный телефон"           Факс              Адрес      Город     "Штат или провинция"     "Почтовый индекс"    "Страна или регион"
   [email protected]            Chris          Green         "Chris Green"   "IT Manager"    "Information Technology"             123451       123-555-1211    123-555-6641  123-555-9821    "1 Microsoft way"   Redmond                      Wa                    98052        "Соединённые Штаты"
     [email protected]              Ben        Andrews         "Ben Andrews"   "IT Manager"    "Information Technology"             123452       123-555-1212    123-555-6642  123-555-9822    "1 Microsoft way"   Redmond                      Wa                    98052        "Соединённые Штаты"
   [email protected]            David       Longmuir      "David Longmuir"   "IT Manager"    "Information Technology"             123453       123-555-1213    123-555-6643  123-555-9823    "1 Microsoft way"   Redmond                      Wa                    98052        "Соединённые Штаты"
 [email protected]          Cynthia          Carey       "Cynthia Carey"   "IT Manager"    "Information Technology"             123454       123-555-1214    123-555-6644  123-555-9824    "1 Microsoft way"   Redmond                      Wa                    98052        "Соединённые Штаты"
 [email protected]          Melissa        MacBeth     "Melissa MacBeth"   "IT Manager"    "Information Technology"             123455       123-555-1215    123-555-6645  123-555-9825    "1 Microsoft way"   Redmond                      Wa                    98052        "Соединённые Штаты"

Модуль Raku Text::CSV может обрабатывать альтернативные разделители столбцов (например, \t), двойные кавычки для значений столбцов с встроенными запятыми, переводами строк и т. д. Самое главное, в процессе обработки ввода текста у вас будет доступ к мощному интерфейсу для работы с регулярными выражениями Raku, если вам нужно фильтровать/заменять символы (например, имена столбцов), строки или столбцы. Каждый приведённый выше пример кода Raku правильно разбирает короткий текстовый пример и выводит следующее (или подобное; укажите Text::CSV.new(:allow_whitespace), чтобы обрезать пробелы до/после каждого поля):

["это, первая запись", "это вторая", "34.5"]

Смотрите приведенные ниже URL для хранения данных в хэше (или массиве хэшей), и/или для задания column-names, sep-char, escape-char, formula-handling, binary, strict параметров и др.

https://raku.land/github:Tux/Text::CSV
https://github.com/Tux/CSV
https://raku.org

csvq - это инструмент командной строки, который использует SQL для запроса CSV файлов:

$ echo '"это, первая запись", это вторая, 34.5' |
  csvq --no-header "SELECT * FROM STDIN"
+--------------------------+----------------------+--------+
|            c1            |          c2          |   c3   |
+--------------------------+----------------------+--------+
| это, первая запись |  это вторая  |  34.5  |
+--------------------------+----------------------+--------+

Обратите внимание, что поскольку CSV-файл не содержит заголовка, csvq задаёт свои собственные внутренние пронумерованные имена столбцов.

Чтобы выбрать первый столбец, укажите имя столбца c1:

$ echo '"это, первая запись", это вторая, 34.5' |
  csvq --no-header "SELECT c1 FROM STDIN"
+--------------------------+
|            c1            |
+--------------------------+
| это, первая запись |
+--------------------------+

Решение на awk

awk -vq='"' '
func csv2del(n) {
  for(i=n; i<=c; i++)
    {if(i%2 == 1) gsub(/,/, OFS, a[i])
    else a[i] = (q a[i] q)
    out = (out) ? out a[i] : a[i]}
  return out}
{c=split($0, a, q); out=X;
  if(a[1]) $0=csv2del(1)
  else $0=csv2del(2)}1' OFS='|' file

Добавляем ещё один инструмент в списке...

csvprintf

Он работает как printf(1).

Например, с таким вводом:

Имя,Фамилия,Идентификационный номер
Джордж,Вашингтон,1
Бетси,Росс,2

И с такой командной строкой:

$ cat input.csv | csvprintf -i '%{IdNumber}04d: %{LastName}s, %{FirstName}s\n'

Вы получите такой вывод:

0001: Вашингтон, Джордж
0002: Росс, Бетси

Отказ от ответственности: я написал это

Вы также можете использовать Lua с роком ftcsv:

$ lua -l ftcsv - filename.csv column_index <