【问题标题】:Formatting sentences in a string using C#使用 C# 格式化字符串中的句子
【发布时间】:2011-01-09 07:15:09
【问题描述】:

我有一个包含多个句子的字符串。如何将每个句子中第一个单词的第一个字母大写。类似于 word 中的段落格式。

eg ."这是一些代码。代码是用 C# 编写的。" 输出必须是“这是一些代码。代码在 C# 中”。

一种方法是根据 '.' 拆分字符串。然后将第一个字母大写,然后重新加入。

有没有更好的解决方案?

【问题讨论】:

  • 我会说 - 转换为 char 数组,使用 while 循环遍历,适当时大写,保存回字符串。几行代码,但应该很快。
  • @Hamish:这个答案不错;这肯定比反复操作字符串要好得多。但是,我认为 StringBuffer 会更简单。
  • 不要忘记 '?'、'!'、':',也许还有 '\n'(如果他们错过了标点符号)
  • 如果你真的想把它弄好,这实际上要复杂得多。想一想:“我出生于 1999 年 1 月 1 日。我的电子邮件地址是 blah@blah.com。”因此,对于您的示例,请确保使用拆分和连接,但我认为实际上您至少正在查看一个复杂的正则表达式。
  • @Greg 我用我提出的正则表达式解决方案尝试了您的示例。看起来还不错,图案也不算太复杂。它只作用于标点符号后的第一个单词,后跟空格(或字符串的开头)。

标签: c# string formatting paragraph text-segmentation


【解决方案1】:

在我看来,当涉及到潜在的复杂的基于规则的字符串匹配和替换时——你不能比基于正则表达式的解决方案更好(尽管它们很难阅读!)。在我看来,这提供了最好的性能和内存效率——你会惊讶于它的速度有多快。

我会使用Regex.Replace overload that accepts an input string, regex pattern and a MatchEvaluator delegate。 MatchEvaluator 是一个接受Match 对象作为输入并返回字符串替换的函数。

代码如下:

public static string Capitalise(string input)
{
  //now the first character
  return Regex.Replace(input, @"(?<=(^|[.;:])\s*)[a-z]",
    (match) => { return match.Value.ToUpper(); });
}

正则表达式使用 (?[.;:] 位中,您可以添加所需的额外部分(例如,[.;:?."] 添加 ? 和 " 字符。

这也意味着,您的 MatchEvaluator 不必进行任何不必要的字符串连接(出于性能原因,您希望避免这种情况)。

从性能的角度来看,其他回答者之一提到的关于使用 RegexOptions.Compiled 的所有其他内容也是相关的。不过,静态 Regex.Replace 方法确实提供了非常相似的性能优势(只是额外的字典查找)。

就像我说的 - 如果这里的任何其他非正则表达式解决方案能够更好地工作并且速度一样快,我会感到惊讶。

编辑

已将此解决方案与 Ahmad 的解决方案相提并论,因为他非常正确地指出,环顾四周可能不如按照自己的方式进行。

这是我做的粗略基准:

public string LowerCaseLipsum
{
  get
  {
    //went to lipsum.com and generated 10 paragraphs of lipsum
    //which I then initialised into the backing field with @"[lipsumtext]".ToLower()
    return _lowerCaseLipsum;
  }
 }
 [TestMethod]
 public void CapitaliseAhmadsWay()
 {
   List<string> results = new List<string>();
   DateTime start = DateTime.Now;
   Regex r = new Regex(@"(^|\p{P}\s+)(\w+)", RegexOptions.Compiled);
   for (int f = 0; f < 1000; f++)
   {
     results.Add(r.Replace(LowerCaseLipsum, m => m.Groups[1].Value
                      + m.Groups[2].Value.Substring(0, 1).ToUpper()
                           + m.Groups[2].Value.Substring(1)));
   }
   TimeSpan duration = DateTime.Now - start;
   Console.WriteLine("Operation took {0} seconds", duration.TotalSeconds);
 }

 [TestMethod]
 public void CapitaliseLookAroundWay()
 {
   List<string> results = new List<string>();
   DateTime start = DateTime.Now;
   Regex r = new Regex(@"(?<=(^|[.;:])\s*)[a-z]", RegexOptions.Compiled);
   for (int f = 0; f < 1000; f++)
   {
     results.Add(r.Replace(LowerCaseLipsum, m => m.Value.ToUpper()));
   }
   TimeSpan duration = DateTime.Now - start;
   Console.WriteLine("Operation took {0} seconds", duration.TotalSeconds);
 }

在发布版本中,我的解决方案比 Ahmad 的解决方案快约 12%(1.48 秒而不是 1.68 秒)。

然而,有趣的是,如果通过静态 Regex.Replace 方法完成,两者都慢了大约 80%,而且我的解决方案比 Ahmad 的慢。

【讨论】:

  • 我的怀疑是,即使使用预编译的正则表达式,它也不会像 StringBuilder 一样快。
  • Regex 无论如何都在内部使用 Stringbuilder - 但我想找出答案的唯一方法是对不同的解决方案进行基准测试。在我们这样做之前,其他任何事情都是纯粹的猜想:)
  • 安德拉斯:感谢您的回答。如果我们有像“?”这样的标点符号,它将不起作用。我猜艾哈迈德在下面的回答很接近。我还没有完全评估它。
  • Yogendra :当然会 - 只需将问号添加到正则表达式中的 [.;:] 位 - 即将其更改为 '[.;:?]'。事实上,您可以在这两个方括号内添加您需要陷印的所有单个标点符号。我也编辑了答案 - 因为“。”在 [] 中不需要前导 '\'。
  • 我应该说您可以使用与 Ahmad 相同的正则表达式 - 只需将 [punctuation_characters] 块替换为标点符号类。除此之外,这个正则表达式的结构更好,因为它不需要 MatchEvaluator 中的 'a + b + c' 和 SubString 操作,因此会更快。
【解决方案2】:

这是一个正则表达式解决方案,它使用标点符号类别来避免必须指定 .!?” 等,尽管您当然应该检查它是否满足您的需求或明确设置它们。阅读“支持”下的“P”类别Unicode 通用类别”部分位于 MSDN Character Classes page

string input = @"this is some code. the code is in C#? it's great! In ""quotes."" after quotes.";
string pattern = @"(^|\p{P}\s+)(\w+)";

// compiled for performance (might want to benchmark it for your loop)
Regex rx = new Regex(pattern, RegexOptions.Compiled);

string result = rx.Replace(input, m => m.Groups[1].Value
                                + m.Groups[2].Value.Substring(0, 1).ToUpper()
                                + m.Groups[2].Value.Substring(1));

如果您决定不使用 \p{P} 类,则必须自己指定字符,类似于:

string pattern = @"(^|[.?!""]\s+)(\w+)";

编辑: 下面是一个更新的示例,用于演示 3 种模式。第一个显示所有标点符号如何影响大小写。第二个展示了如何使用类减法来挑选某些标点符号类别。它使用所有标点符号,同时删除特定的标点符号组。第三个与第二个类似,但使用不同的组。

MSDN 链接没有说明某些标点符号类别所指的内容,所以这里有一个细分:

  • P:所有标点符号(包括以下所有类别)
  • 电脑:下划线_
  • Pd:破折号-
  • Ps:开括号、方括号和大括号([{
  • Pe:右括号、方括号和大括号 ) ] }
  • Pi:初始单引号/双引号(MSDN 说它“可能表现得像 Ps/Pe,具体取决于使用情况”)
  • Pf:最后的单引号/双引号(适用 MSDN Pi 注释)
  • Po:逗号、冒号、分号和斜杠等其他标点符号,:;\/

仔细比较这些组对结果的影响。这应该给你很大的灵活性。如果这看起来不理想,那么您可以在字符类中使用特定字符,如前所示。

string input = @"foo ( parens ) bar { braces } foo [ brackets ] bar. single ' quote & "" double "" quote.
dash - test. Connector _ test. Comma, test. Semicolon; test. Colon: test. Slash / test. Slash \ test.";

string[] patterns = { 
    @"(^|\p{P}\s+)(\w+)", // all punctuation chars
    @"(^|[\p{P}-[\p{Pc}\p{Pd}\p{Ps}\p{Pe}]]\s+)(\w+)", // all punctuation chars except Pc/Pd/Ps/Pe
    @"(^|[\p{P}-[\p{Po}]]\s+)(\w+)" // all punctuation chars except Po
};

// compiled for performance (might want to benchmark it for your loop)
foreach (string pattern in patterns)
{
    Console.WriteLine("*** Current pattern: {0}", pattern);
    string result = Regex.Replace(input, pattern,
                            m => m.Groups[1].Value
                                 + m.Groups[2].Value.Substring(0, 1).ToUpper()
                                 + m.Groups[2].Value.Substring(1));
    Console.WriteLine(result);
    Console.WriteLine();
}

请注意,“Dash”不是使用最后一个模式大写的,而是在一个新行上。使其大写的一种方法是使用RegexOptions.Multiline 选项。试试上面的sn-p,看看是否满足你想要的结果。

另外,为了举例,我没有在上面的循环中使用 RegexOptions.Compiled。要同时使用这两个选项或它们:RegexOptions.Compiled | RegexOptions.Multiline

【讨论】:

  • +1 - 标点符号类很好 - 但是在 MatchEvaluator 中添加所有这些字符串和子字符串并不能从 Regex.Replace 操作将要使用的 StringBuilder 中获得最佳效果。我的解决方案对标识“第一个”字符的位使用零宽度捕获,这意味着 OP 只返回 match.Value.ToUpper()。
  • 艾哈迈德 - 正如你所建议的那样 - 添加了一个粗略但我认为公平的基准作为我的答案。当两个正则表达式都使用 RegexOptions.Compiled 编译成正则表达式实例时,我的速度更快。使用 Regex.Replace 静态方法时,您的速度更快 - 但是这样做时对两者的性能影响也很大(我再也不会使用静态方法了!):)
【解决方案3】:

您有几个不同的选择:

  1. 您拆分字符串、大写然后重新加入的方法
  2. 使用正则表达式替换表达式(这可能有点棘手)
  3. 编写一个 C# 迭代器,它遍历每个字符并生成一个新的 IEnumerable&lt;char&gt;,其中句点后的第一个字母为大写。可能提供流式解决方案的好处。
  4. 循环遍历每个字符并大写在句点之后立即出现的字符(忽略空格)- StringBuffer 可能会使这更容易。

下面的代码使用了一个迭代器:

public static string ToSentenceCase( string someString )
{
  var sb = new StringBuilder( someString.Length );
  bool wasPeriodLastSeen = true; // We want first letter to be capitalized
  foreach( var c in someString )
  {
      if( wasPeriodLastSeen && !c.IsWhiteSpace ) 
      {
          sb.Append( c.ToUpper() );
          wasPeriodLastSeen = false;         
      }        
      else
      {
          if( c == '.' )  // you may want to expand this to other punctuation
              wasPeriodLastSeen = true;
          sb.Append( c );
      }
  }

  return sb.ToString();
}

【讨论】:

  • LBushkin:ToTitleCase 会将字符串中每个单词的首字母大写。在我的情况下,输出将是“这是一些代码。代码在 C# 中”。
  • Steven : 性能是个问题,因为方法是在循环中调用的。
  • 你是对的,我查看了文档,它是逐字的。我将更新我的帖子以反映正确的实现。
【解决方案4】:

我不知道为什么,但根据 LBushkin 的建议,我决定尝试 yield return。只是为了好玩。

static IEnumerable<char> CapitalLetters(string sentence)
        {
            //capitalize first letter
            bool capitalize = true;
            char lastLetter;
            for (int i = 0; i < sentence.Length; i++)
            {
                lastLetter = sentence[i];
                yield return (capitalize) ? Char.ToUpper(sentence[i]) : sentence[i];


                if (Char.IsWhiteSpace(lastLetter) && capitalize == true)
                    continue;

                capitalize = false;
                if (lastLetter == '.' || lastLetter == '!') //etc
                    capitalize = true;
            }
        }

使用它:

string sentence = new String(CapitalLetters("this is some code. the code is in C#.").ToArray());

【讨论】:

    【解决方案5】:
    1. 在 StringBuffer 中工作。
    2. 全部小写。
    3. 循环和大写前导字符。
    4. 调用 ToString。

    【讨论】:

    • 这可能会产生意想不到的后果,即小写其他应保持大写的文本,例如名称。
    • @LBushkin:如果你确定可以跳过第 2 步。
    • 由于没有很多地方可以大写,一个可能可以有效地使用 StringBuilder,但魔鬼在细节中。想粘贴代码吗?
    • 写这篇文章会很有趣,但我现在不能这样做。对不起。
    猜你喜欢
    • 1970-01-01
    • 2014-11-02
    • 2013-07-09
    • 1970-01-01
    • 1970-01-01
    • 2021-05-09
    • 2016-10-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多