web-dev-qa-db-fra.com

Comment diviser CamelCase en python

Ce que j'essayais de réaliser, c'était quelque chose comme ceci:

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

J'ai donc cherché et trouvé ceci expression régulière parfaite :

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

Comme prochaine étape logique, j'ai essayé:

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

Pourquoi cela ne fonctionne-t-il pas et comment puis-je obtenir le résultat de la question liée en python?

Modifier: Résumé de la solution

J'ai testé toutes les solutions fournies avec quelques cas de test:

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']

En résumé, vous pourriez dire que la solution de @kalefranz ne correspond pas à la question (voir le dernier cas) et la solution de @casimir et hippolyte mange un seul espace, et viole ainsi l'idée qu'un fractionnement ne devrait pas changer les différentes parties. La seule différence entre les deux alternatives restantes est que ma solution renvoie une liste avec la chaîne vide sur une entrée de chaîne vide et la solution par @ 200_success renvoie une liste vide. Je ne sais pas comment la communauté python se tient sur ce problème, donc je dis: je suis d'accord avec l'un ou l'autre. Et puisque la solution de 200_success est plus simple, je l'ai acceptée comme la bonne réponse.

40
AplusKminus

Comme l'a expliqué @AplusKminus, re.split() ne se divise jamais sur une correspondance de modèle vide. Par conséquent, au lieu de diviser, vous devriez essayer de trouver les composants qui vous intéressent.

Voici une solution utilisant re.finditer() qui émule le fractionnement:

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]
32
200_success

Utilisez re.sub() et split()

import re

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

Résultat

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

La plupart du temps, lorsque vous n'avez pas besoin de vérifier le format d'une chaîne, une recherche globale est plus simple qu'une scission (pour le même résultat):

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

retour

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

Pour gérer également le dromadaire, vous pouvez utiliser:

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

Remarque: (?=[A-Z]|$) peut être raccourci en utilisant une double négation (un lookahead négatif avec une classe de caractères niée): (?![^A-Z])

6

documentation pour le re.split De python dit:

Notez que le fractionnement ne divisera jamais une chaîne sur une correspondance de modèle vide.

En voyant cela:

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

il devient clair, pourquoi la scission ne fonctionne pas comme prévu. Le module re trouve des correspondances vides, comme prévu par l'expression régulière.

Étant donné que la documentation indique qu'il ne s'agit pas d'un bogue, mais plutôt d'un comportement prévu, vous devez contourner cela lorsque vous essayez de créer une division de cas de chameau:

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
3
AplusKminus

Je suis juste tombé sur ce cas et j'ai écrit une expression régulière pour le résoudre. Cela devrait fonctionner pour n'importe quel groupe de mots, en fait.

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)

La clé ici est le lookahead sur le premier cas possible. Il fera correspondre (et préservera) les mots en majuscules avant les mots en majuscule:

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

Mettre en place une approche plus globale. Il prend en charge plusieurs problèmes tels que les nombres, les chaînes commençant par des minuscules, les mots d'une seule lettre, etc.

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
0
datarpit

Voici une autre solution qui nécessite moins de code et aucune expression régulière compliquée:

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]

Éditer

Le code ci-dessus contient une optimisation qui évite de reconstruire la chaîne entière avec chaque caractère ajouté. En laissant de côté cette optimisation, une version plus simple (avec commentaires) pourrait ressembler à

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
0
kalefranz

Je sais que la question a ajouté le tag de regex. Mais quand même, j'essaie toujours de rester le plus loin possible des regex. Voici donc ma solution sans regex:

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']"

EDIT: Comme suggéré, voici le code pour mettre la fonctionnalité dans une seule fonction.

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']
0
thiruvenkadam