Вам когда-нибудь приходилось сравнивать два файла csv между собой? Например, это могут быть выгрузки одной таблицы из БД до и после выполнения какой-либо операции. Просматривать результаты можно с помощью прекрасных diff
или vimdiff
. А что, если нужно получить такую же таблицу, но только со строками, добавленными "после"? Для этой задачи хорошо подходит comm
.
Установка
Утилита comm входит в состав пакета coreutils:
# fedora
dnf install coreutils
# ubuntu
apt install coreutils
Использование
Перед тем, как сравнивать csv, нужно разобрать несколько моментов.
comm принимает 2 обязательных аргумента - имена файлов для сравнения - поэтому в простейшем случае команда выглядит так:
comm file1 file2
Результат будет выведен на стандартный вывод. Для сохранения в файл нужно использовать перенаправление. Помимо этого, строки в файлах должны быть предварительно отсортированы. Если это не так, можно использовать sort
и специальный синтаксис bash:
comm <(sort file1) <(sort file2) > result
В файле result мы получим результат, записанный в 3 колонки. Первая колонка содержит уникальные строки первого файла. Вторая колонка - уникальные строки второго файла. Третья - общие строки.
В мануале не указано, какой символ является разделителем колонок по умолчанию, но экспериментальным путём выяснено, что это запятая. Поменять можно опцией --output-delimiter=STR
.
Очевидно, что при работе с csv, вывод в три колонки неудобен. У нас каждая колонка отвечает за свой тип данных, а тут в результате какие-то строки окажутся сдвинуты на колонку, а какие-то на две.
Благо, у comm есть опции -1 -2
и -3
. Каждая из которых подавляет вывод соответствующей колонки. Примеры:
# вывести только общие строки:
comm -12 file1 file2
# вывести только строки, уникальные для первого файла:
comm -23 file1 file2
# вывести только строки, уникальные для второго файла:
comm -13 file1 file2
Это всё здорово, но что же с заголовком csv в первой строке файла? Раз файлы нужно отсортировать перед использованием comm, заголовок с большой долей вероятности "уплывёт" куда-нибудь. Более того, если заголовок присутствует в обоих файлах, то с опцией -3, он будет отсутствовать в результате.
Проще сравнить файлы без заголовка и подставить его в результат отдельно.
Автоматизация
А теперь рассмотрим кейс, в котором нужно получить разницу ДО/ПОСЛЕ для набора файлов.
Все файлы "ДО" находятся в директории before
, файлы "ПОСЛЕ" - в директории after
. Имеем следующее дерево директорий и файлов:
.
├── after
│ ├── all_bindings.csv
│ ├── binding_v3.csv
│ └── studs.csv
└── before
├── all_bindings.csv
├── binding_v3.csv
└── studs.csv
Выполняем скрипт:
mkdir -p diff
for before_file in before/*
do
file="${before_file#before/}"
head -1 "before/$file" | sed -e 's/^/action,/' > "diff/$file"
comm -23 <(sort "before/$file") <(sort "after/$file") | sed -e 's/^/DELETED,/' >> "diff/$file"
comm -13 <(sort "before/$file") <(sort "after/$file") | sed -e 's/^/INSERTED,/' >> "diff/$file"
done
В результате выполнения скрипта, получаем дерево:
.
├── after
│ ├── all_bindings.csv
│ ├── binding_v3.csv
│ └── studs.csv
├── before
│ ├── all_bindings.csv
│ ├── binding_v3.csv
│ └── studs.csv
└── diff
├── all_bindings.csv
├── binding_v3.csv
└── studs.csv
Здесь добавилась директория diff
, с файлами, содержащими разницу. В каждом файле есть заголовок.
Здесь ещё в каждый diff-файл с помощью sed
добавлен столбец action, который содержит одно из двух значений:
- DELETED, если строка отсутствует в after-файле
- INSERTED, если строка отсутствует в before-файле
Следует отметить, что строки изменившиеся, например в результате операции UPDATE, будут фигурировать в diff-файле дважды - старое значение с пометкой DELETED, новое - с пометкой INSERTED.
Таким образом, мы получили разницу из двух выгрузок, которую можем просматривать и как-то обработать.