【问题标题】:How do I split a string by strings and include the delimiters using .NET?如何按字符串拆分字符串并使用 .NET 包含分隔符?
【发布时间】:2010-03-20 21:50:49
【问题描述】:

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

我想通过字符串分隔符列表(例如xxyy)拆分随机字符串(例如123xx456yy789)并在结果中包含分隔符(此处:123、@987654329 @、456yy789)。

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

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

  1. Gabe
  2. Guffa
  3. Mafu
  4. Regex

其他解决方案未经过测试,因为它们与其他解决方案相似或出现得太晚。

这是测试代码:

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 10 毫秒
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 109 毫秒
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 364 毫秒
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 1095 毫秒
220 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **

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

  • 所有 4 个函数的速度都足够快,可以正常使用。
  • 就计算时间而言,幼稚版本(也就是我最初写的)是最差的。
  • 正则表达式在小型数据集上有点慢(可能是由于初始化开销)。
  • 正则表达式在大数据上表现出色,并且达到与非正则表达式解决方案相似的速度。
  • 性能方面最好的似乎是 Guffa 的整体版本,这在代码中是意料之中的。
  • Gabe 的版本有时会省略一项,但我没有对此进行调查(错误?)。

为了结束这个话题,我建议使用 Regex,它相当快。如果性能很关键,我更喜欢 Guffa 的实现。

【问题讨论】:

    标签: c# .net string


    【解决方案1】:

    尽管您不愿意使用正则表达式,但它实际上通过使用组和 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 列表恰好为空,则最终模式将错误地为(),这将导致空白匹配。为了防止这种情况,可以使用对分隔符的检查。考虑到所有这些,sn-p 变为:

    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" }。如果您需要完全匹配,那么它会拆分的唯一位置是句子后面的“stack”一词,而不是“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 这是一个不错的解决方案。我确实喜欢正则表达式,我只是觉得它对于一项如此简单的工作来说太大了,以至于 .NET 的字符串类中包含了一个非常相似的版本。
    • 您需要使用pattern = "(" + String.Join("|", (from d in delimeters select Regex.Escape(d)).ToArray()) + ")",因为任何分隔符都可能有.| 或其中的任何内容。
    • +1 我不知道你能做到这一点!很不错。您只需要修复 Regex.Escape 代码...
    • @Ahmad - 接受分隔符数组并在构建模式组之前使用 Regex.Escape。
    • @Sky 我不确定我是否遵循您的建议。也许你在看到我最后一次编辑之前就做到了?使用数组类似于使用列表;在Select 之后还需要一个ToArray() 才能从IEnumerable&lt;string&gt; 变为string[] for String.Join
    【解决方案2】:

    好吧,对不起,也许是这个:

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

    【讨论】:

    • 包含;的分隔符失败。
    • @mafutrct - 不过,他实际上提出了一个可行的想法。也许有一个可能的 new 分隔符列表,每个分隔符可以是一个或多个字符。遍历列表,检查可能的分隔符是否存在,然后将 Nagg 的逻辑用于第一个通过测试的分隔符。
    • 没错,但我真的很想要一个不依赖于字符串中不存在某些分隔符文字的解决方案。我不明白这个想法怎么可能,除了一些可能会严重损害性能的映射。当然,我愿意接受反例。
    • 我用 {".","?","!"} 分隔每个句子到一个新行。我想在最后保留分隔符。这个答案又快又容易。修改了一部分 source = source.Replace(delimiter, delimiter + ";");现在,我的行尾有句号、问号等。谢谢!
    【解决方案3】:

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

    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;
    }
    

    【讨论】:

    • 我注意到这会产生与其他所有不同的输出。显然,有时某件物品会丢失。
    【解决方案4】:

    一个简单的实现

    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】:

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

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

      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;
        }
      }
      

      (保留任何错误,我现在只是把这个版本放在一起,我还没有彻底测试它。)

      【讨论】:

        【解决方案6】:

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

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

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

        代码写成扩展方法

        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);
        }
        

        【讨论】:

        • 啊 - 意识到我在实现上与 gabe 相似。我的节省了一些分配,但基本上是相同的概念。
        • 您的实现如何保存分配?
        • @gabe 我没有为分隔符标记创建子字符串,添加到您的标记的小改进(我看到您已经完成了)
        • 是的,但是您的foreach 循环为输入字符串的每个字符的分隔符数组分配了一个新的枚举数。
        • @gabe foreach 在(编译时间已知)数组上不分配枚举器。试试看。
        【解决方案7】:

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

            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);
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-13
          相关资源
          最近更新 更多