Pull to refresh

Поиск по PDF, DOC, DOCX файлам с помощью Sphinx и PHP

Reading time 5 min
Views 27K
Доброе время суток.

Хочу поделиться своим опытом написания скрипта на PHP для поиска по файлам форматов PDF, DOC и DOCX, используя Sphinx. Все ниже изложенное написано для людей, которые уже имели опыт работы с связкой Sphinx и PHP. Если нет, то в свое время мне очень помогла статья Мартина Стрейчера «Создание собственной поисковой системы с помощью PHP», опубликованная на сайте IBM.

Совсем недавно на одном из проектов, который является веб-интерфейсом для базы пользователей, я получил задание организовать поиск по документам. Проект был написан до меня и уже использовал Sphinx.

Задача вроде тривиальная, но погуглив немного, я не смог найти внятную инструкцию с конкретным примером поиска по файлам форматов PDF, DOC и DOCX, что и стало причиной написания этой статьи.

Sphinx может обрабатывать текстовые файлы только формата XML. Для обработки данных в этом формате используется драйвер xmlpipe или xmlpipe2. То есть, для индексации информации в файлах формата PDF, DOC и DOCX, ее нужно отдать sphinx-у в формате XML.

Для этого надо написать скрипт, который будет ходить файлам, считывать данные и отдавать их в нужном формате.
Ниже приведу пример конфигурации и кода, который поможет нам с индексацией.

В конфигурационном файле sphinx.conf указываем следующее:
# Определяем источник данных
source SourceParseXml
{
	# Используем xmlpipe2, как источник данных
	type = xmlpipe2 
	# Команда, которые должна быть выполнены, чтобы получить XML. Файл xml-parser.php генерирует XML
	xmlpipe_command = /path/to/php /path/to/xml-parser.php 
}
# Определяем индекс
index IndexParseXml
{
    # Указываем, какой источник использовать
    source = SourceParseXml
    # Указываем путь где хранить индекс
    path = /path/where/to/store/index-data
    # Минимальная длина слова для индексации
    min_word_len = 1
    # Указываем кодировку
    charset_type = utf-8
}
# Определяем индексер
indexer
{
    # Максимальный объем используемой памяти для индексации
    mem_limit = 32M
}


Содержимое файла xml-parser.php, который генерирует XML для Sphinx-a:
$allowedMimes =  array(
    'application/pdf', 
    'application/zip', // docx
    'application/vnd.ms-office', // doc созданный с помощью OpenOffice 
    'application/msword'
);

$allowedExtentions = array('pdf', 'docx', 'doc');

$pdfInfoPath = '/usr/bin/pdfinfo'; // полный путь к pdfinfo
$pdfToTextPath = '/usr/bin/pdftotext'; // полный путь к pdftotext
$catDocPath = '/usr/bin/catdoc'; // полный путь к catdoc

// загружаем библиотеку Zеnd-а для считывания файлов формата docx
define('FILE_PATH', realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR);
if (strpos(get_include_path(), 'Zend.phar.gz') === false) {
    ini_set('include_path', ini_get('include_path') . ':phar\://' . FILE_PATH . 'Zend.phar.gz');
}
$phar = new Phar(FILE_PATH . 'Zend.phar.gz', 0, 'Zend.phar.gz');
if (isset($phar['Zend/Search/Lucene/Document/Docx.php'])) {
    require_once($phar['Zend/Search/Lucene/Document/Docx.php']);
} else {
    echo 'ERROR: can\'t load "Zend/Search/Lucene/Document/Docx.php" !' . PHP_EOL;
    die;
}

// Начинаем формирование XML
$xmlWriter = new xmlWriter();
$xmlWriter->openMemory();
$xmlWriter->setIndent(true);
$xmlWriter->startDocument('1.0', 'UTF-8');

$xmlWriter->startElement('sphinx:docset');

$xmlWriter->startElement('sphinx:schema');
$xmlWriter->startElement('sphinx:field');
$xmlWriter->writeAttribute('name', 'content');
$xmlWriter->endElement(); // field
$xmlWriter->endElement(); // schema

/*
 Предположим что файлы лежат в папке files, 
 в которой находятся другие папки с названиями, 
 которые соответсвуют id пользователя.
 Например: files/01/file.pdf
*/
// Запускаем цикл по папке files
foreach (new DirectoryIterator(dirname(__FILE__) . '/files') as $folder) {
    // Проверяем, является ли выбраные обьект папкой
    if (!$folder->isDir() || $folder->isDot()) {
        continue;
    }
    // Запускаем цикл по папке в которой теоретически должны находится файлы
    foreach (new DirectoryIterator($folder->getPathname()) as $file) {
        // Проверяем выбранный объект является ли файлом разрешенного типа 
        if ($file->isDir() || !in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), $allowedExtentions) 
            || !in_array(mime_content_type($file->getPathname()), $allowedMimes)) {
            continue;
        }
        
        $text = '';
        $filePath = $file->getPathname();
        $filePathEscape = escapeshellarg($filePath);
        
        try {
            switch (mime_content_type($filePath)) {
                case 'application/pdf': {
                    $pdfInfo = array();
                    $key = '';
                    $val = '';
                    
                    // Считываем информацию о выбранном PDF файле
                    foreach (explode("\n", shell_exec(escapeshellcmd($pdfInfoPath . ' ' . $filePathEscape))) as $str) {
                        list($key, $val) = count(explode(':', $str)) == 2 ? explode(':', $str) : array('', '');
                        if (trim($key) && trim($val)) {
                            $pdfInfo[trim($key)] = trim($val);
                        }
                    }
                    
                    // Проверяем на ошибки
                    if (empty($pdfInfo) || (isset($pdfInfo['Error']) && $pdfInfo['Error'])) {
                        continue;
                    }
                    
                    // С помощью pdftotext считываем содержимое файла
                    $text = shell_exec(escapeshellcmd($pdfToTextPath . ' -nopgbrk ' . $filePathEscape . ' -'));
                    break;
                }
                case 'application/zip' : {
                    $file = Zend_Search_Lucene_Document_Docx::loadDocxFile($filePath);
                    // С помощью Zend_Search_Lucene_Document_Docx считываем содержимое файла
                    $text = $file->getFieldValue('body');
                    break;
                }
                
                case ('application/vnd.ms-office' || 'application/msword'): {
                    // С помощью catdoc считываем содержимое файла
                    $text = shell_exec(escapeshellcmd($catDocPath . ' ' . $filePathEscape));
                    break;
                }
            }

            if (empty($text)) {
                continue;
            }
            $text = strip_tags($text);
            $givenEncode = mb_detect_encoding($text);
            // Текст должен быть в кодировке UTF-8
            $text = $givenEncode ? iconv($givenEncode, 'UTF-8', $text) : mb_convert_encoding($text, 'UTF-8');
        
        } catch (Exception $e) {
            echo $e->getMessage() . PHP_EOL;
            continue;
        }
        
        $xmlWriter->startElement('sphinx:document');
        // $folder->getBasename() - вернет название папки в которой хранятся наши файлы. 
        // Название папки является идентификатором пользователя
        $xmlWriter->writeAttribute('id', $folder->getBasename()); 
        $xmlWriter->startElement('content');

        $xmlWriter->writeCData($text);

        $xmlWriter->endElement(); // content
        $xmlWriter->endElement(); // field
    }
}
$xmlWriter->endElement();
$xml = $xmlWriter->outputMemory();

$tidy = tidy_repair_string($xml, array( 
    'output-xml' => true, 
    'input-xml'  => true 
), 'utf8');
echo $tidy;

Приведенный выше код является модифицированной версией скрипта с официального сайта Sphinx-a.
Для считывания текста из PDF файла используется утилита pdftotext. Для DOC файлов — catdoc. Для считывания текста из DOCX файлов используется Zend. Про Phar можно прочесть тут. Официальная документация по Sphinx, а так же пример правильного XML для индексера.

Сверху представлен скрипт, который считывает данные из файлов и отдает их в XML формате для индексации.
Теперь при поиске Sphinx будет выдавать id папки в котором найдено совпадение. Можно еще хранить имя файла. rednaxi в своем материале «Создание ознакомительного поискового движка на Sphinx + php» более подробно описал процесс работы с Sphinx и PHP.
Tags:
Hubs:
+59
Comments 4
Comments Comments 4

Articles