【问题标题】:Removing duplicate strings from huge text files从巨大的文本文件中删除重复的字符串
【发布时间】:2018-09-14 21:21:24
【问题描述】:

我想从文本文件中删除重复的字符串。为了做到这一点,我将每一行放在一个 HashSet 中,然后将它们写入另一个文件。它工作正常。但是当涉及到大文件(180mb 500 万行)时,它就不能很好地工作了。假设不可能在 HashSet 或任何其他集合中存储 500 万个字符串,我做了一个循环,所以我存储了前 100 000 行,然后将它们写入文件,然后清除 HashSet 并再次写入,直到出现文件中没有更多行。不幸的是,这不会删除所有重复项,但我认为它可以删除大约 70-90% 的重复项。但它不起作用。当我用 500 万行的 180mb 文件测试它时。我计算了大约 300 000 个重复项,新文件大约有 300 万行。它应该有大约 500 万 - 300 000。当我计算迭代时,它们应该是 500 万,但它们是 340 万。

    public File removeDuplicates(File file) {
    System.out.println("file opened");
    Scanner sc;
    HashSet<String> set = new HashSet<String>();
    JFileChooser chooser = new JFileChooser();
    File createdFile = null;
    int returnVal = chooser.showSaveDialog(parent);
    if (returnVal == JFileChooser.APPROVE_OPTION) {
        BufferedWriter bufferedWriter = null;
        createdFile = chooser.getSelectedFile();
        try {           

            if (!createdFile.exists()) {
                createdFile.createNewFile();
            }
        }catch(Exception e) {
            e.printStackTrace();
        }
    }
    try {
        sc = new Scanner(file);
        boolean hasMore = true;
        while (hasMore) {
            hasMore = false;
            while (sc.hasNextLine() && set.size() < PERIOD) {
                set.add(sc.nextLine());
                repeated++;
            }
            createdFile = this.writeToFile(set,createdFile);
            set.clear();
            hasMore = true;
            if (sc.hasNextLine() == false)
                hasMore = false;
            set.clear();
        }
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return createdFile;

}
private File writeToFile(HashSet<String> set, File f) {
        BufferedWriter bufferedWriter = null;
        try {           
            Writer writer = new FileWriter(f, true);
            bufferedWriter = new BufferedWriter(writer);
            for (String str : set) {
                bufferedWriter.write(str);
                bufferedWriter.newLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (bufferedWriter != null)
                try {
                    bufferedWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }


    return f;
}

repeated 是计算迭代次数的变量。 是来自代码还是来自 RAM 消耗?有没有办法让它工作?

【问题讨论】:

  • 但是当涉及到大文件(180mb 500 万行)时,它不能很好地工作是什么意思?具体是什么问题?
  • 例如……什么?
  • Assuming the fact that it is not possible to store 5 million strings in a HashSet or any other collection 等等,这个假设从何而来? It seems to be wrong。与实际设置实现相比,您更可能受到内存大小(和 JVM 内存限制)的限制。即使这样,也可能会有处理更多元素的实现......只要你能节省内存。
  • 话虽如此,大幅减少内存消耗可能相对简单 - 从 file1 读取行,检查它是否在 file2 中,如果不是 -> 将其写入 file2。不过,这会做更多的磁盘读取,但如果你不能节省内存,它会避免占用内存。
  • 180mb 真的不算多,即使是在谈论虚拟内存时也是如此。我建议您调试脚本与完整文件一起挂起的原因(您可能需要先检查 GC 日志以查看它是否在全时运行,在这种情况下增加 JVM 的 XMX)。您总是可以通过存储行的哈希而不是它们的全部内容来牺牲 CPU 来赢得内存,但我怀疑您是否会想要牺牲读取每个记录的整个文件所需的时间,正如@vlaz 曾经建议的那样。

标签: java


【解决方案1】:

去重

让我们假设您只是想删除该文件的重复数据。我想说最快、最简单的方法是好的旧的 unix utils:

cat myfile.txt | sort -u > sorted.txt

改进您的解决方案

(TL;DR 增加 JVM 堆大小,初始化 HashSet 大小并使用此答案中的最后一个解决方案!)

如果您需要在 Java 中执行此操作,让我们首先尝试提高效率。就像许多人提到的那样,180MB 并不是那么多。只需加载整个文件,无需对其进行分块(此外,您不会消除所有重复项)。以这一行为例:

HashSet<String> set = new HashSet<String>();

这将创建一个初始容量为 n(我认为是 16 个元素?)和 0.75 的负载因子的 HashSet,这意味着当您添加行时,它必须重新分配内存并复制所有内容。 Here is something useful to read, especially "Performance"

所以让我们增加该大小以避免分配:

Set<String> set = new HashSet<String>(5000000);

我将负载因子保持原样,但这意味着它会在 75% 满后重新分配。如果您确定知道文件的大小,则可以调整这些设置。

好吧,我必须以艰苦的方式学习它 - 总是先衡量!这是绩效工作的第一条规则。 我编写了所有这些,然后在我的快速工作站(具有 16GB RAM 和快速多核 CPU)上测试了我自己的实现,并在我的编辑中总结了所有这些。现在我很想尝试你的解决方案(我应该马上做)。 所以我在家里的笔记本上重新运行了它(8GB RAM,4 年以上的 CPU)。

好的,下面是简化代码:

import java.io.*;
import java.util.*;

public class SortTest {

    public static void main(String[] args) throws IOException {
        if (args.length != 1) {
            System.err.println("Pass filename as argument!");
            System.exit(1);
        }

        Set<String> set = new HashSet<String>();
        File createdFile = new File("./outfile");
        createdFile.createNewFile();

        try (BufferedReader br = new BufferedReader(new FileReader(new File(args[0])))) {
            for (String line = br.readLine(); line != null; line = br.readLine()) {
                set.add(line);
            }
        } catch (IOException ex) {
            throw new RuntimeException("Fatal Error.",  ex);
        }

        try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(createdFile, true))) {
            for (String line : set) {
                bufferedWriter.write(line);
                bufferedWriter.newLine();
            }
        }
    }
}

更改:我删除了分块,一次加载整个文件。我正在使用 BufferedReader,bc。 Scanner 对于解析(读取整数等)更有用,并且可能会产生开销。我还将文件的写入添加到最后,我不需要每次都重新创建 BufferedWriter。另请注意, File.createNewFile() 只会在文件不存在时创建文件并返回是否存在,因此您的检查是多余的。 (请注意,为简洁起见,我省略了适当的错误处理)

我使用了来自https://datasets.imdbws.com/ 的name.basics,这是一个509MB 的文件(解压缩),包含8.837.960 行。它们实际上是独一无二的,所以最终的结果是一样的。

它实际上消耗了大量资源,我的系统变得相当慢。起初,我什至遇到了 OutOfMemory 错误!但是用更多的堆空间运行它:time java -Xmx4g SortTest ./name.basics.tsv 给我:

真正的 0m44.289s

用户 1m23.128s

系统 0m2.856s

所以大约 44 秒,还不错。现在让我们避免分配和设置:

Set<String> set = new HashSet<String>(9000000, 0.9f);

结果:

真正的 0m38.443s

用户 1m12.140s

系统 0m2.376s

嗯,这样看起来更好。不过我不得不说,我多次重新运行这些测试,时间可能会变化最多 5 秒,所以实际上,结果非常接近。

只是为了好玩,我还将展示我自己的小实现,它使用更现代和更简洁的 Java(同样,没有适当的错误处理):

import java.nio.file.*;
import java.util.*;

public class SortTest2 {

    public static void main(String[] args) throws Exception {
        Set<String> uniq = new HashSet<>(100000, 0.9f);
        try (Stream<String> stream = Files.lines(Paths.get(args[0]))) {
            stream.forEach(uniq::add);
        }

        Files.write(Paths.get("./outfile2"), (Iterable<String>) uniq::iterator);
    }
}

结果:

真正的 0m38.321s

用户 1m16.452s

系统 0m2.828s

代码更少,但结果几乎相同。 注意:如果您将 HashSet 替换为 LinkedHashSet,它将保留行的顺序!这是一个很好的例子,为什么您应该使用最通用的类​​型声明变量和参数。如果您使用Set&lt;String&gt; uniq,则只需更改该行即可更改实现(HashSet 与 LinkedHashSet)。

其实我是想用分析器看一下,但是运行时间太短了,我什至在程序终止之前都没有得到结果。

如果文件适合您的 RAM,并且您使用了适当的最大堆参数 (-Xmx),则应该没有问题。

顺便说一句:我重新测试了cat | sort -u 版本 - 花了 55 秒!

注意:经过更多测试后大量编辑的帖子

编辑

按照用户 DodgyCodeException 的建议,在第二个版本中删除了多余的 .stream() 调用。

好的,这是最好的解决方案™ - 我想说这是一个协作的努力,感谢用户 Hulk 和 vlaz。

import java.nio.file.*;
import java.util.stream.*;

public class SortTest3 {

    public static void main(String[] args) throws Exception {
        try (Stream<String> stream = Files.lines(Paths.get(args[0]))) {
            Files.write(Paths.get("./outfile3"), (Iterable<String>) stream.distinct()::iterator);
        }
    }
}

这个解决方案不仅非常简洁(可能过于简洁),与另一个解决方案一样快,而且最重要的是它保持了秩序。感谢.distinct()

替代解决方案

我认为上述解决方案应该足以满足大多数用例并且相当简单。但是假设您需要处理一个不适合 RAM 的文件,或者您需要保留行顺序。我们可以采用这个解决方案背后的想法并对其进行一些修改。

您逐行读取文件,因此您将始终在内存中保留一行 - 假设平均长度 m。 然后您需要一些标识符来存储和稍后比较,最好使用恒定大小 kk 。所以你需要一个散列函数,但不是一个有很多冲突的快速函数,而是一个更耐冲突的加密散列函数(例如,SHA1、2 或 3)。但请注意:抗碰撞能力越强,哈希越大,您需要投入的计算工作也越大。

  1. 读行
  2. 计算哈希
  3. 在链表中查找值:
    • 如果您发现更大的,请在前面插入
    • 如果找到一个相等的,丢弃行
  4. 如果未丢弃,则将行写入输出文件

您需要一个链表来降低插入成本(并且该列表必须增长)。列表将按插入策略保持排序,输出文件将通过立即写出行来保持顺序。

这将占用大约n * k + m 的空间,但计算散列函数的计算量会很大。

请注意,这不处理冲突。如果你使用一个好的散列函数,你可以假装它们不会发生(因为它们不太可能发生)。如果它很关键,您可能需要添加另一种机制来确认唯一性,例如,将行号存储在哈希旁边并获取先前看到的行进行比较。然后,您需要找到一个方案来存储具有冲突哈希的行。

【讨论】:

  • 不错的答案!只是观察:log2(5M) ~ 22.2,所以没有那么多的放大。
  • ...还有什么例外?
  • stream.forEach(uniq::add); 实际上,我想知道如果您要执行 Files.lines(path).distinct() 并直接写入结果会发生什么。我想在引擎盖下它可能会做类似的事情,但我很想看看性能将如何与这个解决方案进行比较。稍后我可能会自己测试它,因为我现在无法测试。
  • @BenjaminMaurer 如果您读取一行,您实际上可以保留顺序,将其添加到 Set 并尝试立即写入(或将其添加到某种缓冲区以保存磁盘访问)。如果遇到重复的行,请跳过它。这样一来,您可以删除重复项,并且仍然具有相同的顺序。
  • 好的,所以我尝试了您的 SortTest2 解决方案和 using .distinct() - 结果在我的系统上大致相同 - 在每种情况下大约 16 秒。
猜你喜欢
  • 2012-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-04
  • 1970-01-01
  • 2016-07-13
  • 1970-01-01
  • 2014-12-19
相关资源
最近更新 更多