如何在python中进行CamelCase拆分

Posted

技术标签:

【中文标题】如何在python中进行CamelCase拆分【英文标题】:How to do CamelCase split in python 【发布时间】:2015-07-07 02:32:19 【问题描述】:

我想要达到的目标是这样的:

>>> camel_case_split("CamelCaseXYZ")
['Camel', 'Case', 'XYZ']
>>> camel_case_split("XYZCamelCase")
['XYZ', 'Camel', 'Case']

于是我搜索并找到了这个perfect regular expression:

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

作为我尝试的下一个合乎逻辑的步骤:

>>> re.split("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", "CamelCaseXYZ")
['CamelCaseXYZ']

为什么这不起作用,如何在 python 中实现链接问题的结果?

编辑:解决方案摘要

我用几个测试用例测试了所有提供的解决方案:

string:                 ''
AplusKminus:            ['']
casimir_et_hippolyte:   []
two_hundred_success:    []
kalefranz:              string index out of range # with modification: either [] or ['']

string:                 ' '
AplusKminus:            [' ']
casimir_et_hippolyte:   []
two_hundred_success:    [' ']
kalefranz:              [' ']

string:                 'lower'
all algorithms:         ['lower']

string:                 'UPPER'
all algorithms:         ['UPPER']

string:                 'Initial'
all algorithms:         ['Initial']

string:                 'dromedaryCase'
AplusKminus:            ['dromedary', 'Case']
casimir_et_hippolyte:   ['dromedary', 'Case']
two_hundred_success:    ['dromedary', 'Case']
kalefranz:              ['Dromedary', 'Case'] # with modification: ['dromedary', 'Case']

string:                 'CamelCase'
all algorithms:         ['Camel', 'Case']

string:                 'ABCWordDEF'
AplusKminus:            ['ABC', 'Word', 'DEF']
casimir_et_hippolyte:   ['ABC', 'Word', 'DEF']
two_hundred_success:    ['ABC', 'Word', 'DEF']
kalefranz:              ['ABCWord', 'DEF']

总而言之,您可以说@kalefranz 的解决方案与问题不匹配(参见最后一个案例),@casimir et hippolyte 的解决方案只占用了一个空格,因此违反了拆分不应改变个人的想法部分。其余两个备选方案之间的唯一区别是,我的解决方案在空字符串输入上返回一个包含空字符串的列表,而@200_success 的解决方案返回一个空列表。 我不知道 python 社区在这个问题上的立场,所以我说:我对任何一个都很好。并且由于 200_success 的解决方案比较简单,所以我接受了它作为正确答案。

【问题讨论】:

做你想做的事情的其他 Q:first,second,我很确定还有其他的。 怎么样ABCCamelCase?! @Mihai 我不明白你的问题。如果您想知道正则表达式在"ABCCamelCase" 上的表现如何,它会按预期工作:['ABC', 'Camel', 'Case']。如果您将ABC 解释为代表AbstractBaseClass,那么我很抱歉造成混淆,因为ABC 在我的问题中只是三个任意大写字母。 阅读my answer to a similar question。 也是一个很好的答案,但我没有找到问题,因为措辞对于我的搜索来说太具体了。此外,您的回答并不能完全满足这里的要求,因为它会生成一个带有任意分隔符的转换字符串,您需要使用 str.split(' ') 进行拆分,而不是其部分的(更通用的)列表。 【参考方案1】:

正如@AplusKminus 所解释的,re.split() 永远不会在空模式匹配上分裂。因此,您应该尝试找到您感兴趣的组件,而不是拆分。

这是一个使用re.finditer() 模拟拆分的解决方案:

def camel_case_split(identifier):
    matches = finditer('.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', identifier)
    return [m.group(0) for m in matches]

【讨论】:

我发现你的解决方案和我的解决方案之间有一个区别(根据我的测试用例):camel_case_split("") 在你的情况下返回[],在我的情况下返回[""]。问题是,您更愿意认为哪些是预期的。由于任何一个都适用于我的应用程序,因此我认为这是一个有效的答案! 剩下的另一个问题是,这个或我提出的解决方案是否表现更好。我不是正则表达式复杂性方面的专家,因此必须由其他人评估。 我们的正则表达式基本相同,只是我的正则表达式以.+? 开头,它捕获文本而不是丢弃它,并以$ 结尾以使其一直运行到最后.这两种变化都不会改变搜索策略。 不支持数字。例如,"L2S" 不会拆分为 ["L2", "S"] 。在上面的正则表达式中使用[a-z0-9] 而不是[a-z] 来解决这个问题。 @200_success Parse 1parse 2 是我的分析,我很喜欢t 真的得到正则表达式。你能在这里帮忙吗?【参考方案2】:

使用re.sub()split()

import re

name = 'CamelCaseTest123'
splitted = re.sub('([A-Z][a-z]+)', r' \1', re.sub('([A-Z]+)', r' \1', name)).split()

结果

'CamelCaseTest123' -> ['Camel', 'Case', 'Test123']
'CamelCaseXYZ' -> ['Camel', 'Case', 'XYZ']
'XYZCamelCase' -> ['XYZ', 'Camel', 'Case']
'XYZ' -> ['XYZ']
'IPAddress' -> ['IP', 'Address']

【讨论】:

迄今为止最好的答案恕我直言,优雅而有效,应该是选定的答案。 很好,即使re.sub('([A-Z]+)', r' \1', name).split() 也适用于没有'XYZCamelCase''IPAddress' 之类的输入的简单情况(或者如果您可以接受['XYZCamel', 'Case']['IPAddress']为他们)。另一个 re.sub 也考虑了这些情况(使每个小写字母序列仅附加到前面的一个大写字母)。【参考方案3】:

大多数时候不需要检查字符串的格式,全局研究比拆分更简单(结果相同):

re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', 'CamelCaseXYZ')

返回

['Camel', 'Case', 'XYZ']

也要对付单峰骆驼,你可以使用:

re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)', 'camelCaseXYZ')

注意:(?=[A-Z]|$) 可以使用双重否定(带有否定字符类的否定前瞻)缩短:(?![^A-Z])

【讨论】:

@SheridanVespo:这仅适用于骆驼,不适用于单峰骆驼(如所问)。但也可以用同样的方式做一些改动。 @SheridanVespo:是的,“dromedary-case”不存在,但是由于单峰骆驼只有一个驼峰,骆驼只有两个……关于效率:不是图案本身,而是所有的之后的代码你避免了,因为你直接获得了你想要的字符串列表。关于一般的环视:环视不是直接来自地狱,也不是那么慢(只有在使用不当时,它们才能减慢模式的速度)。正如我对其他 SO 用户所说的,有几分钟时间,在某些情况下您可以使用前瞻优化模式。 测量了所有发布的解决方案。你和mnesarco's 的一个通过了所有Setop's 测试,结果证明是最快的。【参考方案4】:

工作解决方案,没有正则表达式

我不擅长正则表达式。我喜欢在我的 IDE 中使用它们进行搜索/替换,但我尽量避免在程序中使用它们。

这是一个非常简单的纯python解决方案:

def camel_case_split(s):
    idx = list(map(str.isupper, s))
    # mark change of case
    l = [0]
    for (i, (x, y)) in enumerate(zip(idx, idx[1:])):
        if x and not y:  # "Ul"
            l.append(i)
        elif not x and y:  # "lU"
            l.append(i+1)
    l.append(len(s))
    # for "lUl", index of "U" will pop twice, have to filter that
    return [s[x:y] for x, y in zip(l, l[1:]) if x < y]

还有一些测试

def test():
    TESTS = [
        ("aCamelCaseWordT", ['a', 'Camel', 'Case', 'Word', 'T']),
        ("CamelCaseWordT", ['Camel', 'Case', 'Word', 'T']),
        ("CamelCaseWordTa", ['Camel', 'Case', 'Word', 'Ta']),
        ("aCamelCaseWordTa", ['a', 'Camel', 'Case', 'Word', 'Ta']),
        ("Ta", ['Ta']),
        ("aT", ['a', 'T']),
        ("a", ['a']),
        ("T", ['T']),
        ("", []),
        ("XYZCamelCase", ['XYZ', 'Camel', 'Case']),
        ("CamelCaseXYZ", ['Camel', 'Case', 'XYZ']),
        ("CamelCaseXYZa", ['Camel', 'Case', 'XY', 'Za']),
    ]
    for (q,a) in TESTS:
        assert camel_case_split(q) == a

if __name__ == "__main__":
    test()

【讨论】:

谢谢,这是可读的,有效的,并且有测试!在我看来,比正则表达式解决方案要好得多。 请注意World_Wide_Web => ['World_', 'Wide_', 'Web']。它也在这里中断ISO100 => ['IS', 'O100'] @stwhite,原始问题中未考虑这些输入。如果下划线和数字被认为是小写,则输出是正确的。所以这不会中断,这只是做了必须做的事情。其他解决方案可能有不同的行为,但同样,这不是最初问题的一部分。【参考方案5】:

我只是偶然发现了这个案例,并写了一个正则表达式来解决它。实际上,它应该适用于任何一组单词。

RE_WORDS = re.compile(r'''
    # Find words in a string. Order matters!
    [A-Z]+(?=[A-Z][a-z]) |  # All upper case before a capitalized word
    [A-Z]?[a-z]+ |  # Capitalized words / all lower case
    [A-Z]+ |  # All upper case
    \d+  # Numbers
''', re.VERBOSE)

这里的关键是对第一种可能情况的前瞻。它会在大写字母之前匹配(并保留)大写字母:

assert RE_WORDS.findall('FOOBar') == ['FOO', 'Bar']

【讨论】:

我喜欢这个,因为它更清晰,而且它更适合“人们在现实生活中输入的字符串”,例如 URLFinderlistURLReader【参考方案6】:

python 的re.split 的documentation 说:

请注意,split 永远不会在空模式匹配上拆分字符串。

当看到这个时:

>>> re.findall("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", "CamelCaseXYZ")
['', '']

很清楚,为什么拆分没有按预期工作。 remodule 查找空匹配项,正如正则表达式所期望的那样。

由于文档指出这不是错误,而是预期的行为,因此在尝试创建驼峰式拆分时必须解决该问题:

def camel_case_split(identifier):
    matches = finditer('(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])', identifier)
    split_string = []
    # index of beginning of slice
    previous = 0
    for match in matches:
        # get slice
        split_string.append(identifier[previous:match.start()])
        # advance index
        previous = match.start()
    # get remaining string
    split_string.append(identifier[previous:])
    return split_string

【讨论】:

【参考方案7】:
import re

re.sub('(?<=[a-z])(?=[A-Z])', ' ', 'camelCamelCAMEL').split(' ')
# ['camel', 'Camel', 'CAMEL'] <-- result

# '(?<=[a-z])' --> means preceding lowercase char (A)
# '(?=[A-Z])'  --> means following UPPERCASE char (B)
# '(A)(B)'     --> 'aA' or 'aB' or 'bA' and so on

【讨论】:

为什么不直接使用re.split('(?&lt;=[a-z])(?=[A-Z])', 'camelCamelCAMEL') 是的,这样更好)【参考方案8】:

此解决方案还支持数字、空格和自动删除下划线:

def camel_terms(value):
    return re.findall('[A-Z][a-z]+|[0-9A-Z]+(?=[A-Z][a-z])|[0-9A-Z]2,|[a-z0-9]2,|[a-zA-Z0-9]', value)

一些测试:

tests = [
    "XYZCamelCase",
    "CamelCaseXYZ",
    "Camel_CaseXYZ",
    "3DCamelCase",
    "Camel5Case",
    "Camel5Case5D",
    "Camel Case XYZ"
]

for test in tests:
    print(test, "=>", camel_terms(test))

结果:

XYZCamelCase => ['XYZ', 'Camel', 'Case']
CamelCaseXYZ => ['Camel', 'Case', 'XYZ']
Camel_CaseXYZ => ['Camel', 'Case', 'XYZ']
3DCamelCase => ['3D', 'Camel', 'Case']
Camel5Case => ['Camel', '5', 'Case']
Camel5Case5D => ['Camel', '5', 'Case', '5D']
Camel Case XYZ => ['Camel', 'Case', 'XYZ']

【讨论】:

这个正则表达式是否利用了第一个匹配选项会阻止处理器查看其他选项的事实?否则我不明白[a-z0-9]2,[a-zA-Z0-9] 这是因为在我的用例中,我需要支持“3D”,但如果输入已经用空格或下划线分隔,还需要支持“3D”。这个解决方案来自我自己的要求,它比原来的问题有更多的案例。是的,我使用第一场比赛获胜的事实。【参考方案9】:

简单的解决方案:

re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", str(text))

【讨论】:

这会在部件之间创建空格,但是要求创建部件数组的问题。【参考方案10】:

这是另一个需要较少代码且无需复杂正则表达式的解决方案:

def camel_case_split(string):
    bldrs = [[string[0].upper()]]
    for c in string[1:]:
        if bldrs[-1][-1].islower() and c.isupper():
            bldrs.append([c])
        else:
            bldrs[-1].append(c)
    return [''.join(bldr) for bldr in bldrs]

编辑

上面的代码包含一个优化,可以避免使用每个附加字符重建整个字符串。忽略优化,一个更简单的版本(使用 cmets)可能看起来像

def camel_case_split2(string):
    # set the logic for creating a "break"
    def is_transition(c1, c2):
      return c1.islower() and c2.isupper()

    # start the builder list with the first character
    # enforce upper case
    bldr = [string[0].upper()]
    for c in string[1:]:
        # get the last character in the last element in the builder
        # note that strings can be addressed just like lists
        previous_character = bldr[-1][-1]
        if is_transition(previous_character, c):
            # start a new element in the list
            bldr.append(c)
        else:
            # append the character to the last string
            bldr[-1] += c
    return bldr

【讨论】:

@SheridanVespo 我认为第一个版本可能有一个无关紧要的),你发现并纠正了我:) @SheridanVespo 显然有 varied definitions 用于骆驼案。一些定义(以及我最初假设的定义)强制第一个字母大写。不用担心; “错误”是一个简单的修复。只需在初始化列表时删除 .upper() 调用即可。 您能否创建一个满足linked answer 中情况的版本?另外,有没有办法比较你的方法和@Casimir et Hippolyte 的方法的性能?【参考方案11】:

我知道问题添加了正则表达式的标签。但是,我总是尽量远离正则表达式。所以,这是我没有正则表达式的解决方案:

def split_camel(text, char):
    if len(text) <= 1: # To avoid adding a wrong space in the beginning
        return text+char
    if char.isupper() and text[-1].islower(): # Regular Camel case
        return text + " " + char
    elif text[-1].isupper() and char.islower() and text[-2] != " ": # Detect Camel case in case of abbreviations
        return text[:-1] + " " + text[-1] + char
    else: # Do nothing part
        return text + char

text = "PathURLFinder"
text = reduce(split_camel, a, "")
print text
# prints "Path URL Finder"
print text.split(" ")
# prints "['Path', 'URL', 'Finder']"

编辑: 正如建议的那样,这里是将功能放在单个函数中的代码。

def split_camel(text):
    def splitter(text, char):
        if len(text) <= 1: # To avoid adding a wrong space in the beginning
            return text+char
        if char.isupper() and text[-1].islower(): # Regular Camel case
            return text + " " + char
        elif text[-1].isupper() and char.islower() and text[-2] != " ": # Detect Camel case in case of abbreviations
            return text[:-1] + " " + text[-1] + char
        else: # Do nothing part
            return text + char
    converted_text = reduce(splitter, text, "")
    return converted_text.split(" ")

split_camel("PathURLFinder")
# prints ['Path', 'URL', 'Finder']

【讨论】:

能否将reducesplit 合并到方法中?将使您的方法更好地可测试【参考方案12】:

在其他地方采用更全面的方法。它处理了几个问题,例如数字、以小写开头的字符串、单字母单词等。

def camel_case_split(identifier, remove_single_letter_words=False):
    """Parses CamelCase and Snake naming"""
    concat_words = re.split('[^a-zA-Z]+', identifier)

    def camel_case_split(string):
        bldrs = [[string[0].upper()]]
        string = string[1:]
        for idx, c in enumerate(string):
            if bldrs[-1][-1].islower() and c.isupper():
                bldrs.append([c])
            elif c.isupper() and (idx+1) < len(string) and string[idx+1].islower():
                bldrs.append([c])
            else:
                bldrs[-1].append(c)

        words = [''.join(bldr) for bldr in bldrs]
        words = [word.lower() for word in words]
        return words
    words = []
    for word in concat_words:
        if len(word) > 0:
            words.extend(camel_case_split(word))
    if remove_single_letter_words:
        subset_words = []
        for word in words:
            if len(word) > 1:
                subset_words.append(word)
        if len(subset_words) > 0:
            words = subset_words
    return words

【讨论】:

能否在代码中添加更多的 cmets,让不熟悉 python 的人更容易理解它的作用?【参考方案13】:

我的要求比 OP 更具体一点。特别是,除了处理所有 OP 案例之外,我还需要其他解决方案不提供的以下内容: - 将所有非字母数字输入(例如 !@#$%^&*() 等)视为单词分隔符 - 处理数字如下: - 不能在单词中间 - 除非短语以数字开头,否则不能在单词的开头

def splitWords(s):
    new_s = re.sub(r'[^a-zA-Z0-9]', ' ',                  # not alphanumeric
        re.sub(r'([0-9]+)([^0-9])', '\\1 \\2',            # digit followed by non-digit
            re.sub(r'([a-z])([A-Z])','\\1 \\2',           # lower case followed by upper case
                re.sub(r'([A-Z])([A-Z][a-z])', '\\1 \\2', # upper case followed by upper case followed by lower case
                    s
                )
            )
        )
    )
    return [x for x in new_s.split(' ') if x]

输出:

for test in ['', ' ', 'lower', 'UPPER', 'Initial', 'dromedaryCase', 'CamelCase', 'ABCWordDEF', 'CamelCaseXYZand123.how23^ar23e you doing AndABC123XYZdf']:
    print test + ':' + str(splitWords(test))
:[]
 :[]
lower:['lower']
UPPER:['UPPER']
Initial:['Initial']
dromedaryCase:['dromedary', 'Case']
CamelCase:['Camel', 'Case']
ABCWordDEF:['ABC', 'Word', 'DEF']
CamelCaseXYZand123.how23^ar23e you doing AndABC123XYZdf:['Camel', 'Case', 'XY', 'Zand123', 'how23', 'ar23', 'e', 'you', 'doing', 'And', 'ABC123', 'XY', 'Zdf']

【讨论】:

【参考方案14】:

我认为以下是最佳选择

定义 count_word(): return(re.findall('[A-Z]?[a-z]+', input('请输入你的字符串'))

打印(count_word())

【讨论】:

你能详细说明一下吗?

以上是关于如何在python中进行CamelCase拆分的主要内容,如果未能解决你的问题,请参考以下文章

在 R 中拆分 CamelCase

使用php preg_match(正则表达式)将camelCase单词拆分为单词

MacOS 上的 Python 3.6 和 MySQL 8.0.21:如何给表 camelCase 列名? [复制]

如何在python中从头开始获取kfold拆分以进行交叉验证?

将连字符转换为驼峰式 (camelCase)

camelCase中的序列化商店C#类