มี java.util.regex เทียบเท่ากับรูปแบบประเภท "glob" หรือไม่


87

มีไลบรารีมาตรฐาน (โดยเฉพาะอย่างยิ่ง Apache Commons หรือที่คล้ายกันที่ไม่ใช่ไวรัส) สำหรับการจับคู่ประเภท "glob" ใน Java เมื่อฉันต้องทำสิ่งที่คล้ายกันใน Perl ครั้งหนึ่งฉันเพิ่งเปลี่ยน " ." เป็น " \." " *" เป็น " .*" และ " ?" เป็น " ." ทั้งหมดและสิ่งนั้น แต่ฉันสงสัยว่ามีใครทำ ทำงานให้ฉัน

คำถามที่คล้ายกัน: สร้าง regex จาก glob expression


GlobCompiler / GlobEngineจากJakarta OROมีแนวโน้มที่ดี สามารถใช้ได้ภายใต้ Apache License
Steve Trout

คุณช่วยยกตัวอย่างที่ชัดเจนเกี่ยวกับสิ่งที่คุณต้องการทำได้ไหม
Thorbjørn Ravn Andersen

สิ่งที่ฉันต้องการทำ (หรือสิ่งที่ลูกค้าของฉันต้องการทำ) คือจับคู่สิ่งต่างๆเช่น " -2009 /" หรือ "* rss " ใน URL ส่วนใหญ่แล้วการแปลงเป็น regex เป็นเรื่องเล็กน้อย แต่ฉันสงสัยว่ามีวิธีที่ง่ายกว่านี้หรือไม่
Paul Tomblin

ฉันขอแนะนำให้ใช้ Ant style file globing เพราะดูเหมือนว่าจะกลายเป็น globing แบบบัญญัติในโลก Java ดูคำตอบของฉันสำหรับรายละเอียดเพิ่มเติม: stackoverflow.com/questions/1247772/… .
Adam Gent

1
@BradMace เกี่ยวข้อง แต่คำตอบส่วนใหญ่จะถือว่าคุณกำลังข้ามโครงสร้างไดเรกทอรี อย่างไรก็ตามหากใครยังคงมองหาวิธีการจับคู่สไตล์ลูกโลกของสตริงตามอำเภอใจพวกเขาก็น่าจะดูคำตอบนั้นเช่นกัน
Paul Tomblin

คำตอบ:


47

ไม่มีอะไรในตัว แต่มันค่อนข้างง่ายที่จะแปลงสิ่งที่เหมือนโลกให้เป็น 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;
}

สิ่งนี้ใช้ได้สำหรับฉัน แต่ฉันไม่แน่ใจว่ามันครอบคลุม "มาตรฐาน" ของ glob หรือไม่ถ้ามี :)

อัปเดตโดย Paul Tomblin: ฉันพบโปรแกรม perl ที่ทำการแปลง glob และปรับให้เข้ากับ 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) แต่ฉันสงสัยว่ามีอะไรที่หรูหรากว่านี้หรือไม่ ฉันคิดว่าฉันจะทำในแบบของคุณ
Paul Tomblin

1
จริงๆแล้วฉันพบการใช้งานที่ดีกว่าใน Perl ซึ่งฉันสามารถปรับให้เข้ากับ Java ได้ที่kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html
Paul Tomblin

คุณใช้การแทนที่ regex เพื่อเปลี่ยน glob ให้เป็น regex ไม่ได้หรือ
Tim Sylvester

1
บรรทัดที่อยู่ด้านบนสุดที่ตัดส่วนนำและต่อท้าย '*' จำเป็นต้องถูกลบออกสำหรับ java เนื่องจาก String จับคู่กับสตริงทั้งหมดเท่านั้น
KitsuneYMG

10
FYI: มาตรฐานสำหรับ 'globbing' คือภาษา POSIX Shell - opengroup.org/onlinepubs/009695399/utilities/…
Stephen C

62

Globbing คือ นอกจากนี้ยังมีการวางแผนสำหรับการใช้งานใน Java 7

ดูFileSystem.getPathMatcher(String)และ"การค้นหาไฟล์" กวดวิชา


23
มหัศจรรย์. แต่เหตุใดการนำไปใช้งานบนโลกนี้จึง จำกัด อยู่ที่วัตถุ "เส้นทาง"!? ในกรณีของฉันฉันต้องการจับคู่ URI ...
Yves Martin

3
เมื่อมองไปที่แหล่งที่มาของ sun.nio การจับคู่ลูกโลกดูเหมือนจะดำเนินการโดย Globs.java น่าเสียดายที่สิ่งนี้ถูกเขียนขึ้นโดยเฉพาะสำหรับพา ธ ของระบบไฟล์ดังนั้นจึงไม่สามารถใช้กับสตริงทั้งหมดได้ (มีข้อสันนิษฐานเกี่ยวกับตัวคั่นพา ธ และอักขระที่ไม่ถูกต้อง) แต่อาจเป็นจุดเริ่มต้นที่เป็นประโยชน์
Neil Traft

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

}

ขอบคุณสำหรับรหัสนีล! คุณยินดีที่จะให้ใบอนุญาตโอเพนซอร์สหรือไม่
Steven

1
ฉันขออนุญาตว่ารหัสในคำตอบนี้เป็นสาธารณสมบัติ
Neil Traft

ฉันควรทำอะไรอีกไหม :-P
Neil Traft

9

มีห้องสมุดสองแห่งที่จับคู่รูปแบบเหมือน Glob ซึ่งทันสมัยกว่าที่ระบุไว้:

Theres Ants Directory Scanner และ Springs AntPathMatcher

ฉันแนะนำทั้งสองอย่างมากกว่าโซลูชันอื่นเนื่องจากAnt Style Globbing ได้กลายเป็นไวยากรณ์ glob มาตรฐานในโลก Java (Hudson, Spring, Ant และฉันคิดว่า Maven)


1
นี่คือพิกัด Maven สำหรับสิ่งประดิษฐ์ด้วย AntPathMatcher: search.maven.org/… และการทดสอบบางส่วนพร้อมตัวอย่างการใช้งาน: github.com/spring-projects/spring-framework/blob/master/…
seanf

และคุณสามารถปรับแต่งอักขระ "เส้นทาง" ได้ ... ดังนั้นจึงมีประโยชน์สำหรับสิ่งอื่นที่ไม่ใช่เส้นทาง ...
Michael Wiles

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 ตัวแรกและตัวสุดท้าย
Kimball Robinson

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

คล้ายกับคำตอบของTony Edgecombeนี่คือคำตอบสั้น ๆ และเรียบง่ายที่รองรับ*และ?ไม่ต้องใช้ regex หากใครต้องการ

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! นี่เป็นเรื่องง่ายที่จะเข้าใจในการอ่านอย่างรวดเร็วและไม่ทำให้สับสนเกินไป :-)
lmat - Reinstate Monica

3

อาจเป็นแนวทางที่แฮ็คเล็กน้อย ฉันคิดออกจากFiles.newDirectoryStream(Path dir, String glob)รหัสของ NIO2 ให้ความสนใจว่ามีการสร้าง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

ฉันไม่รู้เกี่ยวกับการใช้งาน "มาตรฐาน" แต่ฉันรู้จักโครงการซอร์สฟอร์จที่เผยแพร่ภายใต้ใบอนุญาต BSD ที่ใช้การจับคู่ลูกโลกสำหรับไฟล์ ใช้งานได้ในไฟล์เดียวบางทีคุณอาจปรับให้เข้ากับความต้องการของคุณ



0

นานมาแล้วฉันกำลังทำการกรองข้อความที่ขับเคลื่อนด้วยลูกโลกขนาดใหญ่ดังนั้นฉันจึงเขียนโค้ดชิ้นเล็ก ๆ (โค้ด 15 บรรทัดไม่มีการอ้างอิงเกิน JDK) มันจัดการเฉพาะ '*' (เพียงพอสำหรับฉัน) แต่สามารถขยายได้อย่างง่ายดายสำหรับ '?' เร็วกว่า regexp ที่คอมไพล์ล่วงหน้าหลายเท่าไม่จำเป็นต้องมีการคอมไพล์ล่วงหน้าใด ๆ (โดยพื้นฐานแล้วจะเป็นการเปรียบเทียบระหว่างสตริงกับสตริงทุกครั้งที่จับคู่รูปแบบ)

รหัส:

  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 บรรทัดคุณจึงควรรวมไว้ที่นี่ในกรณีที่หน้าที่เชื่อมโยงหยุดลง
Raniz

0

แก้ปัญหาก่อนหน้านี้โดยวินเซนต์โรเบิร์ต / 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$"
        );
    }

-2

ยังไงก็ตามดูเหมือนว่าคุณทำมันยากใน Perl

นี่เป็นเคล็ดลับใน Perl:

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

1
ใช้งานได้ก็ต่อเมื่อ glob ใช้สำหรับไฟล์ที่ตรงกัน ในกรณี perl globs มาจากรายการที่อยู่ IP ที่เขียนโดยใช้ globs ด้วยเหตุผลที่ฉันจะไม่เข้าไปและในกรณีปัจจุบันของฉัน globs ต้องตรงกับ URL
Paul Tomblin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.