web-dev-qa-db-fra.com

Comment créer un flux de correspondances de regex?

J'essaie d'analyser l'entrée standard et d'extraire chaque chaîne qui correspond à un modèle spécifique, de compter le nombre d'occurrences de chaque correspondance et d'imprimer les résultats par ordre alphabétique. Ce problème semble être une bonne correspondance pour l'API Streams, mais je ne trouve pas de moyen concis pour créer un flux de correspondances à partir d'un Matcher.

J'ai corrigé ce problème en implémentant un itérateur sur les correspondances et en l'enveloppant dans un flux, mais le résultat n'est pas très lisible. Comment puis-je créer un flux de correspondances de regex sans introduire de classes supplémentaires?

public class PatternCounter
{
    static private class MatcherIterator implements Iterator<String> {
        private final Matcher matcher;
        public MatcherIterator(Matcher matcher) {
            this.matcher = matcher;
        }
        public boolean hasNext() {
            return matcher.find();
        }
        public String next() {
            return matcher.group(0);
        }
    }

    static public void main(String[] args) throws Throwable {
        Pattern pattern = Pattern.compile("[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)");

        new TreeMap<String, Long>(new BufferedReader(new InputStreamReader(System.in))
            .lines().map(line -> {
                Matcher matcher = pattern.matcher(line);
                return StreamSupport.stream(
                        Spliterators.spliteratorUnknownSize(new MatcherIterator(matcher), Spliterator.ORDERED), false);
            }).reduce(Stream.empty(), Stream::concat).collect(groupingBy(o -> o, counting()))
        ).forEach((k, v) -> {
            System.out.printf("%s\t%s\n",k,v);
        });
    }
}
21
Alfredo Diaz

Eh bien, dans Java 8, il y a Pattern.splitAsStream qui fournira un flux d'éléments divisé par un motif delimiter, mais malheureusement aucune méthode de support pour obtenir un flux de matches.

Si vous voulez implémenter un tel Stream, je vous recommande de mettre en œuvre Spliterator directement plutôt que de mettre en place et d'encapsuler un Iterator. Iterator vous est peut-être plus familier, mais implémenter un simple Spliterator est simple:

final class MatchItr extends Spliterators.AbstractSpliterator<String> {
    private final Matcher matcher;
    MatchItr(Matcher m) {
        super(m.regionEnd()-m.regionStart(), ORDERED|NONNULL);
        matcher=m;
    }
    public boolean tryAdvance(Consumer<? super String> action) {
        if(!matcher.find()) return false;
        action.accept(matcher.group());
        return true;
    }
}

Vous pouvez toutefois envisager de remplacer forEachRemaining par une boucle simple.


Si je comprends bien votre tentative, la solution devrait ressembler davantage à:

Pattern pattern = Pattern.compile(
                 "[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)");

try(BufferedReader br=new BufferedReader(System.console().reader())) {

    br.lines()
      .flatMap(line -> StreamSupport.stream(new MatchItr(pattern.matcher(line)), false))
      .collect(Collectors.groupingBy(o->o, TreeMap::new, Collectors.counting()))
      .forEach((k, v) -> System.out.printf("%s\t%s\n",k,v));
}

Java 9 fournit une méthode Stream<MatchResult> results() directement sur Matcher. Mais pour trouver des correspondances dans un flux, il existe une méthode encore plus pratique sur Scanner . Avec cela, la mise en œuvre simplifie à

try(Scanner s = new Scanner(System.console().reader())) {
    s.findAll(pattern)
     .collect(Collectors.groupingBy(MatchResult::group,TreeMap::new,Collectors.counting()))
     .forEach((k, v) -> System.out.printf("%s\t%s\n",k,v));
}

Cette réponse contient un back-port de Scanner.findAll qui peut être utilisé avec Java 8.

24
Holger

Hors de la solution de Holger, nous pouvons prendre en charge des opérations Matcher arbitraires (telles que l’obtention du groupe n th) en demandant à l’utilisateur de fournir une opération Function<Matcher, String>. Nous pouvons également masquer la Spliterator en tant que détail d'implémentation, afin que les appelants puissent simplement travailler directement avec la Stream. En règle générale, StreamSupport devrait être utilisé par le code de la bibliothèque plutôt que par les utilisateurs.

public class MatcherStream {
  private MatcherStream() {}

  public static Stream<String> find(Pattern pattern, CharSequence input) {
    return findMatches(pattern, input).map(MatchResult::group);
  }

  public static Stream<MatchResult> findMatches(
      Pattern pattern, CharSequence input) {
    Matcher matcher = pattern.matcher(input);

    Spliterator<MatchResult> spliterator = new Spliterators.AbstractSpliterator<MatchResult>(
        Long.MAX_VALUE, Spliterator.ORDERED|Spliterator.NONNULL) {
      @Override
      public boolean tryAdvance(Consumer<? super MatchResult> action) {
        if(!matcher.find()) return false;
        action.accept(matcher.toMatchResult());
        return true;
      }};

    return StreamSupport.stream(spliterator, false);
  }
}

Vous pouvez ensuite l'utiliser comme ceci:

MatcherStream.find(Pattern.compile("\\w+"), "foo bar baz").forEach(System.out::println);

Ou pour votre tâche spécifique (emprunter à nouveau à Holger):

try(BufferedReader br = new BufferedReader(System.console().reader())) {
  br.lines()
    .flatMap(line -> MatcherStream.find(pattern, line))
    .collect(Collectors.groupingBy(o->o, TreeMap::new, Collectors.counting()))
    .forEach((k, v) -> System.out.printf("%s\t%s\n", k, v));
}
4
dimo414

Si vous souhaitez utiliser une variable Scanner avec des expressions rationnelles à l'aide de la méthode findWithinHorizon, vous pouvez également convertir une expression régulière en un flux de chaînes ..__ Nous utilisons ici un constructeur de flux très pratique lors d'une boucle while classique.

Voici un exemple:

private Stream<String> extractRulesFrom(String text, Pattern pattern, int group) {
    Stream.Builder<String> builder = Stream.builder();
    try(Scanner scanner = new Scanner(text)) {
        while (scanner.findWithinHorizon(pattern, 0) != null) {
            builder.accept(scanner.match().group(group));
        }
    }
    return builder.build();
} 
0
gil.fernandes