1

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

稍后当并发读取器完成读取和处理时,每个线程可以调用 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];
        }
    }
4

5 回答 5

2

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

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

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

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

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

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

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

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

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

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

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

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

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


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

那么这可能很困难:

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

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

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

于 2013-02-21T11:34:58.113 回答
1

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

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

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

于 2013-02-21T09:27:44.403 回答
0

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

于 2013-02-21T15:45:07.097 回答
0

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

于 2013-02-21T09:31:55.593 回答
0

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

于 2013-02-21T09:53:14.513 回答