Список всех файлов из каталога, соответствующих маске файла (также известной как Pattern или Glob)
Я хочу перечислить все файлы в каталоге и подкаталогах в этом каталоге, которые соответствуют маске файла.
Например, "M:\SOURCE\*. Doc", а SOURCE может выглядеть так:
|-- SOURCE
| |-- Folder1
| | |-- File1.doc
| | |-- File1.txt
| |-- File2.doc
| |-- File3.xml
Должен возвращать File1.doc и File2.doc.
Первоначально я использую DirectoryStream, потому что он уже выполняет некоторые проверки синтаксиса маски / глобуса, а также позволяет использовать его для фильтрации, поскольку это НЕ просто регулярное выражение, а фактическая маска файла, которую обычный пользователь находит более понятной.
Files.newDirectoryStream(path, mask);
Проблема в том, что DirectoryStream проверяет только указанный вами каталог непосредственного пути, а не его подкаталоги.
ТОГДА прибывает метод "сглаживания" с Files.walk, который на самом деле может просматривать все подкаталоги, проблема в том, что он НЕ предоставляет возможности "фильтровать" маской файла так же, как DirectoryStream.
Files.walk(path, Integer.MAX_VALUE);
Так что я застрял, не могу сочетать здесь лучшее из обоих методов...
3 ответа
Я думаю, что мог бы решить свой собственный вопрос с помощью полученного здесь понимания и других вопросов, касающихся
PathMatcher
объект
final PathMatcher maskMatcher = FileSystems.getDefault()
.getPathMatcher("glob:" + mask);
final List<Path> matchedFiles = Files.walk(path)
.collect(Collectors.toList());
final List<Path> filesToRemove = new ArrayList<>(matchedFiles.size());
matchedFiles.forEach(foundPath -> {
if (!maskMatcher.matches(foundPath.getFileName()) || Files.isDirectory(foundPath)) {
filesToRemove.add(foundPath);
}
});
matchedFiles.removeAll(filesToRemove);
Так что в основном
.getPathMatcher("glob:" + mask);
то же самое, что DirectoryStream делал для фильтрации файлов
Все, что мне нужно сделать после этого, - это отфильтровать список путей, которые я получаю с помощью Files.walk, путем удаления элементов, которые не соответствуют моему PathMatcher и не относятся к типу File
Вы также можете использовать индивидуальные
FileVisitor
[1], с комбинацией
PathMatcher
[2], который отлично работает с GLOB.
Код может выглядеть так:
public static void main(String[] args) throws IOException {
System.out.println(getFiles(Paths.get("/tmp/SOURCE"), "*.doc"));
}
public static List<Path> getFiles(final Path directory, final String glob) throws IOException {
final var docFileVisitor = new GlobFileVisitor(glob);
Files.walkFileTree(directory, docFileVisitor);
return docFileVisitor.getMatchedFiles();
}
public static class GlobFileVisitor extends SimpleFileVisitor<Path> {
private final PathMatcher pathMatcher;
private List<Path> matchedFiles = new ArrayList<>();
public GlobFileVisitor(final String glob) {
this.pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);
}
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) throws IOException {
if (pathMatcher.matches(path.getFileName())) {
matchedFiles.add(path);
}
return FileVisitResult.CONTINUE;
}
public List<Path> getMatchedFiles() {
return matchedFiles;
}
}
[1] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileVisitor.html
[2] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/PathMatcher.html
Можно использовать обычный поток
filter
получить отфильтрованные имена файлов из
Files.walk
с помощью
String::matches
с соответствующим регулярным выражением:
final String SOURCE_DIR = "test";
Files.walk(Paths.get(SOURCE_DIR));
.filter(p -> p.getFileName().toString().matches(".*\\.docx?"))
.forEach(System.out::println);
Вывод
test\level01\level11\test.doc
test\level02\test-level2.doc
test\t1.doc
test\t3.docx
Структура входного каталога:
│ t1.doc
│ t2.txt
│ t3.docx
│ t4.bin
│
├───level01
│ │ test.do
│ │
│ └───level11
│ test.doc
│
└───level02
test-level2.doc
Обновить
Рекурсивное решение возможно с использованием
newDirectoryStream
однако его нужно преобразовать в Stream:
static Stream<Path> readFilesByMaskRecursively(Path start, String mask) {
List<Stream<Path>> sub = new ArrayList<>();
try {
sub.add(StreamSupport.stream( // read files by mask in current dir
Files.newDirectoryStream(start, mask).spliterator(), false));
Files.newDirectoryStream(start, (path) -> path.toFile().isDirectory())
.forEach(path -> sub.add(recursive(path, mask)));
} catch (IOException ioex) {
ioex.printStackTrace();
}
return sub.stream().flatMap(s -> s); // convert to Stream<Path>
}
// test
readFilesByMaskRecursively(Paths.get(SOURCE_DIR), "*.doc*")
.forEach(System.out::println);
Вывод:
test\t1.doc
test\t3.docx
test\level01\level11\test.doc
test\level02\test-level2.doc
Обновление 2
Префикс
**/
может быть добавлен к
PathMatcher
чтобы пересечь границы каталога, затем
Files.walk
Решение на основе может использовать упрощенный фильтр без необходимости удаления определенных записей:
String mask = "*.doc*";
PathMatcher maskMatcher = FileSystems.getDefault().getPathMatcher("glob:**/" + mask);
Files.walk(Paths.get(SOURCE_DIR))
.filter(path -> maskMatcher.matches(path))
.forEach(System.out::println);
Вывод (такой же, как в рекурсивном решении):
test\level01\level11\test.doc
test\level02\test-level2.doc
test\t1.doc
test\t3.docx