“glob”类型模式是不是有等效的 java.util.regex ?

Posted

技术标签:

【中文标题】“glob”类型模式是不是有等效的 java.util.regex ?【英文标题】:Is there an equivalent of java.util.regex for "glob" type patterns?“glob”类型模式是否有等效的 java.util.regex ? 【发布时间】:2010-11-17 21:07:25 【问题描述】:

是否有用于在 Java 中进行“glob”类型匹配的标准(最好是 Apache Commons 或类似的非病毒)库?当我不得不在 Perl 中进行类似操作时,我只是将所有“.”更改为“\.”,将“*”更改为“.*”,将“?”更改为“@987654331” @" 之类的,但我想知道是否有人为我完成了这项工作。

类似问题:Create regex from glob expression

【问题讨论】:

GlobCompiler/GlobEngine,来自Jakarta ORO,看起来很有希望。它在 Apache 许可下可用。 你能给出一个你想做什么的准确例子吗? 我想要做的(或者更确切地说是我的客户想要做的)是匹配 url 中的“-2009/”或“*rss”之类的内容。大多数情况下,转换为正则表达式非常简单,但我想知道是否有更简单的方法。 我推荐 Ant 风格的文件通配,因为它似乎已成为 Java 世界的规范通配。有关更多详细信息,请参阅我的答案:***.com/questions/1247772/…。 @BradMace,相关但那里的大多数答案都假设您正在遍历目录树。尽管如此,如果有人仍在寻找如何对任意字符串进行 glob 样式匹配,他们可能也应该查看该答案。 【参考方案1】:

Vincent Robert/dimo414 的 previous solution 依赖于 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】:

这可能是一种有点老套的方法。我从 NIO2 的Files.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

更新 适用于 Mac 和 Linux。

【讨论】:

【参考方案3】:

我最近不得不这样做,并使用 \Q\E 来逃避 glob 模式:

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

【讨论】:

如果字符串中某处有 \E,这不会中断吗? @jmo,是的,但是您可以通过使用 glob = Pattern.quote(glob) 预处理 glob 变量来规避这种情况,我相信它可以处理这种边缘情况。但是,在这种情况下,您不需要预先添加第一个和最后一个 \\Q 和 \\E。 @jmo 我已将示例修复为使用 Pattern.quote()。 在 glob 中,否定字符类使用 ! 而不是 ^ 作为 [ 之后的第一个字符,不是吗?【参考方案4】:

很久以前我在做一个大规模的全局驱动的文本过滤,所以我写了一小段代码(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();
    
  

从here复制/粘贴

【讨论】:

因为它只有 15 行,所以你应该在此处包含它,以防链接页面出现故障。【参考方案5】:

感谢大家的贡献。我写了一个比之前任何一个答案都更全面的转换:

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


【讨论】:

感谢此代码,尼尔!你愿意给它一个开源许可证吗? 我在此同意此答案中的代码属于公共领域。 我还需要做什么吗? :-P【参考方案6】:

通配符也计划在 Java 7 中实现

参见FileSystem.getPathMatcher(String) 和the "Finding Files" tutorial。

【讨论】:

太棒了。但是为什么这个实现仅限于“路径”对象?!?就我而言,我想匹配 URI... 查看 sun.nio 的源代码,glob 匹配似乎是由Globs.java 实现的。不幸的是,这是专门为文件系统路径编写的,因此不能用于所有字符串(它对路径分隔符和非法字符做了一些假设)。但这可能是一个有用的起点。【参考方案7】:

有几个库可以进行类似 Glob 的模式匹配,它们比所列的更现代:

有蚂蚁Directory Scanner 和 泉AntPathMatcher

我推荐这两种解决方案,因为 Ant Style Globbing 几乎已成为 Java 世界中的标准 glob 语法(Hudson、Spring、Ant 和我认为是 Maven)。

【讨论】:

这里是使用 AntPathMatcher 的工件的 Maven 坐标:search.maven.org/… 以及一些使用示例的测试:github.com/spring-projects/spring-framework/blob/master/… 而且你可以自定义“路径”字符...所以它对路径以外的东西很有用...【参考方案8】:

类似于Tony Edgecombe 的answer,这里有一个简短而简单的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;
    

【讨论】:

优秀的答案!这很简单,可以快速阅读并且不会太令人困惑:-)【参考方案9】:

这是一个简单的 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);
    

【讨论】:

【参考方案10】:

顺便说一句,好像你在 Perl 中做的很辛苦

这在 Perl 中起到了作用:

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

【讨论】:

只有在 glob 用于匹配文件时才有效。在 perl 的情况下,glob 实际上来自使用 glob 编写的 IP 地址列表,原因我不会深入,而在我目前的情况下,glob 是匹配 url。【参考方案11】:

没有任何内置功能,但将类似 glob 的内容转换为正则表达式非常简单:

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;

这对我有用,但我不确定它是否涵盖全球“标准”,如果有的话:)

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

我正在编辑这个答案而不是自己制作,因为这个答案让我走上了正确的轨道。

【讨论】:

是的,这几乎是我上次不得不这样做(在 Perl 中)时提出的解决方案,但我想知道是否有更优雅的东西。我想我会按照你的方式去做。 实际上,我在 Perl 中找到了一个更好的实现,我可以在 kobesearch.cpan.org/htdocs/Text-Glob/Text/Glob.pm.html 处适应 Java 你不能使用正则表达式替换将一个全局变量转换为一个正则表达式吗? 去除前导和尾随 '*' 的顶部行需要为 java 删除,因为 String.matches 仅针对整个字符串 仅供参考:'globbing' 的标准是 POSIX Shell 语言 - opengroup.org/onlinepubs/009695399/utilities/…【参考方案12】:

我不知道“标准”实现,但我知道一个在 BSD 许可下发布的 sourceforge 项目,它实现了文件的 glob 匹配。它在one file 中实现,也许您可​​以根据您的要求对其进行调整。

【讨论】:

更新链接:sourceforge.net/p/uncle/code/HEAD/tree/uncle/fileglob/trunk/src/…

以上是关于“glob”类型模式是不是有等效的 java.util.regex ?的主要内容,如果未能解决你的问题,请参考以下文章

在 JavaScript 中,除了 URL 之外,是不是有类似 glob 的东西?

找出两个 Glob 模式(或正则表达式)的匹配项是不是相交的算法

Python glob,但针对字符串列表而不是文件系统

javaFX中是不是有等效的枚举属性类型?

精通gulp的关键:文件路径匹配模式glob

java.util.Set 的等效模式数据类型是啥?