web-dev-qa-db-fra.com

Rechercher un modèle dans des fichiers avec Java 8

considère que j'ai un fichier comme (juste un extrait)

name: 'foobar'

J'aime récupérer foobar lorsque je découvre la ligne avec name.

Mon approche actuelle est

Pattern m = Pattern.compile("name: '(.+)'");
try (Stream<String> lines = Files.lines(ruleFile)) {
    Optional<String> message = lines.filter(m.asPredicate()).findFirst();
    if (message.isPresent()) {
        Matcher matcher = m.matcher(message.get());
        matcher.find();
        String group = matcher.group(1);
        System.out.println(group);
    }
}

qui n'a pas l'air bien. L'utilisation excessive du motif et du matcher semble erronée.

Y a-t-il un moyen plus facile/meilleur? Surtout si j'ai plusieurs clés, j'aime bien chercher comme ça?

13
Emerson Cod

Je m'attendrais à quelque chose de plus semblable à ceci, pour éviter de faire correspondre le motif deux fois:

Pattern p = Pattern.compile("name: '([^']*)'");
lines.map(p::matcher)
     .filter(Matcher::matches)
     .findFirst()
     .ifPresent(matcher -> System.out.println(matcher.group(1)));

Autrement dit, pour le matcher de chaque chaîne, obtenez le premier correspondant, pour celui-ci, imprimez le premier groupe.

23
khelwood

Voici à quoi ressemblera probablement la solution Java 9:

Matcher m = Pattern.compile("name: '(.+)'").matcher("");
try(Stream<String> lines = Files.lines(ruleFile)) {
    lines.flatMap(line -> m.reset(line).results().limit(1))
         .forEach(mr -> System.out.println(mr.group(1)));
}

Il utilise la méthode Matcher.results() qui renvoie un flux de toutes les correspondances. La combinaison d'un flux de lignes avec un flux de correspondances via flatMap nous permet de traiter toutes les correspondances d'un fichier. Étant donné que votre code d'origine ne traite que la première correspondance d'une ligne, j'ai simplement ajouté un limit(1) aux correspondances de chaque ligne pour obtenir le même comportement.

Malheureusement, cette fonctionnalité est absente de Java 8, cependant, se faufiler dans les versions à venir permet de se faire une idée de ce à quoi une solution provisoire pourrait ressembler:

Matcher m = Pattern.compile("name: '(.+)'").matcher("");
try(Stream<String> lines = Files.lines(ruleFile)) {
    lines.flatMap(line -> m.reset(line).find()? Stream.of(m.toMatchResult()): null)
         .forEach(mr -> System.out.println(mr.group(1)));
}

Pour simplifier la création de sous-flux, cette solution utilise uniquement la première correspondance et crée un flux à élément unique.

Notez toutefois que, avec le motif 'name: '(.+)' de la question, peu importe si nous limitons le nombre de correspondances, car .+ correspondra à tous les caractères jusqu’au dernier ' de suivi de la ligne. Une autre correspondance est donc impossible. Les choses sont différentes quand on utilise un quantificateur réticent comme avec name: '(.*?)' qui consomme jusqu’à next' plutôt que last ou s’interdit de sauter explicitement ', comme avec name: '([^']*)'.


Les solutions ci-dessus utilisent une Matcher partagée qui fonctionne bien avec une utilisation à un seul thread (et il est peu probable que le traitement en parallèle bénéficie d'un traitement parallèle). Mais si vous voulez être du côté thread-safe, vous ne pouvez partager qu'une Pattern et créer une Matcher au lieu d'appeler m.reset(line):

Pattern pattern = Pattern.compile("name: '(.*)'");
try(Stream<String> lines = Files.lines(ruleFile)) {
    lines.flatMap(line -> pattern.matcher(line).results().limit(1))
         .forEach(mr -> System.out.println(mr.group(1)));
}

resp. avec Java 8

try(Stream<String> lines = Files.lines(ruleFile)) {
    lines.flatMap(line -> {Matcher m=pattern.matcher(line);
                           return m.find()? Stream.of(m.toMatchResult()): null;})
         .forEach(mr -> System.out.println(mr.group(1)));
}

ce qui n’est pas si concis en raison de l’introduction d’une variable locale. Ceci peut être évité par une précédente opération map, mais lorsque nous en sommes à ce stade, tant que nous ne visons qu’une seule correspondance par ligne, nous n’avons pas besoin de flatMap:

try(Stream<String> lines = Files.lines(ruleFile)) {
    lines.map(pattern::matcher).filter(Matcher::find)
         .forEach(m -> System.out.println(m.group(1)));
}

Puisque chaque Matcher est utilisée exactement une fois, de manière non gênante, sa nature mutable ne fait pas de mal ici et une conversion en une MatchResult immuable devient inutile.

Cependant, ces solutions ne peuvent pas être dimensionnées pour traiter plusieurs correspondances par ligne, si cela devient nécessaire…

7
Holger

La réponse de @khelwood aboutit à la création répétée d'un nouvel objet Matcher, ce qui peut être une source d'inefficacité si de longs fichiers sont analysés.

La solution suivante crée le matcher une seule fois et le réutilise pour chaque ligne du fichier.

Pattern p = Pattern.compile("name: '([^']*)'");
Matcher matcher = p.matcher(""); // Create a matcher for the pattern

Files.lines(ruleFile)
    .map(matcher::reset)         // Reuse the matcher object
    .filter(Matcher::matches)
    .findFirst()
    .ifPresent(m -> System.out.println(m.group(1)));

Avertissement - Hack Ahead suspect

L'étape du pipeline .map(matcher::reset) est l'endroit où la magie/piratage se produit. Il appelle effectivement matcher.reset(line), qui réinitialise matcher pour effectuer la correspondance suivante sur la ligne lue dans le fichier et se retourne lui-même pour permettre l'enchaînement des appels. L'opérateur de flux .map(...) voit ceci comme un mappage de la ligne vers un objet Matcher, mais en réalité, nous continuons à mapper vers le même objet matcher à chaque fois, en violant toutes sortes de règles concernant les effets secondaires, etc.

Bien sûr, ce ne peut pas être utilisé pour des flux parallèles, mais heureusement, la lecture d’un fichier est intrinsèquement séquentielle.

Piratage ou optimisation? Je suppose que les votes haut/bas décideront.

0
AJNeufeld