Страницы

Поиск по вопросам

четверг, 11 октября 2018 г.

Как сравнить два csv файла

Есть 2 csv файла с содержимым
1.csv
spain;russia;japan italy;russia;france
2.csv
spain;russia;japan india;iran;pakistan
Считываю оба файла и заношу их содержимое в список
var lst1= File.ReadAllLines("1.csv").ToList(); var lst2= File.ReadAllLines("2.csv").ToList();
Затем я ищу уникальные элементы из обоих списков
var rezList = lst1.Except(lst2).Union(lst2.Except(lst1)).ToList();
В rezlist у нас следюущие данные.
[0] = "italy;russia;france" [1] = "india;iran;pakistan"
Теперь же я хочу сравнить оба csv-файла по второй и третьей колонке. Как мы выдими разделители колонок у нас
class StringLengthEqualityComparer : IEqualityComparer {
public bool Equals(string x, string y) { return (x.Split(';')[1] == y.Split(';')[1] && x.Split(';')[2] == y.Split(';')[2]); }
public int GetHashCode(string obj) { return obj.Split(';')[1].GetHashCode(); } }
StringLengthEqualityComparer stringLengthComparer = new StringLengthEqualityComparer(); var rezList = lst1.Except(lst2,stringLengthComparer ).Union(lst2.Except(lst1,stringLengthComparer),stringLengthComparer).ToList();
Вопрос. Как правильно составить класс StringLengthEqualityComparer чтобы он искал уникальные значения по двум колонкам?


Ответ

Интерфейс IEqualityComparer предоставляет возможность расширения или замены логики сравнения объектов. Он используется, например, в таких структурах, как Dictionary и HashSet, а также в некоторых методах, которые активно пользуются сравнением объектов (как например использованный вами метод расширения Except()).
Как известно, для того, чтобы иметь возможность корректно сравнивать объекты, в них должны быть переопределены методы Equals() и GetHashCode(), причем должны соблюдаться следующие условия:
стандартные условия для равенства (например, транзитивность) два равных объекта должны давать одинаковое значение хэшкода
(При этом два разных объекта могут иметь равные хэшкоды -- это называется коллизией и должно случаться как можно реже.)
Оба этих метода активно используются структурами Dictionary, HashSet, а также внутренним классом Set, который используется в методе Except()
В случае, когда мы не хотим или не имеем возможности изменять логику сравнения для существующих типов, на помощь нам и приходит интерфейс IEqualityComparer -- вместо вызовов методов Equals() и GetHashCode() у самих объектов, эти методы вызываются у компаратора. Именно поэтому этот интерфейс содержит оба метода, а не один Equals(). А также именно поэтому реализация компаратора должна соблюдать обозначенные выше условия.

Приведенная вами реализация компаратора дает корректные результаты сравнения, однако при большом объеме данных может свести на нет все преимущества быстрой работы метода Except(). Для пар a;b и a;c выдастся одинаковый хэшкод, они попадут в одну корзину внутри Set, что ухудшает показатели скорости работы (подробнее читайте в статьях о том, как работают хэш-таблицы).
Поэтому правильная реализация должна использовать те же поля, которые используются в Equals(), т.е. колонки 1 и 2 (про хэш-функции также читайте в статьях о хэш-таблицах). Например:
public int GetHashCode(string obj) { var valuesArray = obj.Split(';') int hashcode = valuesArray[1].GetHashCode(); hashcode = hashcode * 31 + valuesArray[2].GetHashCode(); return hashcode; }
На этом реализация компаратора закончена.

Однако в целом производительность такого решения оставляет желать лучшего. Поскольку вы вызываете Except() дважды, то для каждой пары строк из обоих файлов методы компаратора (по крайней мере, GetHashCode()) будут вызываться дважды. Вкупе с избыточными разбиениями внутри этих методов картина получается нерадостная.
Можно пойти по другому пути, например, заранее разбить строки:
var splittedLst1 = lst1.Select(i => i.Split(';')); var splittedLst2 = lst2.Select(i => i.Split(';'));
Затем получить разницу:
var comparer = new StringLengthEqualityComparer(); var rezList = splittedLst1 .Except(splittedLst2, comparer) .Union(splittedLst2.Except(splittedLst1, comparer));
А при необходимости снова склеить строки:
foreach (var item in rezList) { Console.WriteLine(string.Join(";", item)); }
При таком подходе каждая строка будет разбита всего один раз, а код компаратора упростится:
class StringLengthEqualityComparer : IEqualityComparer { public bool Equals(string[] x, string[] y) { return x[1] == y[1] && x[2] == y[2]; }
public int GetHashCode(string[] obj) { var hashcode = obj[1].GetHashCode(); hashcode = hashcode * 31 + obj[2].GetHashCode(); return hashcode; } }
Если вы ожидаете строки разной длины (в смысле количества колонок), то нужно изменить компаратор таким образом, чтобы он корректно работал со строками разной длины.
Провел небольшой тест для сравнения производительности. В качестве тестовых данных склеил 10000 раз приведенные вам двухстрочники. Разница в результатах при этом получилась следующая:
Radzhab: 332ms andreycha: 63ms

Комментариев нет:

Отправить комментарий