【问题标题】:A faster way of doing multiple string replacements进行多个字符串替换的更快方法
【发布时间】:2010-11-11 14:28:25
【问题描述】:

我需要做以下事情:

    static string[] pats = { "å", "Å", "æ", "Æ", "ä", "Ä", "ö", "Ö", "ø", "Ø" ,"è", "È", "à", "À", "ì", "Ì", "õ", "Õ", "ï", "Ï" };
    static string[] repl = { "a", "A", "a", "A", "a", "A", "o", "O", "o", "O", "e", "E", "a", "A", "i", "I", "o", "O", "i", "I" };
    static int i = pats.Length;
    int j;

     // function for the replacement(s)
     public string DoRepl(string Inp) {
      string tmp = Inp;
        for( j = 0; j < i; j++ ) {
            tmp = Regex.Replace(tmp,pats[j],repl[j]);
        }
        return tmp.ToString();            
    }
    /* Main flow processes about 45000 lines of input */

每行有 6 个元素通过 DoRepl。大约 300,000 个函数调用。每个执行 20 次 Regex.Replace,总计约 600 万次替换。

有没有更优雅的方法可以在更少的通行数中做到这一点?

【问题讨论】:

  • 您的问题标题是“更快”,但您的问题却是“优雅”,您追求的是哪一个?两者可以相互排斥,但肯定不总是相互排斥的。
  • 很好地指出,实际上代码运行速度确实足够快,我一直在寻找一种在计算周期方面更有效的方法。现在是 45,000 行输入,但它可能会扩展到数百万行和每行数十个元素,这意味着相当多的替换。
  • 实际上,如果有任何方法可以轻松地将“特殊”字母更改为最基本的字母(通过更改代码页或任何其他方式,它会同样有帮助) - 所有这些 äåâãàá-变体为 a 等。
  • 最快的(恕我直言) - 是使用地图[65536](见下面我的回答),另一方面更优雅......我猜它也在使用那个地图:),或者,取决于您要使用多少语言和 BCL 功能,可以使其相当复杂。字典方法可以节省内存,但会降低速度,因为它需要为每个字符执行更昂贵的存储桶查找。
  • 别忘了,在你的情况下,你只是想替换重音字符。因此,所有替换字符都大于 ascii 表中的小写“z”。在运行查找之前,请确保 chars 值大于 'z'

标签: c# regex


【解决方案1】:
static Dictionary<char, char> repl = new Dictionary<char, char>() { { 'å', 'a' }, { 'ø', 'o' } }; // etc...
public string DoRepl(string Inp)
{
    var tmp = Inp.Select(c =>
    {
        char r;
        if (repl.TryGetValue(c, out r))
            return r;
        return c;
    });
    return new string(tmp.ToArray());
}

每个字符仅根据字典检查一次,如果在字典中找到则替换。

【讨论】:

  • 我认为这将是公认的答案,它回答了我的具体问题,尽管人们进一步了解了我试图完成的工作的本质并提供了更好的解决方案。当我需要进行不同类型的替换(在单个字符上!)时,我会记住这一点(因为我认为您的替换不会采用多字符模式。
  • 我认为直接 64KB 映射的解决方案要快得多,但会消耗更多内存。 (见下面我的回答)
  • @JesperLarsen-Ledet 你为什么使用字典?
  • @JesperLarsen-Ledet 在提问者使用的 2 个数组上是否具有有效性能?
  • @Yamamoto 是的。字典查找是 O(1)。 DoRepl 在输入字符串的长度上是 O(N)。没有比这更快的了。
【解决方案2】:

这个“技巧”怎么样?

string conv = Encoding.ASCII.GetString(Encoding.GetEncoding("Cyrillic").GetBytes(input));

【讨论】:

  • 这是一个非常好的技巧!它拿走了列表中的所有内容,甚至用 ? 替换了一些奇怪的符号。 (我喜欢这种方式)
  • 我应该提到这是我最终选择的实现。它在没有任何维护的情况下完成了我们需要的工作。如果可以的话,我会加倍支持你! :)
【解决方案3】:

没有正则表达式可能会更快。

    for( j = 0; j < i; j++ ) 
    {
        tmp = tmp.Replace(pats[j], repl[j]);
    }

编辑

使用ZipStringBuilder 的另一种方式:

StringBuilder result = new StringBuilder(input);
foreach (var zipped = patterns.Zip(replacements, (p, r) => new {p, r}))
{
  result = result.Replace(zipped.p, zipped.r);
}
return result.ToString();

【讨论】:

  • 哇。 .NET 中良好的旧 C 语法
  • @ppumkin 这是几年前的事了。今天,我会用 linq 语句来写。
  • 是的,我知道。甚至在 linq 时间之前使用foreach 还是更好;)
  • @ppumkin 您仍然需要使用计数器变量来访问第二个列表。 for 是更简单的计数方法。除非您想直接使用IEnumerator 接口,否则使用MoveNext 之类的,这在IMO 中更糟糕。
【解决方案4】:

首先,我会使用StringBuilder 在缓冲区内执行翻译,避免到处创建新字符串。

接下来,理想情况下,我们想要类似于XPathtranslate(),这样我们就可以使用字符串而不是数组或映射。让我们在extension method 中这样做:

public static StringBuilder Translate(this StringBuilder builder,
    string inChars, string outChars)
{
    int length = Math.Min(inChars.Length, outChars.Length);
    for (int i = 0; i < length; ++i) {
        builder.Replace(inChars[i], outChars[i]);
    }
    return builder;
}

然后使用它:

StringBuilder builder = new StringBuilder(yourString);
yourString = builder.Translate("åÅæÆäÄöÖøØèÈàÀìÌõÕïÏ",
    "aAaAaAoOoOeEaAiIoOiI").ToString();

【讨论】:

  • 这可能不如字典方法有效,因为您需要搜索和替换每个字符的字符串。
  • @Johan,你可能是对的。我承认我的目标只是从字面上解决提问者的问题,并且在这种情况下我非常偏向于扩展方法:)
【解决方案5】:

原始正则表达式的问题在于您没有充分利用它。请记住,正则表达式模式可以有交替。您仍然需要一本字典,但您可以一次性完成,而无需遍历每个字符。

这将实现如下:

string[] pats = { "å", "Å", "æ", "Æ", "ä", "Ä", "ö", "Ö", "ø", "Ø" ,"è", "È", "à", "À", "ì", "Ì", "õ", "Õ", "ï", "Ï" };
string[] repl = { "a", "A", "a", "A", "a", "A", "o", "O", "o", "O", "e", "E", "a", "A", "i", "I", "o", "O", "i", "I" };
// using Zip as a shortcut, otherwise setup dictionary differently as others have shown
var dict = pats.Zip(repl, (k,v) => new { Key = k, Value = v }).ToDictionary(o => o.Key, o => o.Value);

string input = "åÅæÆäÄöÖøØèÈàÀìÌõÕïÏ";
string pattern = String.Join("|", dict.Keys.Select(k => k)); // use ToArray() for .NET 3.5
string result = Regex.Replace(input, pattern, m => dict[m.Value]);

Console.WriteLine("Pattern: " + pattern);
Console.WriteLine("Input: " + input);
Console.WriteLine("Result: " + result);

当然,您应该始终使用Regex.Escape 转义您的模式。在这种情况下,这不是必需的,因为我们知道有限的字符集并且不需要对它们进行转义。

【讨论】:

    【解决方案6】:

    如果您想删除重音,那么这个解决方案可能会有所帮助How do I remove diacritics (accents) from a string in .NET?

    否则我会一次性完成:

    Dictionary<char, char> replacements = new Dictionary<char, char>();
    ...
    StringBuilder result = new StringBuilder();
    foreach(char c in str)
    {
      char rc;
      if (!_replacements.TryGetValue(c, out rc)
      {
        rc = c;
      }
      result.Append(rc);
    }
    

    【讨论】:

      【解决方案7】:

      在一对一字符替换的特殊情况下,最快的(恕我直言)方式(甚至与字典相比)将是一个完整的字符映射:

      public class Converter
      {
          private readonly char[] _map;
      
          public Converter()
          {
              // This code assumes char to be a short unsigned integer
              _map = new char[char.MaxValue];
      
              for (int i = 0; i < _map.Length; i++)
                  _map[i] = (char)i;
      
              _map['å'] = 'a';  // Note that 'å' is used as an integer index into the array.
              _map['Å'] = 'A';
              _map['æ'] = 'a';
              // ... the rest of overriding map
          }
      
          public string Convert(string source)
          {
              if (string.IsNullOrEmpty(source))
                  return source;
      
              var result = new char[source.Length];
      
              for (int i = 0; i < source.Length; i++)
                  result[i] = _map[source[i]]; // convert using the map
      
              return new string(result);
          }
      }
      

      要进一步加快此代码的速度,您可能需要使用“不安全”关键字并使用指针。这样,可以更快地遍历字符串数组并且无需边界检查(理论上这会被 VM 优化掉,但可能不会)。

      【讨论】:

      • 不要使用指针。固定数组会使它变慢。
      • 也许 - 取决于我猜的字符串的大小。但更重要的是,应该使用地图而不是字典来获得最佳性能。最大的 hack 可能是创建一个字符串,将其固定到位,然后对其进行修改。完全违反 .NET 规则,但可以工作...
      • 数组与字典的性能优势将很小。它甚至可能根本没有好处,因为这个阵列有更多的缓存压力。你必须测量才能发现。无论如何,使用地图将是最大的加速 - 无论您使用数组还是字典来实现地图,都可能是一个相对微不足道的差异。
      • 这是一个很好的观点 - 猜测唯一的方法就是运行它。谢谢。
      【解决方案8】:

      我不熟悉 Regex 类,但大多数正则表达式引擎都有一个音译操作,在这里可以很好地工作。那么您每条线路只需要一个呼叫。

      【讨论】:

      • 实际上一个“行”是一个 InputBuffer 行,并且有六列(目前) - 所以每行很难做到,但理论上你是绝对正确的,如果它有是“单行文本”,每行只需要一次调用:)
      猜你喜欢
      • 1970-01-01
      • 2022-08-18
      • 1970-01-01
      • 1970-01-01
      • 2019-04-19
      • 2011-03-25
      • 1970-01-01
      • 1970-01-01
      • 2014-04-29
      相关资源
      最近更新 更多