Сопоставление шаблонов в тысячах файлов - PullRequest
0 голосов
/ 28 августа 2018

У меня есть шаблон регулярных выражений, например welcome1|welcome2|changeme ..., который мне нужно искать в тысячах файлов (от 100 до 8000), размером от 1 КБ до 24 МБ каждый.

Я хотел бы знать, есть ли более быстрый способ сопоставления с образцом, чем выполнение того, что я пробовал.

Окружающая среда:

  1. JDK 1,8
  2. Windows 10
  3. Библиотека Unix4j

Вот что я пытался до сих пор

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 файлов.

Grep для mysys2 / mingw64 занимает около 15 секунд, а exec('grep...') в файле node.js - около 12 секунд.

Я выбрал Unix4j, потому что он предоставляет собственный grep java и чистый код.

Есть ли способ добиться лучших результатов в Java, который мне, к сожалению, не хватает?

Ответы [ 4 ]

0 голосов
/ 28 августа 2018

Основной причиной того, что нативные инструменты могут обрабатывать такие текстовые файлы намного быстрее, является их предположение об одном конкретном наборе символов, особенно когда он имеет 8-битовое кодирование на основе ASCII, тогда как 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 ГиБ. Хотя это решение можно расширить для поддержки файлов большего размера, я бы не стал добавлять это усложнение, если оно действительно не требуется.

0 голосов
/ 28 августа 2018

Я не знаю, что предоставляет «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");
}

Трудно предсказать, будет ли это выгодно, поскольку в большинстве случаев операции ввода-вывода доминируют в такой операции.

0 голосов
/ 28 августа 2018

Пожалуйста, попробуйте это тоже (если это возможно), мне интересно, как это работает с вашими файлами.

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Filescan {

    public static void main(String[] args) throws IOException {
        Filescan sc = new Filescan();
        sc.findWords("src/main/resources/files", new String[]{"author", "book"}, true);
    }

    // kind of Tuple/Map.Entry
    static class Pair<K,V>{
        final K key;
        final V value;

        Pair(K key, V value){
            this.key = key;
            this.value = value;
        }

        @Override
        public String toString() {
            return key + " " + value;
        }
    }

    public void findWords(String directory, String[] words, boolean ignorecase) throws IOException{

        final String[] searchWords = ignorecase ? toLower(words) : words;

        try (Stream<Path> stream =     Files.walk(Paths.get(directory)).filter(Files::isRegularFile)) {
            long startTime = System.nanoTime();
            List<Pair<Path,Map<String, List<Integer>>>> result = stream
                    // you can test it with parallel execution, maybe it is faster
                    .parallel()
                    // searching
                    .map(path -> findWordsInFile(path, searchWords, ignorecase))
                    // filtering out empty optionals
                    .filter(Optional::isPresent)
                    // unwrap optionals
                    .map(Optional::get).collect(Collectors.toList());
            System.out.println("Time taken = " +     TimeUnit.NANOSECONDS.toSeconds(System.nanoTime()
                            - startTime) + " seconds");
            System.out.println("result:");
            result.forEach(System.out::println);
        }
    }

    private String[] toLower(String[] words) {
        String[] ret = new String[words.length];
        for (int i = 0; i < words.length; i++) {
            ret[i] = words[i].toLowerCase();
        }
        return ret;
    }

    private static Optional<Pair<Path,Map<String, List<Integer>>>>     findWordsInFile(Path path, String[] words, boolean ignorecase) {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(path.toFile())))) {
            String line = br.readLine();
            line = ignorecase & line != null ? line.toLowerCase() : line;
            Map<String, List<Integer>> map = new HashMap<>();
            int linecount = 0;
            while(line != null){
                for (String word : words) {
                    if(line.contains(word)){
                        if(!map.containsKey(word)){
                            map.put(word, new ArrayList<Integer>());
                        }
                        map.get(word).add(linecount);
                    }
                }
                line = br.readLine();
                line = ignorecase & line != null ? line.toLowerCase() : line;
                linecount++;
            }
            if(map.isEmpty()){
                // returning empty optional when nothing in the map
                return Optional.empty();
            }else{
                // returning a path-map pair with the words and the rows where each word has been found
                return Optional.of(new Pair<Path,Map<String, List<Integer>>>(path, map));
            }
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }    
}
0 голосов
/ 28 августа 2018

Я еще никогда не использовал Unix4j, но в настоящее время Java также предоставляет хорошие файловые API. Кроме того, Unix4j#grep, кажется, возвращает все найденные совпадения (поскольку вы используете .toStringResult().isEmpty()), в то время как вам, кажется, нужно просто знать, был ли найден хотя бы один совпадение (что означает, что вы должны иметь возможность перерыв как только будет найдено одно совпадение) Может быть, эта библиотека предоставляет другой метод, который мог бы лучше удовлетворить ваши потребности, например, что-то вроде #contains? Без использования Unix4j, Stream#anyMatch может быть хорошим кандидатом здесь. Вот ванильное решение 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");
  }
}
...