Spintax Parser的正则表达式匹配性能问题

时间:2012-12-26 13:48:54

标签: c# regex spintax

我正在编写一个应用程序,旨在以下列格式处理数千篇包含大量spintax的文章/条目:

{Hello|Hi} {World|There!}, how are you?

然而,当我使用分析器运行应用程序时,我注意到处理正则表达式的部分占用了大量资源,我的应用程序最终因内存不足问题而崩溃。任何人都可以建议一种方法来改进我的代码或更好的解析spintax的方法吗?

public static String Spin(String text)
{
        Regex reg = new Regex(@"\{[^\{\}]*\}");
        Random rand = new Random((int)DateTime.Now.Ticks);
        while (true)
        {
            Match m = reg.Match(text);
            if (!m.Success) break;
            String[] parts = m.Value.TrimStart('{').TrimEnd('}').Split('|');
            int i = rand.Next(parts.Length);
            text = text.Substring(0, m.Index) + parts[i] + text.Substring(m.Index + m.Length);
        }
        return text;
  }

3 个答案:

答案 0 :(得分:7)

我已经实现了我的快速版本(没有Regex,没有Split,没有Substring,没有Replace和其他字符串操作方法。复制字符串我正在使用String.CopyTo将符号复制到普通char数组。

此代码完全支持嵌套的Spintaxes(可能无限深度)。一个限制是每个Spintax的最大选项数,目前它是100,但可以更改为1000或更多...另一个限制是输入字符串的最大长度,现在是100000,但也可以增加。

关于性能 - 我的测试表明,此代码比任何优化的Regex解决方案(包括Jim Mischel的解决方案)快15倍,比使用Substring和其他String操作方法的版本快约5倍。我在发布模式下使用VS 2012中的优化代码设置对此进行了测试。

    static int[] partIndices = new int[100];
    static int[] depth = new int[100];
    static char[] symbolsOfTextProcessed = new char[100000];

    public static String SpinEvenMoreFaster(String text)
    {
        int cur = SpinEvenMoreFasterInner(text, 0, text.Length, 0);
        return new String(symbolsOfTextProcessed, 0, cur);
    }

    public static int SpinEvenMoreFasterInner(String text, int start, int end, int symbolIndex)
    {
        int last = start;
        for (int i = start; i < end; i++)
        {
            if (text[i] == '{')
            {
                int k = 1;
                int j = i + 1;
                int index = 0;
                partIndices[0] = i;
                depth[0] = 1;
                for (; j < end && k > 0; j++)
                {
                    if (text[j] == '{')
                        k++;
                    else if (text[j] == '}')
                        k--;
                    else if (text[j] == '|')
                    {
                        if (k == 1)
                        {
                            partIndices[++index] = j;
                            depth[index] = 1;
                        }
                        else
                            depth[index] = k;
                    }
                }
                if (k == 0)
                {
                    partIndices[++index] = j - 1;
                    int part = rand.Next(index);
                    text.CopyTo(last, symbolsOfTextProcessed, symbolIndex, i - last);
                    symbolIndex += i - last;
                    if (depth[part] == 1)
                    {
                        text.CopyTo(partIndices[part] + 1, symbolsOfTextProcessed, symbolIndex, partIndices[part + 1] - partIndices[part] - 1);
                        symbolIndex += partIndices[part + 1] - partIndices[part] - 1;
                    }
                    else
                    {
                        symbolIndex = SpinEvenMoreFasterInner(text, partIndices[part] + 1, partIndices[part + 1], symbolIndex);
                    }
                    i = j - 1;
                    last = j;
                }
            }
        }
        text.CopyTo(last, symbolsOfTextProcessed, symbolIndex, end - last);
        return symbolIndex + end - last;
    }

答案 1 :(得分:4)

这是非正则表达式的替代方案。

更新2012-12-27(参见new ideone demo

  1. 优化OP的代码以使用静态成员而不是在循环中声明变量,使用RegexOptions.Compiled,并使用Substring代替TrimLeftTrimRight。这些优化将OP代码的执行时间缩短了近33%。
  2. 更新SpinNoRE以处理任意嵌套的spintax,优化代码和添加的注释。
  3. 为了清楚起见,分别将SpinSpinFaster重命名为SpinRESpinNoRE
  4. 使用嵌套示例更新了测试用例。 OP的代码处理嵌套的spintaxes要慢得多(可以理解,因为每个级别的嵌套都会强制进行额外的正则表达式匹配)。
  5. ideone demo可用;下面的代码(演示中提供的评论;请参阅链接):

    public static String SpinNoRE(String text)
    {
        int i, j, e = -1;
        char[] curls = new char[] {'{', '}'};
        text += '~';
    
        do
        {
            i =  e;
            e = -1;
            while ((i = text.IndexOf('{', i+1)) != -1)
            {
                j = i;
                while ((j = text.IndexOfAny(curls, j+1)) != -1 && text[j] != '}')
                {
                    if (e == -1) e = i;
                    i = j;
                }
                if (j != -1)
                {
                    parts = text.Substring(i+1, (j-1)-(i+1-1)).Split('|');
                    text = text.Remove(i, j-(i-1)).Insert(i, parts[rand.Next(parts.Length)]);
                }
            }
        }
        while (e-- != -1);
    
        return text.Remove(text.Length-1);
    }
    

    结果:

    Input Text:       Oh! {{I'm|You're} here!|How are you{ doing{|, {buddy|pal|guy}}|}?}
    Testing SpinRE:   Oh! You're here!
    Testing SpinRE:   Oh! How are you doing?
    Testing SpinRE:   Oh! How are you?
    Testing SpinRE:   Oh! How are you doing, buddy?
    Testing SpinRE:   Oh! I'm here!
    Testing SpinRE:   Oh! How are you doing, guy?
    Testing SpinRE:   Oh! How are you doing?
    Testing SpinRE:   Oh! I'm here!
    Testing SpinRE:   Oh! I'm here!
    Testing SpinRE:   Oh! How are you doing?
    Testing SpinNoRE: Oh! How are you doing, buddy?
    Testing SpinNoRE: Oh! You're here!
    Testing SpinNoRE: Oh! How are you?
    Testing SpinNoRE: Oh! How are you?
    Testing SpinNoRE: Oh! You're here!
    Testing SpinNoRE: Oh! I'm here!
    Testing SpinNoRE: Oh! How are you doing?
    Testing SpinNoRE: Oh! How are you?
    Testing SpinNoRE: Oh! How are you doing, buddy?
    Testing SpinNoRE: Oh! I'm here!
    
    Time elapsed over 100,000 runs of each in alternation:
    
    SpinRE:           03.686s
    SpinNoRE:         00.921s
    

    (自从我接触C#以来已经超过6年了。请原谅并指出任何错误。)

答案 2 :(得分:1)

我建议您对代码进行一些更改。首先,将正则表达式定义移出方法,并使用RegexOptions.Compiled选项减少每次调用的设置时间。此外,将随机数生成器的创建移出使用频繁的方法。

此外,您可以通过告诉正则表达式从哪里开始匹配来消除许多不必要的字符串搜索。如果您最终完成循环的多次迭代,这一点很重要。这个想法是,如果你已经完成了替换,直到在字符串中放置M,那么没有理由检查匹配,因为没有任何匹配。

您可以通过将表达式替换为:{/ p>来消除对TrimStartTrimEnd的调用

String[] parts = m.Value.Substring(1, m.Value.Length-2).Split('|');

你已经知道字符串以{开头,以}结尾,并且中间的任何地方都没有这两个字符,所以你所要做的就是砍掉第一个和最后一个字符没有理由承担由TrimStartTrimEnd创建的临时字符串的费用。

另一种可能性是将捕获组添加到正则表达式(将括号放在要捕获的部分周围),并对捕获的文本而不是整个匹配的表达式进行操作。

将所有这些建议放在一起导致:

static Regex reg = new Regex(@"\{([^\{\}]*)\}", RegexOptions.Compiled);
static Random rand = new Random();
public static String Spin(String text)
{
    int matchPos = 0;
    while (true)
    {
        Match m = reg.Match(text, matchPos);
        if (!m.Success) break;
        String[] parts = m.Groups[1].Value.Split('|');
        int i = rand.Next(parts.Length);
        text = text.Substring(0, m.Index) + parts[i] + text.Substring(m.Index + m.Length);
        matchPos = m.Index;
    }
    return text;
}

也就是说,这不支持嵌套,并且制作支持嵌套的正则表达式解决方案可能有点困难。它在速度方面也不是最佳,因为它花费了大量时间来构建和重建text字符串。稍微考虑一下,您可以对其进行更优化,但它永远不会像SergyS supplied这样优化的自定义解析器解决方案那么快。

如果速度至关重要,那么你需要一个自定义解析器。正则表达式版本不会那么快,但如果它足够快,它的好处是比自定义解析器更小,更容易理解和修改。