Изучаем PHP: Пишем parser похожих запросов Yandex
В жизни программиста очень часто возникает задача получения полезной информации из других источников. Это может быть всесторонняя оценка цен конкурентов по определённому товару, сбор изображений и прочего.
Рассмотрим на примере так называемый «parser – spider» (парсер – паук), в задачу которого будет входить сбор информации со страниц yandex для поиска похожих ключевых слов, которые мы будем использовать в раскрутке нашего проекта. И напишем данный парсер на PHP
Как же нам может помочь parser?
Мы получаем список сходных фраз, которыми пользуются посетители яндекса при поиске нашей тематики, и в дальнейшем сможем ориентироваться на них при написании материалов на нашем сайте, оцениваем спрос к тематике нашего сайта. А так же это даст нам возможность определить поисковые запросы с меньшей конкурентной борьбой сходной тематики, чтобы выйти по ним в ТОП поисковой системы Yandex. Как следствие, благодаря этой информации мы расширим аудиторию нашего проекта.
А откуда мы можем получить информацию по похожим поисковым запросам Yandex?
Дело в том, что при поиске Яндекс подсказывает сам, отображая блок внизу страницы: Вместе с «[фраза для поиска]» ищут — несколько подобных поисковых запросов.
Вверху в левой части каждой страницы указывает, сколько нашлось вариантов, например:
создать сайт — Нашлось 104 млн ответов
Итак, какие же задачи поставим перед скриптом – парсером?
- Возможность рекурсивного парсинга похожих поисковых запросов с определением уровня вложенности
- Если не найдено ни одного похожего запроса по указанной фразе (а такое тоже бывает), попробуем просмотреть запросы по её сокращению:Например, если ввести фразу Создать Сайт Харьков, то похожих поисковых запросов не будет в выдаче поисковика, но если ввести: создать сайт – они появятся. Ограничимся урезанием фразы на одно слово вконце.
- Обходить парсер будет не только одну фразу, а займётся обработкой целого списка из фраз (передадим массивом)
- Сохранение найденных поисковых фраз и их встречаемостью в файл
Нюансы:
Поставим задержку на опрос поисковой системы (не меньше 2 секунд, а то и до 30 секунд в случайном порядке) на каждый поисковый запрос. Мы же не хотим, чтобы Яша нас «отругал» за плохое поведение?
PHP:
<?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(" ",' ', $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(" ",' ', $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. После чего регулярным выражением ~<strong[^>]*?class=»b-head-logo__text»[^>]*?>(.*?)</strong>~is узнаём, сколько раз встречается данный поисковой запрос.
Очищаем полученный запрос от html и спец символов. Помещаем выражение и количество найденных страниц по этой поисковой фразе в файл prases.txt.
Исследуем содержимое полученной страницы на предмет содержания «похожих поисковых запросов» ~<table[^>]*?class=»b-related__table»[^>]*?>(.*?)</table>~is.
Если похожие поисковые фразы найдены, извлекаем ссылки на них и их анкоры: ~<a[^>]*?href=(«|’)([^»‘]*?)(\1)[^>]*?>(.*?)</a>~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
Еще один источник информации о том, как создать паука-парсера на PHP
В следующих статьях мы поговорим о примерах использования curl, регулярных выражениях, и о слайдерах изображений на php + javascript (разберём, например: Easy Slider 1.7 — Навигационный jQuery Слайдер).