【问题标题】:How to compare large text files?如何比较大文本文件?
【发布时间】:2011-08-18 12:36:02
【问题描述】:

关于您对我的“技术”的看法,我有一个一般性问题。

有 2 个文本文件(file_1file_2)需要相互比较。两者都非常庞大(3-4 GB,每个从 30,000,000 到 45,000,000 行)。 我的想法是将file_1 的几行(尽可能多)读入内存,然后将它们与file_2all 行进行比较。如果匹配,则两个文件中匹配的行将写入新文件。然后继续file_1 的下 1000 行,并将它们与file_2所有行进行比较,直到我完全通过 file_1

但这对我来说听起来真的非常非常耗时且复杂。 你能想出其他方法来比较这两个文件吗?

您认为比较需要多长时间? 对于我的程序,时间并不重要。我没有处理如此大文件的经验,因此我不知道这可能需要多长时间。不过应该不会超过一天。 ;-) 但我担心我的技术可能需要永远......

我刚想到的另一个问题:你会读多少行到内存中?越多越好?有没有办法在实际尝试之前确定可能的行数? 我想尽可能多地阅读(因为我认为这样会更快)但我经常会耗尽内存。

提前致谢。

编辑 我想我必须多解释一下我的问题。

目的不是看这两个文件是否相同(它们不是)。 每个文件中有一些行具有相同的“特征”。 这是一个例子: file_1 看起来有点像这样:

mat1 1000 2000 TEXT      //this means the range is from 1000 - 2000
mat1 2040 2050 TEXT
mat3 10000 10010 TEXT
mat2 20 500 TEXT

file_2看起来像这样:

mat3 10009 TEXT
mat3 200 TEXT
mat1 999 TEXT

TEXT 指的是我不感兴趣的字符和数字,mat 可以从mat1 - mat50 去并且没有顺序;也可以有 1000x mat2 (但下一列中的数字不同)。我需要以某种方式找到拟合线:matX 在两条比较线中都相同,file_2 中提到的数字适合file_1 中提到的范围。 因此,在我的示例中,我会找到一个匹配项:file_1 的第 3 行和file_2 的第 1 行(因为两者都是 mat3,而 10009 介于 10000 和 10010 之间)。 我希望这能让你明白!

所以我的问题是:您将如何搜索匹配的行?

是的,我使用 Java 作为我的编程语言。

编辑 我现在先分割大文件,这样我就不会遇到内存不足的问题。我还认为将(许多)较小的文件相互比较比这两个大文件更快。之后,我可以按照我上面提到的方式比较它们。这可能不是完美的方法,但我仍在学习 ;-) 尽管如此,您的所有方法都对我很有帮助,谢谢您的回复!

【问题讨论】:

  • 你用java标记了这个问题,这是否意味着你只想用Java来做?
  • 不知道能不能帮到你stackoverflow.com/questions/964332/…
  • 听起来像是内存映射的好用例(并首先对文件进行碎片整理),但我不知道 Java 是否提供。
  • 不确定我是否理解您的要求。您是否需要找到两个文件之间共有的行?或者你真的想要做一个差异?
  • 在这种情况下,您预处理 file_2 以便您拥有 50 个数据结构 (mat1..mat50),每个数据结构都有一个按下限排序的范围数组,因此您可以对其进行二进制搜索。 40.000.000 行占用的空间不应超过 1GB。然后依次遍历file_1,查找每一行。

标签: java file comparison


【解决方案1】:

我认为,你的方式是相当合理的。

我可以想象不同的策略——例如,您可以在比较之前对两个文件进行排序(文件排序的有效实现在哪里,而 unix 排序实用程序可以在几分钟内对几个 Gbs 文件进行排序),并且在排序时,您可以比较文件随后,逐行阅读。

但这是一种相当复杂的方法——你需要运行外部程序(排序),或者自己在 java 中编写类似的高效文件排序实现——这本身并不是一件容易的事。因此,为了简单起见,我认为您的分块读取方式非常有前途;

至于如何找到合理的block——首先,“越多——越好”的说法可能不正确——我认为,所有工作的时间都会渐进地增长,到某个恒定的线。因此,您可能会比您想象的更快地接近那条线——您需要为此进行基准测试。

下一步——你可以像这样读取要缓冲的行:

final List<String> lines = new ArrayList<>();
try{
    final List<String> block = new ArrayList<>(BLOCK_SIZE);
    for(int i=0;i<BLOCK_SIZE;i++){
       final String line = ...;//read line from file
       block.add(line);
    }
    lines.addAll(block); 
}catch(OutOfMemory ooe){
    //break
}

所以你读了尽可能多的行——留下最后一个 BLOCK_SIZE 的空闲内存。 BLOCK_SIZE 对于其他程序来说应该很大,以便在没有 OOM 的情况下运行

【讨论】:

  • 同意,在几兆字节之后,您可能不会通过读取更多数据获得太多收益(例如,考虑磁盘缓存的大小)。您需要确保将一些 CPU 密集型工作与磁盘密集型工作交错,以让磁盘赶上并缓冲更多数据。
【解决方案2】:

在理想情况下,您可以将 file_2 的每一行读入内存(可能使用像 HashSet 这样的快速查找对象,具体取决于您的需要),然后从 file_1 中读取每一行时间并将其与包含 file_2 行的数据结构进行比较。

正如您所说,您的内存不足,但我认为分而治之的策略是最好的。您可以使用与我上面提到的相同的方法,但是从 file_2 中读取一半(或三分之一,四分之一......取决于您可以使用多少内存)并存储它们,然后比较所有行在文件_1 中。然后将下半/第三/季度/任何内容读入内存(替换旧行)并再次通过 file_1。这意味着您必须再通过 file_1,但您必须处理内存限制。


编辑:针对您问题中添加的详细信息,我将部分更改我的答案。而不是读取所有 file_2(或块)并一次读取 file_1 一行,相反,因为 file_1 保存要检查的数据。

另外,关于搜索匹配行。我认为最好的方法是对 file_1 进行一些处理。创建一个HashMap&lt;List&lt;Range&gt;&gt;,将字符串(“mat1” - “mat50”)映射到Ranges 列表(只是 startOfRange int 和 endOfRange int 的包装器)并用来自的数据填充它文件_1。然后写一个函数(忽略错误检查)

boolean isInRange(String material, int value)
{
    List<Range> ranges = hashMapName.get(material);
    for (Range range : ranges)
    {
        if (value >= range.getStart() && value <= range.getEnd())
        {
            return true;
        }
    }
    return false;
}

并为 file_2 的每一行(已解析)调用它。

【讨论】:

    【解决方案3】:

    既然您已经向我们提供了更多细节,我将采用的方法依赖于预分区,并且可以选择在搜索匹配之前进行排序。

    这应该会消除大量比较,这些比较在天真、蛮力的方法中无论如何都不会匹配。为了便于讨论,让我们将这两个文件分别固定为 4000 万行。

    分区: 通读file_1 并将所有以mat1 开头的行发送到file_1_mat1,依此类推。对file_2 执行相同操作。这对于一点点grep 来说是微不足道的,或者如果您希望在 Java 中以编程方式完成它,这是一个初学者的练习。

    这是一次遍历两个文件,总共读取 8000 万行,产生两组 50 个文件,每个文件平均 800,000 行。

    排序:对于每个分区,仅根据第二列中的数值排序(下限来自file_1,实际数字来自file_2)。即使 800,000 行无法放入内存,我想我们也可以采用 2 路外部合并排序并比一种 整个 未分区空间更快地执行此操作(更少的整体读取)。

    比较:现在您只需通过file_1_mat1file_2_mat1 两对迭代一次,无需在内存中保留任何内容,将匹配项输出到您的输出文件。依次重复其余分区。不需要最后的“合并”步骤(除非您正在并行处理分区)。

    即使没有排序阶段,您已经在进行的简单比较也应该在 50 对文件(每对 800,000 行)而不是两个文件(每对 4000 万行)中运行得更快。

    【讨论】:

    • 谢谢您,我昨天没有阅读您的评论,但尝试了您的解释,因为我认为它可以正常工作。只是一个小改动:我首先开始对大文件进行排序,然后将它们拆分,现在将继续进行比较。这比处理大文件要容易得多,而且根本不需要那么多时间。
    【解决方案4】:

    有一个折衷:如果您读取文件的一大块,则保存光盘seek time,但您可能已经读取了您不需要的信息,因为在第一行就遇到了更改。

    您可能应该使用不同的块大小运行一些实验 [基准测试],以找出在平均情况下要读取的最佳块。

    【讨论】:

      【解决方案5】:

      不确定这个答案会有多好 - 但请查看此页面:http://c2.com/cgi/wiki?DiffAlgorithm - 它总结了一些差异算法。 Hunt-McIlroy 算法可能是更好的实现。从该页面还有一个指向 GNU diff 的 java 实现的链接。但是,我认为 C/C++ 中的实现并编译为本机代码会快得多。如果你被 java 卡住了,你可能需要考虑 JNI。

      【讨论】:

      • 在我的 4GB 电脑上,350.000 行文件的差异已经失败。如果内存需求呈线性增长,猜猜你需要多少内存!
      【解决方案6】:

      确实,这可能需要一段时间。您必须进行 1,200.000,000 行比较。 有几种可能性可以将其加速一个数量级:

      一种方法是对 file2 进行排序并在文件级别进行某种二进制搜索。 另一种方法:计算每一行的校验和,然后搜索。根据平均行长度,有问题的文件会小得多,如果您以固定格式(即长)存储校验和,您真的可以进行二进制搜索

      但是,您一次从 file_1 读取的行数并不很重要。这是面对巨大复杂性的微观优化。

      【讨论】:

        【解决方案7】:

        如果您想要一个简单的方法:您可以对两个文件进行哈希处理并比较哈希值。但是使用您的方法可能更快(尤其是在文件不同的情况下)。关于内存消耗:只要确保你使用足够的内存,这种事情不使用缓冲区是个坏主意..

        以及所有关于哈希、校验和等的答案:这些答案并不快。在这两种情况下,您都必须阅读整个文件。使用哈希/校验和,您甚至必须计算一些东西......

        【讨论】:

          【解决方案8】:

          您可以对每个单独的文件进行排序。例如UNIX sort 或 Java 中的类似名称。您可以一次读取一行已排序的文件以执行合并排序。

          【讨论】:

          【解决方案9】:

          我从来没有处理过这么大的文件,但这是我的想法,应该可行。

          您可以查看哈希。使用 SHA-1 哈希。

          导入以下内容

          import java.io.FileInputStream;
          import java.security.MessageDigest;
          

          一旦你的文本文件等被加载,让它遍历每一行,最后打印出散列。下面的示例链接将更加深入。

          StringBuffer myBuffer = new StringBuffer("");
          //For each line loop through
              for (int i = 0; i < mdbytes.length; i++) {
                  myBuffer.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1));
              }
          System.out.println("Computed Hash = " + sb.toString());
          

          SHA Code example focusing on Text File

          SO Question about computing SHA in JAVA (Possibly helpful)

          Another sample of hashing code.

          单独读取每个文件,如果每个文件的哈希值在进程结束时相同,则两个文件相同。如果不是,那就有问题了。

          然后,如果您得到不同的值,您可以逐行检查超级耗时。

          总体而言,似乎逐行阅读等会花费很长时间。如果您试图找到每个个体差异,我会这样做。但我认为散列会更快地查看它们是否相同。

          SHA checksum

          【讨论】:

            【解决方案10】:

            如果您想确切地知道文件是否不同,那么没有比您更好的解决方案了——按顺序比较。

            但是,您可以进行一些启发式方法,以某种概率告诉您文件是否相同。 1) 检查文件大小;这是最简单的。 2) 取一个随机文件位置并比较两个文件中从该位置开始的字节块。 3) 重复步骤 2) 以达到所需的概率。

            您应该计算和测试多少次读取(以及块大小)对您的程序有用。

            【讨论】:

              【解决方案11】:

              我的解决方案是先生成一个文件的索引,然后使用它进行比较。这类似于其他一些答案,因为它使用散列。

              您提到行数高达约 4500 万行。这意味着您可以(可能)存储每个条目使用 16 个字节(128 位)的索引,它将使用大约 45,000,000*16 = ~685MB 的 RAM,这在现代系统上并非不合理。使用我在下面描述的解决方案存在开销,因此您可能仍会发现您需要使用其他技术,例如内存映射文件或基于磁盘的表来创建索引。有关如何将索引存储在基于磁盘的快速哈希表中的示例,请参阅 HypertableHBase

              所以,完整的算法应该是这样的:

              1. 创建一个将 Long 映射到 Long 列表的哈希映射 (HashMap>)
              2. 获取第一个文件中每一行的hash(Object.hashCode应该足够了)
              3. 获取该行文件中的偏移量,以便以后再次查找
              4. 将偏移量添加到哈希图中匹配 hashCodes 的行列表中
              5. 将第二个文件的每一行与索引中的行偏移集进行比较
              6. 保留所有匹配条目的行

              编辑: 针对您编辑的问题,这本身并没有真正的帮助。您可以只散列该行的第一部分,但它只会创建 50 个不同的条目。然后,您可以在数据结构中创建另一个级别,它将每个范围的开始映射到它来自的行的偏移​​量。

              所以像index.get("mat32") 这样的东西会返回一个范围的TreeMap。您可以查找要查找的值之前的范围lowerEntry()。结合起来,您可以非常快速地检查给定的 matX/数字组合是否在您正在检查的范围之一内。

              【讨论】:

                【解决方案12】:

                尽量避免消耗内存并使其消耗磁盘。 我的意思是将每个文件分成可加载大小的部分并进行比较,这可能需要一些额外的时间,但可以让您安全地处理内存限制。

                【讨论】:

                  【解决方案13】:

                  使用像Mercurial 这样的源代码控制怎么样?我不知道,也许这不是您想要的,但这是一个旨在跟踪修订之间更改的工具。您可以创建一个存储库,提交第一个文件,然后用另一个文件覆盖它并提交第二个文件:

                  hg init some_repo
                  cd some_repo
                  cp ~/huge_file1.txt .
                  hg ci -Am "Committing first huge file."
                  cp ~/huge_file2.txt huge_file1.txt
                  hg ci -m "Committing second huge file."
                  

                  从这里你可以得到一个差异,告诉你哪些行不同。如果您能以某种方式使用该差异来确定哪些行是相同的,那么您将万事俱备。

                  这只是一个想法,如果我错了,请纠正我。

                  【讨论】:

                  • 你不需要源代码管理来获得差异,你可以使用 Unix 命令'diff '。
                  【解决方案14】:

                  我会尝试以下操作:对于您要比较的每个文件,在磁盘上创建代​​表每个字母的临时文件(我稍后将其称为部分文件),并为所有其他字符创建一个附加文件。然后逐行读取整个文件。这样做时,将该行插入与其开头的字母相对应的相关文件中。由于您已经对这两个文件都这样做了,因此您现在可以限制一次加载两个较小文件的比较。例如,以 A 开头的行只能出现在一个部分文件中,并且不需要多次比较每个部分文件。如果生成的文件仍然很大,您可以通过根据其中的第二个字母创建文件来对正在比较的部分文件(特定于字母的文件)应用相同的方法。这里的权衡是暂时使用大磁盘空间,直到该过程完成。在这个过程中,这里其他帖子中提到的方法可以帮助更有效地处理部分文件。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2022-12-22
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-06-23
                    • 2023-04-07
                    • 1970-01-01
                    相关资源
                    最近更新 更多