簡體   English   中英

使用 Java 遞歸列出目錄中的所有文件

[英]List all files from a directory recursively with Java

我有這個 function 遞歸地打印目錄中所有文件的名稱。 問題是我的代碼非常慢,因為它必須在每次迭代時訪問 remote.network 設備。

我的計划是首先從目錄中遞歸加載所有文件,然后 go 使用正則表達式過濾掉所有我不需要的文件。 有更好的解決方案嗎?

public static printFnames(String sDir) {
    File[] faFiles = new File(sDir).listFiles();
    for (File file : faFiles) {
        if (file.getName().matches("^(.*?)")) {
            System.out.println(file.getAbsolutePath());
        }
        if (file.isDirectory()) {
            printFnames(file.getAbsolutePath());
        }
    }
}

這只是一個測試。 稍后我不會使用這樣的代碼; 相反,我將把與高級正則表達式匹配的每個文件的路徑和修改日期添加到數組中。

假設這是您將要編寫的實際生產代碼,那么我建議使用解決方案來解決這種已經解決的問題 - Apache Commons IO ,特別是FileUtils.listFiles() 它處理嵌套目錄、過濾器(基於名稱、修改時間等)。

例如,對於您的正則表達式:

Collection files = FileUtils.listFiles(
  dir, 
  new RegexFileFilter("^(.*?)"), 
  DirectoryFileFilter.DIRECTORY
);

這將遞歸搜索與^(.*?)正則表達式匹配的文件,並將結果作為集合返回。

值得注意的是,這不會比滾動您自己的代碼快,它在做同樣的事情 - 在 Java 中拖網文件系統只是很慢。 不同的是,Apache Commons 版本不會有任何錯誤。

在 Java 8 中,它是通過Files.find()具有任意大深度(例如999 )和isRegularFile() BasicFileAttributes的 1-liner

public static printFnames(String sDir) {
    Files.find(Paths.get(sDir), 999, (p, bfa) -> bfa.isRegularFile()).forEach(System.out::println);
}

要添加更多過濾,請增強 lambda,例如在過去 24 小時內修改的所有 jpg 文件:

(p, bfa) -> bfa.isRegularFile()
  && p.getFileName().toString().matches(".*\\.jpg")
  && bfa.lastModifiedTime().toMillis() > System.currentMillis() - 86400000

這是一種非常簡單的遞歸方法,可以從給定的根目錄中獲取所有文件。

它使用 Java 7 NIO Path 類。

private List<String> getFileNames(List<String> fileNames, Path dir) {
    try(DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
        for (Path path : stream) {
            if(path.toFile().isDirectory()) {
                getFileNames(fileNames, path);
            } else {
                fileNames.add(path.toAbsolutePath().toString());
                System.out.println(path.getFileName());
            }
        }
    } catch(IOException e) {
        e.printStackTrace();
    }
    return fileNames;
} 

在 Java 7 中,通過PathsFiles功能引入了一種更快的遍歷目錄樹的方法。 它們比“舊” File方式快得多。

這將是使用正則表達式遍歷和檢查路徑名稱的代碼:

public final void test() throws IOException, InterruptedException {
    final Path rootDir = Paths.get("path to your directory where the walk starts");

    // Walk thru mainDir directory
    Files.walkFileTree(rootDir, new FileVisitor<Path>() {
        // First (minor) speed up. Compile regular expression pattern only one time.
        private Pattern pattern = Pattern.compile("^(.*?)");

        @Override
        public FileVisitResult preVisitDirectory(Path path,
                BasicFileAttributes atts) throws IOException {

            boolean matches = pattern.matcher(path.toString()).matches();

            // TODO: Put here your business logic when matches equals true/false

            return (matches)? FileVisitResult.CONTINUE:FileVisitResult.SKIP_SUBTREE;
        }

        @Override
        public FileVisitResult visitFile(Path path, BasicFileAttributes mainAtts)
                throws IOException {

            boolean matches = pattern.matcher(path.toString()).matches();

            // TODO: Put here your business logic when matches equals true/false

            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path path,
                IOException exc) throws IOException {
            // TODO Auto-generated method stub
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path path, IOException exc)
                throws IOException {
            exc.printStackTrace();

            // If the root directory has failed it makes no sense to continue
            return path.equals(rootDir)? FileVisitResult.TERMINATE:FileVisitResult.CONTINUE;
        }
    });
}

使用 Java 7 NIO 獲取目錄內容的快速方法:

import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.FileSystems;
import java.nio.file.Path;

...

Path dir = FileSystems.getDefault().getPath( filePath );
DirectoryStream<Path> stream = Files.newDirectoryStream( dir );
for (Path path : stream) {
   System.out.println( path.getFileName() );
}
stream.close();

Java 的用於讀取文件系統文件夾內容的接口的性能不是很好(如您所見)。 JDK 7 用一個全新的接口修復了這個問題,它應該為這些類型的操作帶來本機級別的性能。

核心問題是 Java 對每個文件都進行本地系統調用。 在低延遲接口上,這沒什么大不了的——但在延遲適中的網絡上,它確實加起來了。 如果您在上面分析您的算法,您會發現大部分時間都花在討厭的 isDirectory() 調用上——這是因為每次調用 isDirectory() 都會導致往返。 大多數現代操作系統可以在最初請求文件/文件夾列表時提供此類信息(而不是查詢每個單獨的文件路徑的屬性)。

如果您迫不及待地等待 JDK7,那么解決此延遲的一種策略是采用多線程並使用具有最大線程數的 ExecutorService 來執行遞歸。 這不是很好(你必須處理輸出數據結構的鎖定),但它會比做這個單線程快很多。

在關於此類事情的所有討論中,我強烈建議您與使用本機代碼(甚至是執行大致相同操作的命令行腳本)所能做到的最好的情況進行比較。 說遍歷一個網絡結構需要一個小時並沒有多大意義。 告訴我們你可以在 7 秒內完成原生,但在 Java 中需要一個小時才會引起人們的注意。

這將工作得很好......而且它的遞歸

File root = new File("ROOT PATH");
for ( File file : root.listFiles())
{
    getFilesRecursive(file);
}


private static void getFilesRecursive(File pFile)
{
    for(File files : pFile.listFiles())
    {
        if(files.isDirectory())
        {
            getFilesRecursive(files);
        }
        else
        {
            // do your thing 
            // you can either save in HashMap and use it as
            // per your requirement
        }
    }
}

這將正常工作

public void displayAll(File path){      
    if(path.isFile()){
        System.out.println(path.getName());
    }else{
        System.out.println(path.getName());         
        File files[] = path.listFiles();
        for(File dirOrFile: files){
            displayAll(dirOrFile);
        }
    }
}

我個人喜歡這個版本的 FileUtils。 這是一個在目錄或其任何子目錄中查找所有 mp3 或 flac 的示例:

String[] types = {"mp3", "flac"};
Collection<File> files2 = FileUtils.listFiles(/path/to/your/dir, types , true);

爪哇 8

public static void main(String[] args) throws IOException {

        Path start = Paths.get("C:\\data\\");
        try (Stream<Path> stream = Files.walk(start, Integer.MAX_VALUE)) {
            List<String> collect = stream
                .map(String::valueOf)
                .sorted()
                .collect(Collectors.toList());

            collect.forEach(System.out::println);
        }


    }

此函數可能會列出其目錄及其子目錄中的所有文件名及其路徑。

public void listFile(String pathname) {
    File f = new File(pathname);
    File[] listfiles = f.listFiles();
    for (int i = 0; i < listfiles.length; i++) {
        if (listfiles[i].isDirectory()) {
            File[] internalFile = listfiles[i].listFiles();
            for (int j = 0; j < internalFile.length; j++) {
                System.out.println(internalFile[j]);
                if (internalFile[j].isDirectory()) {
                    String name = internalFile[j].getAbsolutePath();
                    listFile(name);
                }

            }
        } else {
            System.out.println(listfiles[i]);
        }

    }

}

感覺訪問文件系統並獲取每個子目錄的內容而不是一次獲取所有內容是愚蠢的。

你的感覺是錯誤的。 這就是文件系統的工作方式。 沒有更快的方法(除非您必須重復執行此操作或針對不同模式執行此操作,您可以將所有文件路徑緩存在內存中,但是您必須處理緩存失效,即在添加/刪除/重命名文件時會發生什么應用程序運行)。

import java.io.*;

public class MultiFolderReading {

public void checkNoOfFiles (String filename) throws IOException {

    File dir=new File(filename);
    File files[]=dir.listFiles();//files array stores the list of files

 for(int i=0;i<files.length;i++)
    {
        if(files[i].isFile()) //check whether files[i] is file or directory
        {
            System.out.println("File::"+files[i].getName());
            System.out.println();

        }
        else if(files[i].isDirectory())
        {
            System.out.println("Directory::"+files[i].getName());
            System.out.println();
            checkNoOfFiles(files[i].getAbsolutePath());
        }
    }
}

public static void main(String[] args) throws IOException {

    MultiFolderReading mf=new MultiFolderReading();
    String str="E:\\file"; 
    mf.checkNoOfFiles(str);
   }
}

在 Guava 中,您不必等待 Collection 返回給您,但實際上可以遍歷文件。 很容易想象以下函數的簽名中有一個IDoSomethingWithThisFile接口:

public static void collectFilesInDir(File dir) {
    TreeTraverser<File> traverser = Files.fileTreeTraverser();
    FluentIterable<File> filesInPostOrder = traverser.preOrderTraversal(dir);
    for (File f: filesInPostOrder)
        System.out.printf("File: %s\n", f.getPath());
}

TreeTraverser還允許您在各種遍歷樣式之間進行。

只是讓您知道 isDirectory() 是一種非常慢的方法。 我發現它在我的文件瀏覽器中很慢。 我將研究一個庫以用本機代碼替換它。

public class GetFilesRecursive {
    public static List <String> getFilesRecursively(File dir){
        List <String> ls = new ArrayList<String>();
        for (File fObj : dir.listFiles()) {
            if(fObj.isDirectory()) {
                ls.add(String.valueOf(fObj));
                ls.addAll(getFilesRecursively(fObj));               
            } else {
                ls.add(String.valueOf(fObj));       
            }
        }

        return ls;
    }
    public static List <String> getListOfFiles(String fullPathDir) {
        List <String> ls = new ArrayList<String> ();
        File f = new File(fullPathDir);
        if (f.exists()) {
            if(f.isDirectory()) {
                ls.add(String.valueOf(f));
                ls.addAll(getFilesRecursively(f));
            }
        } else {
            ls.add(fullPathDir);
        }
        return ls;
    }

    public static void main(String[] args) {
        List <String> ls = getListOfFiles("/Users/srinivasab/Documents");
        for (String file:ls) {
            System.out.println(file);
        }
        System.out.println(ls.size());
    }
}

另一個優化的代碼

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class GetFilesRecursive {
    public static List <String> getFilesRecursively(File dir){
        List <String> ls = new ArrayList<String>();
        if (dir.isDirectory())
            for (File fObj : dir.listFiles()) {
                if(fObj.isDirectory()) {
                    ls.add(String.valueOf(fObj));
                    ls.addAll(getFilesRecursively(fObj));               
                } else {
                    ls.add(String.valueOf(fObj));       
                }
            }
        else
            ls.add(String.valueOf(dir));

        return ls;
    }

    public static void main(String[] args) {
        List <String> ls = getFilesRecursively(new File("/Users/srinivasab/Documents"));
        for (String file:ls) {
            System.out.println(file);
        }
        System.out.println(ls.size());
    }
}

使用 Java 8 filter列出文件和目錄的另一個示例

public static void main(String[] args) {

System.out.println("Files!!");
        try {
            Files.walk(Paths.get("."))
                    .filter(Files::isRegularFile)
                    .filter(c ->
                            c.getFileName().toString().substring(c.getFileName().toString().length()-4).contains(".jpg")
                            ||
                            c.getFileName().toString().substring(c.getFileName().toString().length()-5).contains(".jpeg")
                    )
                    .forEach(System.out::println);

        } catch (IOException e) {
        System.out.println("No jpeg or jpg files");
        }

        System.out.println("\nDirectories!!\n");
        try {
            Files.walk(Paths.get("."))
                    .filter(Files::isDirectory)
                    .forEach(System.out::println);

        } catch (IOException e) {
            System.out.println("No Jpeg files");
        }
}

測試文件夾

我在 Windows 11 上用 284 個文件夾中的 60,000 個文件測試了一些方法:

public class App {

    public static void main(String[] args) throws Exception {

        Path path = Paths.get("E:\\書籍");

        // 1.walkFileTree
        long start1 = System.currentTimeMillis();
        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                // if(pathMatcher.matches(file))
                // files.add(file.toFile());

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                // System.out.println(dir.getFileName());
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException e) {
                return FileVisitResult.CONTINUE;
            }

        });
        long end1 = System.currentTimeMillis();

        // 2. newDirectoryStream
        long start2 = System.currentTimeMillis();
        search(path.toFile());
        long end2 = System.currentTimeMillis();

        // 3. listFiles
        long start3 = System.currentTimeMillis();
        getFileNames(path);
        long end3 = System.currentTimeMillis();

        System.out.println("\r執行耗時:" + (end1 - start1));
        System.out.println("\r執行耗時:" + (end2 - start2));
        System.out.println("\r執行耗時:" + (end3 - start3));
    }


    private static void getFileNames(Path dir) {
        try(DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
            for (Path path : stream) {
                if(Files.isDirectory(path)) {
                    getFileNames(path);
                }
            }
        } catch(IOException e) {
            e.printStackTrace();
        }
    }

    public static void search(File file) {
        Queue<File> q = new LinkedList<>();
        q.offer(file);
        while (!q.isEmpty()) {
            try {
                for (File childfile : q.poll().listFiles()) {
                    // System.out.println(childfile.getName());
                    if (childfile.isDirectory()) {
                        q.offer(childfile);
                    }
                }
            } catch (Exception e) {

            }
        }
    }
}

結果(毫秒):

遍歷文件樹 列表文件 新目錄流
68 451 493
64 464 482
61 478 457
67 477 488
59 474 466

已知性能問題:

  1. 來自凱文戴的回答

    如果您分析上面的算法,您會發現大部分時間花在了煩人的 isDirectory() 調用上——那是因為每次調用 isDirectory() 都會招致一次往返。

  2. listfiles() 將為每個條目創建新文件 Object

我發現在處理數百萬個文件夾和文件時更有效的方法是通過 DOS 命令在某個文件中捕獲目錄列表並對其進行解析。 一旦你解析了數據,你就可以進行分析和計算統計數據。

暫無
暫無

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

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