Как разбить строку на пробелы и сохранить смещения и длины слов
Мне нужно разбить строку на слова, но также получить начальное и конечное смещение слов. Так, например, если входная строка:
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 раза.