如何按字符串拆分字符串并使用.NET包含分隔符?

时间:2010-03-20 21:50:50

标签: c# .net string

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

我想通过字符串分隔符列表(例如123xx456yy789xx)拆分随机字符串(例如yy)并在结果中包含分隔符(这里: 123xx456yy789)。

良好的表现是一个很好的奖励。如果可能的话,应该避免使用正则表达式。

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

  1. Gabe
  2. Guffa
  3. Mafu
  4. Regex
  5. 其他解决方案未经过测试,因为它们与其他解决方案相似或者来得太晚。

    这是测试代码:

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

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

    • 所有4个功能都足够快,适合常用。
    • 天真的版本(也就是我最初写的)在计算时间方面是最糟糕的。
    • 正则表达式在小数据集上有点慢(可能是由于初始化开销)。
    • Regex在大数据方面做得很好,并且与非正则表达式解决方案的速度相似。
    • 表现最好的似乎是Guffa的整体版本,这可以从代码中预期。
    • Gabe的版本有时会省略一个项目,但我没有对此进行调查(错误?)。

    为了结束这个主题,我建议使用速度相当快的正则表达式。如果表现至关重要,我宁愿选择Guffa。

7 个答案:

答案 0 :(得分:44)

尽管您不愿意使用正则表达式,但它实际上通过使用组和Regex.Split方法很好地保留了分隔符:

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

如果从模式中删除括号,仅使用"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元字符。 / p>

例如,考虑这句话(是的,它是老生常谈):"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*";

答案 1 :(得分:12)

好的,抱歉,也许是这个:

    string source = "123xx456yy789";
    foreach (string delimiter in delimiters)
        source = source.Replace(delimiter, ";" + delimiter + ";");
    string[] parts = source.Split(';');

答案 2 :(得分:4)

这是一个不使用正则表达式并且不会产生超出必要条件的字符串的解决方案:

public static List<string> Split(string searchStr, string[] separators)
{
    List<string> result = new List<string>();
    int length = searchStr.Length;
    int lastMatchEnd = 0;
    for (int i = 0; i < length; i++)
    {
        for (int j = 0; j < separators.Length; j++)
        {
            string str = separators[j];
            int sepLen = str.Length;
            if (((searchStr[i] == str[0]) && (sepLen <= (length - i))) && ((sepLen == 1) || (String.CompareOrdinal(searchStr, i, str, 0, sepLen) == 0)))
            {
                result.Add(searchStr.Substring(lastMatchEnd, i - lastMatchEnd));
                result.Add(separators[j]);
                i += sepLen - 1;
                lastMatchEnd = i + 1;
                break;
            }
        }
    }
    if (lastMatchEnd != length)
        result.Add(searchStr.Substring(lastMatchEnd));
    return result;
}

答案 3 :(得分:3)

我想出了类似的东西的解决方案。要有效地拆分字符串,您可以保留每个分隔符的下一个出现的列表。这样,您可以最大限度地减少必须查找每个分隔符的时间。

即使对于长字符串和大量分隔符,此算法也能很好地运行:

string input = "123xx456yy789";
string[] delimiters = { "xx", "yy" };

int[] nextPosition = delimiters.Select(d => input.IndexOf(d)).ToArray();
List<string> result = new List<string>();
int pos = 0;
while (true) {
  int firstPos = int.MaxValue;
  string delimiter = null;
  for (int i = 0; i < nextPosition.Length; i++) {
    if (nextPosition[i] != -1 && nextPosition[i] < firstPos) {
      firstPos = nextPosition[i];
      delimiter = delimiters[i];
    }
  }
  if (firstPos != int.MaxValue) {
    result.Add(input.Substring(pos, firstPos - pos));
    result.Add(delimiter);
    pos = firstPos + delimiter.Length;
    for (int i = 0; i < nextPosition.Length; i++) {
      if (nextPosition[i] != -1 && nextPosition[i] < pos) {
        nextPosition[i] = input.IndexOf(delimiters[i], pos);
      }
    }
  } else {
    result.Add(input.Substring(pos));
    break;
  }
}

(对于任何错误的预订,我现在只是把这个版本放在一起,我没有彻底测试它。)

答案 4 :(得分:2)

天真的实施

public IEnumerable<string> SplitX (string text, string[] delimiters)
{
    var split = text.Split (delimiters, StringSplitOptions.None);

    foreach (string part in split) {
        yield return part;
        text = text.Substring (part.Length);

        string delim = delimiters.FirstOrDefault (x => text.StartsWith (x));
        if (delim != null) {
            yield return delim;
            text = text.Substring (delim.Length);
        }
    }
}

答案 5 :(得分:1)

这将与String.Split默认模式具有相同的语义(因此不包括空标记)。

通过使用不安全的代码迭代源字符串可以加快速度,但这需要您自己编写迭代机制而不是使用yield return。 它分配绝对最小值(每个非分隔符令牌的子串加上包装枚举器),以便现实地提高您的性能:

  • 使用更加不安全的代码(通过使用'CompareOrdinal'我实际上是有效的)
    • 主要是为了避免使用char缓冲区对字符串进行字符查找的开销
  • 利用有关输入源或令牌的领域特定知识。
    • 您可能很乐意消除对分隔符的空检查
    • 您可能知道分隔符几乎不是单个字符

代码是作为扩展方法编写的

public static IEnumerable<string> SplitWithTokens(
    string str,
    string[] separators)
{
    if (separators == null || separators.Length == 0)
    {
        yield return str;
        yield break;
    }
    int prev = 0;
    for (int i = 0; i < str.Length; i++)
    {
        foreach (var sep in separators)
        {
            if (!string.IsNullOrEmpty(sep))
            {
                if (((str[i] == sep[0]) && 
                          (sep.Length <= (str.Length - i))) 
                     &&
                    ((sep.Length == 1) || 
                    (string.CompareOrdinal(str, i, sep, 0, sep.Length) == 0)))
                {
                    if (i - prev != 0)
                        yield return str.Substring(prev, i - prev);
                    yield return sep;
                    i += sep.Length - 1;
                    prev = i + 1;
                    break;
                }
            }
        }
    }
    if (str.Length - prev > 0)
        yield return str.Substring(prev, str.Length - prev);
}

答案 6 :(得分:1)

我的第一篇帖子/答案......这是一种递归方式。

    static void Split(string src, string[] delims, ref List<string> final)
    {
        if (src.Length == 0)
            return;

        int endTrimIndex = src.Length;
        foreach (string delim in delims)
        {
            //get the index of the first occurance of this delim
            int indexOfDelim = src.IndexOf(delim);
            //check to see if this delim is at the begining of src
            if (indexOfDelim == 0)
            {
                endTrimIndex = delim.Length;
                break;
            }
            //see if this delim comes before previously searched delims
            else if (indexOfDelim < endTrimIndex && indexOfDelim != -1)
                endTrimIndex = indexOfDelim;
        }
        final.Add(src.Substring(0, endTrimIndex));
        Split(src.Remove(0, endTrimIndex), delims, ref final);
    }