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

Изучаем 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("&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. После чего регулярным выражением ~<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 Слайдер).

Комментарии