Авг 282013
 

Изучаем PHP. Пишем parser похожих запросов Yandex.

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

Здесь на выручку нам так называемый «скрипт – парсер». В задачу которого входит получение и проверка информации, поиск нужных фрагментов и исключение «мусора».

Рассмотрим на примере так называемый «parser – spider» (парсер – паук), в задачу которого будет входить сбор информации со страниц yandex для поиска похожих ключевых слов, которые мы будем использовать в раскрутке нашего проекта. И напишем данный парсер на PHP

Как же нам может помочь parser?
Мы получаем список сходных фраз, которыми пользуются посетители яндекса при поиске нашей тематики, и в дальнейшем сможем ориентироваться на них при написании материалов на нашем сайте, оцениваем спрос к тематике нашего сайта. А так же это даст нам возможность определить поисковые запросы с меньшей конкурентной борьбой сходной тематики, чтобы выйти по ним в ТОП поисковой системы Yandex. Как следствие, благодаря этой информации мы расширим аудиторию нашего проекта.

А откуда мы можем получить информацию по похожим поисковым запросам Yandex?
Дело в том, что при поиске Яндекс подсказывает сам, отображая блок внизу страницы: Вместе с «[фраза для поиска]» ищут — несколько подобных поисковых запросов.

Вверху в левой части каждой страницы указывает, сколько нашлось вариантов, например:
создать сайт — Нашлось 104 млн ответов

Итак, какие же задачи поставим перед скриптом – парсером?

Возможность рекурсивного парсинга похожих поисковых запросов с определением уровня вложенности
Если не найдено ни одного похожего запроса по указанной фразе (а такое тоже бывает), попробуем просмотреть запросы по её сокращению:

Например, если ввести фразу Создать Сайт Харьков, то похожих поисковых запросов не будет в выдаче поисковика, но если ввести: создать сайт – они появятся. Ограничимся урезанием фразы на одно слово вконце.

Обходить парсер будет не только одну фразу, а займётся обработкой целого списка из фраз (передадим массивом)
Сохранение найденных поисковых фраз и их встречаемостью в файл
Нюансы:

Поставим задержку на опрос поисковой системы (не меньше 2 секунд, а то и до 30 секунд в случайном порядке) на каждый поисковый запрос. Мы же не хотим, чтобы Яша нас «отругал» за плохое поведение?

<?php
function getURIContent($url){
        $tuCurl                         = curl_init();
        $tuData                         = '';
        if($tuCurl && is_resource($tuCurl)){
                $opts                   = array(
                CURLOPT_URL             => $url,
                CURLOPT_HTTPGET         => 1,
                CURLOPT_HEADER          => 0,
                CURLOPT_RETURNTRANSFER  => 1,
                CURLOPT_FOLLOWLOCATION  => 1,
                CURLOPT_BINARYTRANSFER  => 1,
                CURLOPT_AUTOREFERER     => 1,
                CURLOPT_CONNECTTIMEOUT  => 90,
                CURLOPT_USERAGENT       => $_SERVER['HTTP_USER_AGENT'],
                CURLOPT_COOKIEJAR       => dirname(__FILE__)."/cookie.txt",
                CURLOPT_COOKIEFILE      => dirname(__FILE__)."/cookie.txt",
                CURLOPT_REFERER         => $url
                                         );
        foreach($opts as $key=>$value){
          curl_setopt($tuCurl,$key,$value);
        }
        $tuData   = curl_exec($tuCurl);
        curl_close($tuCurl);
        }
        return $tuData;
}
function parseRecursive($question,$max_depth,$first = true){
    global $time_wait;
    $time_wait  = $time_wait < 2 ? 2 : $time_wait;
    $rand       = mt_rand($time_wait, $time_wait + 30);
    sleep($rand);
    $question   = urlencode($question);
    $where      = 'http://yandex.ua/yandsearch?text='.$question;
    $content    = getURIContent($where);
    $found      = false;
    if(!empty($content)){
        $how_many= array();
        preg_match_all('~<strong[^>]*?class="b-head-logo__text"[^>]*?>(.*?)</strong>~is',$content,$how_many);
        $numbers= '';
        if(is_array($how_many)
                     && isset($how_many[1][0])
                           && !empty($how_many[1][0])){
            $numbers   = trim($how_many[1][0]);
            $numbers   = preg_replace("~<br[^>]*?>~is",' ', $numbers);
            $numbers   = str_ireplace("&nbsp;",' ', $numbers);
            $numbers   = str_ireplace("\r\n",' ', $numbers);
            $numbers   = str_ireplace("\r",' ', $numbers);
            $numbers   = str_ireplace("\n",' ', $numbers);
            $numbers   = str_ireplace("\t",' ', $numbers);
            $numbers   = str_ireplace("\p",' ', $numbers);
            $numbers   = str_ireplace("\b",' ', $numbers);
            $numbers   = html_entity_decode($numbers,ENT_QUOTES,'UTF-8');
            $numbers   = strip_tags($numbers);
        }
        if(!empty($numbers)){
            $numbers = urldecode($question) . ' - ' . $numbers. "\n";
            $fp = fopen(WHERE_TO_SAVE,'a+');
            if($fp && is_resource($fp)){
                echo 'ADDING '.$numbers. "<br />\n";
                flock($fp,LOCK_EX);
                fwrite($fp,$numbers);
                flock($fp,LOCK_UN);
                fclose($fp);
            }
        }
        $related= array();
        $links  = array();
        preg_match_all('~<table[^>]*?class="b-related__table"[^>]*?>(.*?)</table>~is',$content,$related);
        /* <a[^>]*?href=("|\')([^"\']*?)(\1)[^>]*?>(.*?)</a> */
        if(is_array($related[1]) &&
                    isset($related[1][0])
                        && $max_depth){
            --$max_depth;
            preg_match_all('~<a[^>]*?href=("|\')([^"\']*?)(\1)[^>]*?>(.*?)</a>~is',$related[1][0],$links);
            if(is_array($links)
                           && isset($links[2])
                                && isset($links[4])
                                     && sizeof($links[2])
                                && sizeof($links[4])
                                     && $max_depth){
                $sizeof     = sizeof($links[4]);
                for($i = 0; $i < $sizeof; $i ++){
                    $text   = '';
                    $text   = trim($links[4][$i]);
                    $text   = preg_replace("~<br[^>]*?>~is",' ', $text);
                    $text   = str_ireplace("&nbsp;",' ', $text);
                    $text   = str_ireplace("\r\n",' ', $text);
                    $text   = str_ireplace("\r",' ', $text);
                    $text   = str_ireplace("\n",' ', $text);
                    $text   = str_ireplace("\t",' ', $text);
                    $text   = str_ireplace("\p",' ', $text);
                    $text   = str_ireplace("\b",' ', $text);
                    $text   = html_entity_decode($text,ENT_QUOTES,'UTF-8');
                    $text   = strip_tags($text);
                    if(!empty($links[2][$i]) && !empty($text)){
parseRecursive($text,$max_depth,false);
                    }
                }
            }
        } elseif (is_array($related[1]) && $max_depth){
            $question    = urldecode($question);
            $words= array();
            $words= explode(' ',$question);
            $sizeof= sizeof($words);
            $words= array_map('trim',$words);
            --$sizeof;
            $words= array_slice($words,0,$sizeof);
            $question    = join(' ',$words);
            if(strlen($question) > 4){
                --$max_depth;
                if($max_depth){
                parseRecursive($question,$max_depth,false);
                }
            }
        }
    }
}
ini_set('max_execution_time',999999);
ini_set('max_input_time',999999);
$max_depth      = 2;
$time_wait      = 2;
$questions      = array('создать сайт',
                          'сайт на joomla',
                          'создать сайт Харьков',
                          'сайт на wordpress');
define('WHERE_TO_SAVE',dirname(__FILE__).'/prases.txt');
if(!is_file(WHERE_TO_SAVE)){
    $fp  = fopen(WHERE_TO_SAVE,'w+');
    if($fp && is_resource($fp)){
        fclose($fp);
    }
    if(is_file(WHERE_TO_SAVE) && !is_writable(WHERE_TO_SAVE)){
        chmod(WHERE_TO_SAVE,0777);
    }
}
if(is_file(WHERE_TO_SAVE)){
    $fp  = fopen(WHERE_TO_SAVE,'w+');
    if($fp && is_resource($fp)){
        fclose($fp);
    }
}
if(is_file(WHERE_TO_SAVE) && is_writable(WHERE_TO_SAVE)){
    $sizeof     = sizeof($questions);
    for($i = 0; $i < $sizeof; $i ++){
        parseRecursive($questions[$i],$max_depth);
    };
};
?>

Итак, что же здесь происходит:

Для начала с помощью ini_set установим побольше время выполнения для скрипта.

В переменной $max_depth укажем, сколько вложенных уровней «обходить» (на какую глубину «погружаться») для сбора похожих фраз.

Например: $max_depth = 2; — находим похожие поисковые фразы, переходим по каждой из них, и собираем результаты по похожим уже на них поисковым фразам.

Совет: не делайте слишком большим уровень вложенности. Иначе сильно отклонитесь от первоначальной тематической фразы.

Переменной $time_wait указываем, сколько секунд ждать до следующего запроса к поисковику.

Обратите внимание – 2 секунды – это минимальное значение. Иначе сайт выдаст вам капчу или наложит бан по ай-пи адресу.

В массив $questions = array(‘создать сайт’,’сайт на joomla’,’создать сайт Харьков’,’сайт на wordpress’); — списком строк через запятую добавляем запросы для обработки parser ом.

Далее, определяем, существует ли файл prases.txt – в который мы и добавим найденный результат. Если не существует, создаём его и делаем доступным на запись.

После этого обходим массив $questions и передаём каждый запрос функции parseRecursive вместе с уровнем вложения.

(void) function parseRecursive($question,$max_depth,$first = true): принимает одну поисковую фразу (string)$questions[$i] и уровень вложенности поиска $max_depth, а так же неявный флаг (bool) $first, для определения, в какой раз подряд вызывается функция (для чего, рассмотрим ниже).

В функции мы кодируем для возможности передачи в виде запроса нашу ключевую фразу с помощью urlencode и получаем содержимое запроса в функции getURIContent, основанной на curl. После чего регулярным выражением ~]*?class=»b-head-logo__text»[^>]*?>(.*?)~is узнаём, сколько раз встречается данный поисковой запрос.

Очищаем полученный запрос от html и спец символов. Помещаем выражение и количество найденных страниц по этой поисковой фразе в файл prases.txt.

Исследуем содержимое полученной страницы на предмет содержания «похожих поисковых запросов» ~]*?class=»b-related__table»[^>]*?>(.*?)

~is.

Если похожие поисковые фразы найдены, извлекаем ссылки на них и их анкоры: ~]*?href=(«|’)([^»‘]*?)(\1)[^>]*?>(.*?)~is

При этом уменьшая на единицу наш уровень вложенности —$max_depth

Если уровень вложенности позволяет (отличен от нуля), передаём анкоры (текст, заключённый в тег a) рекурсивно в функцию parseRecursive. Обрабатываем следующий уровень.

Если же на странице не было найдено по указанной поисковой фразе ни одного результата, и вызов функции parseRecursive был осуществлён впервые (неявный флаг $first со значением true) то проверяем, содержит ли слова поисковый запрос? (Разбиваем по пробельному символу фразу на слова). Исключаем последнее слово, и передаём полученное словосочетание заново функции parseRecursive.

Примечание автора: для получения содержимого страниц по указанным поисковым запросам используется функция getURIContent, которая принимает адрес страницы и возвращает её содержимое.

Её работа основывается на curl, что даёт ряд преимуществ перед url wrapper ами функции file, fopen, file_get_contents и т.п.:

Более быстрый процесс получения информации
Передача referrer, cookie, user agent
Управление временем ожидания (таймаутом) на получение информации
Возможность корректно перейти по всем пере направлениям (Location:), если таковые встречаются
Бинарная передача данных
В следующих статьях мы поведаем Вам более подробно о возможностях функции curl и приведём другие интересные примеры её использования.

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

Убрать дубли фраз, обнаруженные при поиске.
Для получения более достоверных вариантов проверять, встречается хотя бы одно слово из целевого поискового словосочетания, заданного в $questions в фразах, обнаруженных при многоуровневом обходе страниц
Каждый результат поиска записать в отдельный файл.
Визуализировать результат при помощи графиков
Задание для тех, кто учит parser ы на PHP:
Для выполнения этой же задачи напишите парсер, который обходит wordstat yandex