Время прочтения: 2 мин.

Особенность задачи в большом объеме файлов и списке ключевых слов. Поэтому для поиска каждая страница открывается с помощью соответствующей этому формату библиотеки, её текст проходит препроцессинг и единственный раз разбивается на фрагменты. Длина фрагментов вычисляется так:

минимальная = (длина самого короткого ключевого слова) * (порог сходства / 100)

максимальная = (длина самого длинного ключевого слова) * (100 / порог сходства)

Где «порог сходства» — число от нуля до ста. Например, если нам достаточно сходства в 60%, а длина самой длинной фразы – 6 символов, то максимальная длина фрагмента = 6 * 100 / 60 = 10 символов.

Вот так происходит разбиение на фрагменты:

for len_chunk in range(min_lent, max_len + 1):
for start, end in ((i, i + len_chunk) for i in range(0, len_text, len_chunk)):
chunk = text[start: end]

После разбиения на фрагменты в каждом из них ищутся все ключевые слова по циклу. При наличии хотя бы двух фраз для поиска описанный вариант работает быстрее, чем разбиение текста для каждой фразы. Результаты сравнения по ~800мб такие: при поиске четырёх фраз поиск занимает 14 секунд для реализованного варианта, 20 для альтернативного, при поиске двух фраз реализованный справляется за 6 секунд, второй отстаёт на полсекунды. При увеличении количества фраз и файлов разрыв увеличивается.

Но некоторые куски текста попадают в итоговый ответ несколько раз. Например, если в строке «это строка для примера» искать слово «строка», то можно получить в качестве результата сразу несколько фрагментов – «о строка», « строка», «строка», «трока», «строк» и т.д. Эта проблема решается легко – нужно сохранять не сами фрагменты, а индексы их начала и конца в тексте. При добавлении нового необходимо сначала проверить пересечение интервалов с уже существующими, и если для одного ключевого слова пересекаются два интервала, то необходимо взять их объединение. И даже этот вариант работает быстрее разбиения текста несколько раз.

В основном я использовал два способа поиска из rapidfuzz – fuzz.ratio и fuzz.partial_ratio. Они отличаются тем, что вторая не учитывает порядок расположения символов в строке

>>> from rapidfuzz import fuzz
>>> fuzz.partial_ratio('подстрока', 'это не подстрока')
100.0
>>> fuzz.ratio('подстрока', 'это не подстрока')
72.0

Работа с docx изначально не предполагалась, но добавить эту возможность оказалось очень просто – замечательная библиотека docx2pdf позволяет перевести их в pdf в дальнейшем так и обрабатывать, при этом перенос никаких артефактов не создаёт – текст не портится, а номера страниц совпадают.

Для формирования итогового результата в удобной форме, используется pd.ExcelWriter, он позволяет записывать pandas датафреймы на разные страницы xlsx файла.