【问题标题】:Deleting duplicate lines in a file using Java使用Java删除文件中的重复行
【发布时间】:2023-04-08 23:21:02
【问题描述】:

作为我正在进行的项目的一部分,我想清理我生成的重复行条目的文件。然而,这些重复通常不会彼此靠近。我想出了一种在 Java 中这样做的方法(基本上是复制文件,然后使用嵌套的 while 语句将一个文件中的每一行与另一个文件中的每一行进行比较)。问题是我生成的文件非常大并且文本很重(大约 225k 行文本,大约 40 兆)。我估计我目前的流程需要 63 个小时!这绝对不能接受。

不过,我需要一个集成的解决方案。最好用Java。有任何想法吗?谢谢!

【问题讨论】:

  • 9 个答案并且没有投票?这是一个完全有效且表述良好的问题

标签: java file text file-io duplicates


【解决方案1】:

嗯... 40 兆似乎足够小,您可以构建一个Set 行,然后将它们全部打印出来。这将比执行 O(n2) I/O 工作快得多。

应该是这样的(忽略异常):

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。由于元素是通过引用存储的,因此额外链表的开销与实际数据量相比应该是微不足道的。

编辑: 正如 Workshop Alex 指出的那样,如果您不介意制作一个临时文件,您可以在阅读时简单地打印出这些行。这允许您使用简单的HashSet 而不是LinkedHashSet。但我怀疑你会注意到像这样的 I/O 绑定操作的区别。

【讨论】:

  • 这就是我要给出的答案
  • 是的,40 megs 不算什么,将整个内容读入内存,将其转储到哈希集以仅保留唯一行,然后将其写回磁盘。
  • 根据提问者的要求,您可能需要跟踪行号,因为遍历 HashSet 会以任意顺序返回行。
  • 您可以使用 #lines / 0.75 之类的值初始化哈希集,因为 HashSet 将创建一个新表并在达到其默认填充等级 75% 时重新哈希所有内容。另一种可能性是创建填充等级为 1.0f (100%) 且大小比您的数据计数大一点的 HashSet ->“new HashSet(300000, 1.0f)”。这样可以避免昂贵的重新散列。
  • 您可以使用 Commons IO 的 FileUtils commons.apache.org/io/api-release/org/apache/commons/io/… 中的 readLines() 和 writeLines() 来简化此代码。 (我不确定这是否会影响可扩展性。)
【解决方案2】:

好的,大多数答案有点愚蠢和缓慢,因为它涉及向某个哈希集或其他内容添加行,然后再次将其从该集中移回。让我用伪代码展示一下最优解:

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.

请各位,不要让它变得比需要的更困难。 :-) 甚至不用担心排序,你不需要。

【讨论】:

  • +1 表示我在写答案时应该看到的明显流血。哦! :)
  • 真;我在没有临时文件的情况下这样做,但使用一个可能会更有效(不需要 LinkedHashSet)。但我敢猜测 CPU 无论如何都不会成为瓶颈。
  • 呃,我的评论是针对 Workshop Alex,而不是 gustafc。
  • 当然,您可以输出到内存中未排序的字符串列表,而不是使用输出文件。然后,当您添加完没有重复的输入后,将字符串列表写入旧输入文件。这确实意味着您将使用比其他解决方案多一倍的内存,但它仍然非常快。
  • 那是因为它存储了两次字符串:一次在哈希表中,一次在字符串列表中。 (再说一次,hashset 和字符串列表都可能只存储对字符串的引用,在这种情况下它不会消耗那么多。)
【解决方案3】:

类似的方法

public void stripDuplicatesFromFile(String filename) {
    IOUtils.writeLines(
        new LinkedHashSet<String>(IOUtils.readLines(new FileInputStream(filename)),
        "\n", new FileOutputStream(filename + ".uniq"));
}

【讨论】:

  • 后面的FileInputStream不应该是FileOutputStream吗?除此之外,+1 是为了简单和“了解和使用库”。
  • 另外,值得一提的是,IOUtils 来自 Apache Commons IO (commons.apache.org/io);这对每个读者来说可能并不明显。
【解决方案4】:

可能是这样的:

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(虽然查找/插入速度稍快)会重新排序所有行。

【讨论】:

    【解决方案5】:

    您可以在 Collections 库中使用 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);
    }
    

    【讨论】:

      【解决方案6】:

      如果顺序无关紧要,simplest way is shell scripting:

      <infile sort | uniq > outfile
      

      【讨论】:

      • @nanosoft 那将是UUOC
      【解决方案7】:

      尝试一个简单的 HashSet 来存储你已经读过的行。 然后遍历文件。 如果遇到重复项,它们会被忽略(因为 Set 只能包含每个元素一次)。

      【讨论】:

      • 你最好使用某种集合而不是地图
      • 我曾经在 Delphi 中做过类似的事情,尽管我必须编写自己的 HashSet 类来做到这一点。唯一的缺点是您需要大量内存来存储大量文件,如果您在客户端而不是在服务器上执行此操作,这很好。基本上,需要这个的项目设法在两分钟内读取了一个 500k 行的文件并删除了所有重复项。
      • 但是,我只是读了一行,检查它是否在哈希集中,如果不在,我会添加它并将其写入文件。否则,我会跳到下一行。这样一来,我就不会从哈希集中读取数据,而且最重要的是:我将所有行保持在相同的顺序。
      【解决方案8】:
      • 读入文件,存储行号和行:O(n)
      • 按字母顺序排序:O(n log n)
      • 删除重复项:O(n)
      • 按照原来的行号顺序排序:O(n log n)

      【讨论】:

        【解决方案9】:

        哈希集方法是可以的,但您可以对其进行调整,使其不必将所有字符串存储在内存中,而是一个指向文件中位置的逻辑指针,这样您就可以返回读取实际值,以防万一需要它。

        另一种创造性的方法是在每一行附加行号,然后对所有行进行排序,删除重复项(忽略应该是数字的最后一个标记),然后再按最后一个标记对文件进行排序,然后在输出中剥离它。

        【讨论】:

          【解决方案10】:

          如果您可以使用 UNIX shell 命令,您可以执行以下操作:

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

          这将遍历您的整个文件,并且每次 sed 调用仅将每个唯一的事件传递一次。这样一来,您就不会进行以前做过的大量搜索。

          【讨论】:

            【解决方案11】:

            有两种可扩展的解决方案,其中可扩展是指基于磁盘而不是基于内存,具体取决于过程是否应该稳定,其中稳定是指删除重复项后的顺序相同。如果可伸缩性不是问题,那么只需将内存用于相同类型的方法。

            对于不稳定的解决方案,首先对磁盘上的文件进行排序。这是通过将文件拆分为较小的文件,对内存中的较小块进行排序,然后按排序顺序合并文件来完成的,其中合并忽略重复项。

            合并本身几乎不需要内存,只需比较每个文件中的当前行,因为保证下一行更大。

            稳定的解决方案稍微复杂一些。首先,像以前一样按块对文件进行排序,但在每行中注明原始行号。然后,在“合并”期间不要打扰存储 结果,只是要删除的行号。

            然后逐行复制原始文件,忽略上面存储的行号。

            【讨论】:

              【解决方案12】:

              这些行的顺序是否重要,您希望看到多少个重复项?

              如果不是,并且如果您指望很多骗子(即阅读多于写作),我也会考虑 并行化哈希集解决方案,将哈希集作为共享资源。

              【讨论】:

              • 不错的主意,但由于输入文件只有 40 兆字节,我认为这不会有问题。
              • 我猜。但是并行化的东西是 phun! :3
              【解决方案13】:

              我对这个有效的解决方案做了两个假设:

              1. 有一个 Blob 等价于 line 或者我们可以将其处理为二进制
              2. 我们可以保存偏移量或指向每行开头的指针。

              基于这些假设的解决方案是: 1.读取一行,将hashmap中的长度保存为key,这样我们就有了更轻量的hashmap。将列表保存为哈希图中的条目,用于键中提到的所有具有该长度的行。构建这个哈希图是 O(n)。 在映射散列图中每一行的偏移量时,将行 blob 与行列表中的所有现有条目(偏移量)进行比较,以获得此键长度,但条目 -1 作为偏移量除外。如果发现重复,则删除两行并保存偏移量 - 1 在列表中的那些地方。

              因此考虑复杂性和内存使用情况:

              Hashmap 内存,空间复杂度 = O(n) 其中 n 是行数

              时间复杂度 - 如果没有重复但所有等长的行考虑每行的长度 = m,则考虑行数 =n,那么这将是 , O(n)。因为我们假设我们可以比较 blob ,所以 m 无关紧要。 那是最坏的情况。

              在其他情况下,我们节省了比较,尽管我们在 hashmap 中需要的额外空间很少。

              此外,我们可以在服务器端使用 mapreduce 来拆分集合并稍后合并结果。并使用长度或行首作为映射器键。

              【讨论】:

                【解决方案14】:
                void deleteDuplicates(File filename) throws IOException{
                    @SuppressWarnings("resource")
                    BufferedReader reader = new BufferedReader(new FileReader(filename));
                    Set<String> lines = new LinkedHashSet<String>();
                    String line;
                    String delims = " ";
                    System.out.println("Read the duplicate contents now and writing to file");
                    while((line=reader.readLine())!=null){
                        line = line.trim(); 
                        StringTokenizer str = new StringTokenizer(line, delims);
                        while (str.hasMoreElements()) {
                            line = (String) str.nextElement();
                            lines.add(line);
                            BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
                            for(String unique: lines){
                                writer.write(unique+" ");               
                            }
                            writer.close();
                        }
                    }
                    System.out.println(lines);
                    System.out.println("Duplicate removal successful");
                }
                

                【讨论】:

                  【解决方案15】:

                  这些答案都依赖于文件足够小以存储在内存中。

                  如果可以对文件进行排序,这是一种可用于任何大小文件的算法。

                  你需要这个库: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 现在已创建,已排序,没有重复。

                  【讨论】:

                    猜你喜欢
                    • 2021-10-26
                    • 2012-07-22
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-04-03
                    • 2012-12-23
                    • 1970-01-01
                    • 2011-04-16
                    相关资源
                    最近更新 更多