【问题标题】:java efficient way to process big text filesjava处理大文本文件的有效方法
【发布时间】:2011-12-16 16:45:45
【问题描述】:

我正在做一个频率词典,我在其中阅读了 1000 个文件,每个文件大约有 1000 行。我遵循的方法是:

  • BufferedReader 通过文件读取文件
  • 读取第一个文件,获取第一个句子,将句子拆分为数组字符串,然后用字符串数组中的值填充哈希图。
  • 为该文件中的所有句子执行此操作
  • 对所有 1000 个文件执行此操作

我的问题是,这不是一种非常有效的方法,我需要大约 4 分钟来完成所有这些。我增加了堆大小,重构了代码以确保我没有做错什么。对于这种方法,我完全确定代码中没有什么可以改进的。

我敢打赌,每次读取一个句子时,都会应用一个拆分,乘以一个文件中的 1000 个句子,再乘以 1000 个文件,就是要处理的大量拆分。 我的想法是,我可以将每个文件读入一个字符数组,然后每个文件只拆分一次,而不是逐个文件读取和处理。这将减轻拆分所消耗的处理时间。任何实施建议将不胜感激。

【问题讨论】:

  • 那么,您尝试过您的建议了吗?
  • 如果您有个人资料,则无需下注。
  • 我真的不知道该怎么做:p 这就是我需要帮助的原因。 profile 向我展示了 80% 的时间使用 char[],我假设它是按句子阅读的句子。
  • 您可以确定现有代码中没有什么需要改进的地方。不是。显示您的代码。
  • 好吧,我可能夸大了。我的意思是,所花费的大量时间都在我想通过遵循另一种方法来减少的大量拆分中。这就是我需要帮助的地方

标签: java file optimization bufferedreader


【解决方案1】:

一种非常简单的方法,它使用最小的堆空间并且应该(几乎)和其他任何东西一样快

  int c;

  final String SEPARATORS = " \t,.\n"; // extend as needed

  final StringBuilder word = new StringBuilder();

  while( ( c = fileInputStream.read() ) >= 0 ) {
    final char letter = (char) c;

    if ( SEPARATORS.indexOf(letter) < 0 ) {

      word.append(letter);

    } else {

      processWord( word.toString() );
      word.setLength( 0 );

    }

  }

根据需要扩展更多分隔符,可能使用多线程同时处理多个文件,直到磁盘 IO 成为瓶颈...

【讨论】:

  • 我在评论中提到的同样的事情:不要使用 StringBuffer/Builder 的默认构造函数,默认情况下 Buffer/Builder 只能容纳 16 个字符(参考 JavaDoc类),并且需要多次调整自身大小,因为文件可能包含很多字符。
  • 这根本不是问题:当重新使用 StringBuilder(就像我一样)时,它只会在每次遇到比之前所有单词都长的单词时增长;这将很快稳定下来,因此可以预期不会有超过一把调整大小。这是最有效的。
【解决方案2】:

好的,我刚刚实现了您字典的 POC。又快又脏。我的文件每个包含 868 行,但我创建了同一文件的 1024 个副本。 (这是 Spring Framework 文档的目录。)

我进行了测试,耗时 14020 毫秒(14 秒!)。顺便说一句,我从 eclipse 中运行它可能会稍微降低速度。

所以,我不知道你的问题出在哪里。请在您的机器上尝试我的代码,如果它运行得更快,请尝试将其与您的代码进行比较并了解根本问题在哪里。

无论如何,我的代码并不是我能写的最快的。 我可以在循环之前创建 Pattern 并使用它而不是 String.split()。 String.split() 每次都会调用 Pattern.compile()。创建模式非常昂贵。

代码如下:

public static void main(String[] args) throws IOException {
    Map<String, Integer> words = new HashMap<String, Integer>();

    long before = System.currentTimeMillis();

    File dir = new File("c:/temp/files");
    for (File file : dir.listFiles()) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
        for (String line = reader.readLine();  line != null;  line = reader.readLine()) {
            String[] lineWords = line.split("\\s+");
            for (String word : lineWords) {
                int count = 1;
                Integer currentCount = words.get(word);
                if (currentCount != null) {
                    count = currentCount + 1;
                }
                words.put(word, count);
            }
        }
    }

    long after = System.currentTimeMillis();

    System.out.println("run took " + (after - before) + " ms");
    System.out.println(words);
}

【讨论】:

  • 在我的笔记本电脑上运行您的代码,使用 pattern.split(sentence) 而不是每次编译模式需要 108204 毫秒。
【解决方案3】:

既然您使用的是 bufferedReader,为什么需要显式读取整个文件?如果您追求速度,我绝对不会使用 split,请记住,每次运行时它都必须计算正则表达式。

为你的内部循环尝试这样的事情(注意,我没有编译或尝试运行它):

StringBuilder sb = null;
String delimiters = " .,\t"; //Build out all your word delimiters in a string here
for(int nextChar = br.read(); nextChar >= 0; nextChar = br.read()) {
    if(delimiters.indexOf(nextChar) < 0) {
        if(sb == null) sb = new StringBuilder();
        sb.append((char)(nextChar));
    } else {
        if(sb != null) {
            //Add sb.toString() to your map or increment it
            sb = null;
        }
    }
}

您可以尝试显式使用不同大小的缓冲区,但您可能不会因此获得性能提升。

【讨论】:

    【解决方案4】:

    将文件作为一个大字符串读取,然后拆分听起来是个好主意。在垃圾收集方面,字符串拆分/修改可能会出人意料地“繁重”。多行/句子意味着多个字符串,并且所有拆分都意味着大量的字符串(字符串是不可变的,因此对它们的任何更改实际上都会创建一个新的字符串或多个字符串)......这会产生很多垃圾收集,并且垃圾收集可能成为瓶颈(使用较小的堆,始终达到最大内存量,启动垃圾收集,这可能需要清理数十万或数百万个单独的字符串对象) .

    当然,在不知道您的代码的情况下,这只是一个疯狂的猜测,但在过去,我有一个旧的命令行 Java 程序(它是一个生成巨大 SVG 文件的图形算法)运行时间只需将字符串处理修改为使用 StringBuffers/Builders,从大约 18 秒降至不到 0.5 秒。

    我想到的另一件事是使用多个线程(或线程池)同时处理不同的文件,然后在最后组合结果。一旦你让程序“尽可能快”地运行,剩下的瓶颈将是磁盘访问,而解决这个问题的唯一方法(afaik)是更快的磁盘(SSD 等)。

    【讨论】:

    • 这正是我认为我应该改进的方式。但是,如果我将整个文件读入一个 char 数组,我该如何应用拆分?这就是我需要帮助的原因
    • 从头到尾,先将文件读入 StringBuffer,然后调用 .toString().split()。另外,确保 StringBuffer 足够大(使用以初始容量为参数的构造函数),因此缓冲区不需要多次调整自身大小。
    • @user974594 将每个字符附加到 StringBuilder。
    【解决方案5】:

    如果我理解你在做什么,我认为你不想使用字符串,除非你访问你的地图。

    你想:

    遍历文件 将每个文件读入类似 1024 的缓冲区 处理缓冲区以查找词尾字符 从字符数组创建一个字符串 检查你的地图 如果找到,更新您的计数,如果没有,创建一个新条目 当你到达缓冲区末尾时,从文件中获取下一个缓冲区 最后,循环到下一个文件

    Split 可能非常昂贵,因为它每次都必须解释表达式。

    【讨论】:

      【解决方案6】:

      如果您不关心不同文件中的内容,我会采用您推荐的方法。将所有文件和所有行读入内存(字符串或字符数组等),然后根据一个字符串/数据集进行 1 次拆分和散列填充。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-06-28
        • 1970-01-01
        • 2015-03-08
        • 1970-01-01
        • 2011-05-16
        • 1970-01-01
        • 2011-12-06
        • 2015-07-29
        相关资源
        最近更新 更多