Как удалить дубликаты из большого файла?
Есть файл, с очень большим количеством строк. В обычном случае для удаления дублей я добавлял все в List, и делал Distinct(). Но как быть когда файл большого размера? И как указать прогресс удаления?
Ответы (3 шт):
Добавляйте не в список а в Dictionary<string, string>. Прогресс будите указывать обработанной строкой. Получившийся список будет уникальным сам по себе.
Для каких целей? Если просто в память - добавляйте в HashSet по мере чтения из StreamReader, если в другой файл - сразу в цикле в StreamWriter пишите, сохраняя в HashSet и проверяя наличие перед каждой последующей записью. А если тот-же файл на лету собрались менять - заводите Dictionary<int hashcode, List<<int pos, int lenght>> subline>, и с каждой новой строкой сверяйте наличие hashcode в Dictionary, а если есть - читайте участки файла на основе List<<int pos, int lenght>> subline заново и сверяйте.
Если вы хотите убрать дубликаты строк из реально большого файла (то есть, количество записей в нём такое, что весь файл в память не помещается, но записи имеют разумную длину), то можно реализовать идею @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;
}
}
}
Сильно не тестировал, но вроде должно работать.