在Java中读取大文件

67

我需要一位Java专家的建议,关于内存问题。 我有一个很大的文件(大约1.5GB),我需要将这个文件分成很多小文件(例如100个小文件)。

我知道如何做(使用BufferedReader),但我想知道是否有关于内存方面的建议或如何更快地完成它的技巧。

我的文件包含文本,不是二进制文件,每行大约有20个字符。


7
请使用字节API(例如FileInputStream、ByteChannel),而不是字符API(BufferedReader等)。否则,您会进行不必要的编码和解码。 - Matthew Flaschen
3
使用字节来分割文本文件是一个不好的想法。 - james
10个回答

36
为了节省内存,不要在循环外部将数据存储/复制到内存中(即不要将它们分配给变量)。只需在输入到达时立即处理输出即可。
使用BufferedReader或不使用并不重要。它不会像某些人暗示的那样显着增加内存消耗。最多只会使性能下降几个百分点。使用NIO也是如此。它只会提高可伸缩性,而不会增加内存使用。仅当您有数百个线程运行在同一个文件上时才会变得有趣。
只需遍历文件,读取每一行后立即写入另一个文件,计算行数,如果达到100行,则切换到下一个文件,以此类推。
示例:
String encoding = "UTF-8";
int maxlines = 100;
BufferedReader reader = null;
BufferedWriter writer = null;

try {
    reader = new BufferedReader(new InputStreamReader(new FileInputStream("/bigfile.txt"), encoding));
    int count = 0;
    for (String line; (line = reader.readLine()) != null;) {
        if (count++ % maxlines == 0) {
            close(writer);
            writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("/smallfile" + (count / maxlines) + ".txt"), encoding));
        }
        writer.write(line);
        writer.newLine();
    }
} finally {
    close(writer);
    close(reader);
}

是的,只需使用适当大小的字节缓冲区数组将其从FileInputStream传输到FilOutputStream即可。 - Martin Wickman
对我来说,计算行数无法解决问题。 问题是:我有一个文件,需要将其分割成200个文件(这可能会改变,它将来自数据库)。 我该怎么做?仅仅计算行数是不起作用的。还有其他方法吗? - CC.
然后计算写入的字节数,而不是行数。您可以预先知道以字节为单位的文件大小。 - BalusC
使用 lineStr.getBytes().length ? - CC.
例如,不要忘记指定正确的编码!例如 line.getBytes(encoding)。否则会出现混乱。字节长度取决于所使用的字符编码。如果您实际上不担心文本行,则最好使用 InputStream/OutputStream 并计算传输的字节数。顺便说一下,不清楚您是指文件存储在数据库中还是文件分割参数存储在数据库中。如果文件实际上也存储在数据库中,则可能会占用大量内存。确切的解决方案将取决于所使用的数据库。 - BalusC
1
你使用的“close”方法怎么样? - alexcornejo

33

首先,如果您的文件包含二进制数据,则使用BufferedReader将是一个很大的错误(因为您将把数据转换为字符串,这是不必要的,并且很容易破坏数据);相反,您应该使用BufferedInputStream。如果它是文本数据并且您需要沿换行符拆分它,则使用BufferedReader是可以的(假设文件包含合理长度的行)。

关于内存,如果您使用一个大小适当的缓冲区(我会至少使用1MB来确保硬盘进行大部分连续读写),就不应该有任何问题。

如果速度成为问题,您可以查看java.nio软件包 - 那些据说比java.io更快。


1
@CC:你可以简单地累加你正在复制的行的字符串长度。但这取决于字符编码如何转换为文件大小(并且在变长编码(例如UTF-8)中根本不起作用)。 - Michael Borgwardt
1
我建议在 FileOutputStream(底部)和 OutputStreamWriter 之间添加一个自定义的 FilterOutputStream。实现此过滤器只需跟踪通过它的字节数即可(apache commons io 可能已经有这样的实用程序)。 - james
12
另外,一个常见的误解是认为“nio”比“io”更快。尽管在某些情况下可能是这样,但通常情况下,“nio”被设计成比“io”更具_可扩展性_,其中“可扩展性”并不一定等同于“更快”。 - james
@james:当上面有一个BufferedWriter时,过滤器将无法产生正确的结果,尽管差异可能不足以影响结果。 - Michael Borgwardt
1
@MichaelBorgwardt 我也遇到了同样的问题,这是我的信息检索项目,我必须找出最佳缓冲区大小和最佳读写器,我已经在各个地方阅读到NIO工具比IO工具更快,但在我的测试中,IO工具运行得更快!! - Sarah Akhavan
显示剩余5条评论

13

如果你只是直接读取文件,那么这很可能不会给你带来任何东西。 - james
通常来说并没有快很多。上次我进行基准测试时,读取速度只提高了20%。 - user207421

6
这是一篇非常好的文章:http://java.sun.com/developer/technicalArticles/Programming/PerfTuning/ 总之,为了获得更好的性能,你应该:
  1. 避免访问磁盘。
  2. 避免访问底层操作系统。
  3. 避免方法调用。
  4. 避免逐字节或逐字符处理。
例如,为了减少对磁盘的访问,可以使用大缓冲区。本文介绍了各种方法。

4

它必须使用Java完成吗?也就是说,它需要跨平台吗?如果不需要,我建议在*nix中使用'split'命令。如果你真的想要的话,你可以通过你的Java程序执行这个命令。虽然我没有测试过,但我想它比你能想出的任何Java IO实现都要快。


1

1
请查看我在Michael Borgwardt的帖子上的评论。 - james

1

是的。 我也认为使用read()方法并带有参数,例如read(Char[], int init, int end)是读取如此大的文件的更好方式(例如:read(buffer,0,buffer.length))。

我也曾经遇到过使用BufferedReader而不是BufferedInputStreamReader读取二进制数据输入流时出现缺失值的问题。因此,在这种情况下,使用BufferedInputStreamReader会更好。


1

package all.is.well;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import junit.framework.TestCase;

/**
 * @author Naresh Bhabat
 * 
Following  implementation helps to deal with extra large files in java.
This program is tested for dealing with 2GB input file.
There are some points where extra logic can be added in future.


Pleasenote: if we want to deal with binary input file, then instead of reading line,we need to read bytes from read file object.



It uses random access file,which is almost like streaming API.


 * ****************************************
Notes regarding executor framework and its readings.
Please note :ExecutorService executor = Executors.newFixedThreadPool(10);

 *      for 10 threads:Total time required for reading and writing the text in
 *         :seconds 349.317
 * 
 *         For 100:Total time required for reading the text and writing   : seconds 464.042
 * 
 *         For 1000 : Total time required for reading and writing text :466.538 
 *         For 10000  Total time required for reading and writing in seconds 479.701
 *
 * 
 */
public class DealWithHugeRecordsinFile extends TestCase {

 static final String FILEPATH = "C:\\springbatch\\bigfile1.txt.txt";
 static final String FILEPATH_WRITE = "C:\\springbatch\\writinghere.txt";
 static volatile RandomAccessFile fileToWrite;
 static volatile RandomAccessFile file;
 static volatile String fileContentsIter;
 static volatile int position = 0;

 public static void main(String[] args) throws IOException, InterruptedException {
  long currentTimeMillis = System.currentTimeMillis();

  try {
   fileToWrite = new RandomAccessFile(FILEPATH_WRITE, "rw");//for random write,independent of thread obstacles 
   file = new RandomAccessFile(FILEPATH, "r");//for random read,independent of thread obstacles 
   seriouslyReadProcessAndWriteAsynch();

  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  Thread currentThread = Thread.currentThread();
  System.out.println(currentThread.getName());
  long currentTimeMillis2 = System.currentTimeMillis();
  double time_seconds = (currentTimeMillis2 - currentTimeMillis) / 1000.0;
  System.out.println("Total time required for reading the text in seconds " + time_seconds);

 }

 /**
  * @throws IOException
  * Something  asynchronously serious
  */
 public static void seriouslyReadProcessAndWriteAsynch() throws IOException {
  ExecutorService executor = Executors.newFixedThreadPool(10);//pls see for explanation in comments section of the class
  while (true) {
   String readLine = file.readLine();
   if (readLine == null) {
    break;
   }
   Runnable genuineWorker = new Runnable() {
    @Override
    public void run() {
     // do hard processing here in this thread,i have consumed
     // some time and ignore some exception in write method.
     writeToFile(FILEPATH_WRITE, readLine);
     // System.out.println(" :" +
     // Thread.currentThread().getName());

    }
   };
   executor.execute(genuineWorker);
  }
  executor.shutdown();
  while (!executor.isTerminated()) {
  }
  System.out.println("Finished all threads");
  file.close();
  fileToWrite.close();
 }

 /**
  * @param filePath
  * @param data
  * @param position
  */
 private static void writeToFile(String filePath, String data) {
  try {
   // fileToWrite.seek(position);
   data = "\n" + data;
   if (!data.contains("Randomization")) {
    return;
   }
   System.out.println("Let us do something time consuming to make this thread busy"+(position++) + "   :" + data);
   System.out.println("Lets consume through this loop");
   int i=1000;
   while(i>0){
   
    i--;
   }
   fileToWrite.write(data.getBytes());
   throw new Exception();
  } catch (Exception exception) {
   System.out.println("exception was thrown but still we are able to proceeed further"
     + " \n This can be used for marking failure of the records");
   //exception.printStackTrace();

  }

 }
}


0
不要使用没有参数的read函数,它非常慢。最好将其读入缓冲区并快速移动到文件中。
使用BufferedInputStream,因为它支持二进制读取。
就这些了。

0

除非您意外地读取了整个输入文件而不是逐行读取,否则您的主要限制将是磁盘速度。您可以尝试从包含100行的文件开始,并将其写入100个不同的文件中,每个文件一行,并使触发机制根据写入当前文件的行数工作。该程序将很容易扩展到您的情况。


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