一尘不染

如何使用.NET逐个字符串分割并包含定界符?

c#

有很多类似的问题,但是显然没有完美的匹配,这就是我要问的原因。

我想分裂一个随机字符串(如123xx456yy789通过字符串分隔符的列表)(例如xxyy在结果)和包括分隔符(在这里:123xx456yy789)。

良好的表现是不错的奖励。如果可能,应避免使用正则表达式。

更新 :我进行了一些性能检查,并比较了结果(虽然懒得正式检查它们)。测试的解决方案是(随机顺序):

  1. 加布
  2. 古法
  3. 马夫
  4. 正则表达式

其他解决方案未经过测试,因为它们与其他解决方案相似或太迟了。

这是测试代码:

class Program
{
    private static readonly List<Func<string, List<string>, List<string>>> Functions;
    private static readonly List<string> Sources;
    private static readonly List<List<string>> Delimiters;

    static Program ()
    {
        Functions = new List<Func<string, List<string>, List<string>>> ();
        Functions.Add ((s, l) => s.SplitIncludeDelimiters_Gabe (l).ToList ());
        Functions.Add ((s, l) => s.SplitIncludeDelimiters_Guffa (l).ToList ());
        Functions.Add ((s, l) => s.SplitIncludeDelimiters_Naive (l).ToList ());
        Functions.Add ((s, l) => s.SplitIncludeDelimiters_Regex (l).ToList ());

        Sources = new List<string> ();
        Sources.Add ("");
        Sources.Add (Guid.NewGuid ().ToString ());

        string str = "";
        for (int outer = 0; outer < 10; outer++) {
            for (int i = 0; i < 10; i++) {
                str += i + "**" + DateTime.UtcNow.Ticks;
            }
            str += "-";
        }
        Sources.Add (str);

        Delimiters = new List<List<string>> ();
        Delimiters.Add (new List<string> () { });
        Delimiters.Add (new List<string> () { "-" });
        Delimiters.Add (new List<string> () { "**" });
        Delimiters.Add (new List<string> () { "-", "**" });
    }

    private class Result
    {
        public readonly int FuncID;
        public readonly int SrcID;
        public readonly int DelimID;
        public readonly long Milliseconds;
        public readonly List<string> Output;

        public Result (int funcID, int srcID, int delimID, long milliseconds, List<string> output)
        {
            FuncID = funcID;
            SrcID = srcID;
            DelimID = delimID;
            Milliseconds = milliseconds;
            Output = output;
        }

        public void Print ()
        {
            Console.WriteLine ("S " + SrcID + "\tD " + DelimID + "\tF " + FuncID + "\t" + Milliseconds + "ms");
            Console.WriteLine (Output.Count + "\t" + string.Join (" ", Output.Take (10).Select (x => x.Length < 15 ? x : x.Substring (0, 15) + "...").ToArray ()));
        }
    }

    static void Main (string[] args)
    {
        var results = new List<Result> ();

        for (int srcID = 0; srcID < 3; srcID++) {
            for (int delimID = 0; delimID < 4; delimID++) {
                for (int funcId = 3; funcId >= 0; funcId--) { // i tried various orders in my tests
                    Stopwatch sw = new Stopwatch ();
                    sw.Start ();

                    var func = Functions[funcId];
                    var src = Sources[srcID];
                    var del = Delimiters[delimID];

                    for (int i = 0; i < 10000; i++) {
                        func (src, del);
                    }
                    var list = func (src, del);
                    sw.Stop ();

                    var res = new Result (funcId, srcID, delimID, sw.ElapsedMilliseconds, list);
                    results.Add (res);
                    res.Print ();
                }
            }
        }
    }
}

如您所见,它实际上只是一个快速而肮脏的测试,但是我以不同的顺序多次运行了该测试,结果始终非常一致。对于较大的数据集,所测量的时间范围在毫秒到秒的范围内。我在随后的评估中忽略了低毫秒范围内的值,因为在实践中它们似乎可以忽略不计。这是我盒子上的输出:

S 0 D 0 F 3 11毫秒
1个
S 0 D 0 F 2 7毫秒
1个
S 0 D 0 F 1 6毫秒
1个
S 0 D 0 F 0 4ms
0
S 0 D 1 F 3 28毫秒
1个
S 0 D 1 F 2 8ms
1个
S 0 D 1 F 1 7毫秒
1个
S 0 D 1 F 0 3ms
0
S 0 D 2 F 3 30毫秒
1个
S 0 D 2 F 2 8ms
1个
S 0 D 2 F 1 6毫秒
1个
S 0 D 2 F 0 3毫秒
0
S 0 D 3 F 3 30毫秒
1个
S 0 D 3 F 2 10毫秒
1个
S 0 D 3 F 1 8毫秒
1个
S 0 D 3 F 0 3毫秒
0
S 1 D 0 F 3 9毫秒
1 9e5282ec-e2a2-4 ...
S 1 D 0 F 2 6毫秒
1 9e5282ec-e2a2-4 ...
S 1 D 0 F 1 5毫秒
1 9e5282ec-e2a2-4 ...
S 1 D 0 F 0 5毫秒
1 9e5282ec-e2a2-4 ...
S 1 D 1 F 3 63毫秒
9 9e5282ec-e2a2-4265-8276-6dbb50fdae37
S 1 D 1 F 2 37毫秒
9 9e5282ec-e2a2-4265-8276-6dbb50fdae37
S 1 D 1 F 1 29毫秒
9 9e5282ec-e2a2-4265-8276-6dbb50fdae37
S 1 D 1 F 0 22毫秒
9 9e5282ec-e2a2-4265-8276-6dbb50fdae37
S 1 D 2 F 3 30毫秒
1 9e5282ec-e2a2-4 ...
S 1 D 2 F 2 10毫秒
1 9e5282ec-e2a2-4 ...
S 1 D 2 F 1 10毫秒
1 9e5282ec-e2a2-4 ...
S 1 D 2 F 0 12毫秒
1 9e5282ec-e2a2-4 ...
S 1 D 3 F 3 73毫秒
9 9e5282ec-e2a2-4265-8276-6dbb50fdae37
S 1 D 3 F 2 40毫秒
9 9e5282ec-e2a2-4265-8276-6dbb50fdae37
S 1 D 3 F 1 33毫秒
9 9e5282ec-e2a2-4265-8276-6dbb50fdae37
S 1 D 3 F 0 30毫秒
9 9e5282ec-e2a2-4265-8276-6dbb50fdae37
S 2 D 0 F 3 10毫秒
1 0 ** 634226552821 ...
S 2 D 0 F 2 109毫秒
1 0 ** 634226552821 ...
S 2 D 0 F 1 5毫秒
1 0 ** 634226552821 ...
S 2 D 0 F 0 127毫秒
1 0 ** 634226552821 ...
S 2 D 1 F 3 184ms
21 0 ** 634226552821 ...-0 ** 634226552821 ...-0 ** 634226552821 ...-0 ** 634226
552821 ...-0 ** 634226552821 ...-
S 2 D 1 F 2 364毫秒
21 0 ** 634226552821 ...-0 ** 634226552821 ...-0 ** 634226552821 ...-0 ** 634226
552821 ...-0 ** 634226552821 ...-
S 2 D 1 F 1 134毫秒
21 0 ** 634226552821 ...-0 ** 634226552821 ...-0 ** 634226552821 ...-0 ** 634226
552821 ...-0 ** 634226552821 ...-
S 2 D 1 F 0 517毫秒
20 0 ** 634226552821 ...-0 ** 634226552821 ...-0 ** 634226552821 ...-0 ** 634226
552821 ...-0 ** 634226552821 ...-
S 2 D 2 F 3 688毫秒
201 0 ** 634226552821217 ... ** 634226552821217 ... ** 634226552821217 ... ** 6
34226552821217 ... **
S 2 D 2 F 2 2404毫秒
201 0 ** 634226552821217 ... ** 634226552821217 ... ** 634226552821217 ... ** 6
34226552821217 ... **
S 2 D 2 F 1 874毫秒
201 0 ** 634226552821217 ... ** 634226552821217 ... ** 634226552821217 ... ** 6
34226552821217 ... **
S 2 D 2 F 0 717毫秒
201 0 ** 634226552821217 ... ** 634226552821217 ... ** 634226552821217 ... ** 6
34226552821217 ... **
S 2 D 3 F 3 1205ms
221 0 ** 634226552821217 ... ** 634226552821217 ... ** 634226552821217 ... ** 6
34226552821217 ... **
S 2 D 3 F 2 3471ms
221 0 ** 634226552821217 ... ** 634226552821217 ... ** 634226552821217 ... ** 6
34226552821217 ... **
S 2 D 3 F 1 1008毫秒
221 0 ** 634226552821217 ... ** 634226552821217 ... ** 634226552821217 ... ** 6
34226552821217 ... **
S 2 D 3 F 0 1095毫秒
220 0 ** 634226552821217 ... ** 634226552821217 ... ** 634226552821217 ... ** 6
34226552821217 ... **

我比较了结果,这是我发现的结果:

  • 所有4个功能都足够快,可以通用使用。
  • 天真的版本(又名我最初写的)在计算时间方面是最差的。
  • 在小型数据集上,正则表达式有点慢(可能是由于初始化开销)。
  • 正则表达式在大数据方面表现出色,并且达到了与非正则表达式解决方案相似的速度。
  • 从性能角度来看,最好的选择似乎是Guffa的总体版本,这可从代码中得到预期。
  • Gabe的版本有时会省略一个项目,但我没有对此进行调查(错误?)。

结束本主题,我建议使用Regex,它相当快。 如果性能至关重要,我希望使用Guffa的实现。


阅读 235

收藏
2020-05-19

共1个答案

一尘不染

尽管您不愿使用正则表达式,但实际上它可以通过使用组和Regex.Split方法来很好地保留定界符:

string input = "123xx456yy789";
string pattern = "(xx|yy)";
string[] result = Regex.Split(input, pattern);

如果使用just从模式中除去括号"xx|yy",则不会保留定界符。如果您使用任何在正则表达式中具有特殊含义的元字符,请确保在模式上使用Regex.Escape。字符包括`\, *, +, ?,
|, {, [, (,), ^, $,.,

。例如,.应转义的定界符.。给定定界符列表,您需要使用竖线|`符号对它们进行“或”操作,并且该字符也可以转义。要正确构建模式,请使用以下代码(感谢@gabe指出这一点):

var delimiters = new List<string> { ".", "xx", "yy" };
string pattern = "(" + String.Join("|", delimiters.Select(d => Regex.Escape(d))
                                                  .ToArray())
                  + ")";

括号是连接起来的,而不是包含在模式中的,因为出于您的目的它们会被错误地转义。

编辑:
此外,如果delimiters列表碰巧是空的,则最终模式将错误地是(),这将导致空白匹配。为了防止这种情况,可以使用定界符检查。考虑到所有这些,代码片段变成:

string input = "123xx456yy789";
// to reach the else branch set delimiters to new List();
var delimiters = new List<string> { ".", "xx", "yy", "()" }; 
if (delimiters.Count > 0)
{
    string pattern = "("
                     + String.Join("|", delimiters.Select(d => Regex.Escape(d))
                                                  .ToArray())
                     + ")";
    string[] result = Regex.Split(input, pattern);
    foreach (string s in result)
    {
        Console.WriteLine(s);
    }
}
else
{
    // nothing to split
    Console.WriteLine(input);
}

如果您需要不区分大小写的分隔符匹配,请使用以下RegexOptions.IgnoreCase选项:Regex.Split(input, pattern, RegexOptions.IgnoreCase)

编辑#2:
到目前为止的解决方案匹配可能是较大字符串的子字符串的拆分标记。如果拆分标记应完全匹配,而不是子字符串的一部分,例如将句子中的单词用作分隔符的情况,\b则应在模式周围添加单词边界元字符。

例如,考虑以下句子(是的,这很老套): "Welcome to stackoverflow... where the stack never overflows!"

如果定界符为{ "stack", "flow" }当前解决方案,则将拆分“ stackoverflow”并返回3个字符串{ "stack", "over", "flow" }。如果需要精确匹配,则该拆分的唯一位置是句子后面的“堆栈”一词,而不是“ stackoverflow”。

要实现完全匹配行为,请更改模式,使其包含\b在中\b(delim1|delim2|delimN)\b

string pattern = @"\b("
                + String.Join("|", delimiters.Select(d => Regex.Escape(d)))
                + @")\b";

最后,如果需要在定界符之前和之后修剪空格,请\s*像一样在模式周围添加\s*(delim1|delim2|delimN)\s*。可以结合\b如下:

string pattern = @"\s*\b("
                + String.Join("|", delimiters.Select(d => Regex.Escape(d)))
                + @")\b\s*";
2020-05-19