Страницы

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

суббота, 7 декабря 2019 г.

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

#c_sharp #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 чтобы он искал
уникальные значения по двум колонкам?
    


Ответы

Ответ 1



Интерфейс 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

Ответ 2



Как уже написали в комментариях, лучше отказаться от IEqualityComparer, т.к., имхо, это попытка заставить кота лаять. У нас единицы информации — это "ячейки", т.е. слова конкретные, оперирование строками с этими словами порождает неудобные и багоопасные конструкции с прямым индексированием (кстати, легковесное в теории сравнение превратилось в 4-кратный вызов разбиения строки на подмассив; даже по алгоритму достаточно двух раз), что собственно ни к чему, если изначально всё разбить на слова. Т.е. в чём оптимизация, если выделяется больше памяти, больше операций и прямые индексы? Сперва, на мой взгляд надо подготовить строки, т.е. в нашем случае можно всё загнать просто в двумерный массив строк, и все операции производить в нём, с отдельными словами.

Ответ 3



private void GetUnion(List lst1, List lst2) { // List для результата List lstUnion = new List(); foreach (string value in lst1) { string valueColumn1 = value.Split(';')[0]; string valueColumn2 = value.Split(';')[1]; string valueColumn3 = value.Split(';')[2]; // Ищем совпадения, есть ли valueColumn2 и valueColumn3 в lst2 во 2-й и 3-й колонке, // С 1-й и 2-й не перепутаем, // ";russia;japan" - такая последовательность со знаком ';' в начале // может быть только если текст начинается со второй колонки. string result = lst2.FirstOrDefault(s => s.Contains(";" + valueColumn2 + ";" + valueColumn3)); if (result != null) // если совпадения есть { if (!lstUnion.Contains(result)) // и если значение уже не добавлено { lstUnion.Add(result); MessageBox.Show(result); // для проверки } } } // В итоге в lstUnion одно значение - "spain;russia;japan" }

Ответ 4



Открываешь .csv как двумерный массив данных с помощью: Как просто работать с / открыть / изменить / сохранить Excel / CSV файлы Сравниваешь сначала по количеству ячеек в массивах, а потом если оно одинаковое, то по нутрянке ячеек двумя foreach (один во втором). :)

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

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