在Java中对一个巨大的file.txt文件进行行排序

8

我正在处理一个非常大的文本文件(755Mb)。 我需要对这些行进行排序(大约 1890000 行),然后将它们写回另一个文件。

我已经注意到了一个与我的起始文件非常相似的讨论: 按单词作为关键字排序行

问题在于,我无法将这些行存储在内存中的集合中,因为我会收到 Java 堆空间异常(即使我已经将其扩展到最大值)。已经尝试过!

我也不能使用 excel 打开它并使用排序功能,因为该文件太大,无法完全加载。

我考虑使用数据库,但我认为先写入所有行,然后使用 SELECT 查询太耗时。我错了吗?

任何提示都将不胜感激 提前致谢


“太长”取决于你的期望。如果你希望在半秒钟内完成它,那么确实会太长。如果你不介意等待几秒钟或几分钟,那就不应该是问题。试一下,看看时间是否合理。 - JB Nizet
你应该能够使用最新版本的Java,在大约1GB的堆内存中存储文件,即使用-XX:+UseCompressedStrings - Peter Lawrey
6个回答

17

我认为解决方案是使用临时文件进行合并排序:

  1. 读取第一个文件的前 n 行(n 是您可以负担得起在内存中存储和排序的行数),将其排序并写入文件 1.tmp(或称其它名称)。对于下一个 n 行,也同样的操作并将其存储在 2.tmp 中。重复此过程,直至处理完原始文件的所有行。

  2. 读取每个临时文件的第一行,确定最小值(根据排序顺序),将其写入目标文件,并从相应的临时文件中读取下一行。重复此步骤,直到处理完所有行。

  3. 删除所有临时文件。

只要您有足够的磁盘空间,这种方法适用于任意大小的文件。


我完全同意。可以使用“归并排序”算法完成。 - Jaco Van Niekerk

2
您可以使用以下方式运行:
-mx1g -XX:+UseCompressedStrings  # on Java 6 update 29
-mx1800m -XX:-UseCompressedStrings  # on Java 6 update 29
-mx2g  # on Java 7 update 2.

import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {
    public static void main(String... args) throws IOException {
        long start = System.nanoTime();
        generateFile("lines.txt", 755 * 1024 * 1024, 189000);

        List<String> lines = loadLines("lines.txt");

        System.out.println("Sorting file");
        Collections.sort(lines);
        System.out.println("... Sorted file");
        // save lines.
        long time = System.nanoTime() - start;
        System.out.printf("Took %.3f second to read, sort and write to a file%n", time / 1e9);
    }

    private static void generateFile(String fileName, int size, int lines) throws FileNotFoundException {
        System.out.println("Creating file to load");
        int lineSize = size / lines;
        StringBuilder sb = new StringBuilder();
        while (sb.length() < lineSize) sb.append('-');
        String padding = sb.toString();

        PrintWriter pw = new PrintWriter(fileName);
        for (int i = 0; i < lines; i++) {
            String text = (i + padding).substring(0, lineSize);
            pw.println(text);
        }
        pw.close();
        System.out.println("... Created file to load");
    }

    private static List<String> loadLines(String fileName) throws IOException {
        System.out.println("Reading file");
        BufferedReader br = new BufferedReader(new FileReader(fileName));
        List<String> ret = new ArrayList<String>();
        String line;
        while ((line = br.readLine()) != null)
            ret.add(line);
        System.out.println("... Read file.");
        return ret;
    }
}

打印
Creating file to load
... Created file to load
Reading file
... Read file.
Sorting file
... Sorted file
Took 4.886 second to read, sort and write to a file

你能否使用jdk7u2重复测试,以查看它需要多少内存和时间? - dogbane
很遗憾,Java 7不支持这个选项。https://dev59.com/QWoy5IYBdhLWcg3wA5aC - Peter Lawrey
是的,但我仍然想看看在没有该选项的情况下它使用了多少内存。也许他们已经进行了改进,以至于不再需要这个选项了。 - dogbane
@dogbane 一个合理的问题,Java 7相比关闭压缩字符串的Java 6需要多200MB的空间。:] - Peter Lawrey

1

1

算法:

我们有多少可用内存?假设我们有 X MB 的可用内存。

  1. 将文件分成 K 个块,其中 X * K = 2 GB。将每个块带入内存并使用任何 O(n log n) 算法按通常方式对行进行排序。将行保存回文件。

  2. 现在将下一个块带入内存并进行排序。

  3. 完成后,逐个合并它们。

上述算法也称为外部排序。第3步称为N路归并


0
为什么不尝试使用多线程和增加程序的堆大小呢?(这还需要您使用合并排序之类的东西,前提是您的系统内存大于755MB。)

请参见上面留给Eric.Sun的评论。 - Jaco Van Niekerk
是的,你提到的理由在非常非常大的文件大小上显然很有用。但是OP指定的文件大小为755MB,今天的大部分计算机都有超过755MB的存储容量。如果我们可以通过-Xmx1024m来解决他/她的问题,为什么要使用一个复杂的算法呢? - javaCity
1
归并排序并不是一个过于复杂的算法。我不想对算法使用的硬件做出任何假设。此外,该进程可能不是设备上唯一运行的软件。在我看来,编写50行代码以节省超过1GB的内存(如果是字符串,则每行可能占用几个字节)是值得努力的。(无意冒犯) - Jaco Van Niekerk
1
不,我完全同意你的观点。如果我遇到类似的情况,我会首先尝试增加堆大小。如果这不起作用,我可能会采取你建议的方法。一切都好 :) - javaCity
好的。让我们在两种解决方案上做出妥协,即尝试使用内存方法,并记录一个工单,在以后的阶段用更节省内存的方法来替换它(附带测试用例)(+1)。 - Jaco Van Niekerk

-2

也许您可以使用 Perl 格式化文件,并像 MySQL 一样加载到数据库中,这样速度会很快。然后使用索引查询数据,并将其写入另一个文件。

您可以设置 JVM 堆大小,例如 '-Xms256m -Xmx1024m'。希望能对您有所帮助。谢谢。


使用基于文件的归并排序比仅仅分配更多内存要好得多。如果文件变得更大,例如10G,会发生什么? - Jaco Van Niekerk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接