Как разбить строку на пробелы и сохранить смещения и длины слов

Мне нужно разбить строку на слова, но также получить начальное и конечное смещение слов. Так, например, если входная строка:

input_string = "ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"

Я хочу получить:

[('ONE', 0, 2), ('ONE', 5, 7), ('ONE', 9, 11), ('TWO', 17, 19), ('TWO', 21, 23),
 ('ONE', 25, 27), ('TWO', 29, 31), ('TWO', 33, 35), ('THREE', 37, 41)]

У меня есть рабочий код, который делает это с помощью input_string.split и вызывает .index, но он медленный. Я попытался закодировать его, выполнив русскую итерацию по строке, но это было медленнее. У кого-нибудь есть быстрый алгоритм для этого?

Вот мои две версии:

def using_split(line):
    words = line.split()
    offsets = []
    running_offset = 0
    for word in words:
        word_offset = line.index(word, running_offset)
        word_len = len(word)
        running_offset = word_offset + word_len
        offsets.append((word, word_offset, running_offset - 1))

    return offsets

def manual_iteration(line):
    start = 0
    offsets = []
    word = ''
    for off, char in enumerate(line + ' '):
        if char in ' \t\r\n':
            if off > start:
                offsets.append((word, start, off - 1))
            start = off + 1
            word = ''
        else:
            word += char

    return offsets

Используя timeit, "using_split" является самым быстрым, за которым следует "manual_iteration", то самым медленным до сих пор является использование re.finditer, как предлагается ниже.

Ответ 1

Ниже выполняется несколько быстрее - это экономит около 30%. Все, что я сделал, это заранее определить функции:

def using_split2(line, _len=len):
    words = line.split()
    index = line.index
    offsets = []
    append = offsets.append
    running_offset = 0
    for word in words:
        word_offset = index(word, running_offset)
        word_len = _len(word)
        running_offset = word_offset + word_len
        append((word, word_offset, running_offset - 1))
    return offsets

Ответ 2

Это сделает следующее:

import re
s = 'ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE'
ret = [(m.group(0), m.start(), m.end() - 1) for m in re.finditer(r'\S+', s)]
print(ret)

Это дает:

[('ONE', 0, 2), ('ONE', 5, 7), ('ONE', 9, 11), ('TWO', 17, 19), ('TWO', 21, 23),
 ('ONE', 25, 27), ('TWO', 29, 31), ('TWO', 33, 35), ('THREE', 37, 41)]

Ответ 3

def split_span(s):
    for match in re.finditer(r"\S+", s):
        span = match.span()
        yield match.group(0), span[0], span[1] - 1

Ответ 4

Я смог получить около 35% ускорения за несколько минут, просто обманув: я преобразовал вашу функцию use_split() в модуль python на основе C, используя cython. Это было первым оправданием, которое мне когда-либо приходилось пробовать cython, и я вижу, что это довольно легко и полезно - см. Ниже.

Пойнт на C был последним средством: сначала я провел несколько часов, пытаясь найти более быстрый алгоритм, чем ваша версия use_split(). Дело в том, что родной python str.split() на удивление быстрый, быстрее, чем все, что я пытался использовать numpy или re, например. Поэтому, даже если вы дважды сканируете строку, str.split() достаточно быстр, что, похоже, это не имеет значения, по крайней мере, не для этих конкретных тестовых данных.

Чтобы использовать cython, я помещаю ваш синтаксический анализатор в файл с именем parser.pyx:

===================== parser.pyx ==============================
def using_split(line):
    words = line.split()
    offsets = []
    running_offset = 0
    for word in words:
        word_offset = line.index(word, running_offset)
        word_len = len(word)
        running_offset = word_offset + word_len
        offsets.append((word, word_offset, running_offset - 1))
    return offsets
===============================================================

Затем я запустил это для установки cython (при условии, что ящик debian-ish Linux):

sudo apt-get install cython

Затем я вызывал синтаксический анализатор из этого питона script:

================== using_cython.py ============================

#!/usr/bin/python

import pyximport; pyximport.install()
import parser

input_string = "ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"

def parse():
    return parser.using_split(input_string)

===============================================================

Чтобы проверить, я запустил это:

python -m timeit "import using_cython; using_cython.parse();"

На моей машине ваша функция pure-python using_split() усредняет 8.5 usec, в то время как моя версия cython в среднем составляет около 5,5 мкс.

Подробнее на http://docs.cython.org/src/userguide/source_files_and_compilation.html

Ответ 5

Предупреждение. Скорость этого решения ограничена скоростью света:

def get_word_context(input_string):
    start = 0
    for word in input_string.split():
        c = word[0] #first character
        start = input_string.find(c,start)
        end = start + len(word) - 1
        yield (word,start,end)
        start = end + 2

print list(get_word_context("ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"))
   [('ONE', 0, 2), ('ONE', 5, 7), ('ONE', 9, 11), ('TWO', 17, 19), ('TWO', 21, 23), ('ONE', 25, 27), ('TWO', 29, 31), ('TWO', 33, 35), ('THREE', 37, 41)]  

Ответ 6

Следующие идеи могут привести к ускорению:

  • Используйте deque, а не список для хранения смещений и преобразования в список только при возврате. Добавления Deque не приводят к перемещению памяти, как это делает список.
  • Если слова, как известно, короче некоторой длины, укажите это в функции индекса.
  • Определите свои функции в локальном словаре.

Примечание. Я не тестировал их, но вот пример

from collections import deque

def using_split(line):
    MAX_WORD_LENGTH = 10
    line_index = line.index

    words = line.split()

    offsets = deque()
    offsets_append = offsets.append

    running_offset = 0

    for word in words:
        word_offset = line_index(word, running_offset, running_offset+MAX_WORD_LENGTH)
        running_offset = word_offset + len(word)
        offsets_append((word, word_offset, running_offset - 1))

    return list(offsets)

Ответ 7

Здесь у вас есть c-ориентированный подход, который выполняется только один раз над полной строкой. Вы также можете определить свои собственные разделители. Протестировано и работает, но, вероятно, может быть более чистым.

def mySplit(myString, mySeperators):
    w = []
    o = 0
    iW = False
    word = [None, None,None]
    for i,c in enumerate(myString):
        if not c in mySeperators:
            if not iW:
                word[1]=i
                iW = True
        if iW == True and c in mySeperators:
            word[2]=i-1
            word[0] = myString[word[1]:i]
            w.append(tuple(word))
            word=[None,None,None]
            iW = False
    return w

mySeperators = [" ", "\t"]
myString = "ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"
splitted = mySplit(myString, mySeperators)
print splitted

Ответ 8

Это работает довольно быстро:

tuple_list = [(match.group(), match.start(), match.end()) for match in re.compile("\S+").finditer(input_string)]

Ответ 9

Вот несколько идей, которые вы могли бы прочесть, чтобы узнать, достаточно ли они:

input_string = "".join([" ","ONE  ONE ONE   \t TWO TWO ONE TWO TWO THREE"," "])

#pre processing
from itertools import chain
stuff = list(chain(*zip(range(len(input_string)),range(len(input_string)))))
print stuff
stuff = iter(stuff)
next(stuff)

#calculate
switches = (i for i in range(0,len(input_string)-1) if (input_string[next(stuff)] in " \t\r\n") ^ (input_string[next(stuff)] in " \t\r\n"))
print [(word,next(switches),next(switches)-1) for word in input_string.split()]


#pre processing
from itertools import chain
stuff = list(chain(*zip(range(len(input_string)),range(len(input_string)))))
print stuff
stuff = iter(stuff)
next(stuff)


#calculate
switches = (i for i in range(0,len(input_string)-1) if (input_string[next(stuff)] in " \t\r\n") ^ (input_string[next(stuff)] in " \t\r\n"))
print [(input_string[i:j+1],i,j-1) for i,j in zip(switches,switches)]

Ответ 10

Мне кажется, что цикл python - это медленная операция здесь, поэтому я начал работать с растровыми изображениями, я получил это далеко и все еще быстро, но я не могу понять, нет ли свободного цикла, чтобы получить индексы запуска/остановки это:

import string
table = "".join([chr(i).isspace() and "0" or "1" for i in range(256)])
def indexed6(line):
    binline = string.translate(line, table)
    return int(binline, 2) ^ int(binline+"0", 2)

Возвращаемое целое число имеет биты, установленные для каждой начальной позиции, и каждая позиция остановки + 1.

P.S. zip() относительно медленный: достаточно быстрый, чтобы его можно было использовать один раз, слишком медленно, чтобы его можно было использовать 3 раза.