Чи існує еквівалент java.util.regex для шаблонів типу “glob”?


84

Чи існує стандартна (бажано Apache Commons або подібною невірусна) бібліотека для проведення збігів типу "glob" на Java? Коли мені довелося робити подібне в Perl, я просто змінив усі " ." на " \.", " *" на " .*" і " ?" на " ." і подібні речі, але мені цікаво, чи хтось робив працювати для мене.

Подібне запитання: Створіть регулярний вираз із виразу glob


GlobCompiler / GlobEngine , від Jakarta ORO , виглядає багатообіцяючим. Він доступний за ліцензією Apache.
Стів Траут

Не могли б ви дати точний приклад того, що ви хочете зробити?
Thorbjørn Ravn Andersen

Що я хочу зробити (а точніше те, що хоче зробити мій клієнт) - це відповідати таким речам, як " -2009 /" або "* rss " в URL-адресах. Переважно перетворити на регулярний вираз досить тривіально, але мені було цікаво, чи є простіший спосіб.
Пол Томблін,

Я рекомендую глобінг файлів у стилі Ant, оскільки, схоже, він став канонічним глобусом у світі Java. Докладніше див. У моїй відповіді: stackoverflow.com/questions/1247772/… .
Адам Гент,

1
@BradMace, пов'язані, але більшість відповідей там передбачають, що ви обходите дерево каталогів. Тим не менше, якщо хтось все ще шукає, як виконати відповідність стилю glob довільним рядкам, їм, мабуть, слід заглянути і в цю відповідь.
Пол Томблін,

Відповіді:


46

Тут немає нічого вбудованого, але досить просто перетворити щось на зразок глобуса в регулярний вираз:

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;
}

це працює для мене, але я не впевнений, чи охоплює він глобус "стандарт", якщо такий є :)

Оновлення Пола Томбліна: Я знайшов програму perl, яка виконує перетворення глобусів, і, адаптуючи її до Java, я закінчую:

    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();
}

Я редагую цю відповідь, а не роблю свою, бо ця відповідь поставила мене на правильний шлях.


1
Так, це майже те рішення, яке я придумав востаннє, коли мені доводилось це робити (в Perl), але мені було цікаво, чи є щось більш елегантне. Думаю, я зроблю це по-вашому.
Пол Томблін,

1
Власне, я знайшов кращу реалізацію в Perl, яку я можу адаптувати в Java за адресою kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html
Пол Томблін,

Чи не могли б ви використати заміну регулярного виразу, щоб перетворити глобус на регулярний вираз?
Тім Сільвестр

1
Рядки у верхній частині, які викреслюють провідний і кінцевий '*', потрібно видалити для java, оскільки String.matches відповідає лише цілому рядку
KitsuneYMG

10
FYI: Стандарт "глобінгу" - це мова оболонки POSIX - opengroup.org/onlinepubs/009695399/utilities/…
Стівен С

60

Глобінг є також планується впровадити в Java 7.

Дивіться FileSystem.getPathMatcher(String)та підручник "Пошук файлів" .


23
Чудова. Але чому на землі ця реалізація обмежена об'єктами "Шлях"?!? У моєму випадку я хочу зіставити URI ...
Ів Мартін,

3
Вдивляючись у джерело sun.nio, відповідність глобусу, здається, реалізована Globs.java . На жаль, це написано спеціально для шляхів до файлової системи, тому його не можна використовувати для всіх рядків (це робить деякі припущення щодо роздільників шляхів та незаконних символів). Але це може бути корисною відправною точкою.
Ніл Трафт,

33

Дякуємо всім за внесок. Я написав більш повне перетворення, ніж будь-яка з попередніх відповідей:

/**
 * 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();
}

І модульні тести, щоб довести, що це працює:

/**
 * @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},"));
    }

}

Дякую за цей код, Ніл! Чи хотіли б ви надати йому ліцензію з відкритим кодом?
Стівен

1
Цим я погоджуюсь, що код у цій відповіді є загальнодоступним.
Ніл Трафт,

Чи слід робити щось інше? :-P
Ніл Трафт,

9

Існує кілька бібліотек, які виконують відповідність шаблонам, подібних до Glob, які є більш сучасними, ніж перелічені:

Є сканер каталогів мурашок та джерел AntPathMatcher

Я рекомендую обидва варіанти інших рішень, оскільки Ant Style Globbing в значній мірі став стандартним синтаксисом глобусів у світі Java (Hudson, Spring, Ant і, на мою думку, Maven).


1
Ось координати Maven для артефакту з AntPathMatcher: search.maven.org/… І кілька тестів із використанням зразків: github.com/spring-projects/spring-framework/blob/master/…
seanf,

І ви можете налаштувати символ "шлях" ... тому він корисний для речей, окрім шляхів ...
Майкл Вайлз

7

Нещодавно мені довелося це зробити і використовували, \Qі \Eщоб уникнути глобусу:

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

4
Чи не буде ця перерва, якщо десь у рядку є \ E?
jmo

@jmo, так, але ви можете обійти це, попередньо globобробивши змінну за допомогою glob = Pattern.quote (glob), який, на мою думку, обробляє такі випадки ребер. Однак у цьому випадку вам не потрібно додавати та додавати перше та останнє \\ Q та \\ E.
Кімбол Робінсон,

2
@jmo Я зафіксував приклад, щоб використовувати Pattern.quote ().
dimo414

5

Це проста реалізація Glob, яка обробляє * та? у візерунку

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

Подібно Тоні Edgecombe «s відповідь , ось короткий і простий globber , що підтримує *і ?без використання регулярних виразів, якщо хто -то потребує.

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
Відмінна відповідь tihi! Це досить просто, щоб зрозуміти його при швидкому читанні і не надто бентежить :-)
Обмежена Спокута

3

Це може бути трохи хакерський підхід. Я зрозумів це з NIO2Files.newDirectoryStream(Path dir, String glob) коду . Зверніть увагу, що кожен відповідник Pathстворюється новим об’єктом. Поки що мені вдалося протестувати це лише на Windows FS, однак, я вважаю, що це повинно працювати і на 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 Працює як на Mac, так і на Linux.


2

Я не знаю про "стандартну" реалізацію, але я знаю проект sourceforge, випущений за ліцензією BSD, який реалізував відповідність глобусів для файлів. Він реалізований в одному файлі , можливо, ви можете адаптувати його під свої вимоги.



0

Давно я робив масштабну фільтрацію тексту, керовану глобусом, тому написав невеликий фрагмент коду (15 рядків коду, жодних залежностей, окрім JDK). Він обробляє лише "*" (мені було достатньо), але його можна легко розширити для "?". Це в кілька разів швидше, ніж попередньо скомпільований регулярний вираз, не вимагає жодної попередньої компіляції (по суті, це порівняння рядків проти рядків кожного разу, коли шаблон відповідає).

Код:

  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;
    }
  }

Використання:

  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();
    }
  }

Скопіюйте / вставте звідси


Оскільки це всього 15 рядків, ви повинні включити його сюди на випадок, якщо пов’язана сторінка опуститься.
Раніз

0

Попереднє рішення по Vincent Robert / dimo414 залежить від Pattern.quote()реалізуються в термінах \Q... \E, які не описані в API і , отже , не може бути для інших / майбутніх реалізацій Java. Наступне рішення видаляє цю залежність реалізації, уникаючи всіх випадків \Eзамість використання quote(). Він також активує DOTALLрежим ( (?s)) на випадок, якщо відповідний рядок містить нові рядки.

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

-1

До речі, здається, ніби ти зробив це нелегко в Perl

Це робить фокус у Perl:

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

1
Це працює, лише якщо glob призначений для відповідних файлів. У випадку perl, глобуси насправді походили зі списку ip-адрес, який був написаний за допомогою глобусів з причин, якими я не буду займатись, і в моєму поточному випадку глобуси повинні були відповідати URL-адресам.
Пол Томблін,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.