数千个文件中的模式匹配

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数千个文件中的模式匹配相关的知识,希望对你有一定的参考价值。

我有像welcome1|welcome2|changeme这样的正则表达式模式...我需要搜索成千上万个文件(在100到8000之间),大小从1KB到24MB不等。

我想知道模式匹配的方式是否比我一直尝试的更快。

环境:

  1. jdk 1.8
  2. Windows 10
  3. Unix4j Library

这是我到现在为止所尝试的

try (Stream<Path> stream = Files.walk(Paths.get(FILES_DIRECTORY))
                                    .filter(FilePredicates.isFileAndNotDirectory())) {

        List<String> obviousStringsList = Strings_PASSWORDS.stream()
                                                .map(s -> ".*" + s + ".*").collect(Collectors.toList()); //because Unix4j apparently needs this

        Pattern pattern = Pattern.compile(String.join("|", obviousStringsList));

        GrepOptions options = new GrepOptions.Default(GrepOption.count,
                                                        GrepOption.ignoreCase,
                                                        GrepOption.lineNumber,
                                                        GrepOption.matchingFiles);
        Instant startTime = Instant.now();

        final List<Path> filesWithObviousStringss = stream
                .filter(path -> !Unix4j.grep(options, pattern, path.toFile()).toStringResult().isEmpty())
                .collect(Collectors.toList());

        System.out.println("Time taken = " + Duration.between(startTime, Instant.now()).getSeconds() + " seconds");
}

我得到Time taken = 60 seconds让我觉得我做错了什么。

我已经尝试了不同的流方式,平均每个方法大约需要一分钟来处理我当前的6660文件夹。

在mysys2 / mingw64上grep大约需要15秒,而node.js中的exec('grep...')大约需要12秒。

我之所以选择Unix4j是因为它提供了java native grep和clean code。

有没有办法在Java中产生更好的结果,我遗憾地错过了?

答案

本机工具可以更快地处理这些文本文件的主要原因是它们假设一个特定的字符集,特别是当它具有基于ASCII的8位编码时,而Java执行字节到字符的转换,其抽象能够支持任意字符集。

当我们类似地假设具有上述属性的单个字符集时,我们可以使用可以显着提高性能的低级工具。

对于这样的操作,我们定义以下帮助器方法:

private static char[] getTable(Charset cs) {
    if(cs.newEncoder().maxBytesPerChar() != 1f)
        throw new UnsupportedOperationException("Not an 8 bit charset");
    byte[] raw = new byte[256];
    IntStream.range(0, 256).forEach(i -> raw[i] = (byte)i);
    char[] table = new char[256];
    cs.newDecoder().onUnmappableCharacter(CodingErrorAction.REPLACE)
      .decode(ByteBuffer.wrap(raw), CharBuffer.wrap(table), true);
    for(int i = 0; i < 128; i++)
        if(table[i] != i) throw new UnsupportedOperationException("Not ASCII based");
    return table;
}

private static CharSequence mapAsciiBasedText(Path p, char[] table) throws IOException {
    try(FileChannel fch = FileChannel.open(p, StandardOpenOption.READ)) {
        long actualSize = fch.size();
        int size = (int)actualSize;
        if(size != actualSize) throw new UnsupportedOperationException("file too large");
        MappedByteBuffer mbb = fch.map(FileChannel.MapMode.READ_ONLY, 0, actualSize);
        final class MappedCharSequence implements CharSequence {
            final int start, size;
            MappedCharSequence(int start, int size) {
                this.start = start;
                this.size = size;
            }
            public int length() {
                return size;
            }
            public char charAt(int index) {
                if(index < 0 || index >= size) throw new IndexOutOfBoundsException();
                byte b = mbb.get(start + index);
                return b<0? table[b+256]: (char)b;
            }
            public CharSequence subSequence(int start, int end) {
                int newSize = end - start;
                if(start<0 || end < start || end-start > size)
                    throw new IndexOutOfBoundsException();
                return new MappedCharSequence(start + this.start, newSize);
            }
            public String toString() {
                return new StringBuilder(size).append(this).toString();
            }
        }
        return new MappedCharSequence(0, size);
    }
}

这允许将文件映射到虚拟内存并将其直接投影到CharSequence,而无需复制操作,假设映射可以使用简单的表完成,并且对于基于ASCII的字符集,大多数字符甚至不需要表查找,因为它们的数值与Unicode代码点相同。

使用这些方法,您可以将操作实现为

// You need this only once per JVM.
// Note that running inside IDEs like Netbeans may change the default encoding
char[] table = getTable(Charset.defaultCharset());

try(Stream<Path> stream = Files.walk(Paths.get(FILES_DIRECTORY))
                               .filter(Files::isRegularFile)) {
    Pattern pattern = Pattern.compile(String.join("|", Strings_PASSWORDS));
    long startTime = System.nanoTime();
    final List<Path> filesWithObviousStringss = stream//.parallel()
            .filter(path -> {
                try {
                    return pattern.matcher(mapAsciiBasedText(path, table)).find();
                } catch(IOException ex) {
                    throw new UncheckedIOException(ex);
                }
            })
            .collect(Collectors.toList());
    System.out.println("Time taken = "
        + TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()-startTime) + " seconds");
}

这比正常的文本转换运行得快得多,但仍然支持并行执行。

除了要求基于ASCII的单字节编码外,还有一个限制,即此代码不支持大于2 GiB的文件。虽然可以扩展解决方案以支持更大的文件,但除非确实需要,否则我不会添加此复杂功能。

另一答案

我不知道“Unix4j”提供的是JDK中尚未提供的内容,因为以下代码使用内置功能执行所有操作:

try(Stream<Path> stream = Files.walk(Paths.get(FILES_DIRECTORY))
                               .filter(Files::isRegularFile)) {
        Pattern pattern = Pattern.compile(String.join("|", Strings_PASSWORDS));
        long startTime = System.nanoTime();
        final List<Path> filesWithObviousStringss = stream
                .filter(path -> {
                    try(Scanner s = new Scanner(path)) {
                        return s.findWithinHorizon(pattern, 0) != null;
                    } catch(IOException ex) {
                        throw new UncheckedIOException(ex);
                    }
                })
                .collect(Collectors.toList());
        System.out.println("Time taken = "
            + TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()-startTime) + " seconds");
}

此解决方案的一个重要特性是它不会读取整个文件,而是在第一次遇到的匹配时停止。此外,它不处理行边界,它适用于您正在寻找的单词,因为它们从不包含换行符。

在分析了findWithinHorizon操作后,我认为对于较大的文件,逐行处理可能更好,所以,你可以试试

try(Stream<Path> stream = Files.walk(Paths.get(FILES_DIRECTORY))
                               .filter(Files::isRegularFile)) {
        Pattern pattern = Pattern.compile(String.join("|", Strings_PASSWORDS));
        long startTime = System.nanoTime();
        final List<Path> filesWithObviousStringss = stream
                .filter(path -> {
                    try(Stream<String> s = Files.lines(path)) {
                        return s.anyMatch(pattern.asPredicate());
                    } catch(IOException ex) {
                        throw new UncheckedIOException(ex);
                    }
                })
                .collect(Collectors.toList());
        System.out.println("Time taken = "
            + TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()-startTime) + " seconds");
}

代替。

您也可以尝试将流转换为并行模式,例如

try(Stream<Path> stream = Files.walk(Paths.get(FILES_DIRECTORY))
                               .filter(Files::isRegularFile)) {
        Pattern pattern = Pattern.compile(String.join("|", Strings_PASSWORDS));
        long startTime = System.nanoTime();
        final List<Path> filesWithObviousStringss = stream
                .parallel()
                .filter(path -> {
                    try(Stream<String> s = Files.lines(path)) {
                        return s.anyMatch(pattern.asPredicate());
                    } catch(IOException ex) {
                        throw new UncheckedIOException(ex);
                    }
                })
                .collect(Collectors.toList());
        System.out.println("Time taken = "
            + TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()-startTime) + " seconds");
}

很难预测这是否有益,因为在大多数情况下,I / O主导着这样的操作。

另一答案

我从未使用过Unix4j,但Java现在提供了很好的文件API。此外,Unix4j#grep似乎返回所有找到的匹配(因为你正在使用.toStringResult().isEmpty()),而你似乎只需要知道是否找到了至少一个匹配(这意味着你应该能够在找到一个匹配后中断) 。也许这个库提供了另一种方法,可以更好地满足您的需求,例如像#contains这样的东西?如果不使用Unix4jStream#anyMatch可能是一个很好的候选人。如果您想与您的比较,这是一个vanilla Java解决方案:

private boolean lineContainsObviousStrings(String line) {
  return Strings_PASSWORDS // <-- weird naming BTW
    .stream()
    .anyMatch(line::contains);
}

private boolean fileContainsObviousStrings(Path path) {
  try (Stream<String> stream = Files.lines(path)) {
    return stream.anyMatch(this::lineContainsObviousStrings);
  }
}

public List<Path> findFilesContainingObviousStrings() {
  Instant startTime = Instant.now();
  try (Stream<Path> stream = Files.walk(Paths.get(FILES_DIRECTORY))) {
    return stream
      .filter(FilePredicates.isFileAndNotDirectory())
      .filter(this::fileContainsObviousStrings)
      .collect(Collectors.toList());
  } finally {
    Instant endTime = Instant.now();
    System.out.println("Time taken = " + Duration.between(startTime, endTime).getSeconds() + " seconds");
  }
}
另一答案

请尝试一下(如果可能的话),我很好奇它如何对你的文件执行。

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOEx

以上是关于数千个文件中的模式匹配的主要内容,如果未能解决你的问题,请参考以下文章

详解 Scala 模式匹配

关于字符串精确匹配

数千个值的 Redshift IN 条件

将一个文件中的标头值匹配到 R 中的文件列表

如何改进 Python 中列表的模式匹配

使用终端复制文件,Mac