【问题标题】:Solutions for virtually splitting a text file虚拟分割文本文件的解决方案
【发布时间】:2013-02-06 13:24:34
【问题描述】:

我需要阅读和处理一个巨大的文本文件。为了提高数据处理时间,我想到了让多个阅读器同时阅读。这个想法是通过记下开始和结束指针来虚拟分割文件。这是由程序开始时的主线程完成的。实际上我的意思是,不创建物理拆分文件。

稍后当并发读取器进行读取和处理时,每个线程都可以调用 bufferedReader.skip(long) 并跟踪读取的字符数,以免它们越过结束指针边界。

问题是单个线程完成的文件读取是使用 BufferedReader 完成的,因此要跳过我需要知道字符数,而主线程无法确定这一点。要计算开始和结束指针,主线程唯一的数据是以字节为单位的文件长度。

如何根据字符确定开始和结束指针,以便读者可以跳过那么多字符?

注意——

  1. 输入文本文件可能采用不同的字符编码,例如ASCII、EBCDIC、UTF-8、UTF-16 等。
  2. 逐行读取输入文件以确定开始和结束指针不是一种选择,因为它违背了分割文本文件的目的。

更新

注意我只能使用 java 文件 API 而不是像 Hadoop 这样的框架。这是应用架构限制

更新

这里是通过跳过计算的字节数读取输入文件的代码,然后逐字节读取输入文件以确定记录分隔符。如果您发现代码有问题,请回复您的想法(特别是考虑到输入文件可能采用不同的字符编码这一事实)。

        {
        CountingInputStream countingInputStream = new CountingInputStream(new FileInputStream(inputFilePath.toFile()));
        long endPointer;
        while(true) {
            long actualSkipped = countingInputStream.skip(skipCount);
            if(actualSkipped == 0) {
                logger.info("Nothing to skip");
                break; //nothing to skip now.
            }

            byte[] inputBytes = new byte[recordDelimiterBytes.length];
            int noOfBytesRead = countingInputStream.read(inputBytes);
            if(noOfBytesRead == -1) {
                //end of file already reached!
                endPointer = countingInputStream.getCount();                    
                break;
            }
            while (!(Arrays.equals(recordDelimiterBytes, inputBytes))) {
                shiftLeft(inputBytes);
                int readByte = countingInputStream.read();

                if(readByte != -1) {
                    inputBytes[inputBytes.length - 1] = (byte) readByte;
                } else {
                    throw new IllegalStateException("EOF reached before getting the delimiter");
                }

            }
            endPointer = countingInputStream.getCount();
    }

    private void shiftLeft(byte[] inputBytes) {
        for(int i=0; i<inputBytes.length - 1; i++) {
            inputBytes[i] = inputBytes[i+1];
        }
    }

【问题讨论】:

  • 巨大有多大?文本文件中有什么内容?
  • 另外,处理文件涉及什么?
  • MapReduce 听起来像是您可能需要的,因为无法让多个阅读器同时读取同一个文件。您可能希望研究 Hadoop:hadoop.apache.org
  • 出于物理文件系统等原因,将 BufferedReader 替换为 java nio 的内存映射字节缓冲区。进行异步读取/处理。
  • @jlordo - 巨大的以 GB 为单位 - 超过 10 Gb,最高可达 100 GB

标签: java file-io inputstream filereader


【解决方案1】:

您的问题中有几点需要回答:

为了提高数据处理时间,我想到了让多个阅读器同时阅读。

如果您的处理受 I/O 限制,那么尝试读取具有多个流的单个文件不太可能加快速度。这可能会使事情变得更糟。但是,很难给出明确的答案,因为这取决于操作系统如何处理预读、内存中文件系统缓冲、RAID 和其他因素。

另一方面,如果处理受 CPU 限制,适合并行化,并且您有多个可用内核,则多个流可能有效。

如何根据字符确定开始和结束指针,以便读者可以跳过那么多字符?

您计算出大概的分区大小和大概的边界。然后你需要做一些工作来找到确切的边界。

  • 如果您想在一行或单词的开头开始每个段。选择一个点,一次读取一个字节,直到到达相关边界。

  • 如果您想从下一个有效字符的开头开始:

    • 对于 8 位编码(如 ASCII、Latin-1 等)来说,这个问题是微不足道的。

    • 使用 UTF-8,您可以跳到最高位为 00、01 或 11 的下一个字节,这就是代码点的开始。请参阅Wikipedia page on UTF-8 上的表格。

    • 使用 UTF-16,您必须读取字节对。如果您不知道顺序(big-endian 或 little-endian),您可以检查前 2 个字节以查看它们是否为 BOM。确定后,不在 DC00-DFFF 范围内的字节对是代码点的开始。参考Wikipedia page on UTF-16

很明显,一旦你知道一个分区的开始,你就会知道前一个分区的结束。

如您所见,您需要知道文件的字符编码是什么。但如果您知道这一点,您就可以快速可靠地找到合适的位置来设置分区边界。


唯一的问题是当数据中有文本限定符时,即配置的记录分隔符也可能是数据的一部分。

这可能很困难:

  • 如果分隔符只在开头或开头设置一次,那么您只需从头开始阅读,直到弄清楚分隔符是什么。然后进行分区。

  • 如果可以在文件中的任何位置更改分隔符,那么使用单线程读取可能是唯一的选择。 (也许您可以在将输入分解为分隔的记录或行或其他内容后并行处理。)

  • 最后一个选项是让线程对假设一个分隔符进行分区和处理,但还要寻找嵌入的“更改分隔符”指令。如果他们确实检测到实际更改,请告诉稍后分区的线程重新启动。有点复杂……

【讨论】:

  • 感谢您的回复。对多个阅读器的第一点的答案是,对数据进行的处理受 CPU 限制,因此并发执行将是有效的。关于确定相关边界的第二点,是的,您的逻辑应该有效。唯一的问题是当数据中有文本限定符时,即配置的记录分隔符也可能是数据的一部分。所以第二个线程将无法确定它应该开始读取下一条记录的起始指针。
  • 为了实现您的解决方案,我使用了 CountingInputStream (番石榴 - 所以我可以确定边界的结束指针)和 InputStreamReader (以便我可以读取字符),但 InputStreamReader 内部使用了具有默认缓冲区大小的 StreamDecoder 8kb。由于它使用缓冲环,因此在根据拆分文件大小跳过字节后,我无法确定确切的记录边界。有什么建议吗?
  • 请查看我在描述中添加的解决方案,如果您有任何意见,请告诉我。关于您对分隔符的建议,我认为它不起作用(或者可能是我没有完全理解它)。基本上,如果我们跳过计算出的字节数,我们最终可能会出现在文本限定符已经开始的记录之间。另请注意,很少有记录甚至不能将记录分隔符作为数据的一部分(即没有文本限定符),因此预测它变得更加困难。当输入中没有文本限定符时,上述解决方案有效。
【解决方案2】:

你的提议是不可能的。磁盘上的所有 I/O 操作本质上都是串行的。想想普通硬盘的样子。该文件存储在一个带有一个读头的盘片上。您不会从 java 创建更多标题 - 因此即使您创建多个阅读器,它们最终也会互相等待完成阅读。

此外,所有读取都从文件开始处开始。您不能在中间开始读取文件。如果你想向前阅读,你可以使用skip()方法,但是那个方法读取那么多字符而不对数据做任何事情。

编辑:您可以将读取线程与处理线程分开。创建一个读取线程从头到尾读取文件。每次完成读取文件的适当部分时,它都会启动一个新线程来处理读取的数据。同时,读取线程将读取一个新的文件块,启动一个线程到该块,等等......当读取线程到达文件末尾时,它终止,已经启动了几个现在正在同时处理各自部分的新线程文件。

【讨论】:

  • OP 说也涉及到处理
  • @idursun 我只是说同时读取文件不会提供任何加速。
  • @JakubZaverka - 我了解硬件限制,但由于读取文件数据的线程也会处理它,这需要时间。处理数据涉及验证、过滤、转换和转换数据,这是一项耗时的操作,并且可以并行化。另外我知道调用 skip() 会读取字符,但并发读取证明比单线程读取更快,因为数据处理是一个缓慢的操作。
  • @JakubZaverka - 我试过了,处理已经由一个单独的线程完成。通过同时读取多个拆分文件为应用程序添加更多并发性会带来巨大的收益,并有助于提高应用程序的可扩展性
  • @JakubZaverka - 拥有多个阅读器的另一个优势是应用程序可以水平扩展。
【解决方案3】:

请阅读有关 hadoop 和 HDFS 的信息。它们旨在做同样的事情。网上有很多教程。请更清楚您要进行哪种处理。

【讨论】:

  • 请查看描述中的更新以获取 hadoop 建议。
【解决方案4】:

问题是这样的:UTF-8 字符可以有不同的长度。因此,仅以文件长度作为提示,无法确定 x% 的字符在哪里结束。

【讨论】:

  • 是的,我理解,因此问题
  • 好吧,但在这种情况下,您只能通读文件以找到限制。这是你明确表示不想要的。
  • 那么您对虚拟分割输入文件还有其他想法/建议
  • 好吧,如果你让分割重叠(我不确定有多少,我认为是 1 到 3 个字节),那么你可以确定至少对于一个分割,字符完全在分裂中。但是,您需要采取一些措施来确定一个字符是否被处理了两次。此外,开始字符可能看起来像一个有效的 UTF-8 字符,但实际上只是字符的一部分。所以仍然不容易,但也许这是一个给你一些新想法的建议。
【解决方案5】:

我认为最好的方法是让一个读取器负责对数据进行分区,当读取器到达每个分区边界时,它将分区提交到处理队列。然后,您可以拥有一个从队列中读取的处理器池。这样,如果处理一个分区比读取一个分区慢,您将获得并行处理分区的好处。

【讨论】:

  • 使用这种方法,应用程序只能垂直扩展,即通过向机器添加更多内核,而如果我们可以有多个读取器到同一个文件,应用程序也可以水平扩展,即通过生成一个新的 jvm一台不同的机器,它在内部产生多个线程用于读取、处理和写入。
  • 我理解你为什么要水平扩展,但我不确定这个问题是否适合这个问题。根据我的经验,添加更多阅读器会导致需要读取的数据更多,并且总体吞吐量会净减少。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多