null

Как получить diff, сравнивая два .csv файла

Вам когда-нибудь приходилось сравнивать два файла 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.

Таким образом, мы получили разницу из двух выгрузок, которую можем просматривать и как-то обработать.