将驼峰式或标题式的文字分割成单词(高级)的正则表达式

94

我找到了一个很棒的正则表达式,可以提取出camelCase或TitleCase表达式中的部分。

 (?<!^)(?=[A-Z])

它按预期工作:

  • value -> value
  • camelValue -> camel / Value
  • TitleValue -> Title / Value

例如在Java中:

String s = "loremIpsum";
words = s.split("(?<!^)(?=[A-Z])");
//words equals words = new String[]{"lorem","Ipsum"}
我的问题是有些情况下它不起作用:
  • 情况1:VALUE -> V / A / L / U / E
  • 情况2:eclipseRCPExt -> eclipse / R / C / P / Ext

在我看来,结果应该是:

  • 情况1:VALUE
  • 情况2:eclipse/RCP/Ext

换句话说,给定n个大写字符:

  • 如果这n个字符后面跟着小写字符,则分组应该为:(n-1个字符)/(第n个字符+小写字符)
  • 如果这n个字符位于末尾,则分组应该为:(n个字符)。

如何改进这个正则表达式呢?


看起来你可能需要在“^”上使用一个条件修改器,以及在负回顾中的大写字母的另一个条件情况。虽然我没有确定地测试过,但我认为那是解决问题的最佳选择。 - Nightfirecat
如果有人正在检查 - Clam
11个回答

121

以下正则表达式适用于上述所有示例:

public static void main(String[] args)
{
    for (String w : "camelValue".split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
        System.out.println(w);
    }
}   

它的工作原理是通过强制负回顾后置断言不仅忽略字符串开头的匹配,而且忽略在大写字母之前有另一个大写字母的匹配。这处理了像 "VALUE" 这样的情况。

正则表达式的第一部分单独运行时在 "eclipseRCPExt" 上失败,因为它未能在 "RPC" 和 "Ext" 之间进行拆分。这就是第二个子句的目的: (?<!^)(?=[A-Z][a-z]。该子句允许在每个后面跟随小写字母的大写字母之前进行拆分,但不包括字符串开头。


1
这个在PHP上不起作用,而@ridgerunner的可以。在PHP上它会显示“回顾断言在偏移量13处不是固定长度”。 - igorsantos07
15
正则表达式的语法因不同编程语言而异。这个问题是关于Java的,不是PHP,因此回答也是关于Java的。 - NPE
1
虽然这个问题被标记为“java”,但问题仍然是通用的 - 除了代码示例(它们永远不可能是通用的)。因此,如果有一个更简单的版本的正则表达式,并且它也可以跨语言工作,我认为应该有人指出来 :) - igorsantos07
7
“通用正则表达式”是一个虚构的概念。 - Casimir et Hippolyte
为什么,@CasimiretHippolyte?难道正则表达式不是每种语言都可以在某种程度上使用的资源吗?除了这个,我从来没有看到过自定义正则表达式的实现,因为通常在开源中,你可以在平台之间共享正则表达式。 没有j-regex这样的东西,尽管有扩展的regex、基本的regex和perl regex。它们都是标准的风格,因此Java只是在用他们自己的版本。 - igorsantos07
3
不,内置的正则表达式实现在不同平台上有很大差异。一些实现试图模仿Perl,一些试图模仿POSIX,还有一些介于两者之间或完全不同。 - Christoffer Hammarström

99

你似乎把这个问题复杂化了。对于camelCase,分割位置仅在小写字母后面紧跟着大写字母的任何地方:

(?<=[a-z])(?=[A-Z])

以下是此正则表达式将您的示例数据拆分的方式:

  • value -> value
  • camelValue -> camel / Value
  • TitleValue -> Title / Value
  • VALUE -> VALUE
  • eclipseRCPExt -> eclipse / RCPExt

与您期望的输出唯一不同的是 eclipseRCPExt ,我认为在此处正确拆分。

补充说明-改进版本

注意:这个答案最近得到了一次赞,我意识到有更好的方法...

通过向上述正则表达式添加第二种选择,所有OP的测试案例都被正确拆分。

(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])

以下是改进的正则表达式如何拆分示例数据:

  • value -> value
  • camelValue -> camel / Value
  • TitleValue -> Title / Value
  • VALUE -> VALUE
  • eclipseRCPExt -> eclipse / RCP / Ext

编辑:20130824添加了改进版本以处理 RCPExt -> RCP / Ext 情况。


感谢您的意见。在这个例子中,我需要将RCP和Ext分开,因为我将它们转换成常量名称(样式指南:使用下划线分隔单词的全大写)。在这种情况下,我更喜欢ECLIPSE_RCP_EXT而不是ECLIPSE_RCPEXT。 - Jmini
5
谢谢帮助;我已修改您的正则表达式来添加一些选项,以处理字符串中的数字:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[0-9])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=[0-9]) - thoroc
这是最好的答案!简单明了。然而,这个答案和原始的正则表达式对于Javascript和Golang都不起作用! - Viet
对我来说不起作用 - HalfLegend
对我来说不起作用 - undefined

38

这是最简单的解决方案。 - RoBeaToZ
1
这个回答应该得到更多的赞。 - johnlinp

11

我无法使aix的解决方案生效(在RegExr上也不行),因此,我自己想出了一个解决方案,并进行了测试,似乎正好符合你的要求:

((^[a-z]+)|([A-Z]{1}[a-z]+)|([A-Z]+(?=([A-Z][a-z])|($))))

以下是使用该功能的示例:

; Regex Breakdown:  This will match against each word in Camel and Pascal case strings, while properly handling acrynoms.
;   (^[a-z]+)                       Match against any lower-case letters at the start of the string.
;   ([A-Z]{1}[a-z]+)                Match against Title case words (one upper case followed by lower case letters).
;   ([A-Z]+(?=([A-Z][a-z])|($)))    Match against multiple consecutive upper-case letters, leaving the last upper case letter out the match if it is followed by lower case letters, and including it if it's followed by the end of the string.
newString := RegExReplace(oldCamelOrPascalString, "((^[a-z]+)|([A-Z]{1}[a-z]+)|([A-Z]+(?=([A-Z][a-z])|($))))", "$1 ")
newString := Trim(newString)

这里我用空格分隔每个单词,以下是一些示例,展示了字符串如何转换:

  • ThisIsATitleCASEString => This Is A Title CASE String
  • andThisOneIsCamelCASE => and This One Is Camel CASE

上面的解决方案已经能够满足原帖所需,但是我还需要一个正则表达式来查找包含数字的驼峰和帕斯卡字符串,因此我也想出了以下变体以包括数字:

((^[a-z]+)|([0-9]+)|([A-Z]{1}[a-z]+)|([A-Z]+(?=([A-Z][a-z])|($)|([0-9]))))

并附带一个使用示例:

; Regex Breakdown:  This will match against each word in Camel and Pascal case strings, while properly handling acrynoms and including numbers.
;   (^[a-z]+)                               Match against any lower-case letters at the start of the command.
;   ([0-9]+)                                Match against one or more consecutive numbers (anywhere in the string, including at the start).
;   ([A-Z]{1}[a-z]+)                        Match against Title case words (one upper case followed by lower case letters).
;   ([A-Z]+(?=([A-Z][a-z])|($)|([0-9])))    Match against multiple consecutive upper-case letters, leaving the last upper case letter out the match if it is followed by lower case letters, and including it if it's followed by the end of the string or a number.
newString := RegExReplace(oldCamelOrPascalString, "((^[a-z]+)|([0-9]+)|([A-Z]{1}[a-z]+)|([A-Z]+(?=([A-Z][a-z])|($)|([0-9]))))", "$1 ")
newString := Trim(newString)

以下是一个字符串与数字的正则表达式转换示例:

  • myVariable123 => my Variable 123
  • my2Variables => my 2 Variables
  • The3rdVariableIsHere => The 3 rdVariable Is Here
  • 12345NumsAtTheStartIncludedToo => 12345 Nums At The Start Included Too

1
有太多不必要的捕获组。你可以这样写:(^[a-z]+|[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$))作为第一个,以及(^[a-z]+|[0-9]+|[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$|[0-9]))作为第二个。最外层也可以被移除,但是引用整个匹配的语法在不同的编程语言中不可移植($0$&是两种可能性)。 - nhahtdh
相同的简化正则表达式:([A-Z]?[a-z]+)|([A-Z]+(?=[A-Z][a-z])) - Alex Suhinin

6

处理比 A-Z 更多的字母:

s.split("(?<=\\p{Ll})(?=\\p{Lu})|(?<=\\p{L})(?=\\p{Lu}\\p{Ll})");

要么:
  • 在任何小写字母后面分割,其后跟随大写字母。

例如parseXML -> parseXML

或者

  • 在任何字母后面分割,其后跟随大写字母和小写字母。

例如XMLParser -> XMLParser


更易读的形式:

public class SplitCamelCaseTest {

    static String BETWEEN_LOWER_AND_UPPER = "(?<=\\p{Ll})(?=\\p{Lu})";
    static String BEFORE_UPPER_AND_LOWER = "(?<=\\p{L})(?=\\p{Lu}\\p{Ll})";

    static Pattern SPLIT_CAMEL_CASE = Pattern.compile(
        BETWEEN_LOWER_AND_UPPER +"|"+ BEFORE_UPPER_AND_LOWER
    );

    public static String splitCamelCase(String s) {
        return SPLIT_CAMEL_CASE.splitAsStream(s)
                        .collect(joining(" "));
    }

    @Test
    public void testSplitCamelCase() {
        assertEquals("Camel Case", splitCamelCase("CamelCase"));
        assertEquals("lorem Ipsum", splitCamelCase("loremIpsum"));
        assertEquals("XML Parser", splitCamelCase("XMLParser"));
        assertEquals("eclipse RCP Ext", splitCamelCase("eclipseRCPExt"));
        assertEquals("VALUE", splitCamelCase("VALUE"));
    }    
}

4

简介

这里提供的两个顶级答案都使用了正向回溯的代码,而这种方法并不被所有的正则表达式工具支持。下面的正则表达式将捕获PascalCasecamelCase,并可用于多种语言。

注意:我知道这个问题是关于Java的,但是我也看到了其他标记为不同语言的问题中提到了这个帖子,以及对这个问题的一些评论。

代码

在此处查看使用此正则表达式的示例

([A-Z]+|[A-Z]?[a-z]+)(?=[A-Z]|\b)

结果

示例输入

eclipseRCPExt

SomethingIsWrittenHere

TEXTIsWrittenHERE

VALUE

loremIpsum

示例输出

eclipse
RCP
Ext

Something
Is
Written
Here

TEXT
Is
Written
HERE

VALUE

lorem
Ipsum

解释

  • 匹配一个或多个大写字母 [A-Z]+
  • 或者匹配零个或一个大写字母[A-Z]?,后面跟着一个或多个小写字母[a-z]+
  • 确保后面是一个大写字母[A-Z]或单词边界字符\b

4

这是最简单的解决方案。 - RoBeaToZ

0

不要寻找那些“不存在”的分隔符,你也可以考虑找到名称组件(它们肯定存在):

String test = "_eclipse福福RCPExt";

Pattern componentPattern = Pattern.compile("_? (\\p{Upper}?\\p{Lower}+ | (?:\\p{Upper}(?!\\p{Lower}))+ \\p{Digit}*)", Pattern.COMMENTS);

Matcher componentMatcher = componentPattern.matcher(test);
List<String> components = new LinkedList<>();
int endOfLastMatch = 0;
while (componentMatcher.find()) {
    // matches should be consecutive
    if (componentMatcher.start() != endOfLastMatch) {
        // do something horrible if you don't want garbage in between

        // we're lenient though, any Chinese characters are lucky and get through as group
        String startOrInBetween = test.substring(endOfLastMatch, componentMatcher.start());
        components.add(startOrInBetween);
    }
    components.add(componentMatcher.group(1));
    endOfLastMatch = componentMatcher.end();
}

if (endOfLastMatch != test.length()) {
    String end = test.substring(endOfLastMatch, componentMatcher.start());
    components.add(end);
}

System.out.println(components);

这将输出[eclipse, 福福, RCP, Ext]。当然,将其转换为数组也很简单。


0

您可以在Java中使用以下表达式:

(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?=[A-Z][a-z])|(?<=\\d)(?=\\D)|(?=\\d)(?<=\\D)

3
嗨,Maicon,欢迎来到StackOverflow,并感谢你的回答。虽然这个回答可能回答了问题,但它没有提供任何解释让其他人学习如何解决问题。您能否编辑您的答案,包括您代码的解释?谢谢! - Tim Malone

0

我可以确认ctwheels提供的正则表达式字符串([A-Z]+|[A-Z]?[a-z]+)(?=[A-Z]|\b)适用于Microsoft的正则表达式。

我还想建议以下基于ctwheels的正则表达式的替代方案,它可以处理数字字符:([A-Z0-9]+|[A-Z]?[a-z]+)(?=[A-Z0-9]|\b)

这能够将以下字符串拆分为:

DrivingB2BTradeIn2019Onwards

变成

Driving B2B Trade in 2019 Onwards


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