RSS из любого сайта средствами PHP
Здравствуйте, хабра-сообщество! В своём первом посте хотелось бы затронуть тему сбора данных с любого сайта, и формирования из них лент RSS.
Предисловие
Когда в нашем городе только провели интернет, и домашний компьютер начал постигать просторы всемирной паутины, в этой паутине начали появляться сайты, которые хочется посещать многократно, чтобы быть в курсе каких то новостей/новинок. Список избранного в браузере стал потихоньку пополняться ссылками на разнообразные ресурсы. Как только появлялась свободная минутка, я быстренько пробегался по этим сайтам и смотрел что там нового. Пока список этот был небольшим, это не вызывало особой сложности. Но время шло, список рос, пробегаться стало сложно, тогда я для себя открыл RSS, который до этого казался мне чем то непонятным/ненужным.
Так началась у меня эпоха использования RSS-клиента (Mozilla Thunderbird). Но и с помощью этого способа остались некоторые неудобства, а точнее появилась проблема синхронизации рабочего и домашнего компьютера: то что прочитал на работе не будет отмечено как прочитанное дома. На помощь пришёл великий Google Reader, мир стал прекраснее, чудеснее и лучше. Но, за исключением одного большого «НО». Есть единичные, но очень нужные сайты, новости с которых очень хочется почитать в Google Reader, но RSS в них разработчики не предусмотрели…
Варианты
Я стал искать решения, для того чтобы побороть эту проблему. И они находились: Feed43, feedfire.com, но по тем или иным соображениям они мне не подходили. Уж больно ленты специфичные мне потребовались.
Задача
Выдернуть новости с этого сайта и с этого форума (на момент, когда я писал этот код, на форуме не было RSS-подписки на темы).
Сложность первого сайта в том, что там нет семантики, и какого то отделения новостей. Новости — на нём текст сплошняком. А второго как раз наоборот, в сложности и излишних данных. Я расчехлил шашечки и ринулся писать код.
Решение
- использование CSS-синтаксиса для поиска элементов
- использование регулярных выражений для дополнительной фильтрации
Класс CHTML2RSS (файл CHTML2RSS.class.php):
params = $params; > // загружаем страницу и немного обрабатываем function getDocument($url) < $cnt = $this->params['charset'] ? '' : ''; $cnt.=file_get_contents($url); $cnt = preg_replace('/(?s)/', '', $cnt); // bug with CDATA $this->dom = new DomDocument(); @$this->dom->loadHTML($cnt); > // callback для обхода DOM function domWalk($root, $callback, &$args) < if($root->childNodes) foreach($root->childNodes as $i => $elem) < call_user_func_array($callback, array_merge(array($elem), $args)); $this->domWalk($elem, $callback, $args); > > // callback для фильтрации в стиле CSS function walkCallback($elem, &$attr, $result, $idx) < $add=true; $filter = true; foreach($attr as $sel) < if(!$add) break; switch($sel[1]) < case '': if($elem->nodeName!=$sel[2]) $add=false; break; case '.': if(!$elem->attributes) < $add = false; break; >$node=$elem->attributes->getNamedItem('class'); if(!$node || $node->nodeValue != $sel[2]) $add=false; break; case '#': if(!$elem->attributes) < $add = false; break; >$node=$elem->attributes->getNamedItem('id'); if(!$node || $node->nodeValue != $sel[2]) $add=false; break; case ':': switch($sel[2]) < case 'eq': if($idx!=$sel[3]) $filter = false; break; case 'lt': if($idx>=$sel[3]) $filter = false; break; case 'gt': if($idx <=$sel[3]) $filter = false; break; >break; > > if($add) $idx++; if($add && $filter) $result[]=$elem; > function parseDom($selector, $parent = false) < $result=array(); $arr=explode(' ',$selector); $root = $parent ? $parent : array($this->dom); foreach($arr as $item) < preg_match_all('/(^|\.|#|:)(\w+)(?:\((\d+)\))*/', $item, $attr, PREG_SET_ORDER); $newRoot=array(); $idx = 0; $attr=array($attr, &$newRoot, &$idx); foreach($root as $elem) $this->domWalk($elem, array($this, 'walkCallback'), $attr); $root=$newRoot; > return $root; > // получение outerHTML для элемента function outerHTML($element) < $d = new DomDocument(); $d->appendChild($d->importNode($element, true)); return html_entity_decode($d->saveHTML(), ENT_COMPAT, 'UTF-8'); > // получение innerHTML для элемента function innerHTML($element) < $d = new DomDocument(); foreach($element->childNodes as $node) $d->appendChild($d->importNode($node, true)); return html_entity_decode($d->saveHTML(), ENT_COMPAT, 'UTF-8'); > // основная функция парсинга function parseHTML() < $this->getDocument($this->params['url']); $v = array(); $elems = $this->parseDom($this->params['elems'][0]); if($this->params['elems'][1]) foreach($elems as $elem) < preg_match_all($this->params['elems'][1], $this->outerHTML($elem), $vi, PREG_SET_ORDER); $v = array_merge($v, $vi); > else < // удаляем if($this->params['remove']) foreach($this->params['remove'] as $sel) < $delElems = $this->parseDom($sel, $elems); foreach($delElems as $elem) $elem->parentNode->removeChild($elem); > // ищем совпадения foreach($elems as $i => $elem) < $v[$i] = array(); foreach($this->params['search'] as $srch) < if($srch[0]) < $ei = $this->parseDom($srch[0], array($elem)); if(!count($ei)) continue; $ei = $ei[0]; > else $ei = $elem; if($srch[1]) < preg_match($srch[1], $this->outerHTML($ei), $vi); $v[$i] = array_merge($v[$i], $vi); > else $v[$i][] = $this->innerHTML($ei); > > > return $v; > // вывод RSS function showRSS() < $v = $this->parseHTML(); echo ' '.htmlspecialchars($this->params['rdesc']).' '.htmlspecialchars($this->params['rlink']).''; if($this->params['reverse']) $v = array_reverse($v); foreach($v as $item) < echo " - \n"; echo " \n"; echo " ".htmlspecialchars(vsprintf($this->params['ilink'], $item))."\n"; echo "
params['idesc'], $item)."]]> \n"; if($this->params['idate']) echo " ".htmlspecialchars(vsprintf($this->params['idate'], $item))." \n"; if($this->params['guid']) echo " ".htmlspecialchars(vsprintf($this->params['guid'], $item))." \n"; echo " \n"; > echo END; > > ?>
Использование (файл html2rss.php):
array ( 'url' => 'http://news.metro.ru/', 'charset' => 'windows-1251', 'elems' => array('tr#mnews td', '/([^<>]*)(.+)/'), 'search' => false, 'reverse' => false, 'rtitle' => 'news.metro.ru', 'rdesc' => 'Moscow Subway - Metro de Moscou - Москва, метрополитен', 'rlink' => 'http://news.metro.ru/', 'ititle' => 'новость от %2$s', 'idesc' => '%3$s', 'ilink' => 'http://news.metro.ru/', 'idate' => false, 'guid' => false ), '4pda' => array ( 'url' => 'http://4pda.ru/forum/index.php?showtopic=116079&st=9999', 'charset' => false, 'elems' => array('.borderwrap .ipbtable:gt(0)', false), 'remove' => array('span.edit'), 'search' => array ( array('.postdetails a', false), array('.postdetails a', '/href="([^"]+)"/'), array('.postcolor', false) ), 'reverse' => true, 'rtitle' => '4pda.ru - HTC MAX 4G - обсуждение', 'rdesc' => '4pda.ru - HTC MAX 4G - обсуждение', 'rlink' => 'http://4pda.ru/forum/index.php?showtopic=116079&st=9999', 'ititle' => 'пост %1$s', 'idesc' => '%4$s', 'ilink' => '%3$s', 'idate' => false, 'guid' => false ) ); header('Content-Type: text/xml; charset=utf-8'); if(!isset($_GET['source'])) exit('Укажите источник новостей!'); if(!isset($config[$_GET['source']])) exit('Источника не существует!'); //file_put_contents(dirname(__FILE__).'/update.log', date('Y-m-d h:i:s')." \r\n", FILE_APPEND); $h2r = new CHTML2RSS($config[$_GET['source']]); $h2r->showRSS(); ?>
Использование
В файле html2rss.php пример использования, для ресурсов которые я привёл. Создаётся экземпляр класса CHTML2RSS с необходимыми параметрами, где:
url — адрес страницы для парсинга
charset — кодировка страницы, если не UTF-8
elems — массив из CSS-запроса и регулярного выражения для отделения новостей
remove — какие элементы нужно удалить из новостей
search — здесь можно осуществить поиск дополнительных параметров, для выставления заголовков/ссылок на новость/id новости
reverse — перевернуть порядок записей на обратный
rtitle — заголовок ленты
rdesc — описание ленты
rlink — ссылка на сайт ленты
ititle — шаблон заголовка записи, здесь можно применять переменные которые мы нашли с использованием параметра search, вывод происходит с помощью функции PHP sprintf, в которую передаётся массив найденных значений, которые мы ищем с помоoью параметра search
idesc — текст записи, аналогично с помощью sprintf
ilink — ссылка на запись
idate — дата записи
guid — уникальный идентификатор записи
Недостатки и сложности
При разработке мне пришлось столкнуться с одним багом в функции php loadHTML, суть в том что мета-тэг с заданием нужной кодировки должен быть указан ДО того, как в тексте встретится любой текст. Из-за этой проблемы не парсилась страница из news.metro.ru, т. к. тэг был до тэга .
Так же можно в коде не реализовано, хотя было бы лучше, если применять нативный xpath, нежели реализацию фильтрации CSS (хотя так более понятнее).
PHP RSS Feed
RSS icon you see on some websites that mean, the RSS feed is available on site.
XML: RSS feed basic structure
Webpage URL About Webpage en-us - Article URL
Article Content
SQL: Creating the MySQL RSS Table
CREATE TABLE rss_info ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200), link VARCHAR(200), description TEXT );
Creating a valid RSS 2.0 feed with PHP
Feed link will be something like this: http://www.example.com/rss.php
$sql = "SELECT * FROM rss_info ORDER BY id DESC LIMIT 20"; $query = mysqli_query($con,$sql); header( "Content-type: text/xml"); echo " https://www.w3schools.in/ Cloud RSS en-us "; while($row = mysqli_fetch_array($con,$query)) < $title=$row["title"]; $link=$row["link"]; $description=$row["description"]; echo "- $link
$description "; > echo " "; ?>
Reading RSS with PHP
To get RSS content, you can use this script.
load("rss.xml");//XML page URL $content = $domOBJ- >getElementsByTagName("item"); foreach( $content as $data ) < $title = $data->getElementsByTagName("title")->item(0)->nodeValue; $link = $data->getElementsByTagName("link")->item(0)->nodeValue; echo "$title :: $link"; > ?>