В инете есть куча примеров RSS-грабберов, извлекающих тексты с новостных лент, но мне лично нужен не какой-то RSS, которым я ни разу в жизни не пользовался, а полноценный скрипт, который легко настроить для извлечения любой нужной мне информации с любой из доступных в сети страниц.
Так что эта небольшая статья - как раз пример написания граббера на языке PHP.
Задача состоит, собственно, из 3 этапов.
Для этого в 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. На основе этой статьи нетрудно модифицировать граббер под свои задачи.