No Python, como faço para dividir uma string e manter os separadores?

226

Aqui está a maneira mais simples de explicar isso. Aqui está o que estou usando:

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

Aqui está o que eu quero:

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

O motivo é que eu quero dividir uma string em tokens, manipulá-la e juntá-la novamente.

Ken Kinder
fonte
3
o que \Wsignifica? Eu falhei no google.
Ooker
8
Um caractere que não seja uma palavra , veja aqui para detalhes
Russell
Para a pergunta aplicada a uma sequência de bytes bruta e baixada para "Dividir uma sequência e manter os delimitadores como parte dos pedaços de sequência dividida, não como elementos de lista separados", consulte stackoverflow.com/questions/62591863/…
Lorenz

Respostas:

295
>>> re.split('(\W)', 'foo/bar spam\neggs')
['foo', '/', 'bar', ' ', 'spam', '\n', 'eggs']
Comodoro Jaeger
fonte
22
Isso é legal. Eu não sabia que re.split fazia isso com grupos de captura.
Laurence Gonsalves
16
@Laurence: Bem, está documentado: docs.python.org/library/re.html#re.split : "Divida a string pelas ocorrências do padrão. Se a captura de parênteses for usada no padrão, o texto de todos os grupos no padrão também são retornados como parte da lista resultante ".
Vinay Sajip
40
Está seriamente mal documentado. Uso o Python há 14 anos e só descobri isso.
SMCI
19
Existe uma opção para que a saída da correspondência do grupo seja anexada ao que estiver à esquerda (ou analogamente à direita) da divisão? Por exemplo, isso pode ser facilmente modificado para que a saída seja ['foo', '/bar', ' spam', '\neggs']?
ely
3
@ Mr.F Você pode fazer algo com o re.sub. Eu queria dividir em um por cento terminando então eu só subbed em um duplo caráter e depois dividir, hacky, mas trabalhou para o meu caso: re.split('% ', re.sub('% ', '%% ', '5.000% Additional Whatnot'))->['5.000%', 'Additional Whatnot']
Kyle James Walker
29

Se você estiver dividindo em nova linha, use splitlines(True).

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

(Não é uma solução geral, mas adicione-a aqui caso alguém venha aqui sem perceber que esse método existia.)

Mark Lodato
fonte
12

Outra solução sem regex que funciona bem em 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, '<'))
ootwch
fonte
10

Se você tiver apenas 1 separador, poderá empregar a compreensão da lista:

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

Separador de anexos / anexos:

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

Separador como elemento próprio:

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
Granitosaurus
fonte
1
você também pode adicionar if xpara garantir que o pedaço produzido por splittenha algum conteúdo, ou seja,result = [x + sep for x in text.split(sep) if x]
eu assustei o alienígena
Para mim, a tira foi removida demais e eu tive que usar isso:result = [sep+x for x in data.split(sep)] result[0] = result[0][len(sep):]
scottlittle
9

outro exemplo, divida em não alfanumérico e mantenha os separadores

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

resultado:

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

explicação

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

() <- keep the separators
[] <- match everything in between
^a-zA-Z0-9 <-except alphabets, upper/lower and numbers.
anurag
fonte
Embora, como dizem os documentos , isso seja equivalente à resposta aceita, eu gosto da legibilidade desta versão - embora \Wseja uma maneira mais compacta de expressá-la.
ephsmith
3

Você também pode dividir uma string com uma matriz de strings em vez de uma expressão regular, assim:

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 = ["'''", '+=', '+', "/", "*", "\\'", '\\"', "-=", "-", " ", '"""', "(", ")"]))
Anderson Green
fonte
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']
Moisey Oysgelt
fonte
3

Uma solução simples e preguiçosa

Suponha que seu padrão regex seja split_pattern = r'(!|\?)'

Primeiro, você adiciona o mesmo caractere que o novo separador, como '[cut]'

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

Então você divide o novo separador, new_string.split('[cut]')

Yilei Wang
fonte
Essa abordagem é inteligente, mas falhará quando a string original já contiver [cut]algum lugar.
Matthijs Kooijman
Pode ser mais rápido em problemas de larga escala, pois finalmente usa string.split (), no caso de re.split () custar mais do que re.sub () com string.split () (que eu não conheço).
Lorenz
1

Se alguém quiser dividir uma string enquanto mantém os separadores por regex sem capturar o grupo:

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)

Se alguém assumir que a regex está agrupada no grupo de captura:

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)

Ambas as formas também removerão grupos vazios que são inúteis e irritantes na maioria dos casos.

Dmitriy Sintsov
fonte
1

Aqui está uma .splitsolução simples que funciona sem regex.

Esta é uma resposta para o Python split () sem remover o delimitador ; portanto, não exatamente o que a postagem original pergunta, mas a outra pergunta foi fechada como duplicada para esta.

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

Testes aleatórios:

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
orestisf
fonte
O regex deve ser evitado em problemas de grande escala por motivos de velocidade, por isso é uma boa dica.
Lorenz
0

Tive um problema semelhante ao tentar dividir o caminho de um arquivo e lutei para encontrar uma resposta simples. Isso funcionou para mim e não envolveu a necessidade de substituir delimitadores novamente no texto dividido:

my_path = 'folder1/folder2/folder3/file1'

import re

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

retorna:

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

Conor
fonte
Isto pode ser um pouco simplificada usando: re.findall('[^/]+/?', my_path)(por exemplo, fazendo a barra final opcional usando um ?em vez de fornecer duas alternativas com |.
Matthijs Kooijman
0

Achei essa abordagem baseada em gerador mais satisfatória:

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

Isso evita a necessidade de descobrir o regex correto, enquanto na teoria deve ser bastante barato. Ele não cria novos objetos de sequência e delega a maior parte do trabalho de iteração para o método find eficiente.

... e no Python 3.8 pode ser tão curto quanto:

def split_keep(string, sep):
    start = 0
    while (end := string.find(sep, start) + 1) > 0:
        yield string[start:end]
        start = end
    yield string[start:]
Chen Levy
fonte
0
  1. substitua tudo seperator: (\W)porseperator + new_seperator: (\W;)

  2. dividido pelo 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')
kobako
fonte