Как удалить дубликаты из большого файла?

Есть файл, с очень большим количеством строк. В обычном случае для удаления дублей я добавлял все в List, и делал Distinct(). Но как быть когда файл большого размера? И как указать прогресс удаления?


Ответы (3 шт):

Автор решения: Aziz Umarov

Добавляйте не в список а в Dictionary<string, string>. Прогресс будите указывать обработанной строкой. Получившийся список будет уникальным сам по себе.

→ Ссылка
Автор решения: Blackmeser

Для каких целей? Если просто в память - добавляйте в HashSet по мере чтения из StreamReader, если в другой файл - сразу в цикле в StreamWriter пишите, сохраняя в HashSet и проверяя наличие перед каждой последующей записью. А если тот-же файл на лету собрались менять - заводите Dictionary<int hashcode, List<<int pos, int lenght>> subline>, и с каждой новой строкой сверяйте наличие hashcode в Dictionary, а если есть - читайте участки файла на основе List<<int pos, int lenght>> subline заново и сверяйте.

→ Ссылка
Автор решения: VladD

Если вы хотите убрать дубликаты строк из реально большого файла (то есть, количество записей в нём такое, что весь файл в память не помещается, но записи имеют разумную длину), то можно реализовать идею @Andrei Khotko: устроить внешнюю сортировку, а потом убирание дубликатов достигается за один проход по отсортированным данным.

Для реализации внешней сортировки воспользуемся таким приёмом: «порежем» данные на куски, которые помещаются в память, отсортируем их и запишем в файлы. Потом сольём файлы в один, при этом удобно сразу и отбросить дубликаты.

Итак, первая часть: разрезание. Здесь сложность составляет прочитать столько записей, чтобы быть «на грани» переполнения памяти (или вытеснения рабочей памяти на диск), в том числе и при сортировке. Я не пытался решить эту проблему, а просто порезал данные на куски фиксированной длины (а вы, наверное, захотите подбирать длину более интеллектуальным методом). Получается вот что:

const string name = @"path\to\dataforsort.txt";
const int recordsPerPart = 10_000_000;
int partNo = 0;
using (var inF = File.OpenText(name))
{
    int recordNo = 0;
    List<string> currentBatch = new List<string>(recordsPerPart);
    while (true)
    {
        string r = inF.ReadLine();
        if (r != null)
        {
            currentBatch.Add(r);
            recordNo++;
        }
        // мы достигли нужного количества, или входной файл окончился
        if (recordNo == recordsPerPart || (r == null && recordNo > 0))
        {
            currentBatch.Sort();
            var partName = GetPartName(partNo);
            // тут можно было бы убрать локальные дубликаты
            File.WriteAllLines(partName, currentBatch);
            partNo++;
            recordNo = 0;
            currentBatch.Clear();
        }
        if (r == null)
            break;
    }
}

int totalParts = partNo;

Функцию GetPartName можно определить например как

string GetPartName(int partNo) =>
    Path.ChangeExtension(name, $".part{partNo}" + Path.GetExtension(name));

Теперь слияние. Я руководствовался идеей Эрика использовать SortedDictionary, чтобы не искать каждый раз максимум. Получилось вот что:

var buf = new SortedDictionary<string, TextReader>();
for (int part = 0; part < totalParts; part++)
{
    var stream = File.OpenText(GetPartName(part));
    TryAddFromStream(stream);
}

var resultName = Path.ChangeExtension(name, ".result" + Path.GetExtension(name));
using (var outF = File.CreateText(resultName))
{
    while (buf.Count > 0)
    {
        var (s, reader) = buf.First();
        outF.WriteLine(s);
        TryAddFromStream(reader); // порядок этих двух строк
        buf.Remove(s);            // важен!
    }
}

bool TryAddFromStream(TextReader reader)
{
    while (true)
    {
        var s = reader.ReadLine();
        if (s == null)
        {
            reader.Dispose();
            return false;
        }
        if (!buf.ContainsKey(s)) // игнорируем дубликаты
        {
            buf[s] = reader;
            return true;
        }
    }
}

Сильно не тестировал, но вроде должно работать.

→ Ссылка