Có phiên bản trình tạo của `string.split ()` trong Python không?


113

string.split()trả về một trường hợp danh sách . Có phiên bản nào trả về trình tạo thay thế không? Có bất kỳ lý do nào chống lại việc có một phiên bản máy phát điện không?


3
Câu hỏi này có thể liên quan.
Björn Pollex

1
Lý do là rất khó để nghĩ ra trường hợp nào nó hữu ích. Tại sao bạn muốn điều này?
Glenn Maynard

10
@Glenn: Gần đây tôi thấy một câu hỏi về việc tách một chuỗi dài thành các phần n từ. Một trong những giải pháp splitcho chuỗi và sau đó trả về một trình tạo hoạt động dựa trên kết quả của split. Điều đó khiến tôi suy nghĩ nếu có cách nào splitđể trả lại một máy phát điện để bắt đầu.
Manoj Govindan

5
Có một cuộc thảo luận có liên quan về trình theo dõi Vấn đề Python: bug.python.org/issue17343
saffsd

@GlennMaynard nó có thể hữu ích cho thực sự lớn trần phân tích chuỗi / tập tin, nhưng ai cũng có thể viết phân tích cú pháp máy phát điện tự rất dễ sử dụng tự ủ DFA và năng suất
Dmitry Ponyatov

Câu trả lời:


77

Rất có thể xảy ra trường hợp re.finditersử dụng chi phí bộ nhớ khá tối thiểu.

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

Bản giới thiệu:

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

chỉnh sửa: Tôi vừa xác nhận rằng điều này chiếm bộ nhớ không đổi trong python 3.2.1, giả sử phương pháp thử nghiệm của tôi là đúng. Tôi đã tạo một chuỗi có kích thước rất lớn (1GB hoặc hơn), sau đó lặp lại qua chuỗi có thể lặp lại bằng một forvòng lặp (KHÔNG PHẢI là khả năng hiểu danh sách, điều này sẽ tạo ra bộ nhớ bổ sung). Điều này không dẫn đến sự tăng trưởng đáng chú ý của bộ nhớ (có nghĩa là, nếu có sự tăng trưởng trong bộ nhớ, nó ít hơn nhiều so với chuỗi 1GB).


5
Thông minh! Tôi đã quên về công cụ tìm kiếm. Nếu ai đó quan tâm đến việc làm một cái gì đó như dòng chia tách, tôi sẽ đề xuất sử dụng RE: '(. * \ N |. + $)' Str.splitlines này sẽ cắt bỏ dòng mới đào tạo (điều mà tôi không thực sự thích ... ); nếu bạn muốn sao chép phần đó của hành vi, bạn có thể sử dụng nhóm: (m.group (2) hoặc m.group (3) cho m trong re.finditer ('((. *) \ n | (. +)) $) ', s)). PS: Tôi đoán dấu ngoặc ngoài trong RE là không cần thiết; Tôi chỉ cảm thấy không thoải mái khi sử dụng | không có dấu ngoặc: P
allyourcode

3
Còn về hiệu suất thì sao? kết hợp lại sẽ chậm hơn tìm kiếm thông thường.
anatoly techtonik

1
Bạn sẽ viết lại hàm split_iter này hoạt động như thế a_string.split("delimiter")nào?
Moberg

split chấp nhận biểu thức thông thường anyway vì vậy nó không thực sự nhanh hơn, nếu bạn muốn sử dụng giá trị trả về trong một thời trang trước hôm sau, nhìn vào câu trả lời của tôi ở phía dưới ...
Veltzer Doron

str.split()không chấp nhận các cụm từ thông dụng, đó là re.split()bạn đang nghĩ đến ...
alexis

17

Cách hiệu quả nhất mà tôi có thể nghĩ ra là viết một cái bằng cách sử dụng offsettham số của str.find()phương thức. Điều này tránh sử dụng nhiều bộ nhớ và dựa vào chi phí của regexp khi không cần thiết.

[chỉnh sửa 2016-8-2: đã cập nhật cái này để tùy chọn hỗ trợ dấu phân cách regex]

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

Điều này có thể được sử dụng như bạn muốn ...

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

Mặc dù có một chút chi phí tìm kiếm trong chuỗi mỗi khi thực hiện tìm kiếm () hoặc cắt, điều này sẽ được tối thiểu vì các chuỗi được biểu diễn dưới dạng mảng dự phòng trong bộ nhớ.


10

Đây là phiên bản trình tạo của được split()thực hiện thông qua re.search()mà không có vấn đề phân bổ quá nhiều chuỗi con.

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('][')

EDIT: Đã sửa lỗi xử lý khoảng trắng xung quanh nếu không có ký tự phân cách nào được đưa ra.


12
tại sao điều này bất kỳ tốt hơn re.finditer?
Erik Kaplun

@ErikKaplun Vì logic regex cho các mục có thể phức tạp hơn so với các dấu phân tách của chúng. Trong trường hợp của tôi, tôi muốn xử lý từng dòng riêng lẻ, vì vậy tôi có thể báo cáo lại nếu một dòng không khớp.
rovyko

8

Đã thực hiện một số thử nghiệm hiệu suất trên các phương pháp khác nhau được đề xuất (tôi sẽ không nhắc lại chúng ở đây). Một số kết quả:

  • str.split (mặc định = 0,3461570239996945
  • tìm kiếm thủ công (theo ký tự) (một trong những câu trả lời của Dave Webb) = 0,8260340550004912
  • re.finditer (câu trả lời của ninjagecko) = 0,698872097000276
  • str.find (một trong những câu trả lời của Eli Collins) = 0,7230395330007013
  • itertools.takewhile (Câu trả lời của Ignacio Vazquez-Abrams) = 2.023023967998597
  • str.split(..., maxsplit=1) đệ quy = N / A †

† Các câu trả lời đệ quy ( string.splitvới maxsplit = 1) không hoàn thành trong thời gian hợp lý, string.splitvới tốc độ cho trước , chúng có thể hoạt động tốt hơn trên các chuỗi ngắn hơn, nhưng sau đó tôi không thể thấy trường hợp sử dụng cho các chuỗi ngắn, nơi bộ nhớ không phải là vấn đề.

Đã kiểm tra bằng cách sử dụng timeittrên:

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

Điều này đặt ra một câu hỏi khác là tại sao string.splitnó nhanh hơn nhiều mặc dù nó sử dụng bộ nhớ.


1
Điều này là do bộ nhớ chậm hơn cpu và trong trường hợp này, danh sách được tải theo các phần trong đó tất cả các phần khác được tải từng phần tử. Đồng thời, nhiều học giả sẽ cho bạn biết danh sách được liên kết nhanh hơn và ít phức tạp hơn trong khi máy tính của bạn thường nhanh hơn với các mảng, điều này cho thấy dễ tối ưu hóa hơn. Bạn không thể cho rằng một tùy chọn nhanh hơn một tùy chọn khác, hãy thử nghiệm nó! +1 để thử nghiệm.
Benoît P

Vấn đề nảy sinh trong các bước tiếp theo của một chuỗi xử lý. Nếu sau đó bạn muốn tìm một đoạn cụ thể và bỏ qua phần còn lại khi tìm thấy nó, thì bạn có lý do để sử dụng phân tách dựa trên trình tạo thay vì giải pháp tích hợp sẵn.
jgomo3

6

Đây là cách thực hiện của tôi, nhanh hơn và đầy đủ hơn nhiều so với các câu trả lời khác ở đây. Nó có 4 chức năng con riêng biệt cho các trường hợp khác nhau.

Tôi sẽ chỉ sao chép chuỗi doc của str_splithàm chính :


str_split(s, *delims, empty=None)

Chia chuỗi stheo phần còn lại của các đối số, có thể bỏ qua các phần trống ( emptyđối số từ khóa chịu trách nhiệm về điều đó). Đây là một chức năng máy phát điện.

Khi chỉ có một dấu phân cách được cung cấp, chuỗi chỉ đơn giản được phân tách bởi nó. emptysau đó là Truetheo mặc định.

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

Khi cung cấp nhiều dấu phân cách, chuỗi được chia theo chuỗi dài nhất có thể của các dấu phân cách đó theo mặc định, hoặc, nếu emptyđược đặt thành True, các chuỗi trống giữa các dấu phân cách cũng được bao gồm. Lưu ý rằng các dấu phân cách trong trường hợp này có thể chỉ là các ký tự đơn.

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

Khi không có dấu phân cách nào được cung cấp, string.whitespaceđược sử dụng, do đó, hiệu ứng giống như str.split(), ngoại trừ hàm này là một trình tạo.

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)

Hàm này hoạt động trên Python 3 và một bản sửa lỗi dễ dàng, mặc dù khá xấu, có thể được áp dụng để làm cho nó hoạt động ở cả phiên bản 2 và 3. Các dòng đầu tiên của hàm nên được thay đổi thành:

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

3

Không, nhưng nó phải đủ dễ dàng để viết một cách sử dụng itertools.takewhile().

BIÊN TẬP:

Cách thực hiện rất đơn giản, nửa vời:

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: Ví dụ trong tài liệu sử dụng danh sách các số nguyên để minh họa việc sử dụng takeWhile. Điều gì sẽ tốt predicatecho việc tách một chuỗi thành các từ (mặc định split) bằng cách sử dụng takeWhile()?
Manoj Govindan

Tìm kiếm sự hiện diện trong string.whitespace.
Ignacio Vazquez-Abrams

Dấu phân tách có thể có nhiều ký tự,'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
kennytm

@Ignacio: Bạn có thể thêm ví dụ vào câu trả lời của mình không?
Manoj Govindan

1
Dễ dàng để viết, nhưng nhiều thứ tự cường độ chậm hơn. Đây là một hoạt động thực sự nên được triển khai trong mã gốc.
Glenn Maynard

3

Tôi không thấy bất kỳ lợi ích rõ ràng nào đối với phiên bản trình tạo của split(). Đối tượng trình tạo sẽ phải chứa toàn bộ chuỗi để lặp lại vì vậy bạn sẽ không tiết kiệm bất kỳ bộ nhớ nào bằng cách có trình tạo.

Tuy nhiên, nếu bạn muốn viết một cái thì sẽ khá dễ dàng:

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
Bạn sẽ giảm một nửa bộ nhớ được sử dụng, bằng cách không phải lưu trữ bản sao thứ hai của chuỗi trong mỗi phần kết quả, cộng với chi phí của mảng và đối tượng (thường nhiều hơn chính các chuỗi). Tuy nhiên, điều đó nói chung không thành vấn đề (nếu bạn đang chia nhỏ các chuỗi lớn đến mức quan trọng, thì có thể bạn đang làm sai) và thậm chí việc triển khai trình tạo C gốc sẽ luôn chậm hơn đáng kể so với thực hiện tất cả cùng một lúc.
Glenn Maynard

@Glenn Maynard - Tôi vừa nhận ra điều đó. Tôi vì lý do nào đó mà ban đầu, trình tạo sẽ lưu trữ một bản sao của chuỗi thay vì một tham chiếu. Kiểm tra nhanh với id()đưa tôi đúng. Và rõ ràng vì các chuỗi là bất biến, bạn không cần phải lo lắng về việc ai đó thay đổi chuỗi ban đầu trong khi bạn đang lặp lại nó.
Dave Webb

6
Không phải điểm chính trong việc sử dụng trình tạo không phải là sử dụng bộ nhớ, mà là bạn có thể tiết kiệm được việc phải chia toàn bộ chuỗi nếu bạn muốn thoát sớm? (Đó không phải là nhận xét về giải pháp cụ thể của bạn, tôi chỉ ngạc nhiên về cuộc thảo luận về trí nhớ).
Scott Griffiths

@Scott: Thật khó để nghĩ ra trường hợp mà đó thực sự là một chiến thắng - trong đó 1: bạn muốn dừng việc tách từng phần, 2: bạn không biết mình đang chia nhỏ bao nhiêu từ, 3: bạn có chuỗi đủ lớn để nó quan trọng, và 4: bạn liên tục dừng đủ sớm để nó giành chiến thắng đáng kể trước str.split. Đó là một tập hợp các điều kiện rất hẹp.
Glenn Maynard

4
Bạn có thể có lợi ích cao hơn nhiều nếu chuỗi của bạn cũng được tạo một cách lười biếng (ví dụ: từ lưu lượng mạng hoặc đọc tệp)
Lie Ryan

3

Tôi đã viết một phiên bản câu trả lời của @ ninjagecko hoạt động giống như string.split hơn (tức là khoảng trắng được phân định theo mặc định và bạn có thể chỉ định dấu phân cách).

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))

Đây là các bài kiểm tra tôi đã sử dụng (trong cả python 3 và 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

mô-đun regex của python nói rằng nó thực hiện "điều đúng đắn" đối với khoảng trắng unicode, nhưng tôi chưa thực sự kiểm tra nó.

Cũng có sẵn dưới dạng ý chính .


3

Nếu bạn cũng muốn có thể đọc một trình lặp (cũng như trả về một trình lặp ), hãy thử điều này:

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)

Sử dụng

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

3

more_itertools.split_atcung cấp một tương tự str.splitcho các trình vòng lặp.

>>> 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 là một gói của bên thứ ba.


1
Lưu ý rằng more_itertools.split_at () vẫn đang sử dụng danh sách mới được cấp phát trên mỗi cuộc gọi, vì vậy trong khi điều này trả về một trình lặp, nó không đạt được yêu cầu bộ nhớ không đổi. Vì vậy, tùy thuộc vào lý do tại sao bạn muốn một trình lặp bắt đầu, điều này có thể hữu ích hoặc không.
jcater

@jcater Điểm tốt. Các giá trị trung gian thực sự được đệm dưới dạng danh sách con bên trong trình vòng lặp, tùy theo cách triển khai của nó . Người ta có thể điều chỉnh nguồn để thay thế danh sách bằng các trình vòng lặp, nối với itertools.chainvà đánh giá kết quả bằng cách sử dụng khả năng hiểu danh sách. Tùy theo nhu cầu và yêu cầu, mình có thể đăng ví dụ.
pylang

2

Tôi muốn chỉ cách sử dụng giải pháp find_iter để trả về một trình tạo cho các dấu phân cách đã cho và sau đó sử dụng công thức ghép nối từ itertools để xây dựng lần lặp tiếp theo trước đó sẽ nhận được các từ thực tế như trong phương pháp tách ban đầu.


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()])

Ghi chú:

  1. Tôi sử dụng prev & curr thay vì pres & next vì ghi đè next trong python là một ý tưởng rất tồi
  2. Điều này khá hiệu quả

1

Phương thức ngu ngốc nhất, không có 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

tại sao bạn mang lại [f[j:i]]và không f[j:i]?
Moberg

0

đây là một câu trả lời đơn giản

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:]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.