W Pythonie, jak podzielić ciąg i zachować separatory?


226

Oto najprostszy sposób na wyjaśnienie tego. Oto czego używam:

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

Oto czego chcę:

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

Powodem jest to, że chcę podzielić ciąg na tokeny, manipulować nim, a następnie złożyć go ponownie.


3
co to \Woznacza? Nie udało mi się google go.
Ooker

8
Aby uzyskać więcej informacji, zobacz postać niebędącą słowem
Russell

Aby zapoznać się z rozdziałem surowego bajtu zamiast podziału łańcucha, zobacz duplikat stackoverflow.com/questions/62591863/…
Lorenz

Odpowiedzi:


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

22
To super. Nie wiedziałem, że re.split zrobił to z grupami przechwytywania.
Laurence Gonsalves

16
@Laurence: Cóż, jest to udokumentowane: docs.python.org/library/re.html#re.split : "Podziel ciąg według wystąpień wzorca. Jeśli przechwytywanie nawiasów jest używane we wzorcu, to tekst wszystkich grup we wzorcu są również zwracane jako część wynikowej listy. ”
Vinay Sajip

40
Jest to poważnie niedokumentowane. Używam Pythona od 14 lat i dopiero się o tym dowiedziałem.
smci,

19
Czy istnieje opcja, aby wynik dopasowania grupowego był dołączony do wszystkiego, co jest po lewej (lub analogicznie po prawej) podziału? Na przykład, czy można to łatwo zmodyfikować, aby uzyskać wynik ['foo', '/bar', ' spam', '\neggs']?
ely

3
@ Mr.F Możesz być w stanie zrobić coś z re.sub. Chciałem podzielić na końcowy procent, więc po prostu podłączyłem się do podwójnej postaci, a następnie podzieliłem, hacky, ale pracowałem w mojej sprawie: re.split('% ', re.sub('% ', '%% ', '5.000% Additional Whatnot'))->['5.000%', 'Additional Whatnot']
Kyle James Walker

29

Jeśli dzielisz na nową linię, użyj splitlines(True).

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

(Nie jest to ogólne rozwiązanie, ale dodanie tego tutaj na wypadek, gdyby ktoś tu przyszedł, nie zdając sobie sprawy z istnienia tej metody).


12

Kolejne rozwiązanie bez wyrażenia regularnego, które działa dobrze w Pythonie 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

Jeśli masz tylko 1 separator, możesz zastosować wyrażenia listowe:

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

Dołączanie / dodawanie separatora:

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

Separator jako jego własny element:

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
możesz również dodać, if xaby upewnić się, że fragment wyprodukowany przez splitma jakąś zawartość, tj.result = [x + sep for x in text.split(sep) if x]
zaalarmowałem obcego

Dla mnie pasek usunięto za dużo i musiałem użyć tego:result = [sep+x for x in data.split(sep)] result[0] = result[0][len(sep):]
scottlittle

9

inny przykład, podziel na nie alfanumeryczne i zachowaj separatory

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

wynik:

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

wyjaśnienie

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

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

Chociaż, jak mówią doktorzy , jest to równoważne z przyjętą odpowiedzią, podoba mi się czytelność tej wersji - chociaż \Wjest to bardziej zwarty sposób na wyrażenie tego.
efmith

3

Możesz również podzielić ciąg za pomocą tablicy ciągów zamiast wyrażenia regularnego, tak jak to:

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

Jedno leniwe i proste rozwiązanie

Załóżmy, że masz wzorzec wyrażenia regularnego split_pattern = r'(!|\?)'

Najpierw dodajesz taki sam znak jak nowy separator, na przykład „[cut]”

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

Następnie podzielisz nowy separator, new_string.split('[cut]')


To podejście jest sprytne, ale zawiedzie, gdy oryginalny ciąg znaków już [cut]gdzieś zawiera .
Matthijs Kooijman,

W przypadku problemów na dużą skalę może być szybsze, ponieważ w końcu używa string.split (), w przypadku gdy re.split () kosztuje więcej niż re.sub () z string.split () (czego nie wiem).
Lorenz

1

Jeśli ktoś chce podzielić ciąg znaków, zachowując separatory przez wyrażenie regularne bez przechwytywania grupy:

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)

Jeśli założymy, że wyrażenie regularne jest opakowane w grupę przechwytującą:

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)

Oba sposoby usuwają również puste grupy, które w większości przypadków są bezużyteczne i irytujące.


1

Oto proste .splitrozwiązanie, które działa bez wyrażenia regularnego.

To jest odpowiedź na split () Python bez usuwania separatora , więc nie dokładnie to, o co pyta oryginalny post, ale drugie pytanie zostało zamknięte jako duplikat tego.

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

Losowe testy:

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

Wyrażenia regularne należy unikać w przypadku problemów na dużą skalę ze względu na szybkość, dlatego jest to dobra wskazówka.
Lorenz

0

Miałem podobny problem, próbując podzielić ścieżkę pliku i próbowałem znaleźć prostą odpowiedź. To działało dla mnie i nie wymagało zastępowania ograniczników z powrotem w podzielonym tekście:

my_path = 'folder1/folder2/folder3/file1'

import re

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

zwroty:

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


Można to nieco uprościć, stosując: re.findall('[^/]+/?', my_path)(np. Czyniąc ukośnik końcowy opcjonalnym przy użyciu ?zamiast zamiast dwóch alternatyw z |.
Matthijs Kooijman,

0

Uważam, że podejście oparte na generatorze jest bardziej satysfakcjonujące:

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

Pozwala to uniknąć konieczności znalezienia poprawnego wyrażenia regularnego, a teoretycznie powinno być dość tanie. Nie tworzy nowych obiektów łańcuchowych i przekazuje większość pracy iteracyjnej do wydajnej metody find.

... a w Pythonie 3.8 może być tak krótki, jak:

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. zastąpić wszystko seperator: (\W)zseperator + new_seperator: (\W;)

  2. podzielone przez 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')
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.