簡體   English   中英

Java處理大文本文件的有效方法

[英]java efficient way to process big text files

我正在做一個頻率字典,其中我讀取了1000個文件,每個文件大約有1000行。 我遵循的方法是:

  • BufferedReader讀取fileByFile
  • 讀取第一個文件,獲取第一個句子,將句子拆分為一個數組字符串,然后使用字符串數組中的值填充一個hashmap。
  • 對文件中的所有語句執行此操作
  • 對所有1000個文件執行此操作

我的問題是,這不是一種非常有效的方法,我花了大約4分鍾來完成所有這些工作。 我增加了堆大小,重構了代碼以確保我沒有出錯。 對於這種方法,我完全確定我在代碼中沒有什么可以改進的。

我敢打賭,每當讀到一個句子時,就會應用拆分,將拆分乘以一個文件中的1000個句子和1000個文件,這是要處理的拆分的巨大數量。 我的想法是,我可以將每個文件讀取到一個char數組中,而不是逐文件讀取和處理文件,然后每個文件僅分割一次。 這將減少拆分所消耗的處理時間。 任何實施建議將不勝感激。

好的,我剛剛實現了字典的POC。 又快又臟。 我的文件每行包含868行,但我創建了1024個相同文件的副本。 (這是Spring Framework文檔的目錄。)

我進行了測試,結果耗時14020毫秒(14秒!)。 順便說一句,我從日食運行它可能會稍微降低速度。

因此,我不知道您的問題在哪里。 請在您的計算機上嘗試我的代碼,如果運行速度更快,請嘗試將其與您的代碼進行比較,並了解根本問題所在。

無論如何,我的代碼並不是我能寫的最快的代碼。 我可以在循環之前創建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);
}

如果您不關心內容在不同的文件中,我會推薦您使用的方法。 將所有文件和所有行讀取到內存(字符串或char數組,無論如何)中,然后基於一個字符串/數據集進行1 split和hash填充。

如果我了解自己在做什么,除了訪問地圖時,我不希望使用字符串。

你想要:

循環遍歷文件,將每個文件讀入一個類似1024的緩沖區中,然后在緩沖區中尋找單詞結尾字符,從字符數組中創建一個String,檢查是否找到了地圖,更新了計數,如果沒有,則在到達末尾時創建新條目緩沖區,最后從文件中獲取下一個緩沖區,循環到下一個文件

拆分可能非常昂貴,因為它必須每次都解釋表達式。

將文件讀取為一個大字符串,然后將其拆分聽起來不錯。 當涉及垃圾回收時,字符串拆分/修改可能令人驚訝地“繁重”。 多行/句子意味着多個字符串,並且所有拆分都意味着大量的字符串(字符串是不可變的,因此對它們的任何更改實際上都會創建一個新的字符串或多個字符串)...這會產生大量垃圾收集,垃圾收集可能成為瓶頸(具有較小的堆,始終達到最大內存量,從而啟動了垃圾收集,這可能需要清理成千上萬個單獨的String對象) 。

當然,在不知道您的代碼的情況下,這只是一個瘋狂的猜測,但是在過去,我有一個舊的命令行Java程序(它是一個圖形算法,生成一個巨大的SVG文件),運行時間從大約僅需修改字符串處理以使用StringBuffers / Builders,即可將時間從18秒縮短到不到0.5秒。

我想到的另一件事是使用多個線程(或線程池)同時處理不同的文件,然后在最后合並結果。 一旦使程序“盡可能快地”運行,剩下的瓶頸將是磁盤訪問,而通過磁盤的唯一途徑(afaik)就是更快的磁盤(SSD等)。

一種非常簡單的方法,它使用最小的堆空間,並且應該(幾乎)與其他任何東西一樣快

  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成為瓶頸...

由於您使用的是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;
        }
    }
}

您可以嘗試顯式地使用不同大小的緩沖區,但是可能不會因此獲得性能上的改善。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM