使用正则表达式生成字符串而不是匹配它们

133

我正在编写一个Java工具,以帮助我生成大量数据进行性能测试。如果能够为字符串指定正则表达式,让我的生成器输出匹配该正则表达式的内容,那将是非常酷的。

是否已经有这样的现成工具可以使用?或者是否有类库可以帮我实现这个功能?


1
这里有一个有用的 Java 库,提供了许多使用正则表达式生成字符串的功能(随机生成、基于索引生成字符串、生成所有字符串等)。请在此处查看。 - Mifmif
另一种选择可能是这个 - Vladislav Varslavans
12个回答

57

首先,用足够复杂的正则表达式来说,我认为这是不可能的。但是您应该能够为简单的正则表达式编写一些代码。

如果您查看 java.util.regex.Pattern 类的源代码,您将看到它使用 Node 实例的内部表示法。每个不同的模式组件都有它们自己的 Node 子类实现。这些节点被组织成一棵树。

通过创建一个遍历此树的访问者,您应该能够调用重载生成器方法或某种构建器,然后拼凑出一些东西。


2
我不确定Xeger有多好。它无法处理字符类。它无法识别一个简单的[\w]。查看他们的维基的最后一行告诉我们这一点。 - John Red
2
请注意,这些依赖于 dk.brics.automaton,因此请准备添加第三方 pom 依赖项。大多数人不介意,但我希望有更紧凑的东西。 - Sridhar Sarnobat
有一种替代xeger和generex的工具,它没有这些缺点,也不会过时。请向下滚动查看我的答案。 - Vladislav Varslavans
3
首先,使用足够复杂的正则表达式,我认为这是不可能的,这并不完全正确:任何能够匹配某些内容的正则表达式也可以生成有效的输入。解释:正则表达式属于 Chomsky 层次结构中的第三类,意味着它们可以被表示为有限状态机。当通过有限状态机进行步进时,每个边缘都被解释为下一个字符的规则,因此有限状态机可以用于解析或生成序列。如果有限状态机有通往终端的路径,则可以确定有效序列。因此,只有当没有通向终端的路径(这将是无用的正则表达式)时,才会“不可能”。 - Lawrence Wagerfield

25

虽然已经来不及帮助原始帖子的发布者,但它可能有助于新手。 Generex 是一个有用的 Java 库,为使用正则表达式生成字符串(随机生成、基于索引生成字符串、生成所有字符串等)提供了许多功能。

示例:

Generex generex = new Generex("[0-3]([a-c]|[e-g]{1,2})");

// generate the second String in lexicographical order that matches the given Regex.
String secondString = generex.getMatchedString(2);
System.out.println(secondString);// it print '0b'

// Generate all String that matches the given Regex.
List<String> matchedStrs = generex.getAllMatchedStrings();

// Using Generex iterator
Iterator iterator = generex.iterator();
while (iterator.hasNext()) {
    System.out.print(iterator.next() + " ");
}
// it prints 0a 0b 0c 0e 0ee 0e 0e 0f 0fe 0f 0f 0g 0ge 0g 0g 1a 1b 1c 1e
// 1ee 1e 1e 1f 1fe 1f 1f 1g 1ge 1g 1g 2a 2b 2c 2e 2ee 2e 2e 2f 2fe 2f 2f 2g
// 2ge 2g 2g 3a 3b 3c 3e 3ee 3e 3e 3f 3fe 3f 3f 3g 3ge 3g 3g 1ee

// Generate random String
String randomStr = generex.random();
System.out.println(randomStr);// a random value from the previous String list

披露

本帖所提到的项目归回答问题的用户(Mifmif)所有。根据规定,需要进行披露。


12
看起来Generex是你自己的项目。请问您是否介意在帖子中提到这是您自己的项目,根据这里的规定? - Brian McCutchon

21

Xeger(Java)也能够实现此功能:

String regex = "[ab]{4,6}c";
Xeger generator = new Xeger(regex);
String result = generator.generate();
assert result.matches(regex);

2
Xeger工作得很好。但请确保您的类路径或pom/gradle中有automaton jar - Delicia Brummitt

19

这个问题很古老,但对我来说仍然是实际问题。我尝试了xegerGenerex,但它们似乎都不能满足我的要求。它们实际上无法处理一些正则表达式模式(例如a{60000}),对于其他一些模式(例如(A|B|C|D|E|F)),它们只能产生部分可能的值。由于我没有找到任何其他合适的解决方案 - 我创建了自己的库。

https://github.com/curious-odd-man/RgxGen

该库可用于生成匹配和不匹配的字符串。

maven central也有相关的构件。

使用示例:

RgxGen rgxGen = new RgxGen(aRegex);                     // Create generator
String s = rgxGen.generate();                           // Generate new random value

2
我尝试了RxGen,它比Xeger和Generex都要好得多。 - spacether
但是你的库不支持前瞻和后顾,即使使用暴力重构,有时仍然会产生无效字符串。 - shinzou
该库有一些限制,这些限制在Readme部分中进行了描述。 - Vladislav Varslavans
运行得非常好。我正在使用它来生成XSD模式的示例XML。 - Vladimir Dyuzhev

5
我已经采取了自己编写库的方法(用C#编写,但Java开发人员也应该很容易理解)。
Rxrdg最初是为解决在实际项目中创建测试数据的问题而诞生的。基本想法是利用现有的(正则表达式)验证模式来创建符合这些模式的随机数据。这样就可以创建有效的随机数据。
编写一个简单的正则表达式模式解析器并不那么困难。使用抽象语法树生成字符串甚至更容易。

链接不再指向存储库。我会选择https://www.openhub.net/p/rxrdg。但是该解决方案无法构建,怎么办? - Veverke

4

我正在飞行中,刚看到这个问题:我已经写了一个最简单但低效和不完整的解决方案。我希望它能帮助你开始编写自己的解析器:

public static void main(String[] args) {

    String line = "[A-Z0-9]{16}";
    String[] tokens = line.split(line);
    char[] pattern = new char[100];
    int i = 0;
    int len = tokens.length;
    String sep1 = "[{";
    StringTokenizer st = new StringTokenizer(line, sep1);

    while (st.hasMoreTokens()) {
        String token = st.nextToken();
        System.out.println(token);

        if (token.contains("]")) {
            char[] endStr = null;

            if (!token.endsWith("]")) {
                String[] subTokens = token.split("]");
                token = subTokens[0];

                if (!subTokens[1].equalsIgnoreCase("*")) {
                    endStr = subTokens[1].toCharArray();
                }
            }

            if (token.startsWith("^")) {
                String subStr = token.substring(1, token.length() - 1);
                char[] subChar = subStr.toCharArray();
                Set set = new HashSet<Character>();

                for (int p = 0; p < subChar.length; p++) {
                    set.add(subChar[p]);
                }

                int asci = 1;

                while (true) {
                    char newChar = (char) (subChar[0] + (asci++));

                    if (!set.contains(newChar)) {
                        pattern[i++] = newChar;
                        break;
                    }
                }
                if (endStr != null) {
                    for (int r = 0; r < endStr.length; r++) {
                        pattern[i++] = endStr[r];
                    }
                }

            } else {
                pattern[i++] = token.charAt(0);
            }
        } else if (token.contains("}")) {
            char[] endStr = null;

            if (!token.endsWith("}")) {
                String[] subTokens = token.split("}");
                token = subTokens[0];

                if (!subTokens[1].equalsIgnoreCase("*")) {
                    endStr = subTokens[1].toCharArray();
                }
            }

            int length = Integer.parseInt((new StringTokenizer(token, (",}"))).nextToken());
            char element = pattern[i - 1];

            for (int j = 0; j < length - 1; j++) {
                pattern[i++] = element;
            }

            if (endStr != null) {
                for (int r = 0; r < endStr.length; r++) {
                    pattern[i++] = endStr[r];
                }
            }
        } else {
            char[] temp = token.toCharArray();

            for (int q = 0; q < temp.length; q++) {
                pattern[i++] = temp[q];
            }
        }
    }

    String result = "";

    for (int j = 0; j < i; j++) {
        result += pattern[j];
    }

    System.out.print(result);
}

1
您可能希望指示使用哪种字符串作为模式输入。首先,从源代码中确定这些内容并不容易。其次,如果源代码中存在任何错误或不明确的地方,就无法确定它们是否是故意的。 - Maarten Bodewes
StringTokenizer是一个遗留类,为了兼容性而保留,尽管在新代码中不建议使用它。建议任何寻求此功能的人使用String的split方法或java.util.regex包代替。 - Rohit

4

在stackoverflow播客11中:

Spolsky:是的。还有一个新产品,如果您不想使用Team System,我们的朋友Redgate有一个名为SQL Data Generator的产品[http://www.red-gate.com/products/sql_data_generator/index.htm]。它售价295美元,可以生成一些逼真的测试数据。它会在城市列中生成实际存在的城市,并在生成这些城市时正确获取州名,而不是将州名放入德国城市等等。你知道的,它生成的数据看起来相当逼真。我不太确定所有的功能都是什么。

这可能不是您要找的,但它可能是一个好的起点,而不是创建自己的数据。

我似乎在谷歌上找不到任何东西,所以建议通过将给定的正则表达式解析为最小的工作单元(\w,[x-x],\d等)并编写一些支持这些正则表达式短语的基本方法来解决问题。

因此,对于\w,您将拥有一个方法getRandomLetter(),它返回任何随机字母,您还将拥有getRandomLetter(char startLetter,char endLetter),它为您提供两个值之间的随机字母。


2

我知道已经有一个被接受的答案,但是我一直在使用RedGate的数据生成器(在Craig的答案中提到的那个),它对我投入的所有东西都非常有效。它很快,这让我想使用相同的正则表达式来生成像注册码这样的真实数据。

它需要一个正则表达式,如下所示:

[A-Z0-9]{3,3}-[A-Z0-9]{3,3}

它会生成许多独特的代码,例如:

LLK-32U

这是一种RedGate发现的重要算法吗?还是我们这些凡人也可以做到的呢?


2
你需要编写自己的解析器,就像 String::Random(Perl)的作者所做的那样。实际上,在该模块中他没有使用正则表达式,只是 Perl 编码人员习惯使用而已。
另一方面,也许你可以查看源代码,以获取一些指示。
编辑:该死,布莱尔比我快了15秒。

只有年轻人才会编写自己的解析器。 - Vladimir Dyuzhev

1

虽然它远未支持完整的PCRE正则表达式,但我编写了以下Ruby方法来接受类似正则表达式的字符串并生成其变体。(用于基于语言的验证码。)

# q = "(How (much|many)|What) is (the (value|result) of)? :num1 :op :num2?"
# values = { :num1=>42, :op=>"plus", :num2=>17 }
# 4.times{ puts q.variation( values ) }
# => What is 42 plus 17?
# => How many is the result of 42 plus 17?
# => What is the result of 42 plus 17?
# => How much is the value of 42 plus 17?
class String
  def variation( values={} )
    out = self.dup
    while out.gsub!( /\(([^())?]+)\)(\?)?/ ){
      ( $2 && ( rand > 0.5 ) ) ? '' : $1.split( '|' ).random
    }; end
    out.gsub!( /:(#{values.keys.join('|')})\b/ ){ values[$1.intern] }
    out.gsub!( /\s{2,}/, ' ' )
    out
  end
end

class Array
  def random
    self[ rand( self.length ) ]
  end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接