10/09/2006

Быстрая сортировка по релевантности PDF-документов: скрипты

Задача: с помощью поисковой машины SWISH++ искать и сортировать по каталогам pdf-документы
Решение: при помощи программы pdftotext, утилиты find, скриптовых языков Bash и Perl, а также некоторой смекалки и использования мощи линуксовой консоли, решаем проблему.

В этой статье я начал рассказ про замечательную поисковую систему SWISH++ и о том, как её приспособить под такие специфичные нужды, как поиск внутри pdf-документов. Здесь я напишу о том, как это автоматизировать ещё больше.

Вариант решения задачи
Как говорят поклонники Perl, "there is more than one way to do it". Я решил её так, если кто укажет на более лёгкий / правильный / экономичный / красивый путь - просьба отписаться в комментариях.
Итак, вкратце алгоритм таков:
  • утилитой find ищем все документы pdf и вытаскиваем из них текст утилитой pdftotext
  • индексируем найденные файлы программой index++
  • задаёмся порогом релевантности (подбираем опытным путём)
  • ищем по ключевым словам программой search++
  • найденные файлы pdf перемещаем в каталог со словами поиска
  • при необходимости повторить :-)
Ниже этот процесс описывается подробнее.

Поиск документов pdf и вытаскивание текста
Собственно, для этого просим утилиту find найти все файлы по маске *.pdf и для каждого выполнить вытаскивание текста программой pdftotext без выдачи предупреждений и без вставки символов разрыва страницы. Это делается командой:
find -name '*.pdf' -exec pdftotext -nopgbrk -q {} \;
С русским языком программа pdftotext дружит слабо, хотя и пытается что-то вытащить. Тем не менее, большинство статей на английском, так что это нам не страшно.

Построение индекса
Тут всё очень просто - просим index++ проиндексировать все текстовые файлы, начиная с текущего каталога:
index++ -e "text:*.txt" .
Точка в конце и означает, что поиск ведётся с текущего каталога.

Поиск по ключевым словам скриптом
На десерт - мой скрипт на PERL, который выполняет автоматизированный поиск и сортировку. Просьба аксакалов перловедения ногами не пинать - "не ругайте баяниста, он играет, как умеет". Цветовая разметка, сделанная в Kate, приведена для удобства чтения, скрипт подробно прокомментирован (чтобы я сам потом не забыл, что да как).

#! /usr/bin/perl
use strict;
# use warnings;

`index++ -e "text:*.txt" .`; #Создание индексированного списка текстовых файлов
в текущей директории с подробной выдачей статистики пользователю

# ### <-------- Блок создания переменных и определения порога
my $kmvThreshold = 30; #Обрубать по порогу: практика показала, что ниже
порога релевантности 30-40 документы не соответствуют критериям поиска
my $ii;
my
$iimax;
### <-------- Блок создания переменных и определения порога


# ### <-------- Блок ввода запроса и организация каталога для перемещнения
туда найденный файлов
print "Query to PDF search?", "\n"; #Выдать запрос на ввод поисковой фразы
my $kmvQuery = ; #Поисковый запрос к базе индексированных файлов
chop $kmvQuery;#обрубить символ возврата каретки

my $kmvNewDirName = $kmvQuery; #Создаём переменную для имени каталога
$kmvNewDirName =~ s/\s+//xgi; #Замена в тексте запроса пробелов для
создания каталога, в который будут перемещены найденные файлы
`mkdir $kmvNewDirName`; #Создаём новый подкаталог в текущем каталоге,
в который будут перемещены найденные файлы
### <-------- Блок ввода запроса и организация каталога для перемещения
туда найденный файлов


# ### <-------- Блок обработки запроса
my @kmvSearchResultsRaw = `search++ $kmvQuery`; #Поиск по запросу и его результаты
$iimax = scalar @kmvSearchResultsRaw;#Размер массива результатов - сколько строк
### <-------- Блок обработки запроса


### <-------- Блок принятия решения о перемещении файлов в соответствии
с релевантностью
for ($ii=0;$ii<$iimax;$ii++) #перебираем в цикле строки
{
my
$kmvCurrent = substr($kmvSearchResultsRaw[$ii],0,3);#берём первые три
символа - информация о релевантности (три - так как она может быть и 100)
if (int $kmvCurrent > 0 and $kmvCurrent > $kmvThreshold) #Если в строках
первое число и оно больше порога (может быть и не число, а количество найденных совпадений)
{
my (
$kmvResultRelev,$kmvResultHomeName,$kmvResultFilesize,$kmvResultFilename) =
(
$kmvSearchResultsRaw[$ii] =~ m/(\S+)\s(\S+)\s(\S+)\s(\S+)/xgi);
#Эта строка стоила мне половину вечера курения книги по Перл.
Для "Не Достигших Полного Просветления на Пути Perl" объясню всю эту адскую конструкцию.
Строка $kmvSearchResultsRaw[$ii] разбирается на четыре переменные (которые слева)
по следующему сценарию: (символы_не_пробела) пробел (символы_не_пробела) пробел
(символы_не_пробела) пробел (символы_не_пробела) - поиск глобальный
(не до первого совпадения), допускает пробелы и не учитывает регистра.
$kmvResultFilename =~ s/.txt/.pdf/xgi; #Выданное имя файла txt заменяется на pdf
print "Moving ", $kmvResultFilename, "\n"; #напечатать сообщение о перемещении
найденного файла
`mv $kmvResultFilename $kmvNewDirName`; #перемещение найденного файла в
новый каталог
}
}
### <-------- Блок принятия решения о перемещении файлов в соответствии
с релевантностью



Что-то вроде этого. Может быть, в скрипте есть некоторые топорные решения, но мне не до изящества кода - надо архивы разгребать.

Плоды прогресса
Проведение натурных экспериментов показало достаточно высокую точность и адекватность сортировки. Запрашивая сначала по точным критериям, а потом по всё более общим (запросы два-три слова), я разгрёб 2400 документов за полтора часа. Задача для системы усложнялась тем, что тексты были однородны по тематике. Точность (после визуального сравнения) составляет где-то 60-70%. Конечно, отобранный материал пробегается глазами по заголовкам, то есть реализуется полуавтоматический режим.
Впереди ещё оптимизация поиска, надо будет подучить систему немного, и тогда уже вволю конкурировать с Гуглом и Яндексом.

5 комментариев:

  1. Я ещё при предыдущей статье задумался о том, чтобы натравить енто дело на мои завалы, теперь вот попробую на досуге ;)

    ОтветитьУдалить
  2. Шурик, дело стоящее - точность довольно высокая, а уж как это быстро всё делается... прямо душа поёт! :-)

    ОтветитьУдалить
  3. my ($kmvResultRelev,$kmvResultHomeName,$kmvResultFilesize,$kmvResultFilename) =
    ($kmvSearchResultsRaw[$ii] =~ m/(\S+)\s(\S+)\s(\S+)\s(\S+)/xgi);
    #Эта строка стоила мне половину вечера курения половины книги по Перл.


    И все-таки... можно проще ;)

    my ($kmvResultRelev,$kmvResultHomeName,$kmvResultFilesize,$kmvResultFilename = split(' ', $kmvSearchResultsRaw[$ii]);

    ОтветитьУдалить
  4. 2 Portnov
    Огромное спасибо! Поправлю скрипт обязательно - я в перле не силён, нашёл первый рецепт в книге и применил его. Сплит искал-искал, так и не отыскал. Так что спасибо!

    ОтветитьУдалить
  5. После ввода команды
    find -name '*.pdf' -exec pdftotext -nopgbrk -q{}\;

    терминал выдаёт

    find: отсутствует аргумент у `-exec'

    Что я делаю не так?

    ОтветитьУдалить