Удаление повторяющихся строк в файле с помощью Java

В рамках проекта, над которым я работаю, я хотел бы очистить файл, который я создаю, от повторяющихся записей строк. Однако эти дубликаты часто не встречаются рядом друг с другом. Я придумал способ сделать это на Java (который в основном делал копию файла, а затем использовал вложенный оператор while для сравнения каждой строки в одном файле с остальной частью другого). Проблема в том, что мой сгенерированный файл довольно большой и содержит много текста (около 225 тысяч строк текста и около 40 мегабайт). Я оцениваю, что мой текущий процесс займет 63 часа! Это определенно неприемлемо.

Однако мне нужно комплексное решение для этого. Желательно на Яве. Любые идеи? Спасибо!


person Monster    schedule 15.06.2009    source источник
comment
9 ответов и ни одного голоса? это совершенно правильный и хорошо сформулированный вопрос   -  person Peter Perháč    schedule 15.06.2009


Ответы (14)


Хм... 40 мегабайт кажется достаточно маленьким, чтобы вы могли построить Set строк, а затем распечатать их все обратно. Это было бы намного быстрее, чем выполнение операций ввода-вывода за O(n2).

Это будет примерно так (игнорируя исключения):

public void stripDuplicatesFromFile(String filename) {
    BufferedReader reader = new BufferedReader(new FileReader(filename));
    Set<String> lines = new HashSet<String>(10000); // maybe should be bigger
    String line;
    while ((line = reader.readLine()) != null) {
        lines.add(line);
    }
    reader.close();
    BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
    for (String unique : lines) {
        writer.write(unique);
        writer.newLine();
    }
    writer.close();
}

Если порядок важен, вы можете использовать LinkedHashSet вместо HashSet. Поскольку элементы хранятся по ссылке, накладные расходы на дополнительный связанный список должны быть незначительными по сравнению с фактическим объемом данных.

Редактировать: Как отметил Алекс из Мастерской, если вы не возражаете против создания временного файла, вы можете просто распечатать строки по мере их чтения. Это позволяет вам использовать простой HashSet вместо LinkedHashSet. Но я сомневаюсь, что вы заметите разницу в такой операции ввода-вывода, как эта.

person Michael Myers    schedule 15.06.2009
comment
это ответ, который я собирался дать - person David Johnstone; 15.06.2009
comment
да, 40 мегов - это ничто, прочитайте все это в память, выгрузите его в хэш-набор, чтобы сохранить только уникальные строки, запишите его обратно на диск. - person z -; 15.06.2009
comment
В зависимости от требований спрашивающего вам может потребоваться отслеживать номер строки, потому что итерация по HashSet будет возвращать строки в довольно произвольном порядке. - person Simon Nickerson; 15.06.2009
comment
Вы можете инициализировать хэш-набор значением, например #lines / 0,75, потому что HashSet создаст новую таблицу и перехэширует все, если он достигнет уровня заполнения по умолчанию, равного 75%. Другой возможностью было бы создать HashSet с fillgrade 1.0f (100%) и размером, который немного больше, чем ваш счетчик данных -> новый HashSet (300000, 1.0f). Таким образом, вы можете избежать дорогостоящего перефразирования. - person Philipp; 15.06.2009
comment
Вы можете упростить этот код, используя readLines() и writeLines() из FileUtils Commons IO, commons.apache.org/io/api-release/org/apache/commons/io/. (Я не уверен, повлияет ли это на масштабируемость.) - person Jonik; 15.06.2009
comment
Хм, я пытался это реализовать, но получаю ошибку java.lang.OutOfMemoryError: пространство кучи Java. Я пытался увеличить размер HashSet, но безрезультатно. Идеи? Спасибо! - person Monster; 15.06.2009
comment
Передайте -Xmx64m (где 64 — количество мегабайт в куче) программе при запуске, например java -Xmx64m MyProgram или java -Xmx100m -jar MyJar.jar. - person Michael Myers; 15.06.2009
comment
ему, скорее всего, потребуется более 64 МБ оперативной памяти. Почему? 40 МБ us-ascii-test-file -> 80 МБ в виде строк + служебные данные HashSet + служебные данные объекта + .... Я бы выбрал 512 МБ или около того :) - person Philipp; 15.06.2009
comment
Ах, но он не хранит повторяющиеся строки, так что это зависит от того, сколько дубликатов есть. (Однако вы, скорее всего, правы, и не помешает выделить больше ресурсов для такой краткосрочной программы, как эта.) - person Michael Myers; 15.06.2009
comment
Ах, я установил xmx на 512, и это, похоже, сработало. Отличное исправление! Дубликаты исчезли! Спасибо, парни! - person Monster; 15.06.2009
comment
И, наконец, я остановился на LinkedHashSet. Хотя порядок не имеет большого значения, он значительно упрощает отслеживание вещей. И накладные расходы нулевые. Еще раз всем спасибо! - person Monster; 15.06.2009
comment
Эта точная реализация в блоге Scala . Cyberwhale.tech/2017/01/09/ - person Vladimir Stazhilov; 09.01.2017
comment
Набор предназначен только для этого. (у) - person Vaibs; 01.04.2017

Хорошо, большинство ответов немного глупы и медленны, поскольку они включают добавление строк в какой-то хеш-набор или что-то еще, а затем снова перемещение их обратно из этого набора. Покажу самое оптимальное решение в псевдокоде:

Create a hashset for just strings.
Open the input file.
Open the output file.
while not EOF(input)
  Read Line.
  If not(Line in hashSet)
    Add Line to hashset.
    Write Line to output.
  End If.
End While.
Free hashset.
Close input.
Close output.

Пожалуйста, ребята, не усложняйте задачу больше, чем нужно. :-) Даже не беспокойтесь о сортировке, вам это не нужно.

person Wim ten Brink    schedule 15.06.2009
comment
+1 за указание на очевидное кровотечение, которое я должен был увидеть, когда писал свой ответ. О! :) - person gustafc; 15.06.2009
comment
Истинный; Я делал это без временного файла, но с ним может быть немного эффективнее (LinkedHashSet не требуется). Но рискну предположить, что ЦП в любом случае не будет узким местом. - person Michael Myers; 15.06.2009
comment
Э-э, мой комментарий был адресован мастерской Alex, а не gustafc. - person Michael Myers; 15.06.2009
comment
Конечно, вместо использования выходного файла вы можете вывести в несортированный список строк в памяти. Затем, когда вы закончите добавлять входные данные без дубликатов, напишите список строк поверх старого входного файла. Это означает, что вы будете использовать в два раза больше памяти, чем с другими решениями, но это все еще очень быстро. - person Wim ten Brink; 16.06.2009
comment
@Workshop Alex: В основном это то, что я сделал. Почему вы говорите, что он использует вдвое больше памяти? - person Michael Myers; 16.06.2009
comment
Это потому, что он сохраняет строки дважды: один раз в хеш-таблице и один раз в списке строк. (Опять же, есть вероятность, что и хэш-набор, и список строк хранят только ссылки на строки, и в этом случае он не будет потреблять так много.) - person Wim ten Brink; 17.06.2009
comment
Да, они хранят ссылки. Дополнительных накладных расходов, вероятно, даже недостаточно, чтобы их заметить, при 8 байтах на уникальную строку. - person Michael Myers; 17.06.2009
comment
Простой расчет: 225к строк, умноженных на 8 на каждую ссылку, получается 1,8 мегабайта. С двумя списками это удваивается до 3,6 мегабайт. Опять же, если 90% являются дубликатами, вы можете снова уменьшить это число на 90%... - person Wim ten Brink; 18.06.2009
comment
супер эффективно! Мне пришлось обработать 30 000 файлов по 100 строк в каждом и удалить дубликаты. Это заняло 10 минут, в то время как другое решение заняло 3 часа. - person HopeKing; 18.01.2018

Подобный подход

public void stripDuplicatesFromFile(String filename) {
    IOUtils.writeLines(
        new LinkedHashSet<String>(IOUtils.readLines(new FileInputStream(filename)),
        "\n", new FileOutputStream(filename + ".uniq"));
}
person Peter Lawrey    schedule 16.06.2009
comment
Разве последний FileInputStream не должен быть FileOutputStream? В остальном +1 за простоту, знание и использование библиотек. - person Jonik; 24.06.2009
comment
Кроме того, стоит упомянуть, что IOUtils взят из Apache Commons IO (commons.apache.org/io) ; это, вероятно, не очевидно для каждого читателя. - person Jonik; 24.06.2009

Что-то вроде этого, пожалуй:

BufferedReader in = ...;
Set<String> lines = new LinkedHashSet();
for (String line; (line = in.readLine()) != null;)
    lines.add(line); // does nothing if duplicate is already added
PrintWriter out = ...;
for (String line : lines)
    out.println(line);

LinkedHashSet сохраняет порядок вставки, в отличие от HashSet, который (хотя и немного быстрее для поиска/вставки) меняет порядок всех строк.

person gustafc    schedule 15.06.2009

Вы можете использовать Set в библиотеке коллекций для хранения уникальных, видимых значений при чтении файла.

Set<String> uniqueStrings = new HashSet<String>();

// read your file, looping on newline, putting each line into variable 'thisLine'

    uniqueStrings.add(thisLine);

// finish read

for (String uniqueString:uniqueStrings) {
  // do your processing for each unique String
  // i.e. System.out.println(uniqueString);
}
person brabster    schedule 15.06.2009

Если порядок не имеет значения, простейшим способом является сценарий оболочки:

<infile sort | uniq > outfile
person phihag    schedule 15.06.2009
comment
@nanosoft Это будет UUOC. - person phihag; 26.03.2019

Попробуйте простой HashSet, в котором хранятся строки, которые вы уже прочитали. Затем переберите файл. Если вы сталкиваетесь с дубликатами, они просто игнорируются (поскольку набор может содержать каждый элемент только один раз).

person Kevin Dungs    schedule 15.06.2009
comment
лучше с набором, чем с картой - person David Johnstone; 15.06.2009
comment
Однажды я сделал что-то подобное в Delphi, хотя для этого мне пришлось написать свой собственный класс HashSet. Единственным недостатком является то, что вам нужно много памяти с огромными файлами, и это нормально, если вы делаете это на стороне клиента, а не на сервере. По сути, проект, который нуждался в этом, смог прочитать файл из 500 тысяч строк и удалить все дубликаты в течение двух минут. - person Wim ten Brink; 15.06.2009
comment
Однако я просто читал строку, проверял, есть ли она в хеш-наборе, а если нет, то добавлял и записывал в файл. В противном случае я бы просто перешел к следующей строке. Таким образом, я не считываю данные из хеш-набора и, что самое приятное, сохраняю все строки в том же порядке. - person Wim ten Brink; 15.06.2009

  • Прочитать в файле, сохранив номер строки и строку: O(n)
  • Отсортируйте его в алфавитном порядке: O (n log n)
  • Удалить дубликаты: O(n)
  • Отсортируйте его в исходном порядке номеров строк: O (n log n)
person Simon Nickerson    schedule 15.06.2009

Подход Hash Set в порядке, но вы можете настроить его так, чтобы не хранить все строки в памяти, а логический указатель на местоположение в файле, чтобы вы могли вернуться, чтобы прочитать фактическое значение, только если вам это нужно.

Другой творческий подход состоит в том, чтобы добавить к каждой строке номер строки, затем отсортировать все строки, удалить дубликаты (игнорируя последний токен, который должен быть номером), а затем снова отсортировать файл по последнему токену и выделить его. на выходе.

person fortran    schedule 15.06.2009

Если бы вы могли использовать команды оболочки UNIX, вы могли бы сделать что-то вроде следующего:

for(i = line 0 to end)
{
    sed 's/\$i//2g' ; deletes all repeats
}

Это будет перебирать весь ваш файл и передавать каждое уникальное вхождение только один раз за вызов sed. Таким образом, вы не выполняете кучу поисков, которые вы делали раньше.

person samoz    schedule 15.06.2009

Существует два масштабируемых решения, где под масштабируемостью я подразумеваю диск, а не память, в зависимости от того, должна ли процедура быть стабильной или нет, где под стабильной я подразумеваю, что порядок после удаления дубликатов одинаков. если масштабируемость не является проблемой, просто используйте память для того же метода.

Для нестабильного решения сначала отсортируйте файл на диске. Это делается путем разбиения файла на более мелкие файлы, сортировки меньших фрагментов в памяти и последующего слияния файлов в отсортированном порядке, при этом слияние игнорирует дубликаты.

Само слияние можно сделать почти без использования памяти, сравнивая только текущую строку в каждом файле, так как следующая строка гарантированно будет больше.

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

Затем скопируйте исходный файл построчно, игнорируя номера строк, которые вы сохранили выше.

person user44242    schedule 15.06.2009

Имеет ли значение, в каком порядке идут строки и сколько дубликатов вы рассчитываете увидеть?

Если нет, и если вы рассчитываете на множество дубликатов (т. е. намного больше чтения, чем записи), я бы также подумал о распараллеливании решения хэш-набора с хэш-набором в качестве общего ресурса.

person mikek    schedule 15.06.2009
comment
Неплохая идея, но поскольку входной файл всего 40 мегабайт, я не думаю, что это будет проблемой. - person Michael Myers; 15.06.2009
comment
Наверное. Но параллелизм — это пхун! :3 - person mikek; 15.06.2009

Я сделал два предположения для этого эффективного решения:

  1. Существует Blob-эквивалент строки, или мы можем обработать его как двоичный файл.
  2. Мы можем сохранить смещение или указатель на начало каждой строки.

Основываясь на этих предположениях, решение: 1. Прочитайте строку, сохраните длину в хэш-карте как ключ, чтобы у нас была более легкая хэш-карта. Сохраните список как запись в hashmap для всех строк, длина которых указана в ключе. Построение этой хэш-карты — O(n). При сопоставлении смещений для каждой строки в хэш-карте сравните BLOB-объекты строк со всеми существующими записями в списке строк (смещений) для этой длины ключа, за исключением записи -1 в качестве смещения. Если обнаружен дубликат, удалите обе строки и сохраните смещение - 1 в тех местах в списке.

Поэтому учитывайте сложность и использование памяти:

Память хеш-карты, сложность пространства = O (n), где n - количество строк

Сложность времени - если нет дубликатов, но все строки одинаковой длины, учитывая длину каждой строки = m, учтите, что количество строк = n, тогда это будет , O (n). Поскольку мы предполагаем, что можем сравнивать blob , m не имеет значения. Это был худший случай.

В других случаях мы экономим на сравнениях, хотя нам потребуется немного дополнительного места в хэш-карте.

Кроме того, мы можем использовать mapreduce на стороне сервера, чтобы разделить набор и объединить результаты позже. И используя длину или начало строки в качестве ключа сопоставления.

person AAW    schedule 16.05.2015

Все эти ответы основаны на том, что файл достаточно мал для хранения в памяти.

Если файл можно отсортировать, этот алгоритм можно использовать для файла любого размера.

Вам понадобится эта библиотека: https://github.com/lemire/externalsortinginjava

Я предполагаю, что вы начинаете с файла fileDumpCsvFileUnsorted, а закончите с новым файлом fileDumpCsvFileSorted, который отсортирован и не содержит дубликатов.

ExternalSort.sort(fileDumpCsvFileUnsorted, fileDumpCsvFileSorted);
int numDupes = 0;
File dupesRemoved = new File(fileDumpCsvFileSorted.getAbsolutePath() + ".nodupes");
String previousLine = null;
try (FileWriter fw = new FileWriter(dupesRemoved);
     BufferedWriter bw = new BufferedWriter(fw);
     FileReader fr = new FileReader(fileDumpCsvFileSorted);
     LineIterator lineIterator = new LineIterator(fr)
) {
  while (lineIterator.hasNext()) {
    String nextLine = lineIterator.nextLine();
    if (StringUtils.equals(nextLine, previousLine)) {
      ++numDupes;
      continue;
    }
    bw.write(String.format("%s%n", nextLine));
    previousLine = nextLine;
  }
}
logger.info("Removed {} dupes from {}", numDupes, fileDumpCsvFileSorted.getAbsolutePath());
FileUtils.deleteQuietly(fileDumpCsvFileSorted);
FileUtils.moveFile(dupesRemoved, fileDumpCsvFileSorted);

Файл fileDumpCsvFileSorted теперь создается отсортированным без дубликатов.

person Nicholas DiPiazza    schedule 05.02.2021