En Python, comment diviser une chaîne et conserver les séparateurs?


226

Voici la façon la plus simple d'expliquer cela. Voici ce que j'utilise:

re.split('\W', 'foo/bar spam\neggs')
-> ['foo', 'bar', 'spam', 'eggs']

Voici ce que je veux:

someMethod('\W', 'foo/bar spam\neggs')
-> ['foo', '/', 'bar', ' ', 'spam', '\n', 'eggs']

La raison en est que je veux diviser une chaîne en jetons, la manipuler, puis la recomposer.


3
qu'est-ce que cela signifie \W? J'ai échoué sur Google.
Ooker

8
Un non-mot caractère voir ici pour plus de détails
Russell

Pour la question appliquée à une chaîne d'octets bruts et posée sur "Fractionner une chaîne et conserver les délimiteurs dans le cadre des morceaux de chaîne fractionnés, pas en tant qu'éléments de liste séparés", voir stackoverflow.com/questions/62591863/…
Lorenz

Réponses:


295
>>> re.split('(\W)', 'foo/bar spam\neggs')
['foo', '/', 'bar', ' ', 'spam', '\n', 'eggs']

22
C'est super. Je ne savais pas que re.split avait fait ça avec des groupes de capture.
Laurence Gonsalves

16
@Laurence: Eh bien, c'est documenté: docs.python.org/library/re.html#re.split : " Fractionner la chaîne par les occurrences du modèle. Si la capture des parenthèses est utilisée dans le modèle, alors le texte de tous les groupes du modèle sont également renvoyés dans la liste résultante. "
Vinay Sajip

40
C'est sérieusement sous-documenté. J'utilise Python depuis 14 ans et je viens juste de le découvrir.
smci

19
Existe-t-il une option pour que la sortie de la correspondance de groupe soit attachée à tout ce qui se trouve à gauche (ou de façon analogue à droite) de la division? Par exemple, cela peut-il être facilement modifié pour que la sortie soit ['foo', '/bar', ' spam', '\neggs']?
ely

3
@ Mr.F Vous pourriez peut-être faire quelque chose avec re.sub. Je voulais me séparer sur un pourcentage final, alors j'ai simplement remplacé un double caractère, puis je me suis séparé, hacky mais j'ai travaillé pour mon cas: re.split('% ', re.sub('% ', '%% ', '5.000% Additional Whatnot'))->['5.000%', 'Additional Whatnot']
Kyle James Walker

29

Si vous fractionnez sur la nouvelle ligne, utilisez splitlines(True).

>>> 'line 1\nline 2\nline without newline'.splitlines(True)
['line 1\n', 'line 2\n', 'line without newline']

(Pas une solution générale, mais l'ajouter ici au cas où quelqu'un viendrait ici sans se rendre compte que cette méthode existait.)


12

Une autre solution sans regex qui fonctionne bien sur Python 3

# Split strings and keep separator
test_strings = ['<Hello>', 'Hi', '<Hi> <Planet>', '<', '']

def split_and_keep(s, sep):
   if not s: return [''] # consistent with string.split()

   # Find replacement character that is not used in string
   # i.e. just use the highest available character plus one
   # Note: This fails if ord(max(s)) = 0x10FFFF (ValueError)
   p=chr(ord(max(s))+1) 

   return s.replace(sep, sep+p).split(p)

for s in test_strings:
   print(split_and_keep(s, '<'))


# If the unicode limit is reached it will fail explicitly
unicode_max_char = chr(1114111)
ridiculous_string = '<Hello>'+unicode_max_char+'<World>'
print(split_and_keep(ridiculous_string, '<'))

10

Si vous n'avez qu'un seul séparateur, vous pouvez utiliser des listes de compréhension:

text = 'foo,bar,baz,qux'  
sep = ','

Séparateur ajout / ajout:

result = [x+sep for x in text.split(sep)]
#['foo,', 'bar,', 'baz,', 'qux,']
# to get rid of trailing
result[-1] = result[-1].strip(sep)
#['foo,', 'bar,', 'baz,', 'qux']

result = [sep+x for x in text.split(sep)]
#[',foo', ',bar', ',baz', ',qux']
# to get rid of trailing
result[0] = result[0].strip(sep)
#['foo', ',bar', ',baz', ',qux']

Séparateur comme son propre élément:

result = [u for x in text.split(sep) for u in (x, sep)]
#['foo', ',', 'bar', ',', 'baz', ',', 'qux', ',']
results = result[:-1]   # to get rid of trailing

1
vous pouvez également ajouter if xpour vous assurer que le morceau produit par splita du contenu, c'estresult = [x + sep for x in text.split(sep) if x]
j'ai alarmé un extraterrestre

Pour moi, la bande enlevée trop et j'ai dû utiliser ceci:result = [sep+x for x in data.split(sep)] result[0] = result[0][len(sep):]
scottlittle

9

un autre exemple, divisez sur non alphanumérique et gardez les séparateurs

import re
a = "foo,bar@candy*ice%cream"
re.split('([^a-zA-Z0-9])',a)

production:

['foo', ',', 'bar', '@', 'candy', '*', 'ice', '%', 'cream']

explication

re.split('([^a-zA-Z0-9])',a)

() <- keep the separators
[] <- match everything in between
^a-zA-Z0-9 <-except alphabets, upper/lower and numbers.

Même si, comme le disent les docs , cela équivaut à la réponse acceptée, j'aime la lisibilité de cette version - même si \Wc'est une façon plus compacte de l'exprimer.
ephsmith

3

Vous pouvez également fractionner une chaîne avec un tableau de chaînes au lieu d'une expression régulière, comme ceci:

def tokenizeString(aString, separators):
    #separators is an array of strings that are being used to split the the string.
    #sort separators in order of descending length
    separators.sort(key=len)
    listToReturn = []
    i = 0
    while i < len(aString):
        theSeparator = ""
        for current in separators:
            if current == aString[i:i+len(current)]:
                theSeparator = current
        if theSeparator != "":
            listToReturn += [theSeparator]
            i = i + len(theSeparator)
        else:
            if listToReturn == []:
                listToReturn = [""]
            if(listToReturn[-1] in separators):
                listToReturn += [""]
            listToReturn[-1] += aString[i]
            i += 1
    return listToReturn


print(tokenizeString(aString = "\"\"\"hi\"\"\" hello + world += (1*2+3/5) '''hi'''", separators = ["'''", '+=', '+', "/", "*", "\\'", '\\"', "-=", "-", " ", '"""', "(", ")"]))

3
# This keeps all separators  in result 
##########################################################################
import re
st="%%(c+dd+e+f-1523)%%7"
sh=re.compile('[\+\-//\*\<\>\%\(\)]')

def splitStringFull(sh, st):
   ls=sh.split(st)
   lo=[]
   start=0
   for l in ls:
     if not l : continue
     k=st.find(l)
     llen=len(l)
     if k> start:
       tmp= st[start:k]
       lo.append(tmp)
       lo.append(l)
       start = k + llen
     else:
       lo.append(l)
       start =llen
   return lo
  #############################

li= splitStringFull(sh , st)
['%%(', 'c', '+', 'dd', '+', 'e', '+', 'f', '-', '1523', ')%%', '7']

3

Une solution paresseuse et simple

Supposons que votre modèle d'expression régulière est split_pattern = r'(!|\?)'

Tout d'abord, vous ajoutez un même caractère que le nouveau séparateur, comme «[couper]»

new_string = re.sub(split_pattern, '\\1[cut]', your_string)

Ensuite, vous divisez le nouveau séparateur, new_string.split('[cut]')


Cette approche est intelligente, mais échouera lorsque la chaîne d'origine contient déjà [cut]quelque part.
Matthijs Kooijman

Cela pourrait être plus rapide sur des problèmes à grande échelle car il utilise finalement string.split (), au cas où re.split () coûte plus cher que re.sub () avec string.split () (que je ne connais pas).
Lorenz

1

Si l'on veut diviser la chaîne tout en gardant les séparateurs par regex sans capturer le groupe:

def finditer_with_separators(regex, s):
    matches = []
    prev_end = 0
    for match in regex.finditer(s):
        match_start = match.start()
        if (prev_end != 0 or match_start > 0) and match_start != prev_end:
            matches.append(s[prev_end:match.start()])
        matches.append(match.group())
        prev_end = match.end()
    if prev_end < len(s):
        matches.append(s[prev_end:])
    return matches

regex = re.compile(r"[\(\)]")
matches = finditer_with_separators(regex, s)

Si l'on suppose que l'expression régulière est enveloppée dans un groupe de capture:

def split_with_separators(regex, s):
    matches = list(filter(None, regex.split(s)))
    return matches

regex = re.compile(r"([\(\)])")
matches = split_with_separators(regex, s)

Les deux façons supprimeront également les groupes vides qui sont inutiles et ennuyeux dans la plupart des cas.


1

Voici une .splitsolution simple qui fonctionne sans regex.

Il s'agit d'une réponse pour Python split () sans supprimer le délimiteur , donc pas exactement ce que le message d'origine demande, mais l'autre question a été fermée en double pour celle-ci.

def splitkeep(s, delimiter):
    split = s.split(delimiter)
    return [substr + delimiter for substr in split[:-1]] + [split[-1]]

Tests aléatoires:

import random

CHARS = [".", "a", "b", "c"]
assert splitkeep("", "X") == [""]  # 0 length test
for delimiter in ('.', '..'):
    for idx in range(100000):
        length = random.randint(1, 50)
        s = "".join(random.choice(CHARS) for _ in range(length))
        assert "".join(splitkeep(s, delimiter)) == s

regex doit être évité sur des problèmes à grande échelle pour des raisons de vitesse, c'est pourquoi c'est un bon indice.
Lorenz

0

J'ai eu un problème similaire en essayant de diviser un chemin de fichier et j'ai eu du mal à trouver une réponse simple. Cela a fonctionné pour moi et n'a pas nécessité de remplacer les délimiteurs dans le texte fractionné:

my_path = 'folder1/folder2/folder3/file1'

import re

re.findall('[^/]+/|[^/]+', my_path)

Retour:

['folder1/', 'folder2/', 'folder3/', 'file1']


Cela peut être légèrement simplifié en utilisant: re.findall('[^/]+/?', my_path)(par exemple, rendre la barre oblique facultative en utilisant un ?plutôt que de fournir deux alternatives avec |.
Matthijs Kooijman

0

J'ai trouvé cette approche basée sur le générateur plus satisfaisante:

def split_keep(string, sep):
    """Usage:
    >>> list(split_keep("a.b.c.d", "."))
    ['a.', 'b.', 'c.', 'd']
    """
    start = 0
    while True:
        end = string.find(sep, start) + 1
        if end == 0:
            break
        yield string[start:end]
        start = end
    yield string[start:]

Cela évite d'avoir à trouver la bonne expression rationnelle, alors qu'en théorie, cela devrait être assez bon marché. Il ne crée pas de nouveaux objets chaîne et délègue la majeure partie du travail d'itération à la méthode de recherche efficace.

... et en Python 3.8, il peut être aussi court que:

def split_keep(string, sep):
    start = 0
    while (end := string.find(sep, start) + 1) > 0:
        yield string[start:end]
        start = end
    yield string[start:]

0
  1. remplacer tout seperator: (\W)parseperator + new_seperator: (\W;)

  2. divisé par le new_seperator: (;)

def split_and_keep(seperator, s):
  return re.split(';', re.sub(seperator, lambda match: match.group() + ';', s))

print('\W', 'foo/bar spam\neggs')
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.