Сен
Как написать парсер для сайта
Недавно мне была поставлена задача написать php парсер сайта. Поставленная задача была выполнена и благодаря ей появилась эта заметка. До этого я ничего подобного не делал, так что не судите строго. Это мой первый парсер php.
И так с чего начать решение вопроса "как написать парсер". Давайте для начала разберёмся что это такое. В простонародье парсер(parser) или синтаксический анализатор - это программа которая получает данные (например веб страница), как то их анализирует структурирует, делает выборку и потом проводит какие то операции с ними (пишем данные в файл, в БД или выводим на экран). Данную задачу нам нужно выполнить в рамках веб программирования.
Для заметки я придумал такую тестовую задачу. Нужно спарсить по определённому поисковому запросу ссылки на сайты с 5 первых страниц выдачи и вывести их на экран. Парсить я решил выдачу поисковой системы bing. А почему бы не написать парсер яндекса или гугла спросите вы. Такие матёрые поисковики имеют не хилую защиту от парсинго(капча, бан ip, меняющаяся разметка, куки и тд), и это тема отдельной статьи. В этом плане с бингом таких проблемм нет. И так что нам нужно будет сделать:
- Получить (спарсить) контент html страницы средствами php
- Получить интересующие нас данные(а конкретно ссылки)
- Спарсить постраничную навигацию и получить ссылку на следующую страницу
- Опять спарсить страницу по ссылке, получить данные, получить следующую ссылку
- Проделать выше описанную операцию N количество раз
- Вывести все полученные ссылки на экран
Получение и парсинг страницы
Сначала напишем функцию, потом её разберём
function getBingLink($link){ $url="https://www.bing.com/search"; //получаем контент сайта $content= file_get_contents($url.$link); //убираем вывод ошибок libxml_use_internal_errors(true); //получаем объект класса DOMDocument $mydom = new DOMDocument(); //задаём настройки $mydom->preserveWhiteSpace = false; $mydom->resolveExternals = false; $mydom->validateOnParse = false; //разбираем HTML $mydom->loadHTML($content); //получаем объект класса DOMXpath $xpath = new DOMXpath($mydom); //делаем выборку с помощью xpath $items=$xpath->query("//*[@class='b_algo']/h2/a"); //выводим в цикле полученные ссылк static $a=1; foreach ($items as $item){ $link=$item->getAttribute('href'); echo $a."-".$link."
"; $a++; } }
И так разберём функцию. Для получения контента сайта используем php функцию file_get_contents($url.$link)
. В неё подставляем адрес запроса.
Есть ещё много методов получения контента html страницы, например cUrl, но на мой взгляд file_get_contents самый простой. Потом вызываем объект DOMDocument
и так далее. Это всё стандартно и об этом можно почитать в интернете поподробнее. Хочу заакцентировать внимание на методе выборки нужных нам элементов. Для этой
цели я использую xpath. Мою xpath шпаргалку можно глянуть здесь. Есть и другие методы выборки такие как регулярные выражения, Simple HTML DOM, phpQuery. Но на мой взгляд лучше разобраться с xpath,
это даст дополнительные возможности при работе с xml документами, синтаксис полегче чем с регулярными выражениями, в отличии от css селекторов можно найти элемент
по находящемуся в нём тексту. Для примера прокомментирую выражение //*[@class='b_algo']/h2/a
. Подробнее синтаксис можно посмотреть в моей шпаргалке xpath. Мы выбираем со всей страницы ссылки лежащие в теге h2 в диве с классом
b_algo
. Сделав выборку мы получим массив и которого в цикле выведем на экран все полученные ссылки.
Парсинг постраничной навигации и получение ссылки на следующую страницу
Напишем новую функцию и по традиции разберём её позже
function getNextLink($link){ $url="https://www.bing.com/search"; $content= file_get_contents($url.$link); libxml_use_internal_errors(true); $mydom = new DOMDocument(); $mydom->preserveWhiteSpace = false; $mydom->resolveExternals = false; $mydom->validateOnParse = false; $mydom->loadHTML($content); $xpath = new DOMXpath($mydom); $page = $xpath->query("//*[@class='sb_pagS']/../following::li[1]/a"); foreach ($page as $p){ $nextlink=$p->getAttribute('href'); } return $nextlink; }
Почти идентичная функция, изменился только xpath запрос. //*[@class='sb_pagS']/../following::li[1]/a
получаем элемент с классом sb_pagS
( это класс активной
кнопки постраничной навигации), поднимаемся на элемент вверх по dom дереву, получаем первый соседний элемент li
и получаем в нём ссылку. Эта и есть ссылка на
следующую страницу.
Парсим выдачу N количество раз
Пишем функцию
function getFullList($link){ static $j=1; getBingLink($link); $nlink=getNextLink($link); if($j<5){ $j++; getFullList($nlink); } }
Данная функция вызывает getBingLink($link) и getNextLink($link) пока не кончится счётчик j. Функция рекурсивная, то есть вызывает сама себя. Про рекурсию
почитайте подробнее в интернете. Обратите внимание что $j статическая, то есть она не удаляется при следующем вызове функции. Если бы это было не так, то
рекурсия бы была бесконечной. Ещё добавлю из опыта, если хотите пройти всю постраничную навигацию то пишите if условие пока есть переменная $nlink. Есть ещё
пара подводных камней. Если парсер работает долго то это может вызвать ошибку из за времени выполнения скрипта. По умолчанию 30с. Для увеличения времени в
начале файла ставте ini_set("max_execution_time", "480");
и задавайте нужное значение. Так же может возникать ошибка из за большого количества вызовов одной
функции (более 100 раз). Фиксится отключением ошибки, ставим в начало скрипта ini_set('xdebug.max_nesting_level', 0);
Теперь нам осталось написать html форму для ввода запроса и собрать парсер воедино. Смотрите листинг ниже.
<?php //обнуляем значение кук setcookie ("err", ""); setcookie ("msg", ""); //получаем страницу и выводим ссылки function getBingLink($link){ $url="https://www.bing.com/search"; $content= file_get_contents($url.$link); libxml_use_internal_errors(true); $mydom = new DOMDocument(); $mydom->preserveWhiteSpace = false; $mydom->resolveExternals = false; $mydom->validateOnParse = false; $mydom->loadHTML($content); $xpath = new DOMXpath($mydom); $items=$xpath->query("//*[@class='b_algo']/h2/a"); static $a=1; foreach ($items as $item){ $link=$item->getAttribute('href'); echo $a."-".$link."<br>"; $a++; } } //получаем след. ссылку function getNextLink($link){ $url="https://www.bing.com/search"; $content= file_get_contents($url.$link); libxml_use_internal_errors(true); $mydom = new DOMDocument(); $mydom->preserveWhiteSpace = false; $mydom->resolveExternals = false; $mydom->validateOnParse = false; $mydom->loadHTML($content); $xpath = new DOMXpath($mydom); $page = $xpath->query("//*[@class='sb_pagS']/../following::li[1]/a"); foreach ($page as $p){ $nextlink=$p->getAttribute('href'); } return $nextlink; } //делаем запрос к N страницам function getFullList($link){ static $j=1; getBingLink($link); $nlink=getNextLink($link); if($j<5){ $j++; getFullList($nlink); } } $err=""; // Проверяем, была ли корректным образом отправлена форма //если пользователь пришол POST if (!empty($_POST)){ function clearDate($date){ $date=stripslashes($date); $date=strip_tags($date); $date=trim($date); return $date; } $search=clearDate($_POST['search']); if(!empty($search)){//и пост не пустой $s=urlencode($search); $link="?q=".$s; }else{ $errMsg="Заполните поле запроса"; setcookie ("err", $errMsg); header("Location: ". $_SERVER['PHP_SELF']); exit; } } $errMsg=$_COOKIE["err"]; ?> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>parser</title> <style type="text/css"> .wrapp{ width: 960px; margin: 0 auto; } </style> </head> <body> <div class="wrapp"> <form id='join' class="join" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" > <h2>Парсер</h2> <div class="string"> <label for="email">поисковый запрос</label> <input type="text" size="25" id="search" value="" name="search"/> </div> <?php if($errMsg){//если есть кука то выводим её значение echo "<p style='color: red'>".$errMsg."</p>"; } ?> <input type="submit" name="" value="Отправить запрос" class="submit"/> </form> <?php echo "Поисковый запрос: ".$s."<br>"; if($link){ //вызываем ф-ю парсера getFullList($link); } ?> </div> </body> </html>;
P.S.
Решил улучшить юзабилити парсера. Была поставлена задача во время работы парсера выводить гифку загрузки, а по окончанию загрузки её убирать. Для этих целей я решил использовать ajax. Код парсера пришлось изменить, но логика работы осталась. Подробнее о передаче данных средствами ajax и php можно прочитать в моей заметке здесь. Итак мы теперь разбили код парсера на два файла. Файл index.php содержит в себе форму из которой данные силами ajax передаются в файл get.php. Там они обрабатываются производится запуск кода парсера, формирование массива с ответами на запрос и передача их на вывод в index.php силами javascript. Листинг index.php
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="keywords" content="пример парсера php" /> <meta name="description" content="пример парсера php" /> <title>пример парсера php</title> <link href="/css/bootstrap.min.css" rel="stylesheet"> <link rel="stylesheet" href="/css/style.css"> <script src="/js/jquery-3.1.0.min.js" type="text/javascript"></script> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> <script type="text/javascript"> $(document).ready(function(){ //вешаем событие на кнопку отправить $('.submit').click(function(){ //получаем значение поля ввода var sh=$(":text").val(); //если поле пустое if(sh==''){ var a='<div class="alert alert-danger col-sm-9 col-sm-offset-2 col-xs-offset-1 col-xs-10"><p class="text-center"><strong>заполните поле поискового запроса</strong></p></div>'; //выводим предупреждение $('.myalert').html(a); //стираем старый вывод данных $('#result').html(' '); return; } //убираем ранее выведенный алерт $('.myalert').html(' '); $.ajax({ //как будем передавать данные type: "POST", //куда передаём url: "get.php", //какие данные передаём data: {data: sh}, //событие перед отправкой ajax beforeSend: function(){ //стираем старый вывод $('#result').html(' '); //выводим гифку $(".load").show(); }, //событие после получения ответа, //получаем массив в data success: function(data){ //скрываем гифку $(".load").hide(); var html = ''; //ф-я JSON.parse переводит json данные в объект var dt=JSON.parse(data); //вывод данных запроса $('.mydata').html(dt[dt.length-1]); for (var i = 0; i < dt.length-1; i++) { //счётчик с 1 var num=i+1; html +='<tr><td><code>'+num+'</code></td>'; html +='<td>'+dt[i]+'</td></tr>'; } $('#result').html(html); } }); }); }); </script> </head> <body> <div class="header"> <div class="headwrap"> <div class="logo"> <div class="logotip"><a href="http://aweb34.ru">Aweb<span>34</span>.ru</a></div> </div> </div> </div> <div class="wraper container"> <h1>Пример парсера Bing</h1> <a class="back" href="/javascript/validatsiya-formy-jquery"><< к статье</a> <div class="row"> <div class="col-md-12 col-sm-12"> <form class="form-horizontal" role="form"> <div class="form-group"> <label for="search" class="col-sm-3 col-sm-offset-1 control-label">Поисковый запрос</label> <div class="col-sm-7 "> <input type="text" class="form-control" id="search" placeholder="Введите ваш поисковый запрос" name="search"/> </div> </div> <div class="form-group"> <div class="myalert form-group"></div> </div> <div class="form-group"> <label class="col-sm-3 col-sm-offset-1 control-label">Введённый запрос:</label> <div class="col-sm-8"> <p class="mydata form-control-static"></p> </div> </div> </form> <div class="col-sm-offset-4 col-sm-4 col-xs-offset-2 col-xs-7"> <button class="submit btn btn-primary col-sm-12 col-xs-12 btn-lg">Отправить запрос</button> </div> <div class="load col-sm-offset-1 col-sm-10 center" style="display: none;"> <img class="img-responsive" src="/css/preload.gif"> </div> </div> </div> <div class="col-sm-offset-2 col-sm-8 mytable"> <table class="table table-striped " > <tbody id='result'></tbody> </table> </div> </div> <script src="/js/bootstrap.min.js"></script> </body>
Файл get.php. Функцию для парсера я немного переписал для улучшения компановки массива с данными но логика осталась та же.
<?php ini_set("max_execution_time", "60"); function getBingLink($link){ $url="https://www.bing.com/search"; $content= file_get_contents($url.$link); libxml_use_internal_errors(true); $mydom = new DOMDocument(); $mydom->preserveWhiteSpace = false; $mydom->resolveExternals = false; $mydom->validateOnParse = false; $mydom->loadHTML($content); $xpath = new DOMXpath($mydom); $items=$xpath->query("//*[@class='b_algo']/h2/a"); $page = $xpath->query("//*[@class='sb_pagS']/../following::li[1]/a"); static $j=1; $myitem=array(); static $myitem; foreach ($items as $item){ $myitem[]=$item->getAttribute('href'); } foreach ($page as $p){ $nextlink=$p->getAttribute('href'); } if($j<5){ $j++; getBingLink($nextlink); } //сформировали массив с полученными данными return $myitem; } if($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { if(isset($_POST['data'])) { $link="?q=".$_POST['data']; $linkarr=getBingLink($link); //добавили в массив с полученными //данными текст запроса из формы $linkarr[]=$_POST['data']; $linkarr_json = json_encode($linkarr); //отправили массив на вывод echo $linkarr_json; } } ?>
Комментарии
Роман
05.05.2017 17:38 Ответить
А парсер выдачи Яндекса и Гугла можите ? Только не через xml - выдача сильна отличается.
Алексей Григорьев
05.05.2017 17:56 Ответить
Для парсера выдачи Яндекса и Гугла мне кажется лучше использовать javascript. Есть очень хороший плагин для firefox, называется imacros. Можно автоматизироват ь действия браузера. Используя его можно легко написать парсер яндекса и гугла
Роман
05.05.2017 20:19 Ответить
А вы оказываете подобные услуги по написанию программ на web языках ?
Алексей Григорьев
06.05.2017 09:14 Ответить
Могу попробовать написать. Но есть нюансы. Каждую задачу надо смотреть индивидуально. Правда сейчас у меня на подобную задачу просто нет свободного времени.