Скрипт Human Emulator парсинга HTML-страницы с использованием XPath

В этой статье мы рассмотрим один из примеров написания скрипта для парсинга HTML-страниц с использованием XPath на примере сайта bing.com.


Сперва определимся с тем, что такое XPath и зачем оно нужно, если есть регулярные выражения?


XPath (XML Path Language) — это язык запросов к элементам XML-подобного документа (далее для краткости просто XML).

XPath призван реализовать навигацию по DOM в XML.

Regexp — формальный язык поиска и осуществления манипуляций с подстроками в тексте, основанный на использовании метасимволов.

По сути это строка-образец (шаблон), состоящая из символов и метасимволов и задающая правило поиска.

Итак, главная разница в том, что XPath специализируется на XML, а Regexp — на любом виде текста.

В: Зачем использовать XPath, если есть regexp, в котором можно сделать тоже самое?
О: Простота поддержки.

Синтаксис у regexp такой, что уже через неделю может быть проще всё переписать, чем вносить изменения,
а с XPath можно спокойно работать. И синтаксис у xpath довольно компактный,xml’ё-фобы могут быть спокойны.

Простой пример для вдохновения — получим значение атрибута «href» у, например, тега «a».

<a href="https://ya.ru">Yohoho!</a>

Regexp:

<a.*?href=("|')(.*?)("|').*?>

XPath:

"string(//a/@href)"

XPath + PHP:

$dom = new DOMDocument;

// "@" Это, конечно, плохо. Но HTML не обязан быть
// валидным, в отличие от XML.
@$dom->loadHTML("<a href=\"https://ya.ru\">Yohoho!</a>");

$xpath = new DOMXpath($dom);
    
$res = $xpath->query("//a");
    
echo $res->item(0)->getAttribute("href") . PHP_EOL;

Быстро (несколько небольших страниц) пробежаться по основам XPath можно в туториале от W3Schools.

Как использовать XPath в PHP можно почитать в документации на php.net.
И в небольшом тутораильчике от IBM DeveloperWorks.

Теперь определимся с необходимым функционалом скрипта:

* Возможность указывать произвольный поисковый запрос


* Парсим только первую страницу поисковой выдачи


* Из поисковой выдачи нам нужно:


* заголовок


* ссылка


* номер в выдаче

Исходя из нашего ТЗ составляем примерный алгоритм работы скрипта:

1) Заходим на bing.com


2) Вводим поисковую фразу


3) Получаем со страницы необходимый результат

Приступим к написанию парсера поисковой выдачи http://bing.com.
Для начала, создадим базовый каркас скрипта.

    // coding: windows-1251
    // Настройка HumanEmulator
    // -----------------------------------------------
    // Где запущен XHE
    $xhe_host = "127.0.0.1:7010";
    // HumanEmulator lib
    require "../../Templates/xweb_human_emulator.php";
    // Our tools
    require "tools/functions.php";
    
    // Настройки скрипта
    // -----------------------------------------------
    
    // Скрипт
    // -----------------------------------------------
    
    // Quit
    $app->quit();

В настройки добавим переменную для хранения поискового запроса.

// Поисковый запрос
$text = "ХуманЭмулятор";

Заходим на сайт.

// Базовый URL
$base_url = "https://www.bing.com/?setlang=en";
$browser->navigate($base_url);

Вводим поисковую фразу.

$input->set_value_by_attribute("name", "q", true, $text);
sleep(1);
$element->click_by_attribute("type", "submit");
sleep(5);

Сохраним в переменную содержимое страницы.

// Получаем содержимое страницы
$content = $webpage->get_body();

Настроим xpath-объект:

$dom = new DOMDocument;
@$dom->loadHTML($content);
$xpath = new DOMXpath($dom);

Теперь у объекта $xpath есть метод «query» в который мы будем передавать наше xpath-выражение.
Давайте начнём создавать xpath-выражение.
Открыв исходный код страницы с результатами поисковой выдачи увидим, что сами результаты находятся внутри тега «li».

<li class='b_algo'>
<!-- ... -->
</li>

Т.о. наше xpath-выражение выберет со страницы все поисковые результаты.

$results = $xpath->query("//li[@class=\"b_algo\"]");

На одной странице у нас должно быть 1 или больше результатов, проверим себя:

if($results === false)
    {
        echo "С нашим xpath-выражением что-то не так." . PHP_EOL;
        $app->quit();
    }
    elseif($results->length === 0)
    {
        echo "Поисковый запрос '{$text}' не принёс результатов." . PHP_EOL:
        $app->quit();
    }
    echo "Нашли {$results->length} совпадений." . PHP_EOL;

Здесь стоит обратить внимание на ветку if, где мы сравниваем кол-во результатов xpath-поиска с нулём.
Если наше xpath-выражение ничего не нашло, то это может означать две вещи:


* Bing действительно ничего не нашёл.


* Bing что-то нашёл, но поменял вёрстку на странице, и наше xpath-выражение необходимо исправлять.

2-й пункт достаточно коварный, в таких случаях, когда xpath-выражение ничего не находит необходимо дополнительно
сверятся, чтобы удостоверится, что xpath-выражение не устарело (хотя и это не даст 100% гарантий).

В нашем случае будем сверяться с тем, что Bing пишет кол-во найденных результатов.

<span class="sb_count" data-bm="4">14 results</span>

А если результатов по поисковому запросу нет, то:

<li class="b_no" data-bm="4">
<h1>No results found for ...</h1>
<!-- ... -->
</li>

Т.о. мы получаем такую конструкцию проверки:


— Если xpath-запрос ничего не нашёл и поисковый запрос ничего не нашёл, то на странице будет html-код с «No results found».


— Если xpath-запрос ничего не нашёл, а поисковый запрос что-то нашёл, то на странице будет html-код с «N results».

Обновим проверку результата xpath-запроса.

if($results === false)
    {
        echo "С нашим xpath-выражением что-то не так." . PHP_EOL;
        $app->quit();
    }
    elseif($results->length === 0)
    {
        // Если bing ничего не нашёл
        $check_results1 = $xpath->query("//li[@class=\"b_no\"]");
        // Если bing что-то нашёл
        $check_results2 = $xpath->query("//span[@class=\"sb_count\"]");
        
        if($check_results1 === false or $check_results2 === false)
        {
            echo "С нашим xpath-выражением что-то не так." . PHP_EOL;
            $app->quit();
        }
        
        if($check_results1->length > 0 and $check_results2->length === 0)
        {
            echo "Поисковый запрос '{$text}' не принёс результатов." . PHP_EOL:
            $app->quit();
        }
        else
        {
            echo "С нашим xpath-выражением что-то не так." . PHP_EOL;
            $app->quit();
        }
    }
    else
    {
        echo "Нашли {$results->length} совпадений для '{$text}' на 1-й странице поисковой выдачи." . PHP_EOL;
    }

Если всё хорошо и что-то нашлось, то у нас в $results будет N сущностей с результатами отработки xpath-запроса.

Каждая сущность будет содержать такой HTML-код

  <div class="b_title">
    <h2>
      <a h="ID=SERP,5082.1" href="http://www.armadaboard.com/topic25915.html">
        Для чего нужен <strong>Хуман Эмулятор</strong>? > …
      </a>
    </h2>
    
    <!-- // ... -->
  </div>

Теперь можно приступить непосредственно к выборке интересующих нас данных.

foreach($results as $num => $item)
    {
        // В $item у нас сущность с одним результатом из поисковой выдачи
    }

Получаем номер в поисковой выдаче.

echo "Номер в поисковой выдаче: " . ($num + 1) . PHP_EOL;

Получаем заголовок.

$title = $xpath->query("div[contains(@class, \"title\")]/h2/a", $item);
    if($title === false or $title->length !== 1)
    {
        echo "С нашим xpath-выражением что-то не так." . PHP_EOL;
        $app->quit();
    }
    $title = utf8_decode($title->item(0)->textContent);
    echo "Заголовок: '{$title}'" . PHP_EOL;

В данном случае мы в метод «query» передали вторым параметром текущий $item и в xpath-запросе не указывали «//» (т.е. искать сначала страницы).

2-й параметр означает контекст поиска для xpath-запроса, т.е. искать будем не по всей странице, а только по маленькому html-кусочку из $item.
И, наконец-то, получаем ссылку.

$link = $xpath->query("div[contains(@class, \"title\")]/h2/a", $item);
    if($link === false or $link->length !== 1)
    {
        echo "С нашим xpath-выражением что-то не так." . PHP_EOL;
        $app->quit();
    }
    $link = $link->item(0)->getAttribute("href");
    echo "Ссылка: '{$link}'" . PHP_EOL;

Вообще-то, ссылку можно было получить и без совершения дополнительного xpath-запроса, использовав результат из поиска заголовка.
Но оставим так, для наглядности.

    // Получаем заголовок и ссылку
    //
    $title_link = $xpath->query("div[contains(@class, \"title\")]/h2/a", $item);
    if($title_link === false or $title_link->length !== 1)
    {
        echo "С нашим xpath-выражением что-то не так." . PHP_EOL;
        $app->quit();
    }
    $title = utf8_decode($title_link->item(0)->textContent);
    echo "Заголовок: '{$title}'" . PHP_EOL;
    $link = $title_link->item(0)->getAttribute("href");
    echo "Ссылка: '{$link}'" . PHP_EOL;

Полезные ссылки:

Wikipedia XPatch
Wikipedia Rugular expression
W3.org XPatch
W3schools XPatch
Mozilla.org XPatch
PHP.net
Ibm.com x-xpatch
Ibm.com x-xpatchphp

Скрипт написан 28.04.2015 в Human Emulator 4.9.18 Advanced.

скачать скрипт

Как написать простой скрипт парсер на Human Emulator

Предположим у нас стоит задача собрать контактные данные организаций с какого то сайта. Для примера возьмём сайт
«Желтые страницы Рунета» http://www.yellowpagesrussia.ru. Нам нужно собрать данные организаций из раздела
«Морской транспорт – организации» http://www.yellowpagesrussia.ru/190/154/.

Для начала работы нам понадобится стандартная заготовка скрипта. Её можно создать по нажатию в меню Файл->Новый->Скрипт
или по нажатию «горячих» клавиш «Ctrl+N». После чего создать папку в папке My Scripts с именем нашего будущего
скрипта в нашем случае yellowpages и переместить наш скрипт в эту папку через меню «Файл->Сохранить как» yellowpages.php.

Далее мы вставляем шапку с настройками:

$xhe_host ="127.0.0.1:7010";

// The following code is required to properly run XWeb Human Emulator
require("../../Templates/xweb_human_emulator.php");

// //////////////////////// настройки скрипта /////////////////////////
// файл с результатами
$str_res="res/result.txt";
// скрипт работает в режиме отладки
$dbg = true;
// сайт
$site = "http://www.yellowpagesrussia.ru/190/154/";
// ожидание
$wt = 5;
$wt_long = 10;
// текущая страница
$crnt_page = 1;
// количество страниц
$cnt_pages = -1;
// //////////////////////// дополнительные модули ///////////////
// функции 
require_once("tools/functions.php");
// /////////////////////// скрипт ///////////////////////////////////////////

Примечание: Файл functions.php можно взять из любого скрипта, который вы можете скачать у нас на сайте.
Его нужно поместить в папку tools, которую создать в папке со скриптом.

Для начала работы перейдем на нужный нам сайт для этого используем команду хумана:

$browser->navigate($site);

Далее нам нужно через правую панель «Дерево элементов» найти общий id контекста
кликаем по данным правой кнопкой и выбираем «посмотреть в дереве элементов»
находим наиболее подходящий элемент, подглядывая в «инспектор», чтобы внутренний текст
(или html код) содержал максимум нужной нам информации!
Найдя нужным идентификатор, с помощью функции Human Emulator выпарсиваем все данные со страницы
в нашем случае это id = content для объекта $div
пишем в скрипте(либо правой кнопкой мыши вызываем контекстное меню и кликаем на нужный нам контент и выбираем нужный пункт):

$content = $div->get_all_inner_texts_by_attribute("id","content");

после выполнения этого шага хуманом мы получаем массив с данными,
которые сохраним с помощью функции добавления строки в файл:

$textfile->add_string_to_file($str_res,$content."\r\n");

данные будут добавлены в указанный текстовый файл. Тело скрипта теперь выглядит следующим образом:

// /////////////////////// скрипт ///////////////////////////////////////////
// переходим на сайт
$browser->navigate("$site");
sleep($wt);

// получаем нужные данные со страницы
$content = $div->get_all_inner_texts_by_attribute("id","content");
sleep($wt);
// записываем в файл
$textfile->add_string_to_file($str_res,$content);
sleep($wt);

Далее проверяем результат…и видим, что записались лишние данные!
используем для решения этой проблемы заранее написанную нами функцию get_string().
Исходники её лежат в файле «tools/functions.php».

// получение строки по префиксам
function get_string($str1, $pr1, $pr2, &$ind_st = 0)
{
   //echo $str1.'<br>';
   $ind1 = strpos($str1, $pr1, $ind_st);
   // echo "index 1 ".$ind1.'<br>';
   if($ind1 === false)
      return "";

   $ind2 = strpos($str1, $pr2, $ind1+ strlen($pr1));
   //echo "index 2 ".$ind2.'<br>';
   if ($ind2 === false)
      return "";
   
   // запомним стартовый индекс
   $ind_st = $ind2 + 1;
   // 
   $sres = substr($str1, $ind1 + strlen($pr1), $ind2 - $ind1-strlen($pr1));

   return trim($sres); 
}

C помощью этой функции выкусываем нужные данные:

$text = get_string($content[0],"Морской транспорт – организации","Страницы");
затем записываем полученные данные в файл
$textfile->add_string_to_file($str_res,$text);

В итоге у нас должен получится следующий скрипт:

// переходим на сайт
$browser->navigate("$site");
//sleep($wt);

// получаем нужные данные со страницы
$content = $div->get_all_inner_texts_by_attribute("id","content");
sleep($wt);

// удаляем лишнее
echo $text = get_string($content[0],"Морской транспорт – организации","Страницы" );
sleep($wt);

// записываем данные в файл
$textfile->add_string_to_file($str_res,$text);
sleep($wt);

Так же можно использовать заранее написанную нами функцию для перехода на следующую страницу
next_page();, которая так же находится в файле «tools/functions.php».

// функция осуществляющая переход со страницы на страницу
// $crnt_page - текущая страница
// $cnt_pages - количество страниц для обработки
function next_page()
{
 global $anchor, $browser, $app,$crnt_page, $cnt_pages;
 
 // количество поисковых страниц
 $crnt_page = $crnt_page + 1;
 
 // если $cnt_pages == -1, значит скрипт будет искать все страницы до конца поиска
 if ($cnt_pages != -1)
 {
  // если обработали все нужные нам страницы - останавливаем скрипт
  if ($crnt_page>$cnt_pages)
  { 
   debug_mess("обработали все заданные страницы ".($crnt_page - 1));
   $crnt_page = 1;
   return false;
  }
 }
 // перейдём на следующую страницу
 if (!$anchor->click_by_inner_text($crnt_page, true))
 {
  debug_mess("обработали все доступные страницы " . ($crnt_page - 1));
  $crnt_page = 1;
  return false;
 }
 debug_mess("обработали страницу " . ($crnt_page - 1));
 return true;
}

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

debug_mess(" скрипт запустили");

// переходим на сайт
$browser->navigate("$site");
sleep($wt);
	
while(true)
{
	// получаем нужные данные со страницы
	$content = $div->get_all_inner_texts_by_attribute("id","content");
	sleep($wt);
	
	// удаляем лишнее
	echo $text = get_string($content[0],"Морской транспорт – организации","Страницы" );
	sleep($wt);
	
	// записываем данные в файл
	$textfile->add_string_to_file($str_res,$text);
	sleep($wt);
	
	// перейти на следующую страницу
	debug_mess("
<b>переходим на следующую страницу</b>
");
	next_page();
	sleep($wt_long);
}

debug_mess("скрипт закончил работу");
// Quit
$app->quit();

В итоге за 15 минут мы накидали скрипт парсер, который собирает данные со всех страниц, либо только с заданных.
Если бы мы стали писать парсер на php, то это заняло у нас гораздо больше времени.