En Python, ¿cómo divido una cadena y mantengo los separadores?

226

Aquí está la forma más simple de explicar esto. Esto es lo que estoy usando:

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

Esto es lo que quiero:

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

La razón es que quiero dividir una cadena en tokens, manipularla y luego volver a armarla.

Ken Kinder
fuente
3
¿Qué significa \W? Fallé en google.
Ooker
8
Un carácter que no es de palabra , vea aquí para más detalles
Russell
Para la pregunta aplicada a una cadena de bytes sin procesar y poner a "Dividir una cadena y mantener los delimitadores como parte de los fragmentos de cadena divididos, no como elementos de lista separados", consulte stackoverflow.com/questions/62591863/…
Lorenz

Respuestas:

295
>>> re.split('(\W)', 'foo/bar spam\neggs')
['foo', '/', 'bar', ' ', 'spam', '\n', 'eggs']
El comodoro Jaeger
fuente
22
Eso es genial. No sabía que re.split hacía eso con los grupos de captura.
Laurence Gonsalves
16
@Laurence: Bueno, está documentado: docs.python.org/library/re.html#re.split : "Dividir la cadena por las apariciones del patrón. Si se usan paréntesis de captura en el patrón, entonces el texto de todos los grupos en el patrón también se devuelven como parte de la lista resultante ".
Vinay Sajip
40
Está muy poco documentado. He estado usando Python durante 14 años y solo acabo de descubrirlo.
smci
19
¿Existe una opción para que la salida de la coincidencia de grupo se adjunte a lo que esté a la izquierda (o análogamente a la derecha) de la división? Por ejemplo, ¿se puede modificar fácilmente para que la salida sea ['foo', '/bar', ' spam', '\neggs']?
ely
3
@ Mr.F Es posible que pueda hacer algo con re.sub. Quería dividir en un porcentaje final, así que simplemente subtitulé con un doble carácter y luego me separé, hacky pero trabajé para mi caso: re.split('% ', re.sub('% ', '%% ', '5.000% Additional Whatnot'))->['5.000%', 'Additional Whatnot']
Kyle James Walker
29

Si está dividiendo en nueva línea, use splitlines(True).

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

(No es una solución general, pero agregue esto aquí en caso de que alguien venga aquí sin darse cuenta de que este método existe).

Mark Lodato
fuente
12

Otra solución sin expresiones regulares que funciona bien en 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
fuente
10

Si solo tiene 1 separador, puede emplear listas de comprensión:

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

Separador anexado / antepuesto:

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

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
Granitosaurio
fuente
1
También puede agregar if xpara asegurarse de que el fragmento producido por splittiene algún contenido, es decirresult = [x + sep for x in text.split(sep) if x]
alarmado extraterrestre
Para mí, la tira eliminó demasiado y tuve que usar esto:result = [sep+x for x in data.split(sep)] result[0] = result[0][len(sep):]
scottlittle
9

otro ejemplo, dividir en no alfanumérico y mantener los separadores

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

salida:

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

explicación

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
fuente
Aunque, como dicen los documentos , esto es equivalente a la respuesta aceptada, me gusta la legibilidad de esta versión, aunque \Wes una forma más compacta de expresarla.
ephsmith
3

También puede dividir una cadena con una matriz de cadenas en lugar de una expresión regular, como esta:

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

Una solución perezosa y simple

Suponga que su patrón regex es split_pattern = r'(!|\?)'

Primero, agrega el mismo carácter que el nuevo separador, como '[corte]'

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

Luego divides el nuevo separador, new_string.split('[cut]')

Yilei Wang
fuente
Este enfoque es inteligente, pero fallará cuando la cadena original ya contenga [cut]algún lugar.
Matthijs Kooijman
Podría ser más rápido en problemas a gran escala, ya que finalmente usa string.split (), en caso de que re.split () cueste más que re.sub () con string.split () (que no sé).
Lorenz hace
1

Si se quiere dividir una cadena mientras se mantienen los separadores por expresiones regulares sin capturar el 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)

Si se supone que la expresión regular está envuelta en el 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 formas también eliminarán grupos vacíos que son inútiles y molestos en la mayoría de los casos.

Dmitriy Sintsov
fuente
1

Aquí hay una .splitsolución simple que funciona sin expresiones regulares.

Esta es una respuesta para Python split () sin eliminar el delimitador , por lo que no es exactamente lo que pregunta la publicación original, pero la otra pregunta se cerró como un duplicado para esta.

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

Pruebas aleatorias:

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
fuente
regex debe evitarse en problemas a gran escala por razones de velocidad, por eso es una buena pista.
Lorenz
0

Tuve un problema similar al tratar de dividir una ruta de archivo y luché por encontrar una respuesta simple. Esto funcionó para mí y no implicó tener que sustituir los delimitadores en el texto dividido:

my_path = 'folder1/folder2/folder3/file1'

import re

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

devoluciones:

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

Conor
fuente
Esto puede simplificarse ligeramente mediante el uso de: re.findall('[^/]+/?', my_path)(por ejemplo, hacer que la barra inclinada final sea opcional usando un en ?lugar de proporcionar dos alternativas con |.
Matthijs Kooijman
0

Encontré este enfoque basado en generador más satisfactorio:

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

Evita la necesidad de descubrir la expresión regular correcta, mientras que en teoría debería ser bastante barata. No crea nuevos objetos de cadena y delega la mayor parte del trabajo de iteración al método de búsqueda eficiente.

... y en Python 3.8 puede ser tan corto como:

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
fuente
0
  1. reemplazar todo seperator: (\W)conseperator + new_seperator: (\W;)

  2. dividido por el 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
fuente