虚拟分割文本文件的解决方案

时间:2013-02-21 09:24:15

标签: java file-io inputstream filereader

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

稍后当读取和处理由并发读者完成时,每个线程都可以调用bufferedReader.skip(long)并跟踪读取的字符数,以便它们不会越过末尾指针边界。

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

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

注意 -

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

    注意我被限制使用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];
            }
        }
    

5 个答案:

答案 0 :(得分:2)

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

  

为了改善数据处理时间,我想通过拥有多个读者同时阅读它。

如果您的处理是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

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

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


  

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

那可能很难:

  • 如果分隔符在开始时或接近开始时设置一次,那么您只需从头开始阅读,直到找出分隔符为止。然后进行分区。

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

  • 最后一个选项是线程分区和处理假设一个分隔符,但也要查找嵌入的“更改分隔符”指令。如果他们确实检测到实际更改,请告诉线程以便以后的分区再次启动。这有点复杂......

答案 1 :(得分:1)

你的建议是不可能的。磁盘上的所有I / O操作本质上都是串行的。试想一下常见的硬盘是怎样的。该文件存储在一个带有一个读头的盘子上。你不会从java创建更多的标题 - 所以即使你创建了多个读者,他们最终也会等待彼此完成阅读。

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

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

答案 2 :(得分:0)

请阅读有关hadoop和HDFS的信息。它们的设计也是如此。有许多可用于网络的教程。请更清楚你想要做什么样的处理。

答案 3 :(得分:0)

问题是:UTF-8字符可以有不同的长度。因此,只需将文件长度作为提示,就无法确定x%字符的结束位置。

答案 4 :(得分:0)

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