Có tương đương với java.util.regex cho các mẫu kiểu "hình cầu" không?


84

Có thư viện chuẩn nào (tốt nhất là Apache Commons hoặc thư viện tương tự không có tính lan truyền) để thực hiện các đối sánh kiểu "cầu" trong Java không? Khi tôi phải làm điều tương tự trong Perl một lần, tôi chỉ thay đổi tất cả " ." thành " \.", " *" thành " .*" và " ?" thành " ." và đại loại như vậy, nhưng tôi tự hỏi liệu ai đó đã làm làm việc cho tôi.

Câu hỏi tương tự: Tạo regex từ biểu thức cầu


GlobCompiler / GlobEngine , từ Jakarta ORO , có vẻ đầy hứa hẹn. Nó có sẵn theo Giấy phép Apache.
Steve Trout

Bạn có thể đưa ra một ví dụ chính xác về những gì bạn muốn làm không?
Thorbjørn Ravn Andersen

Những gì tôi muốn làm (hay đúng hơn là những gì khách hàng của tôi muốn làm) là khớp những thứ như " -2009 /" hoặc "* rss " trong url. Phần lớn việc chuyển đổi sang regex khá đơn giản, nhưng tôi tự hỏi liệu có cách nào dễ dàng hơn không.
Paul Tomblin

Tôi khuyên bạn nên định hướng tệp kiểu Ant vì nó dường như đã trở thành găng tay chuẩn trong thế giới Java. Xem câu trả lời của tôi để biết thêm chi tiết: stackoverflow.com/questions/1247772/… .
Adam Gent

1
@BradMace, có liên quan nhưng hầu hết các câu trả lời ở đó đều giả định rằng bạn đang duyệt qua một cây thư mục. Tuy nhiên, nếu ai đó vẫn đang tìm cách thực hiện kết hợp kiểu cầu của các chuỗi tùy ý, họ có thể cũng nên xem câu trả lời đó.
Paul Tomblin

Câu trả lời:


46

Không có gì tích hợp sẵn, nhưng khá đơn giản để chuyển đổi thứ gì đó giống hình cầu thành 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;
}

điều này phù hợp với tôi, nhưng tôi không chắc liệu nó có bao gồm "tiêu chuẩn" toàn cầu hay không, nếu có :)

Cập nhật bởi Paul Tomblin: Tôi đã tìm thấy một chương trình perl thực hiện chuyển đổi hình cầu và điều chỉnh nó thành Java mà tôi kết thúc với:

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

Tôi đang chỉnh sửa câu trả lời này thay vì làm cho câu trả lời của riêng mình vì câu trả lời này đưa tôi đi đúng hướng.


1
Vâng, đó là khá nhiều giải pháp mà tôi đã đưa ra vào lần cuối cùng tôi phải làm điều này (ở Perl) nhưng tôi đang tự hỏi liệu có thứ gì đó thanh lịch hơn không. Tôi nghĩ tôi sẽ làm theo cách của bạn.
Paul Tomblin

1
Trên thực tế, tôi thấy một thực hiện tốt hơn trong Perl mà tôi có thể thích ứng vào Java tại kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html
Paul Tomblin

Bạn không thể sử dụng thay thế regex để biến một khối cầu thành một regex?
Tim Sylvester 09/08/09

1
Các dòng ở phía trên mà dải ra sự cần thiết ở đầu và đuôi '*' để được gỡ bỏ cho java từ String.matches chống lại toàn bộ chỉ chuỗi
KitsuneYMG

10
FYI: Tiêu chuẩn cho 'Globbing' là ngôn ngữ POSIX Shell - opengroup.org/onlinepubs/009695399/utilities/…
Stephen C

60

Globbing cũng được lên kế hoạch triển khai trong Java 7.

Xem FileSystem.getPathMatcher(String)hướng dẫn "Tìm tệp" .


23
Tuyệt vời. Nhưng tại sao trên trái đất việc triển khai này chỉ giới hạn ở các đối tượng "Đường dẫn"?!? Trong trường hợp của tôi, tôi muốn khớp với URI ...
Yves Martin

3
Nhìn vào nguồn của sun.nio, đối sánh toàn cầu dường như được thực hiện bởi Globs.java . Thật không may, điều này được viết riêng cho đường dẫn hệ thống tệp, vì vậy nó không thể được sử dụng cho tất cả các chuỗi (nó tạo ra một số giả định về dấu phân cách đường dẫn và các ký tự không hợp lệ). Nhưng nó có thể là một điểm khởi đầu hữu ích.
Neil Traft

33

Cảm ơn tất cả mọi người ở đây vì những đóng góp của họ. Tôi đã viết một chuyển đổi toàn diện hơn bất kỳ câu trả lời nào trước đây:

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

Và đơn vị kiểm tra để chứng minh nó hoạt động:

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

}

Cảm ơn vì mã này, Neil! Bạn có sẵn sàng cấp cho nó một giấy phép nguồn mở không?
Steven

1
Do đó, tôi cho rằng mã trong câu trả lời này thuộc phạm vi công cộng.
Neil Traft

Tôi có nên làm gì khác không? :-P
Neil Traft

9

Có một số thư viện thực hiện khớp mẫu giống Glob hiện đại hơn những thư viện được liệt kê:

Theres Ants Directory Scanner and Springs AntPathMatcher

Tôi khuyên bạn nên sử dụng cả hai giải pháp này vì Ant Style Globbing đã trở thành cú pháp cầu tiêu chuẩn trong thế giới Java (Hudson, Spring, Ant và tôi nghĩ là Maven).


1
Dưới đây là các tọa độ Maven cho artifact với AntPathMatcher: search.maven.org/... Và một số xét nghiệm với việc sử dụng mẫu: github.com/spring-projects/spring-framework/blob/master/...
seanf

Và bạn có thể tùy chỉnh các "con đường" nhân vật ... vì vậy nó rất hữu ích cho những thứ khác hơn là con đường ...
Michael Wiles

7

Gần đây tôi đã phải làm điều đó và sử dụng \Q\Ethoát khỏi mô hình hình cầu:

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

4
Điều này sẽ không bị phá vỡ nếu có một \ E ở đâu đó trong chuỗi?
jmo

@jmo, vâng, nhưng bạn có thể phá vỡ điều đó bằng cách xử lý trước globbiến với global = Pattern.quote (global), mà tôi tin rằng sẽ xử lý các trường hợp cạnh như vậy. Tuy nhiên, trong trường hợp đó, bạn không cần phải thêm trước và nối thêm \\ Q và \\ E đầu tiên và cuối cùng.
Kimball Robinson

2
@jmo Tôi đã sửa ví dụ để sử dụng Pattern.quote ().
dimo414,

5

Đây là một triển khai Glob đơn giản xử lý * và? trong mô hình

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

Tương tự như câu trả lời của Tony Edgecombe , đây là một hình cầu ngắn và đơn giản hỗ trợ và không sử dụng regex, nếu ai đó cần.*?

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
Câu trả lời xuất sắc tihi! Đây là đủ đơn giản để hiểu tại một đọc nhanh và không quá bối rối :-)
TNHH Chuộc Tội

3

Nó có thể là một cách tiếp cận hơi khó hiểu. Tôi đã tìm ra nó từ Files.newDirectoryStream(Path dir, String glob)mã của NIO2 . Chú ý rằng mọi Pathđối tượng mới phù hợp đều được tạo. Cho đến nay tôi chỉ có thể kiểm tra điều này trên Windows FS, tuy nhiên, tôi tin rằng nó cũng sẽ hoạt động trên 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
}

CẬP NHẬT Hoạt động trên cả - Mac và Linux.



0

Từ lâu, tôi đã thực hiện một bộ lọc văn bản theo hướng toàn cầu lớn nên tôi đã viết một đoạn mã nhỏ (15 dòng mã, không có phụ thuộc nào ngoài JDK). Nó chỉ xử lý '*' (đối với tôi là đủ), nhưng có thể dễ dàng mở rộng cho '?'. Nó nhanh hơn nhiều lần so với regexp được biên dịch trước, không yêu cầu bất kỳ biên dịch trước nào (về cơ bản nó là một so sánh chuỗi với chuỗi mỗi khi mẫu được khớp).

Mã:

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

Sử dụng:

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

Sao chép / dán từ đây


Vì nó chỉ có 15 dòng, bạn nên đưa nó vào đây trong trường hợp trang liên kết bị hỏng.
Raniz

0

Các giải pháp trước đây bởi Vincent Robert / dimo414 dựa trên Pattern.quote()được thực hiện về \Q... \E, mà không được ghi nhận trong API và do đó không thể là trường hợp cho / tương lai khác triển khai Java. Giải pháp sau đây loại bỏ sự phụ thuộc triển khai đó bằng cách thoát tất cả các lần xuất hiện \Ethay vì sử dụng quote(). Nó cũng kích hoạt DOTALLmode ( (?s)) trong trường hợp chuỗi được so khớp chứa các dòng mới.

    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

Nhân tiện, có vẻ như bạn đã làm điều đó một cách khó khăn trong Perl

Đây là thủ thuật trong Perl:

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

1
Điều đó chỉ hoạt động nếu hình cầu dành cho các tệp phù hợp. Trong trường hợp perl, các quả địa cầu thực sự đến từ danh sách các địa chỉ ip được viết bằng cách sử dụng các quả địa cầu vì lý do tôi sẽ không đi vào và trong trường hợp hiện tại của tôi, các quả địa cầu phải khớp với các url.
Paul Tomblin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.