파이썬에`string.split ()`의 생성기 버전이 있습니까?


113

string.split()목록 인스턴스를 반환 합니다. 대신 생성기 를 반환하는 버전이 있습니까? 생성기 버전을 사용하지 않는 이유가 있습니까?


3
이 질문 은 관련 있을 수 있습니다.
비욘 폴 렉스

1
그 이유는 그것이 유용한 경우를 생각하기가 매우 어렵 기 때문입니다. 왜 이것을 원합니까?
Glenn Maynard

10
@Glenn : 최근에 긴 문자열을 n 개의 단어로 분할하는 것에 대한 질문을 보았습니다. 솔루션 중 하나는 split문자열을 반환 한 다음 split. 그로 split인해 발전기를 반환하는 방법이 있는지 생각 했습니다.
Manoj Govindan

5
파이썬 이슈 트래커에 대한 관련 토론이 있습니다 : bugs.python.org/issue17343
saffsd

정말 큰 베어 문자열 / 파일을 구문 분석하는 데 유용 할 수 있습니다 @GlennMaynard 있지만, 사람은 매우 쉽게 사용하는 발전기 파서 자신을 쓸 수있는자가 양조 DFA 수율
드미트리 Ponyatov

답변:


77

re.finditer상당히 최소한의 메모리 오버 헤드 를 사용 하는 것은 매우 가능성이 높습니다 .

def split_iter(string):
    return (x.group(0) for x in re.finditer(r"[A-Za-z']+", string))

데모:

>>> list( split_iter("A programmer's RegEx test.") )
['A', "programmer's", 'RegEx', 'test']

편집 : 내 테스트 방법이 정확하다고 가정하고 파이썬 3.2.1에서 일정한 메모리를 차지한다는 것을 방금 확인했습니다. 나는 매우 큰 크기 (1GB 정도)의 문자열을 만든 다음 for루프 (추가 메모리를 생성 할 수있는 목록 이해가 아님) 를 사용하여 반복 가능 항목을 반복했습니다 . 이로 인해 메모리가 눈에 띄게 증가하지는 않았습니다 (즉, 메모리가 증가하면 1GB 문자열보다 훨씬 적음).


5
우수한! 나는 finditer에 대해 잊었다. 분할 선과 같은 작업에 관심이 있다면이 RE를 사용하는 것이 좋습니다. '(. * \ n |. + $)'str.splitlines는 훈련 줄 바꿈을 잘라냅니다 (내가 정말 좋아하지 않는 것 ... ); 동작의 해당 부분을 복제하려면 그룹화를 사용할 수 있습니다. (m.group (2) 또는 m.group (3) for m in re.finditer ( '((. *) \ n | (. +) $) ', s)). 추신 : RE의 바깥 쪽 괄호는 필요하지 않은 것 같습니다. 그냥 사용이 불편 해요 | 괄호없이 : P
allyourcode

3
성능은 어떻습니까? 재 매칭은 일반 검색보다 느려 야합니다.
anatoly techtonik

1
이 split_iter 함수를 어떻게 다시 작성 a_string.split("delimiter")하시겠습니까?
Moberg

분할을 사용하면 이전 다음 패션, 바닥에 내 대답 봐 ...에 반환 된 값을 사용하려는 경우 그래서, 빨리 정말 아니다 어쨌든 정규 표현식을 허용
Veltzer 도론

str.split()정규 표현식을 허용하지 않는, 그건 re.split()당신의있는 거 생각 ...
알렉시스

17

내가 생각할 수있는 가장 효율적인 방법 offsetstr.find()메소드 의 매개 변수를 사용하여 작성 하는 것입니다. 이것은 많은 메모리 사용을 피하고 필요하지 않을 때 regexp의 오버 헤드에 의존합니다.

[2016-8-2 편집 : 정규식 구분 기호를 선택적으로 지원하도록 업데이트 됨]

def isplit(source, sep=None, regex=False):
    """
    generator version of str.split()

    :param source:
        source string (unicode or bytes)

    :param sep:
        separator to split on.

    :param regex:
        if True, will treat sep as regular expression.

    :returns:
        generator yielding elements of string.
    """
    if sep is None:
        # mimic default python behavior
        source = source.strip()
        sep = "\\s+"
        if isinstance(source, bytes):
            sep = sep.encode("ascii")
        regex = True
    if regex:
        # version using re.finditer()
        if not hasattr(sep, "finditer"):
            sep = re.compile(sep)
        start = 0
        for m in sep.finditer(source):
            idx = m.start()
            assert idx >= start
            yield source[start:idx]
            start = m.end()
        yield source[start:]
    else:
        # version using str.find(), less overhead than re.finditer()
        sepsize = len(sep)
        start = 0
        while True:
            idx = source.find(sep, start)
            if idx == -1:
                yield source[start:]
                return
            yield source[start:idx]
            start = idx + sepsize

원하는대로 사용할 수 있습니다 ...

>>> print list(isplit("abcb","b"))
['a','c','']

find () 또는 슬라이싱이 수행 될 때마다 문자열 내에서 검색하는 데 약간의 비용이 발생하지만 문자열은 메모리에서 연속적인 배열로 표현되기 때문에 최소화되어야합니다.


10

이것은 너무 많은 하위 문자열을 할당하는 문제가없는 split()via 구현의 생성기 버전입니다 re.search().

import re

def itersplit(s, sep=None):
    exp = re.compile(r'\s+' if sep is None else re.escape(sep))
    pos = 0
    while True:
        m = exp.search(s, pos)
        if not m:
            if pos < len(s) or sep is not None:
                yield s[pos:]
            break
        if pos < m.start() or sep is not None:
            yield s[pos:m.start()]
        pos = m.end()


sample1 = "Good evening, world!"
sample2 = " Good evening, world! "
sample3 = "brackets][all][][over][here"
sample4 = "][brackets][all][][over][here]["

assert list(itersplit(sample1)) == sample1.split()
assert list(itersplit(sample2)) == sample2.split()
assert list(itersplit(sample3, '][')) == sample3.split('][')
assert list(itersplit(sample4, '][')) == sample4.split('][')

편집 : 구분 문자가 제공되지 않은 경우 주변 공백 처리를 수정했습니다.


12
이게 왜 더 낫지 re.finditer?
Erik Kaplun 2013 년

@ErikKaplun 항목에 대한 정규식 논리는 구분 기호보다 복잡 할 수 있기 때문입니다. 제 경우에는 각 라인을 개별적으로 처리하고 싶었 기 때문에 라인이 일치하지 않으면 다시보고 할 수 있습니다.
rovyko

9

제안 된 다양한 방법에 대한 성능 테스트를 수행했습니다 (여기서는 반복하지 않겠습니다). 몇 가지 결과 :

  • str.split (기본값 = 0.3461570239996945
  • 수동 검색 (문자 별) (Dave Webb의 답변 중 하나) = 0.8260340550004912
  • re.finditer (ninjagecko의 답변) = 0.698872097000276
  • str.find (Eli Collins의 답변 중 하나) = 0.7230395330007013
  • itertools.takewhile (Ignacio Vazquez-Abrams의 답변) = 2.023023967998597
  • str.split(..., maxsplit=1) 재귀 = N / A †

† (재귀 답변 string.split과 함께 maxsplit = 1, 적절한 시간에 완료하는 데 실패) 지정된 string.splitS가 짧은 문자열에 더 잘 작동 할 수 있습니다 속도를하지만, 메모리 어쨌든 문제가되지 않는 경우 그때 나는 짧은 문자열에 대한 사용 사례를 볼 수 없습니다.

다음을 사용하여 테스트 timeit했습니다.

the_text = "100 " * 9999 + "100"

def test_function( method ):
    def fn( ):
        total = 0

        for x in method( the_text ):
            total += int( x )

        return total

    return fn

이것은 string.split메모리 사용량에도 불구하고 왜 훨씬 더 빠른지에 대한 또 다른 질문을 제기 합니다.


1
이는 메모리가 cpu보다 느리고이 경우 목록은 다른 모든 항목이 요소별로로드되는 청크 단위로로드되기 때문입니다. 같은 메모에서 많은 학자들은 연결 목록이 더 빠르고 복잡하지 않은 반면 컴퓨터는 종종 배열을 사용하면 더 빠르므로 최적화하기가 더 쉽다고 말합니다. 옵션이 다른 옵션보다 빠르다고 가정 할 수 없습니다. 테스트하십시오! 테스트를 위해 +1.
Benoît P

문제는 처리 체인의 다음 단계에서 발생합니다. 그런 다음 특정 청크를 찾고 찾을 때 나머지는 무시하려는 경우 기본 제공 솔루션 대신 생성기 기반 분할을 사용할 수 있습니다.
jgomo3

6

여기에 다른 답변보다 훨씬 빠르고 완벽하게 구현 된 구현이 있습니다. 다른 경우에 대해 4 개의 별도 하위 기능이 있습니다.

str_split함수 의 독 스트링을 복사하겠습니다 .


str_split(s, *delims, empty=None)

s나머지 인수로 문자열 을 분할하여 빈 부분을 생략 할 수 있습니다 ( empty키워드 인수가이를 담당합니다). 이것은 생성기 함수입니다.

구분 기호가 하나만 제공되면 문자열이 단순히 분할됩니다. empty다음입니다 True기본적으로.

str_split('[]aaa[][]bb[c', '[]')
    -> '', 'aaa', '', 'bb[c'
str_split('[]aaa[][]bb[c', '[]', empty=False)
    -> 'aaa', 'bb[c'

여러 구분 기호가 제공되면 기본적으로 해당 구분 기호의 가능한 가장 긴 시퀀스로 문자열이 분할됩니다. 또는 empty로 설정된 경우 True구분 기호 사이의 빈 문자열도 포함됩니다. 이 경우 구분 기호는 단일 문자 일 수 있습니다.

str_split('aaa, bb : c;', ' ', ',', ':', ';')
    -> 'aaa', 'bb', 'c'
str_split('aaa, bb : c;', *' ,:;', empty=True)
    -> 'aaa', '', 'bb', '', '', 'c', ''

구분 기호가 제공되지 않으면 string.whitespace이 사용 str.split()되므로이 함수가 생성기라는 점을 제외 하면 효과는와 동일 합니다.

str_split('aaa\\t  bb c \\n')
    -> 'aaa', 'bb', 'c'

import string

def _str_split_chars(s, delims):
    "Split the string `s` by characters contained in `delims`, including the \
    empty parts between two consecutive delimiters"
    start = 0
    for i, c in enumerate(s):
        if c in delims:
            yield s[start:i]
            start = i+1
    yield s[start:]

def _str_split_chars_ne(s, delims):
    "Split the string `s` by longest possible sequences of characters \
    contained in `delims`"
    start = 0
    in_s = False
    for i, c in enumerate(s):
        if c in delims:
            if in_s:
                yield s[start:i]
                in_s = False
        else:
            if not in_s:
                in_s = True
                start = i
    if in_s:
        yield s[start:]


def _str_split_word(s, delim):
    "Split the string `s` by the string `delim`"
    dlen = len(delim)
    start = 0
    try:
        while True:
            i = s.index(delim, start)
            yield s[start:i]
            start = i+dlen
    except ValueError:
        pass
    yield s[start:]

def _str_split_word_ne(s, delim):
    "Split the string `s` by the string `delim`, not including empty parts \
    between two consecutive delimiters"
    dlen = len(delim)
    start = 0
    try:
        while True:
            i = s.index(delim, start)
            if start!=i:
                yield s[start:i]
            start = i+dlen
    except ValueError:
        pass
    if start<len(s):
        yield s[start:]


def str_split(s, *delims, empty=None):
    """\
Split the string `s` by the rest of the arguments, possibly omitting
empty parts (`empty` keyword argument is responsible for that).
This is a generator function.

When only one delimiter is supplied, the string is simply split by it.
`empty` is then `True` by default.
    str_split('[]aaa[][]bb[c', '[]')
        -> '', 'aaa', '', 'bb[c'
    str_split('[]aaa[][]bb[c', '[]', empty=False)
        -> 'aaa', 'bb[c'

When multiple delimiters are supplied, the string is split by longest
possible sequences of those delimiters by default, or, if `empty` is set to
`True`, empty strings between the delimiters are also included. Note that
the delimiters in this case may only be single characters.
    str_split('aaa, bb : c;', ' ', ',', ':', ';')
        -> 'aaa', 'bb', 'c'
    str_split('aaa, bb : c;', *' ,:;', empty=True)
        -> 'aaa', '', 'bb', '', '', 'c', ''

When no delimiters are supplied, `string.whitespace` is used, so the effect
is the same as `str.split()`, except this function is a generator.
    str_split('aaa\\t  bb c \\n')
        -> 'aaa', 'bb', 'c'
"""
    if len(delims)==1:
        f = _str_split_word if empty is None or empty else _str_split_word_ne
        return f(s, delims[0])
    if len(delims)==0:
        delims = string.whitespace
    delims = set(delims) if len(delims)>=4 else ''.join(delims)
    if any(len(d)>1 for d in delims):
        raise ValueError("Only 1-character multiple delimiters are supported")
    f = _str_split_chars if empty else _str_split_chars_ne
    return f(s, delims)

이 함수는 Python 3에서 작동하며 매우 추악하지만 쉬운 수정을 적용하여 2 버전과 3 버전 모두에서 작동하도록 할 수 있습니다. 함수의 첫 줄은 다음과 같이 변경해야합니다.

def str_split(s, *delims, **kwargs):
    """...docstring..."""
    empty = kwargs.get('empty')

3

아니요,하지만 itertools.takewhile().

편집하다:

매우 간단하고 반쯤 손상된 구현 :

import itertools
import string

def isplitwords(s):
  i = iter(s)
  while True:
    r = []
    for c in itertools.takewhile(lambda x: not x in string.whitespace, i):
      r.append(c)
    else:
      if r:
        yield ''.join(r)
        continue
      else:
        raise StopIteration()

@Ignacio : 문서의 예제는 정수 목록을 사용하여 takeWhile. 무엇을 사용하여 predicate문자열을 단어 (기본값 split) 로 분할 하는 것이 takeWhile()좋을까요?
Manoj Govindan

에서 존재를 찾습니다 string.whitespace.
Ignacio Vazquez-Abrams

구분 기호는 여러 문자를 가질 수 있습니다.'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
kennytm

@Ignacio : 답변에 예를 추가 할 수 있습니까?
Manoj Govindan

1
쓰기는 쉽지만 배 더 느립니다. 이것은 실제로 네이티브 코드로 구현되어야하는 작업입니다.
Glenn Maynard

3

생성기 버전의 split(). 생성기 객체는 반복 할 전체 문자열을 포함해야하므로 생성기를 사용하여 메모리를 절약하지 않을 것입니다.

하나를 작성하고 싶다면 꽤 쉬울 것입니다.

import string

def gsplit(s,sep=string.whitespace):
    word = []

    for c in s:
        if c in sep:
            if word:
                yield "".join(word)
                word = []
        else:
            word.append(c)

    if word:
        yield "".join(word)

3
각 결과 부분에 문자열의 두 번째 복사본을 저장할 필요가없고 배열 및 개체 오버 헤드 (일반적으로 문자열 자체보다 더 많음)를 저장하지 않아도되므로 사용되는 메모리를 절반으로 줄일 수 있습니다. 그것은 일반적으로 중요하지 않습니다. (너무 큰 문자열을 분할하여 이것이 중요하다면 아마도 뭔가 잘못하고있을 것입니다), 심지어 네이티브 C 생성기 구현도 항상 한꺼번에하는 것보다 훨씬 느립니다.
Glenn Maynard

@Glenn Maynard-방금 깨달았습니다. 나는 어떤 이유로 원래 생성기가 참조가 아닌 문자열의 사본을 저장합니다. 와 빠른 검사 id()바로 내를 넣어. 그리고 분명히 문자열은 불변하기 때문에 반복하는 동안 누군가가 원래 문자열을 변경하는 것에 대해 걱정할 필요가 없습니다.
Dave Webb

6
제너레이터 사용의 요점은 메모리 사용량이 아니라 일찍 종료하려는 경우 전체 문자열을 분할해야하는 시간을 절약 할 수 있다는 것입니까? (이것은 특정 솔루션에 대한 의견이 아니며 메모리에 대한 토론에 놀랐습니다.)
Scott Griffiths

@Scott : 그것이 정말로 승리 한 경우를 생각하기 어렵습니다. 여기서 1 : 당신은 중간에 쪼개는 것을 멈추고 싶습니다, 2 : 당신은 얼마나 많은 단어를 미리 쪼개는지 모릅니다, 3 : 당신은 문제가 될만큼 충분히 큰 문자열, 4 : str.split보다 중요한 승리가 될 수있을만큼 일찍 중단합니다. 그것은 매우 좁은 조건의 집합입니다.
Glenn Maynard

4
문자열이 느리게 생성되는 경우 (예 : 네트워크 트래픽 또는 파일 읽기) 훨씬 더 많은 이점을 얻을 수 있습니다.
Lie Ryan

3

나는 string.split (즉, 기본적으로 공백으로 구분되고 구분 기호를 지정할 수 있음)처럼 동작하는 @ninjagecko의 답변 버전을 작성했습니다.

def isplit(string, delimiter = None):
    """Like string.split but returns an iterator (lazy)

    Multiple character delimters are not handled.
    """

    if delimiter is None:
        # Whitespace delimited by default
        delim = r"\s"

    elif len(delimiter) != 1:
        raise ValueError("Can only handle single character delimiters",
                        delimiter)

    else:
        # Escape, incase it's "\", "*" etc.
        delim = re.escape(delimiter)

    return (x.group(0) for x in re.finditer(r"[^{}]+".format(delim), string))

내가 사용한 테스트는 다음과 같습니다 (python 3 및 python 2).

# Wrapper to make it a list
def helper(*args,  **kwargs):
    return list(isplit(*args, **kwargs))

# Normal delimiters
assert helper("1,2,3", ",") == ["1", "2", "3"]
assert helper("1;2;3,", ";") == ["1", "2", "3,"]
assert helper("1;2 ;3,  ", ";") == ["1", "2 ", "3,  "]

# Whitespace
assert helper("1 2 3") == ["1", "2", "3"]
assert helper("1\t2\t3") == ["1", "2", "3"]
assert helper("1\t2 \t3") == ["1", "2", "3"]
assert helper("1\n2\n3") == ["1", "2", "3"]

# Surrounding whitespace dropped
assert helper(" 1 2  3  ") == ["1", "2", "3"]

# Regex special characters
assert helper(r"1\2\3", "\\") == ["1", "2", "3"]
assert helper(r"1*2*3", "*") == ["1", "2", "3"]

# No multi-char delimiters allowed
try:
    helper(r"1,.2,.3", ",.")
    assert False
except ValueError:
    pass

파이썬의 정규식 모듈은 유니 코드 공백에 대해 "올바른 일" 을 한다고 말하지만 실제로 테스트하지는 않았습니다.

요점 으로도 사용할 수 있습니다 .


3

반복자 를 읽을 수도 있고 반환하고 싶다면 다음을 시도하십시오.

import itertools as it

def iter_split(string, sep=None):
    sep = sep or ' '
    groups = it.groupby(string, lambda s: s != sep)
    return (''.join(g) for k, g in groups if k)

용법

>>> list(iter_split(iter("Good evening, world!")))
['Good', 'evening,', 'world!']

3

more_itertools.split_atstr.split반복 자를 위한 아날로그를 제공합니다.

>>> import more_itertools as mit


>>> list(mit.split_at("abcdcba", lambda x: x == "b"))
[['a'], ['c', 'd', 'c'], ['a']]

>>> "abcdcba".split("b")
['a', 'cdc', 'a']

more_itertools 타사 패키지입니다.


1
more_itertools.split_at ()는 여전히 각 호출에서 새로 할당 된 목록을 사용하고 있으므로 반복기를 반환하지만 일정한 메모리 요구 사항을 충족하지 못합니다. 따라서 반복자를 시작하려는 이유에 따라 도움이 될 수도 있고 그렇지 않을 수도 있습니다.
jcater

@jcater 좋은 지적입니다. 중간 값은 실제로 구현 에 따라 반복기 내에서 하위 목록으로 버퍼링됩니다 . 목록을 반복자로 대체 itertools.chain하고 목록 이해를 사용하여 결과를 추가 하고 평가 하도록 소스를 조정할 수 있습니다. 필요와 요청에 따라 예제를 게시 할 수 있습니다.
pylang

2

find_iter 솔루션을 사용하여 지정된 구분 기호에 대한 생성기를 반환 한 다음 itertools의 pairwise 레시피를 사용하여 원래 split 메소드에서와 같이 실제 단어를 가져올 이전 다음 반복을 빌드하는 방법을 보여주고 싶었습니다.


from more_itertools import pairwise
import re

string = "dasdha hasud hasuid hsuia dhsuai dhasiu dhaui d"
delimiter = " "
# split according to the given delimiter including segments beginning at the beginning and ending at the end
for prev, curr in pairwise(re.finditer("^|[{0}]+|$".format(delimiter), string)):
    print(string[prev.end(): curr.start()])

노트:

  1. 파이썬에서 next를 재정의하는 것은 매우 나쁜 생각이기 때문에 prev & next 대신 prev & curr을 사용합니다.
  2. 이것은 매우 효율적입니다

1

regex / itertools가없는 멍청한 방법 :

def isplit(text, split='\n'):
    while text != '':
        end = text.find(split)

        if end == -1:
            yield text
            text = ''
        else:
            yield text[:end]
            text = text[end + 1:]

0
def split_generator(f,s):
    """
    f is a string, s is the substring we split on.
    This produces a generator rather than a possibly
    memory intensive list. 
    """
    i=0
    j=0
    while j<len(f):
        if i>=len(f):
            yield f[j:]
            j=i
        elif f[i] != s:
            i=i+1
        else:
            yield [f[j:i]]
            j=i+1
            i=i+1

왜 양보 [f[j:i]]하지 f[j:i]않습니까?
Moberg

0

여기에 간단한 응답이 있습니다

def gen_str(some_string, sep):
    j=0
    guard = len(some_string)-1
    for i,s in enumerate(some_string):
        if s == sep:
           yield some_string[j:i]
           j=i+1
        elif i!=guard:
           continue
        else:
           yield some_string[j:]
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.