将一个PascalCase字符串拆分为单独的单词

16

我正在寻找一种将 PascalCase 字符串,例如 "MyString",拆分为单独单词的方法 - "My", "String"。另一位用户 bash 中提出了这个问题,但我想知道如何使用通用正则表达式或至少在 .NET 中实现它。

如果您能找到一种方法来拆分(并可选择大写)camelCase字符串,那就更好了:例如,"myString" 变成 "my" 和 "String",并可选择大小写其中一个或两个字符串。


这个问题特定于 .NET,但是正则表达式的答案也可以在其他地方应用。 - Pat
请查看重复问题:已接受的答案包含正则表达式,可以将 AnXMLAndXSLT2.0Tool 拆分为 [An][XML][And][XSLT][2.0][Tool]。它使用了环视,可以说是相当易读的。 - polygenelubricants
10个回答

28

请参考这个问题:有没有一种优雅的方法来解析一个单词并在大写字母前添加空格? 其中被接受的答案涵盖了你所需要的内容,包括连续数字和几个大写字母。虽然示例中的单词以大写字母开头,但当第一个单词为小写字母时,它同样有效。

string[] tests = {
   "AutomaticTrackingSystem",
   "XMLEditor",
   "AnXMLAndXSLT2.0Tool",
};


Regex r = new Regex(
    @"(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])"
  );

foreach (string s in tests)
  r.Replace(s, " ");
上述代码将输出:
[Automatic][Tracking][System]
[XML][Editor]
[An][XML][And][XSLT][2.0][Tool]

2
@Steven Sudit:是的。正则表达式是解决这种问题的最佳工具之一。另一个问题只是通过更大的样例用例得到了澄清。 - chilltemp
我自己编写了一个不使用正则表达式的函数。 - Shimmy Weitzhandler
1
@Shimmy:性能因许多因素而异,包括正则表达式的复杂程度和是否已编译。就像使用C#的方式不同,其性能也会有所不同。话虽如此,我始终发现.NET中的正则表达式对于我的需求(具有高吞吐量的实时交易系统)来说足够快。真正进行比较的唯一方法是查看生成的IL和/或进行定时测试运行。 - chilltemp
同意。我已经查看了你的函数。顺便说一下,我编辑了你的答案,这样用户就不必再说“那又怎样?”了。 - Shimmy Weitzhandler
请注意,这个程序不会将“2days”分成“2天”,它只适用于大写的单词。 - Savage
显示剩余3条评论

12

这里提供了一种使用LINQ的替代正则表达式和循环解决方案,它还可以处理驼峰命名法和缩略语:

    string[] testCollection = new string[] { "AutomaticTrackingSystem", "XSLT", "aCamelCaseWord" };
    foreach (string test in testCollection)
    {
        // if it is not the first character and it is uppercase
        //  and the previous character is not uppercase then insert a space
        var result = test.SelectMany((c, i) => i != 0 && char.IsUpper(c) && !char.IsUpper(test[i - 1]) ? new char[] { ' ', c } : new char[] { c });
        Console.WriteLine(new String(result.ToArray()));
    }
这是输出结果:
Automatic Tracking System  
XSLT  
a Camel Case Word 

1
这是我绝对最喜欢的 :) - kzu
1
值得注意的是,如果期望将首字母缩略词视为单独的单词,则此方法无法处理与其他单词混合的首字母缩略词。例如,HTTPResponseException 转换为 HTTPResponse Exception - dvlsg

8

另一个问题中已经回答了该问题:


该问题在另一个帖子中已有解答。
void Main()
{
    "aCamelCaseWord".ToFriendlyCase().Dump();
}

public static class Extensions
{
    public static string ToFriendlyCase(this string PascalString)
    {
        return Regex.Replace(PascalString, "(?!^)([A-Z])", " $1");
    }
}

输出一个驼峰式单词 (.Dump() 只是打印到控制台)。


对于这样的字符串aCamelCaseXML,必须发生什么?阅读问题,我会期望得到a Camel Case XML。但是实际上,它给出了a Camel Case X M L - Arseni Mourzenko
@MainMa 这是真的。根据.NET命名规范,任何三个字母或更长的缩写(例如XML)都应该使用正确的大小写(即Xml),但是两个字母的缩写(例如IPAddress中的IP)仍然会导致问题。最好让算法处理这种情况。 - Pat
有没有现成的函数可以做到这一点? - Shimmy Weitzhandler
我建议使用以下代码:new Regex( @" (?<=[A-Z])(?=[A-Z][a-z]) | (?<=[^A-Z])(?=[A-Z]) | (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace,如https://dev59.com/9nA75IYBdhLWcg3wy8Qi 所述。 - Custodio

4
如何呢:
static IEnumerable<string> SplitPascalCase(this string text)
{
    var sb = new StringBuilder();
    using (var reader = new StringReader(text))
    {
        while (reader.Peek() != -1)
        {
            char c = (char)reader.Read();
            if (char.IsUpper(c) && sb.Length > 0)
            {
                yield return sb.ToString();
                sb.Length = 0;
            }

            sb.Append(c);
        }
    }

    if (sb.Length > 0)
        yield return sb.ToString();
}

这将是一种“手工”解决方案。 - Steven Sudit
@Steven Sudit:是的...那是被禁止的还是怎么了? - Dan Tao
不,完全没有。当我向帕特提出手动替代正则表达式时,有些混淆了“手动”一词的含义。实际上,我认为正则表达式虽然功能强大,但被过度使用。对于许多工作来说,它并不适合,会导致晦涩难懂的代码和性能下降。 - Steven Sudit

3

旨在:

  • a)创建一个优化性能的函数
  • b)采用自己的CamelCase风格,其中大写首字母缩写不会分隔(我完全接受这不是驼峰或帕斯卡命名法的标准定义,但这不是一种罕见的用法):"TestTLAContainingCamelCase" 变成 "Test TLA Containing Camel Case"(TLA = 三字母缩写)

因此,我创建了以下(非正则表达式、冗长但性能导向的)函数:

public static string ToSeparateWords(this string value)
{
    if (value==null){return null;}
    if(value.Length <=1){return value;}
    char[] inChars = value.ToCharArray();
    List<int> uCWithAnyLC = new List<int>();
    int i = 0;
    while (i < inChars.Length && char.IsUpper(inChars[i])) { ++i; }
    for (; i < inChars.Length; i++)
    {
        if (char.IsUpper(inChars[i]))
        {
            uCWithAnyLC.Add(i);
            if (++i < inChars.Length && char.IsUpper(inChars[i]))
            {
                while (++i < inChars.Length) 
                {
                    if (!char.IsUpper(inChars[i]))
                    {
                        uCWithAnyLC.Add(i - 1);
                        break;
                    }
                }
            }
        }
    }
    char[] outChars = new char[inChars.Length + uCWithAnyLC.Count];
    int lastIndex = 0;
    for (i=0;i<uCWithAnyLC.Count;i++)
    {
        int currentIndex = uCWithAnyLC[i];
        Array.Copy(inChars, lastIndex, outChars, lastIndex + i, currentIndex - lastIndex);
        outChars[currentIndex + i] = ' ';
        lastIndex = currentIndex;
    }
    int lastPos = lastIndex + uCWithAnyLC.Count;
    Array.Copy(inChars, lastIndex, outChars, lastPos, outChars.Length - lastPos);
    return new string(outChars);
}

最让人惊讶的是性能测试,每个函数使用100万次迭代。

regex pattern used = "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))"
test string = "TestTLAContainingCamelCase":
static regex:      13 302ms
Regex instance:    12 398ms
compiled regex:    12 663ms
brent(above):         345ms
AndyRose:           1 764ms
DanTao:               995ms

即使在一百万次迭代中,Regex实例方法的速度仅比静态方法略快(而我看不出使用RegexOptions.Compiled标志的好处),丹·陶(Dan Tao)非常简洁的代码几乎与我不太清晰的代码一样快!


1
var regex = new Regex("([A-Z]+[^A-Z]+)");
var matches = regex.Matches("aCamelCaseWord")
    .Cast<Match>()
    .Select(match => match.Value);
foreach (var element in matches)
{
    Console.WriteLine(element);
}

打印

Camel
Case
Word

正如您所看到的,它无法处理驼峰式命名——它省略了前导的“a”。


  1. 为了提高速度编译正则表达式。
  2. 它仍然比手动处理要慢。
- Steven Sudit
@Steven 我同意应该为速度进行编译,但现在我追求的是功能。你说的“比手动慢”是什么意思?如果我反射一个带有许多公共属性的对象,并将名称从PascalCase转换为单独的单词,那么通过编程方式完成会比手动完成更快(开发和维护时间)。 - Pat
我没有看到速度被提及为一个要求。同时,我认为“手动完成”指的是编写自己的字符串解析代码,这可能会更快,但需要编写更多的代码和进行更多的测试。 - Ron Warholic
@Ken 这个方法不能处理驼峰式命名,所以 "a" 被删除了(请参见答案的编辑)。 - Pat
@Pat:Ron说得对:“手写”意味着编写自己的代码来循环遍历字符串,逐个字符地构建每个单词到一个StringBuilder中,并根据需要输出。 - Steven Sudit

1
string.Concat(str.Select(x => Char.IsUpper(x) ? " " + x : x.ToString())).TrimStart(' ').Dump();

这种方法比使用正则表达式更好,Dump 只是用来打印到控制台的。

0
    public static string PascalCaseToSentence(string input)
    {
        if (input == null) return "";

        string output = Regex.Replace(input, @"(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])", m => " " + m.Value);
        return output;
    }

基于Shimmy的回答。


0

使用 \W 检查正则表达式开头是否有非单词字符,并将单个字符串保持在一起,然后拆分单词。

例如:\W([A-Z][A-Za-z]+)+

对于:sdcsds sd aCamelCaseWord as dasd as aSscdcacdcdc PascelCase DfsadSsdd sd 输出:

48: PascelCase
59: DfsadSsdd

嗯,那对于.NET的正则表达式来说不太好用,但也许需要查一下文档就行了... - Pat
你应该使用\b(单词边界)来匹配单词的开头,而不是\W - Alan Moore

0
在Ruby中:
"aCamelCaseWord".split /(?=[[:upper:]])/
=> ["a", "Camel", "Case", "Word"]

我在这里使用正向预查,以便我可以在每个大写字母之前分割字符串。这让我也能保存任何初始的小写部分。

那是一个正向先行断言,不是吗?即使我用 [A-Z] 替换 [[:upper:]](http://en.wikipedia.org/wiki/Regular_expression),我也无法在 .NET 中找到等效的工作方式。 - Pat
.NET正则表达式不支持POSIX字符类语法。你可以使用\p{Lu}代替,但[A-Z]可能已经足够了。无论如何,这种方法过于简单化。请查看其他问题,特别是Poly提出的“split”正则表达式。确实很复杂。 - Alan Moore
@Pat:那篇维基百科文章不适合用作参考;太笼统而且过于理论化。这个网站要实用得多:http://www.regular-expressions.info/。 - Alan Moore

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接