Справочники, инструменты, документация

PHP: Граббер

Грабберами в народе называют серверные скрипты, предназначенные для получения данных с различных серверов и встраивания их в свои страницы.

В инете есть куча примеров RSS-грабберов, извлекающих тексты с новостных лент, но мне лично нужен не какой-то RSS, которым я ни разу в жизни не пользовался, а полноценный скрипт, который легко настроить для извлечения любой нужной мне информации с любой из доступных в сети страниц.

Так что эта небольшая статья - как раз пример написания граббера на языке PHP.

Задача состоит, собственно, из 3 этапов.

Получение данных с нужного нам URL

Для этого в PHP существует несколько возможностей:

Стандартная функция fopen, служащая для открытия файла

Применять ее не очень удобно, так как нельзя контролировать время соединения, получать ответы ошибок сервера и т.д. Кроме того, она может быть запрещена на хостинге через http. Тем не менее, вот пример откуда-то. Здесь мы парсим выдачу популярного сайта bash.org:

$url='http://www.bash.org.ru/best';
$file = @fopen ($url, 'r');
if ($file==false) print '<p>Не могу открыть сайт '.$url.'!';
else {
 $contents = fread ($file, 100000);
 $contents = preg_match_all('|<div>(.+)</div>|U',$contents,$frazes); 
 for($i=0;$i<5;$i++){ 
  if ($i<>5) echo "<hr>".$frazes[1][$i]."\r\n<hr>"; 
 } 
 fclose ($file);
}

Популярный вариант этого же подхода еще проще:

$file = file_get_contents('http://www.bash.org.ru/best'); 
$file = preg_match_all('|<div>(.+)</div>|U',$file,$frazes); 
for($i=0;$i<11;$i++){ 
  if ($i<>5) echo "<hr>".$frazes[1][$i]."\r\n<hr>"; 
 }
$str=file_get_contents("http://google.com/");
// по сути, file_get_contents - это fopen, fread, fclose одной командой

Библиотека cURL - удобнее, но также может быть не установлена или запрещена на хостинге.

Соединение через сокеты - именно его мы используем, чтоб HTTP-заголовок формировался полностью под нашим контролем. Полноценно проверять коды ошибок в учебной статье не будем, не надейтесь, но все же скрипт должен получиться похожим на человеческий.

Следующая функция получает содержимое, расположенное на хосте $host по абслютному пути $path. Имя хоста не включает в себя префиксов http://www, путь начинается с символа корневого каталога /.

function get_URL_by_socket ($host,$path) { 
 // получает URL $path с хоста $host через сокеты.
 $fp = fsockopen($host, 80); 
 if (!$fp) { 
 die ("Не могу получить данные с url http://$host/$path"); 
 } 
 else { 
 $out = "GET $path HTTP/1.0\r\n"; 
 $out .= "Accept: image/gif, application/xhtml+xml, */*\r\n"; 
 $out .= "Accept-Language: ru\r\n"; 
 $out .= "Host: $host\r\n"; 
 // имитируем браузер Opera Mini:
 $out .= "User-Agent: Opera/8.01 (J2ME/MIDP; ".
  "Opera Mini/2.0.4509/1716; ru; U; ssr)\r\n"; 
 $out .= "Cache-Control: no-cache\r\n"; // не кэшировать 
 $out .= "Connection: Close\r\n\r\n"; 
 fwrite($fp, $out); 
 $headers = ""; 
 while ($str = trim(fgets($fp))) 
  $headers .= "$str\n"; 
 $body = ""; 
 while (!feof($fp)) 
  $body .= fgets($fp); 
 fclose($fp); 
 } 
 return $body; 
}

Извлечение содержимого из страницы

На следующем этапе мы должны извлечь из кода страницы, полученного функцией get_URL_by_socket, полезную для нас часть. Для этой цели в PHP существют регулярные выражения (ссылка на статью внизу страницы) и строковые функции. Я для простоты взял здесь случай, когда мы можем выделить в коде страницы куски текста, однозначно ограничивающие нужную нам часть снизу $end и сверху $start. В принципе, при внимательном анализе исходного кода любой страницы (в браузере обратитесь к меню Вид, пункту Исходный текст или Источник легко выделить такие куски. Так как мы будем писать их внутрь строковых переменных, ограниченных двойными кавычками, то если в тексте строки встречается двойная кавычка ", ее нужно заменить на сочетание символов \", как здесь:

$start="<div class=\"temper\">";

Всю информацию будет обрабатывать следующая функция:

function process($s,$start,$end,$include) { 
 //Парсит полученный файл - здесь-то и пишется главное
 //У нас это извлечение содержимого от $start до $end
 $s1=strpos ($s,$start);
 $s2=strpos ($s,$end);
 if (!is_integer($s1)) {
 return "Не найден начальный сегмент: ".htmlspecialchars($start);
 }
 if (!is_integer($s2)) {
 return "Не найден конечный сегмент: ".htmlspecialchars($end);
 }
 if ($s1>$s2) {
 return "Конечный сегмент предшествует начальному";
 }
 if ($include) { //Включать начало и конец
 return substr ($s,$s1,$s2-$s1+strlen($end));
 }
 else { //Исключить начало и конец
 $s1+=strlen($start);
 return substr ($s,$s1,$s2-$s1);
 }
}

Параметр $include должен быть равен true, если строки $start и $end надо оставить в выводе или false, если их надо исключить.

Дополнительная обработка и вывод

Строку, возвращенную функцией process, можно дополнительно обработать (например, исключить лишние стили или ссылки, сделать относительные пути абсолютными и т.п.), либо сразу вывести ее на экран функцией PHP print или echo. В приведенном ниже примере единственная вызываемая пользователем парсера функция parser вызывает 2 остальные функции и дополнительно один раз шлет заголовок с кодировкой документа (если модуль работает из готового движка, блок с вызовом header нужно убрать).

function parser ($host,$path,$start,$end,$include) {
 //Основной вызов парсера:
 //$host, $path - хост без http://www. и путь к файлу, начиная с /
 //$start, $end - строки начала и конца извлекаемого содержимого
 //$include - если true, включать в вывод строки $start и $end
 static $first=true;
 $s= get_URL_by_socket ($host,$path); 
 if ($first) { //Заголовок посылается только при 1-м вызове
 $first=false; //Если вызывается из "движка" - можно убрать этот блок
 header('Content-type:text/html;charset=windows-1251'); 
 }
 return process($s,$start,$end,$include);
}

Вызвать наш парсер можно, например, так:

$host="ngs.ru";
$path="/";
$start="<div class=\"temper\">";
$end="width=\"30\" height=\"15\"></td>\n</tr>\n</table>";
$include=true;
print parser ($host,$path,$start,$end,$include);

Здесь мы вытаскиваем краткий прогноз погоды с новосибирского городского сервера НГС. Обратите внимание, что все пробелы, которые были в полученном по адресу файле, я сохранил в строке параметра $end, а переносы строк заменил на \n:

Еще пример:

$s=parser ("pers.narod.ru","/index.html",
 "<title>","</title>",false);
print'<br>'.$s;

Здесь просто берется титул (содержимое тега TITLE) со странички pers.narod.ru. На основе этой статьи нетрудно модифицировать граббер под свои задачи.