Esiste un equivalente di java.util.regex per i modelli di tipo "glob"?


87

Esiste una libreria standard (preferibilmente Apache Commons o similmente non virale) per eseguire corrispondenze di tipo "glob" in Java? Quando ho dovuto fare una cosa simile in Perl una volta, ho cambiato solo " ." in " \.", " *" in " .*" e " ?" in " ." e quel genere di cose, ma mi chiedo se qualcuno ha fatto il lavora per me.

Domanda simile: crea regex dall'espressione glob


GlobCompiler / GlobEngine , di Jakarta ORO , sembra promettente. È disponibile con la licenza Apache.
Steve Trout

Potresti fare un esempio preciso di quello che vuoi fare?
Thorbjørn Ravn Andersen

Quello che voglio fare (o meglio quello che vuole fare il mio cliente) è abbinare cose come " -2009 /" o "* rss " negli URL. Per lo più è piuttosto banale convertire in regex, ma mi chiedevo se ci fosse un modo più semplice.
Paul Tomblin

Raccomando il globing di file in stile Ant in quanto sembra essere diventato il globing canonico nel mondo Java. Vedi la mia risposta per maggiori dettagli: stackoverflow.com/questions/1247772/… .
Adam Gent,

1
@ BradMace, correlato ma la maggior parte delle risposte presume che tu stia attraversando un albero di directory. Tuttavia, se qualcuno sta ancora cercando come eseguire la corrispondenza in stile glob di stringhe arbitrarie, probabilmente dovrebbe guardare anche in quella risposta.
Paul Tomblin

Risposte:


47

Non c'è nulla di integrato, ma è piuttosto semplice convertire qualcosa di simile a glob in una regex:

public static String createRegexFromGlob(String glob)
{
    String out = "^";
    for(int i = 0; i < glob.length(); ++i)
    {
        final char c = glob.charAt(i);
        switch(c)
        {
        case '*': out += ".*"; break;
        case '?': out += '.'; break;
        case '.': out += "\\."; break;
        case '\\': out += "\\\\"; break;
        default: out += c;
        }
    }
    out += '$';
    return out;
}

questo funziona per me, ma non sono sicuro che copra lo "standard" glob, se ce n'è uno :)

Aggiornamento di Paul Tomblin: ho trovato un programma perl che esegue la conversione glob, e adattandolo a Java mi ritrovo con:

    private String convertGlobToRegEx(String line)
    {
    LOG.info("got line [" + line + "]");
    line = line.trim();
    int strLen = line.length();
    StringBuilder sb = new StringBuilder(strLen);
    // Remove beginning and ending * globs because they're useless
    if (line.startsWith("*"))
    {
        line = line.substring(1);
        strLen--;
    }
    if (line.endsWith("*"))
    {
        line = line.substring(0, strLen-1);
        strLen--;
    }
    boolean escaping = false;
    int inCurlies = 0;
    for (char currentChar : line.toCharArray())
    {
        switch (currentChar)
        {
        case '*':
            if (escaping)
                sb.append("\\*");
            else
                sb.append(".*");
            escaping = false;
            break;
        case '?':
            if (escaping)
                sb.append("\\?");
            else
                sb.append('.');
            escaping = false;
            break;
        case '.':
        case '(':
        case ')':
        case '+':
        case '|':
        case '^':
        case '$':
        case '@':
        case '%':
            sb.append('\\');
            sb.append(currentChar);
            escaping = false;
            break;
        case '\\':
            if (escaping)
            {
                sb.append("\\\\");
                escaping = false;
            }
            else
                escaping = true;
            break;
        case '{':
            if (escaping)
            {
                sb.append("\\{");
            }
            else
            {
                sb.append('(');
                inCurlies++;
            }
            escaping = false;
            break;
        case '}':
            if (inCurlies > 0 && !escaping)
            {
                sb.append(')');
                inCurlies--;
            }
            else if (escaping)
                sb.append("\\}");
            else
                sb.append("}");
            escaping = false;
            break;
        case ',':
            if (inCurlies > 0 && !escaping)
            {
                sb.append('|');
            }
            else if (escaping)
                sb.append("\\,");
            else
                sb.append(",");
            break;
        default:
            escaping = false;
            sb.append(currentChar);
        }
    }
    return sb.toString();
}

Sto modificando questa risposta piuttosto che crearne una mia perché questa risposta mi ha messo sulla strada giusta.


1
Sì, è praticamente la soluzione che ho trovato l'ultima volta che ho dovuto farlo (in Perl) ma mi chiedevo se ci fosse qualcosa di più elegante. Penso che farò a modo tuo.
Paul Tomblin

1
In realtà, ho trovato un'implementazione migliore in Perl che posso adattare a Java su kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html
Paul Tomblin

Non potresti usare una regex sostitutiva per trasformare un glob in una regex?
Tim Sylvester

1
Le linee nella parte superiore che rimuovono il '*' iniziale e finale devono essere rimosse per java poiché String.match si confronta solo con l'intera stringa
KitsuneYMG

10
FYI: lo standard per il "globbing" è il linguaggio della shell POSIX - opengroup.org/onlinepubs/009695399/utilities/…
Stephen C

62

Anche il globbing è previsto per l' implementazione in Java 7.

Vedere FileSystem.getPathMatcher(String)e il tutorial "Ricerca di file" .


23
Meravigliosa. Ma come mai questa implementazione è limitata agli oggetti "Path"?!? Nel mio caso, voglio far corrispondere l'URI ...
Yves Martin

3
Esaminando la fonte di sun.nio, la corrispondenza glob sembra essere implementata da Globs.java . Sfortunatamente, questo è scritto specificamente per i percorsi del filesystem, quindi non può essere usato per tutte le stringhe (fa alcune supposizioni sui separatori di percorso e sui caratteri illegali). Ma potrebbe essere un utile punto di partenza.
Neil Traft

33

Grazie a tutti qui per i loro contributi. Ho scritto una conversione più completa rispetto a qualsiasi delle risposte precedenti:

/**
 * Converts a standard POSIX Shell globbing pattern into a regular expression
 * pattern. The result can be used with the standard {@link java.util.regex} API to
 * recognize strings which match the glob pattern.
 * <p/>
 * See also, the POSIX Shell language:
 * http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_13_01
 * 
 * @param pattern A glob pattern.
 * @return A regex pattern to recognize the given glob pattern.
 */
public static final String convertGlobToRegex(String pattern) {
    StringBuilder sb = new StringBuilder(pattern.length());
    int inGroup = 0;
    int inClass = 0;
    int firstIndexInClass = -1;
    char[] arr = pattern.toCharArray();
    for (int i = 0; i < arr.length; i++) {
        char ch = arr[i];
        switch (ch) {
            case '\\':
                if (++i >= arr.length) {
                    sb.append('\\');
                } else {
                    char next = arr[i];
                    switch (next) {
                        case ',':
                            // escape not needed
                            break;
                        case 'Q':
                        case 'E':
                            // extra escape needed
                            sb.append('\\');
                        default:
                            sb.append('\\');
                    }
                    sb.append(next);
                }
                break;
            case '*':
                if (inClass == 0)
                    sb.append(".*");
                else
                    sb.append('*');
                break;
            case '?':
                if (inClass == 0)
                    sb.append('.');
                else
                    sb.append('?');
                break;
            case '[':
                inClass++;
                firstIndexInClass = i+1;
                sb.append('[');
                break;
            case ']':
                inClass--;
                sb.append(']');
                break;
            case '.':
            case '(':
            case ')':
            case '+':
            case '|':
            case '^':
            case '$':
            case '@':
            case '%':
                if (inClass == 0 || (firstIndexInClass == i && ch == '^'))
                    sb.append('\\');
                sb.append(ch);
                break;
            case '!':
                if (firstIndexInClass == i)
                    sb.append('^');
                else
                    sb.append('!');
                break;
            case '{':
                inGroup++;
                sb.append('(');
                break;
            case '}':
                inGroup--;
                sb.append(')');
                break;
            case ',':
                if (inGroup > 0)
                    sb.append('|');
                else
                    sb.append(',');
                break;
            default:
                sb.append(ch);
        }
    }
    return sb.toString();
}

E i test unitari per dimostrare che funziona:

/**
 * @author Neil Traft
 */
public class StringUtils_ConvertGlobToRegex_Test {

    @Test
    public void star_becomes_dot_star() throws Exception {
        assertEquals("gl.*b", StringUtils.convertGlobToRegex("gl*b"));
    }

    @Test
    public void escaped_star_is_unchanged() throws Exception {
        assertEquals("gl\\*b", StringUtils.convertGlobToRegex("gl\\*b"));
    }

    @Test
    public void question_mark_becomes_dot() throws Exception {
        assertEquals("gl.b", StringUtils.convertGlobToRegex("gl?b"));
    }

    @Test
    public void escaped_question_mark_is_unchanged() throws Exception {
        assertEquals("gl\\?b", StringUtils.convertGlobToRegex("gl\\?b"));
    }

    @Test
    public void character_classes_dont_need_conversion() throws Exception {
        assertEquals("gl[-o]b", StringUtils.convertGlobToRegex("gl[-o]b"));
    }

    @Test
    public void escaped_classes_are_unchanged() throws Exception {
        assertEquals("gl\\[-o\\]b", StringUtils.convertGlobToRegex("gl\\[-o\\]b"));
    }

    @Test
    public void negation_in_character_classes() throws Exception {
        assertEquals("gl[^a-n!p-z]b", StringUtils.convertGlobToRegex("gl[!a-n!p-z]b"));
    }

    @Test
    public void nested_negation_in_character_classes() throws Exception {
        assertEquals("gl[[^a-n]!p-z]b", StringUtils.convertGlobToRegex("gl[[!a-n]!p-z]b"));
    }

    @Test
    public void escape_carat_if_it_is_the_first_char_in_a_character_class() throws Exception {
        assertEquals("gl[\\^o]b", StringUtils.convertGlobToRegex("gl[^o]b"));
    }

    @Test
    public void metachars_are_escaped() throws Exception {
        assertEquals("gl..*\\.\\(\\)\\+\\|\\^\\$\\@\\%b", StringUtils.convertGlobToRegex("gl?*.()+|^$@%b"));
    }

    @Test
    public void metachars_in_character_classes_dont_need_escaping() throws Exception {
        assertEquals("gl[?*.()+|^$@%]b", StringUtils.convertGlobToRegex("gl[?*.()+|^$@%]b"));
    }

    @Test
    public void escaped_backslash_is_unchanged() throws Exception {
        assertEquals("gl\\\\b", StringUtils.convertGlobToRegex("gl\\\\b"));
    }

    @Test
    public void slashQ_and_slashE_are_escaped() throws Exception {
        assertEquals("\\\\Qglob\\\\E", StringUtils.convertGlobToRegex("\\Qglob\\E"));
    }

    @Test
    public void braces_are_turned_into_groups() throws Exception {
        assertEquals("(glob|regex)", StringUtils.convertGlobToRegex("{glob,regex}"));
    }

    @Test
    public void escaped_braces_are_unchanged() throws Exception {
        assertEquals("\\{glob\\}", StringUtils.convertGlobToRegex("\\{glob\\}"));
    }

    @Test
    public void commas_dont_need_escaping() throws Exception {
        assertEquals("(glob,regex),", StringUtils.convertGlobToRegex("{glob\\,regex},"));
    }

}

Grazie per questo codice, Neil! Saresti disposto a dargli una licenza open source?
Steven

1
Con la presente concedo che il codice in questa risposta è di pubblico dominio.
Neil Traft

Devo fare qualcos'altro? :-P
Neil Traft

9

Ci sono un paio di librerie che eseguono la corrispondenza di pattern tipo Glob che sono più moderne di quelle elencate:

Theres Ants Directory Scanner e Springs AntPathMatcher

Consiglio entrambe le altre soluzioni poiché Ant Style Globbing è praticamente diventata la sintassi glob standard nel mondo Java (Hudson, Spring, Ant e penso Maven).


1
Ecco le coordinate di Maven per l'artefatto con AntPathMatcher: search.maven.org/… E alcuni test con utilizzo di esempio: github.com/spring-projects/spring-framework/blob/master/…
seanf

E puoi personalizzare il carattere "percorso" ... quindi è utile per cose diverse dai percorsi ...
Michael Wiles,

7

Di recente ho dovuto farlo e ho usato \Qe \Eper sfuggire al modello glob:

private static Pattern getPatternFromGlob(String glob) {
  return Pattern.compile(
    "^" + Pattern.quote(glob)
            .replace("*", "\\E.*\\Q")
            .replace("?", "\\E.\\Q") 
    + "$");
}

4
Non si interromperà se c'è un \ E da qualche parte nella stringa?
jmo

@ jmo, sì, ma puoi aggirarlo pre-elaborando la globvariabile con glob = Pattern.quote (glob), che credo gestisca questi casi limite. In tal caso, tuttavia, non è necessario anteporre e aggiungere la prima e l'ultima \\ Q e \\ E.
Kimball Robinson

2
@jmo Ho corretto l'esempio per utilizzare Pattern.quote ().
dimo414

5

Questa è una semplice implementazione Glob che gestisce * e? nel modello

public class GlobMatch {
    private String text;
    private String pattern;

    public boolean match(String text, String pattern) {
        this.text = text;
        this.pattern = pattern;

        return matchCharacter(0, 0);
    }

    private boolean matchCharacter(int patternIndex, int textIndex) {
        if (patternIndex >= pattern.length()) {
            return false;
        }

        switch(pattern.charAt(patternIndex)) {
            case '?':
                // Match any character
                if (textIndex >= text.length()) {
                    return false;
                }
                break;

            case '*':
                // * at the end of the pattern will match anything
                if (patternIndex + 1 >= pattern.length() || textIndex >= text.length()) {
                    return true;
                }

                // Probe forward to see if we can get a match
                while (textIndex < text.length()) {
                    if (matchCharacter(patternIndex + 1, textIndex)) {
                        return true;
                    }
                    textIndex++;
                }

                return false;

            default:
                if (textIndex >= text.length()) {
                    return false;
                }

                String textChar = text.substring(textIndex, textIndex + 1);
                String patternChar = pattern.substring(patternIndex, patternIndex + 1);

                // Note the match is case insensitive
                if (textChar.compareToIgnoreCase(patternChar) != 0) {
                    return false;
                }
        }

        // End of pattern and text?
        if (patternIndex + 1 >= pattern.length() && textIndex + 1 >= text.length()) {
            return true;
        }

        // Go on to match the next character in the pattern
        return matchCharacter(patternIndex + 1, textIndex + 1);
    }
}

5

Simile a Tony Edgecombe 's risposta , ecco una Globber breve e semplice che supporta *e ?senza l'utilizzo di espressioni regolari, se qualcuno ha bisogno di uno.

public static boolean matches(String text, String glob) {
    String rest = null;
    int pos = glob.indexOf('*');
    if (pos != -1) {
        rest = glob.substring(pos + 1);
        glob = glob.substring(0, pos);
    }

    if (glob.length() > text.length())
        return false;

    // handle the part up to the first *
    for (int i = 0; i < glob.length(); i++)
        if (glob.charAt(i) != '?' 
                && !glob.substring(i, i + 1).equalsIgnoreCase(text.substring(i, i + 1)))
            return false;

    // recurse for the part after the first *, if any
    if (rest == null) {
        return glob.length() == text.length();
    } else {
        for (int i = glob.length(); i <= text.length(); i++) {
            if (matches(text.substring(i), rest))
                return true;
        }
        return false;
    }
}

1
Ottima risposta tihi! Questo è abbastanza semplice da capire a una lettura veloce e non troppo sconcertante :-)
lmat - Reinstate Monica

3

Potrebbe essere un approccio leggermente hacker. L'ho capito dal Files.newDirectoryStream(Path dir, String glob)codice di NIO2 . Fai attenzione che ogni nuovo Pathoggetto corrispondente venga creato. Finora sono stato in grado di testarlo solo su Windows FS, tuttavia, credo che dovrebbe funzionare anche su Unix.

// a file system hack to get a glob matching
PathMatcher matcher = ("*".equals(glob)) ? null
    : FileSystems.getDefault().getPathMatcher("glob:" + glob);

if ("*".equals(glob) || matcher.matches(Paths.get(someName))) {
    // do you stuff here
}

UPDATE Funziona su entrambi: Mac e Linux.



0

Molto tempo fa stavo facendo un enorme filtraggio del testo basato su glob, quindi ho scritto una piccola parte di codice (15 righe di codice, nessuna dipendenza oltre JDK). Gestisce solo "*" (era sufficiente per me), ma può essere facilmente esteso per "?". È molte volte più veloce della regexp precompilata, non richiede alcuna pre-compilazione (essenzialmente si tratta di un confronto stringa vs stringa ogni volta che viene trovato il pattern).

Codice:

  public static boolean miniglob(String[] pattern, String line) {
    if (pattern.length == 0) return line.isEmpty();
    else if (pattern.length == 1) return line.equals(pattern[0]);
    else {
      if (!line.startsWith(pattern[0])) return false;
      int idx = pattern[0].length();
      for (int i = 1; i < pattern.length - 1; ++i) {
        String patternTok = pattern[i];
        int nextIdx = line.indexOf(patternTok, idx);
        if (nextIdx < 0) return false;
        else idx = nextIdx + patternTok.length();
      }
      if (!line.endsWith(pattern[pattern.length - 1])) return false;
      return true;
    }
  }

Utilizzo:

  public static void main(String[] args) {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    try {
      // read from stdin space separated text and pattern
      for (String input = in.readLine(); input != null; input = in.readLine()) {
        String[] tokens = input.split(" ");
        String line = tokens[0];
        String[] pattern = tokens[1].split("\\*+", -1 /* want empty trailing token if any */);

        // check matcher performance
        long tm0 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; ++i) {
          miniglob(pattern, line);
        }
        long tm1 = System.currentTimeMillis();
        System.out.println("miniglob took " + (tm1-tm0) + " ms");

        // check regexp performance
        Pattern reptn = Pattern.compile(tokens[1].replace("*", ".*"));
        Matcher mtchr = reptn.matcher(line);
        tm0 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; ++i) {
          mtchr.matches();
        }
        tm1 = System.currentTimeMillis();
        System.out.println("regexp took " + (tm1-tm0) + " ms");

        // check if miniglob worked correctly
        if (miniglob(pattern, line)) {
          System.out.println("+ >" + line);
        }
        else {
          System.out.println("- >" + line);
        }
      }
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }

Copia / incolla da qui


Dato che sono solo 15 righe, dovresti includerlo qui nel caso in cui la pagina collegata vada giù.
Raniz

0

La precedente soluzione di Vincent Robert / dimo414 si basa Pattern.quote()sull'implementazione in termini di \Q... \E, che non è documentato nell'API e quindi potrebbe non essere il caso per altre / future implementazioni Java. La seguente soluzione rimuove la dipendenza dall'implementazione facendo l'escape di tutte le occorrenze \Einvece di usare quote(). Attiva anche DOTALLmode ( (?s)) nel caso in cui la stringa da trovare contiene delle nuove righe.

    public static Pattern globToRegex(String glob)
    {
        return Pattern.compile(
            "(?s)^\\Q" +
            glob.replace("\\E", "\\E\\\\E\\Q")
                .replace("*", "\\E.*\\Q")
                .replace("?", "\\E.\\Q") +
            "\\E$"
        );
    }

-2

A proposito, sembra che tu lo abbia fatto nel modo più duro in Perl

Questo fa il trucco in Perl:

my @files = glob("*.html")
# Or, if you prefer:
my @files = <*.html> 

1
Funziona solo se il glob è per i file corrispondenti. Nel caso di perl, i glob in realtà provenivano da un elenco di indirizzi ip che è stato scritto usando globs per ragioni che non approfondirò, e nel mio caso attuale i globs dovevano corrispondere agli URL.
Paul Tomblin
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.