Python: оптимальный поиск подстроки в списке строк

у меня есть конкретная проблема, когда я хочу искать много подстрок в списке многих строк. Ниже суть того, что я пытаюсь сделать:

listStrings = [ACDE, CDDE, BPLL, ... ]

listSubstrings = [ACD, BPI, KLJ, ...]

приведенные выше записи являются лишь примерами. len(listStrings)- ~ 60,000, len(listSubstrings) - ~50,000-300,000, а len (listStrings[i]) - от 10 до 30,000.

моя текущая попытка Python:

for i in listSubstrings:
   for j in listStrings:
       if i in j:
          w.write(i+j)

или что-то в этом роде. Хотя это работает для моей задачи, это ужасно медленно, используя одно ядро и занимая порядка 40 минут для выполнения задачи. Есть ли способ ускорить это?

Я не верю, что я могу сделать дикт из listStrings:listSubstrings, потому что есть возможность дублировать записи, которые должны храниться на обоих концах (хотя я могу попробовать это, если я могу найти способ добавить уникальный тег к каждому, так как дикты намного быстрее). Аналогично, я не думаю, что могу предварительно вычислить возможные подстроки. Я даже не знайте, если поиск ключей dict быстрее, чем поиск списка (так как dict.get() собирается дать конкретный вход, а не искать суб-входы). Является ли поиск списков в памяти настолько медленным относительно говоря?

5 ответов


может быть, вы можете попробовать откусить один из двух списков (самый большой ? хотя интуитивно я бы вырезал listStrings) в меньших, затем используйте threading для запуска этих поисков параллельно (Pool класс multiprocessing предлагает удобный способ сделать это) ? У меня было значительное ускорение, используя что-то вроде :

from multiprocessing import Pool
from itertools import chain, islice

# The function to be run in parallel :
def my_func(strings):
    return [j+i for i in strings for j in listSubstrings if i.find(j)>-1]

# A small recipe from itertools to chunk an iterable :
def chunk(it, size):
    it = iter(it)
    return iter(lambda: tuple(islice(it, size)), ())

# Generating some fake & random value :
from random import randint
listStrings = \
    [''.join([chr(randint(65, 90)) for i in range(randint(1, 500))]) for j in range(10000)]
listSubstrings = \
    [''.join([chr(randint(65, 90)) for i in range(randint(1, 100))]) for j in range(1000)]

# You have to prepare the searches to be performed:
prep = [strings for strings in chunk(listStrings, round(len(listStrings) / 8))]
with Pool(4) as mp_pool:
    # multiprocessing.map is a parallel version of map()
    res = mp_pool.map(my_func, prep)
# The `res` variable is a list of list, so now you concatenate them
# in order to have a flat result list
result = list(chain.from_iterable(res))

тогда вы могли бы написать все result переменная (вместо того, чтобы писать ее строками):

with open('result_file', 'w') as f:
    f.write('\n'.join(result))

изменить 01/05/18: сгладить результат с помощью itertools.chain.from_iterable вместо уродливого обходного пути с помощью map побочные эффекты, следуя совету ShadowRanger.


для того, что вы пытаетесь (поиск фиксированного набора целой связки строк в целой связке других строк), распараллеливание и незначительные настройки не помогут. Вам нужны алгоритмические улучшения.

для начала, я бы посоветовал использовать алгоритм сопоставления строк Aho-Corasick. В принципе, в обмен на некоторые предварительные вычисления для создания объекта matcher из вашего набора фиксированных строк вы можете сканировать другую строку для все of эти фиксированные струны сразу, за один проход.

Итак, вместо сканирования 60K строк 50K + раз каждый (три миллиарда сканирований?!?), вы можете сканировать их каждый раз с немного более высокой стоимостью, чем обычное одно сканирование, и получить все хиты.

лучше всего то, что вы не пишете его сами. PyPI (индекс пакета Python) уже имеет pyahocorasick пакет написан для вас. Так что попробуйте.

пример использовать:

import ahocorasick

listStrings = [ACDE, CDDE, BPLL, ...]
listSubstrings = [ACD, BPI, KLJ, ...]

auto = ahocorasick.Automaton()
for substr in listSubstrings:
    auto.add_word(substr, substr)
auto.make_automaton()

...

for astr in listStrings:
    for end_ind, found in auto.iter(astr):
        w.write(found+astr)

это write несколько раз, если подстрока ("игла") найдена в строке поиска ("стог сена") более одного раза. Вы можете изменить цикл, чтобы сделать его только write при первом попадании для данной иглы в данном стоге сена с помощью set для дедупликации:

for astr in listStrings:
    seen = set()
    for end_ind, found in auto.iter(astr):
        if found not in seen:
            seen.add(found)
            w.write(found+astr)

вы можете дополнительно настроить это, чтобы вывести иглы для данного стога сена в том же порядке, в котором они появились в listSubstrings (и uniquifying пока вы на нем) путем хранить индекс слова as или с их значениями, чтобы вы могли сортировать хиты (предположительно, небольшие числа, поэтому сортировка накладных расходов тривиальна):

from future_builtins import map  # Only on Py2, for more efficient generator based map
from itertools import groupby
from operator import itemgetter

auto = ahocorasick.Automaton()
for i, substr in enumerate(listSubstrings):
    # Store index and substr so we can recover original ordering
    auto.add_word(substr, (i, substr))
auto.make_automaton()

...

for astr in listStrings:
    # Gets all hits, sorting by the index in listSubstrings, so we output hits
    # in the same order we theoretically searched for them
    allfound = sorted(map(itemgetter(1), auto.iter(astr)))
    # Using groupby dedups already sorted inputs cheaply; the map throws away
    # the index since we don't need it
    for found, _ in groupby(map(itemgetter(1), allfound)):
        w.write(found+astr)

для сравнения производительности я использовал вариант ответа mgc, который, скорее всего, будет содержать совпадения, а также увеличение стогов сена. Во-первых, код установки:

>>> from random import choice, randint
>>> from string import ascii_uppercase as uppercase
>>> # 5000 haystacks, each 1000-5000 characters long
>>> listStrings = [''.join([choice(uppercase) for i in range(randint(1000, 5000))]) for j in range(5000)]
>>> # ~1000 needles (might be slightly less for dups), each 3-12 characters long
>>> listSubstrings = tuple({''.join([choice(uppercase) for i in range(randint(3, 12))]) for j in range(1000)})
>>> auto = ahocorasick.Automaton()
>>> for needle in listSubstrings:
...     auto.add_word(needle, needle)
...
>>> auto.make_automaton()

и теперь, чтобы проверить его (используя ipython %timeit магия для microbenchmarks):

>>> sum(needle in haystack for haystack in listStrings for needle in listSubstrings)
80279  # Will differ depending on random seed
>>> sum(len(set(map(itemgetter(1), auto.iter(haystack)))) for haystack in listStrings)
80279  # Same behavior after uniquifying results
>>> %timeit -r5 sum(needle in haystack for haystack in listStrings for needle in listSubstrings)
1 loops, best of 5: 9.79 s per loop
>>> %timeit -r5 sum(len(set(map(itemgetter(1), auto.iter(haystack)))) for haystack in listStrings)
1 loops, best of 5: 460 ms per loop

так для проверки для ~ 1000 небольших строк в каждом из 5000 строки умеренного размера,pyahocorasick бьет индивидуальные тесты членства в ~21x раз на моей машине. Он хорошо масштабируется, как размер listSubstrings также увеличивается; когда я инициализировал его таким же образом, но с 10 000 мелкими строками вместо 1000, общее время, необходимое, увеличилось с ~460 мс до ~852 МС, множитель времени 1.85 x для выполнения 10x столько логических поисков.

для записи время создания автоматов тривиально в таком контексте. Вы платите один раз вперед, а не один раз на стог сена, и тестирование показывает, что строковый автомат ~1000 занял ~1,4 мс для сборки и занял ~277 КБ памяти (выше и за пределами самих строк); строковый автомат ~10000 занял ~21 мс для сборки и занял ~2,45 МБ памяти.


все ли ваши подстроки одинаковой длины? В вашем примере используются 3-буквенные подстроки. В этом случае вы можете создать dict с 3-буквенными подстроками в качестве ключей к списку строк:

index = {}
for string in listStrings:
    for i in range(len(string)-2):
        substring = string[i:i+3]
        index_strings = index.get(substring, [])
        index_strings.append(string)
        index[substring] = index_strings

for substring in listSubstrings:
    index_strings = index.get(substring, [])
    for string in index_strings:
        w.write(substring+string)

вы можете значительно ускорить внутренний цикл, объединив listString в одну длинную строку (или прочитать строки из файла, не разделяя его на разрывы строк).

with open('./testStrings.txt') as f:
    longString = f.read()               # string with seqs separated by \n

with open('./testSubstrings.txt') as f:
    listSubstrings = list(f)

def search(longString, listSubstrings):
    for n, substring in enumerate(listSubstrings):
        offset = longString.find(substring)
        while offset >= 0:
            yield (substring, offset)
            offset = longString.find(substring, offset + 1)

matches = list(search(longString, listSubstrings))

смещения могут быть сопоставлены beck со строковым индексом.

from bisect import bisect_left
breaks = [n for n,c in enumerate(longString) if c=='\n']

for substring, offset in matches:
    stringindex = bisect_left(breaks, offset)

мой тест показывает скорость 7x по сравнению с вложенными циклами for (11 сек против 77 сек).


вы можете получить некоторую скорость, используя встроенные функции списка.

for i in listSubstrings:
   w.write(list(map(lambda j: i + j, list(lambda j: i in j,listStrings))))

из анализа сложности времени выполнения кажется, что ваш худший случай будет N^2 сравнения, так как вам нужно пройти через каждый список с учетом вашей текущей структуры проблемы. Еще одна проблема, о которой вам нужно беспокоиться,-это потребление памяти, поскольку при больших масштабах больше памяти обычно является бутылочным горлышком.

Как вы сказали, Вы можете индексировать список строк. Есть ли шаблон список подстрок или список строк, которые мы можем знать? Например, в вашем примере мы могли бы индексировать, какие строки имеют какие символы в алфавите {"A": ["ABC", "BAW", "CMAI"]...}и, таким образом, нам не нужно будет проходить через список строк каждый раз для каждого списка элемента подстроки.