如何从给定的字符串列表自动生成正则表达式?

30

给定两个字符串列表 A 和 B,请找出最短的正则表达式,它匹配了 A 中的所有字符串但不匹配 B 中的任何字符串。请注意,此正则表达式可以匹配/不匹配其他不在 A 和 B 中的字符串。为简单起见,我们可以假设字母表大小为 2 个字符 - 0 和 1。只允许使用以下运算符:

* - 零个或多个
? - 零个或一个
+ - 一个或多个
() - 括号

为简单起见,不允许使用正则表达式非运算符。我不知道是否允许使用 or 运算符(|)会简化问题。当然,A 和 B 没有共同元素。以下是一些示例:

A=[00,01,10]
B=[11]
answer = 1*0+1*


A=[00,01,11]
B=[10]
answer = 0*1*

5
听起来这是一个相当困难的问题,设计一种能够产生相对较短表示的算法可能并不难,但证明它所产生的是最短的则可能很棘手。 - biziclop
3
有趣的是不允许进行任何替换。似乎可以生成一些“病态”的集合,无法为其生成正则表达式。比如[0, 00, 0000, 00000][000, 0000000] - Anon.
2
你打算如何证明它是最短的? - Dr. belisarius
2
我已经毕业很久了,这不是作业,但感谢你认为我能清晰地写出问题。是的,我认为这是可行的,但需要花费相当长的时间。首先可以尝试一些简化方法来解决问题,比如如果我们可以在不考虑集合B的情况下解决这个问题(假设我们被告知B始终为空),或者说我们被告知不仅B为空,而且A只有2个元素。 - pathikrit
3
类似的问题和长时间的讨论:http://cstheory.stackexchange.com/questions/1854/is-finding-the-minimum-regular-expression-an-np-complete-problem - royas
显示剩余6条评论
4个回答

18
一种解决方法是使用遗传算法。我碰巧有一个名为genetic solver的程序,所以我用以下算法将其应用于你的问题:
  • 从所需输入中获取不同的标记作为基因
  • 将正则表达式特殊字符添加到基因中
  • 对于适应度算法
    • 确保生成的字符串是有效的正则表达式
    • 根据匹配所需内容和不需要内容的数量获得适应度值
  • 直到找到成功的正则表达式
    • 从不同标记的数量开始,并根据需要递增
    • 尝试生成一个满足适应度要求的该长度的正则表达式
这是我的C#实现。
private static void GenerateRegex(IEnumerable<string> target, IEnumerable<string> dontMatch)
{
    string distinctSymbols = new String(target.SelectMany(x => x).Distinct().ToArray());
    string genes = distinctSymbols + "?*()+";

    Func<string, uint> calcFitness = str =>
        {
            if (str.Count(x => x == '(') != str.Count(x => x == ')'))
            {
                return Int32.MaxValue;
            }
            if ("?*+".Any(x => str[0] == x))
            {
                return Int32.MaxValue;
            }
            if ("?*+?*+".ToArray().Permute(2)
                .Any(permutation => str.IndexOf(new string(permutation.ToArray())) != -1))
            {
                return Int32.MaxValue;
            }
            Regex regex;
            try
            {
                regex = new Regex("^" + str + "$");
            }
            catch (Exception)
            {
                return Int32.MaxValue;
            }
            uint fitness = target.Aggregate<string, uint>(0, (current, t) => current + (regex.IsMatch(t) ? 0U : 1));
            uint nonFitness = dontMatch.Aggregate<string, uint>(0, (current, t) => current + (regex.IsMatch(t) ? 10U : 0));
            return fitness + nonFitness;
        };

    for (int targetGeneLength = distinctSymbols.Length; targetGeneLength < genes.Length * 2; targetGeneLength++)
    {
        string best = new GeneticSolver(50).GetBestGenetically(targetGeneLength, genes, calcFitness, true);
        if (calcFitness(best) != 0)
        {
            Console.WriteLine("-- not solved with regex of length " + targetGeneLength);
            continue;
        }
        Console.WriteLine("solved with: " + best);
        break;
    }
}

应用它到你的样本中的结果:

public void Given_Sample_A()
{
    var target = new[] { "00", "01", "10" };
    var dontMatch = new[] { "11" };

    GenerateRegex(target, dontMatch);
}

输出:

Generation  1 best: 10 (2)
Generation  2 best: 0+ (2)
Generation  5 best: 0* (2)
Generation  8 best: 00 (2)
Generation  9 best: 01 (2)
-- not solved with regex of length 2
Generation  1 best: 10* (2)
Generation  3 best: 00* (2)
Generation  4 best: 01+ (2)
Generation  6 best: 10+ (2)
Generation  9 best: 00? (2)
Generation 11 best: 00+ (2)
Generation 14 best: 0?1 (2)
Generation 21 best: 0*0 (2)
Generation 37 best: 1?0 (2)
Generation 43 best: 10? (2)
Generation 68 best: 01* (2)
Generation 78 best: 1*0 (2)
Generation 79 best: 0*1 (2)
Generation 84 best: 0?0 (2)
Generation 127 best: 01? (2)
Generation 142 best: 0+1 (2)
Generation 146 best: 0+0 (2)
Generation 171 best: 1+0 (2)
-- not solved with regex of length 3
Generation  1 best: 1*0+ (1)
Generation  2 best: 0+1* (1)
Generation 20 best: 1?0+ (1)
Generation 31 best: 1?0* (1)
-- not solved with regex of length 4
Generation  1 best: 1*00? (1)
Generation  2 best: 0*1?0 (1)
Generation  3 best: 1?0?0 (1)
Generation  4 best: 1?00? (1)
Generation  8 best: 1?00* (1)
Generation 12 best: 1*0?0 (1)
Generation 13 best: 1*00* (1)
Generation 41 best: 0*10* (1)
Generation 44 best: 1*0*0 (1)
-- not solved with regex of length 5
Generation  1 best: 0+(1)? (1)
Generation 36 best: 0+()1? (1)
Generation 39 best: 0+(1?) (1)
Generation 61 best: 1*0+1? (0)
solved with: 1*0+1?

第二个样例:

public void Given_Sample_B()
{
    var target = new[] { "00", "01", "11" };
    var dontMatch = new[] { "10" };

    GenerateRegex(target, dontMatch);
}

输出:

Generation  1 best: 00 (2)
Generation  2 best: 01 (2)
Generation  7 best: 0* (2)
Generation 12 best: 0+ (2)
Generation 33 best: 1+ (2)
Generation 36 best: 1* (2)
Generation 53 best: 11 (2)
-- not solved with regex of length 2
Generation  1 best: 00* (2)
Generation  2 best: 0+0 (2)
Generation  7 best: 0+1 (2)
Generation 12 best: 00? (2)
Generation 15 best: 01* (2)
Generation 16 best: 0*0 (2)
Generation 19 best: 01+ (2)
Generation 30 best: 0?0 (2)
Generation 32 best: 0*1 (2)
Generation 42 best: 11* (2)
Generation 43 best: 1+1 (2)
Generation 44 best: 00+ (2)
Generation 87 best: 01? (2)
Generation 96 best: 0?1 (2)
Generation 125 best: 11? (2)
Generation 126 best: 1?1 (2)
Generation 135 best: 11+ (2)
Generation 149 best: 1*1 (2)
-- not solved with regex of length 3
Generation  1 best: 0*1* (0)
solved with: 0*1*

1
+1,非常好。你尝试过更大的集合和更长的字符串吗?我在想用长度约为10的10个字符串在A中,并使B为空以简化问题。我想知道那时速度会有多快。当你不需要精确解决方案时,遗传算法往往效果更好,否则它们通常相当低效。 - IVlad
@IVlad 没有,我没有尝试过更长的字符串。针对 @MK 的观点,遗传算法可能不适用于复杂的输入集。 - Handcraftsman
遗传算法是一种聪明的方法,但不幸的是它无法泛化(它试图尽可能地约束)。也许与递归神经网络相结合,它可以产生有趣的结果;这绝对值得研究。 - 6infinity8

1

这个项目可以从给定的单词列表生成正则表达式: https://github.com/bwagner/wordhierarchy

然而,它只使用了"|"、非捕获组"(?:)"和选项"?"。

示例用法:

java -jar dist/wordhierarchy.jar 00 01 10
-> 10|0(?:1|0)

java -jar dist/wordhierarchy.jar 00 01 11
-> 0(?:0|1)|11

java -jar dist/wordhierarchy.jar 000 001 010 011 100 101 110 111
-> 1(?:0(?:0|1)|1(?:0|1))|0(?:1(?:1|0)|0(?:1|0))

java -jar dist/wordhierarchy.jar 000 001 010     100 101 110 111
-> 1(?:0(?:0|1)|1(?:1|0))|0(?:10|0(?:1|0))

它对于varchar(123) varchar(...)失败了。 - silentsudo
@silentsudo 请提供一个完整的、可重现的示例。 - Bernhard Wagner

1

如果这是一个作业问题,那就像是“一份作业,得到整个班的A”类型的。 我认为这个问题中缺少了“或”运算符。

有一个显而易见的解决方案是A0|A1|A2|...,但在尝试找到最短解决方案时似乎更难。

我建议使用递归来尝试缩短正则表达式,但这并不是一个理想的解决方案。


0

"当不确定时,就使用暴力算法。"

import re

def learn(ayes, noes, max_size=7):
    def is_ok(rx):
        rx += '$'
        return (all(re.match(rx, s) for s in ayes)
                and not any(re.match(rx, s) for s in noes))
    return find(find(gen_sized(size), is_ok)
                for size in range(max_size + 1))

def find(xs, ok=lambda x: x):
    for x in xs:
        if ok(x):
            return x

def gen_sized(size):
    if 0 == size:
        yield ''
    if 0 < size:
        for rx in gen_sized(size-1):
            yield rx + '0'
            yield rx + '1'
            if rx and rx[-1] not in '*?+':
                yield rx + '*'
                yield rx + '?'
                yield rx + '+'
    if 5 < size:
        for rx in gen_sized(size-3):
            yield '(%s)*' % rx
            yield '(%s)?' % rx
            yield '(%s)+' % rx

这将为第一个问题生成一个不同但同样好的答案:0 * 1? 0 *。它检查了1241个试验正则表达式以解决两个测试用例(总计)。

搜索有一个大小限制--由于这个问题的通用正则表达式版本是NP-hard的,任何针对它的程序都会在足够复杂的输入上遇到麻烦。我承认我没有真正考虑过这个简化问题。我很想看到一些巧妙而不明显的答案。


但是对于learn(("0","00","0000","00000"),("000","000000")),它无法正常工作。 - royas
@royas,你期望的答案是什么? - Darius Bacon
@royas,这个匹配了“000”,其中之一。 - Darius Bacon
@Darius Bacon,怎么做?正则表达式中的哪些零匹配字符串000中的哪些零?等效且更易分析的是0(0(000?)?)? - royas
啊,抱歉,在我放置你的测试用例时,“000000”不知怎么变成了“00000”。 - Darius Bacon
显示剩余2条评论

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