PHPを使ってRSSをMySQLへ保存
RSSをHTTPで取得して各itemをパース→DBへ書き込み
上記をcronで定期的に実行
作成したプログラム
RSSを取得してパースするクラス
DBへの書き込みクラス
cron実行スクリプト
RSSの取得
phpのfile_get_contentsでRSSのURLを渡して取得
ステータスコードが200場合のみ処理を続行
RSSにはRSS1.0, RSS2.0, Atomなどいくつかの種類があり、要素名が各フォーマットにによって異なるためそれぞれに応じたParserが必要
タグ | RSS1.0 | RSS2.0 | Atom |
---|---|---|---|
要素 | item | channel | entry |
タイトル | title | title | title |
リンク | link | link | link |
説明 | description | description | description |
日付 | dc:date | pubDate | issued |
それぞれ以下のようなフォーマット
RSS1.0(RDF)
<?xml version="1.0" encoding="UTF-8" ?> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#" xmlns:media="http://search.yahoo.com/mrss" xmlns:opensearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"> <channel rdf:about="http://b.hatena.ne.jp/hotentry"> <title>はてなブックマーク - 人気エントリー</title> <link>http://b.hatena.ne.jp/hotentry</link> <atom10:link rel="self" type="application/rdf+xml" href="http://feeds.feedburner.com/hatena/b/hotentry" xmlns:atom10="http://www.w3.org/2005/Atom" /> <atom10:link rel="hub" href="http://pubsubhubbub.appspot.com/" xmlns:atom10="http://www.w3.org/2005/Atom" /> <feedburner:info uri="hatena/b/hotentry" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" /> <description>最近の人気エントリー</description> <items> <rdf:Seq> <rdf:li rdf:resource="http://anond.hatelabo.jp/20140516005154" /> <rdf:li rdf:resource="http://www.cinematoday.jp/movie/T0019009" /> ・・・ </rdf:Seq> </items> </channel> <item rdf:about="http://anond.hatelabo.jp/20140516005154"> <title>文系学部って必要なの?</title> <link>http://anond.hatelabo.jp/20140516005154</link> <content:encoded>・・・</content:encoded> <dc:date>2014-05-17T12:27:53+09:00</dc:date> <dc:subject>学び</dc:subject> <hatena:bookmarkcount>68</hatena:bookmarkcount> <description>もうちょっと詳しく言うと、旧帝大クラス以下のレベルの大学で文系学部って必要なの?安倍政権の大学改革で、文系不要論が台頭してるので、文系の皆さんが大騒ぎしてるんだけど、理系の自分としては、複雑な心境になるんだよね。それって自業自得ってところもあるんじゃないの?っていうか。旧帝大クラスの文系でも,ぬるま湯につかりすぎだろって、理系の人間なら誰でも思ってるんじゃないかな。毎年毎年授業は変わり映えもせず、...</description> </item> <item rdf:about="http://www.cinematoday.jp/movie/T0019009"> <title>映画『デスブログ 劇場版』 - シネマトゥデイ</title> ・・・
RSS2.0
<?xml version="1.0" encoding="UTF-8" ?> <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#" xmlns:media="http://search.yahoo.com/mrss" xmlns:opensearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"> <channel> <title>はてなブックマーク - 人気エントリー</title> <link>http://b.hatena.ne.jp/hotentry</link> <description>最近の人気エントリー</description> <item> <title>文系学部って必要なの?</title> <link>http://anond.hatelabo.jp/20140516005154</link> <category>学び</category> <content:encoded>・・・</content:encoded> <guid isPermaLink="true">http://anond.hatelabo.jp/20140516005154</guid> <hatena:bookmarkcount>63</hatena:bookmarkcount> <pubDate>Sat, 17 May 2014 12:27:53 +0900</pubDate> <description>もうちょっと詳しく言うと、旧帝大クラス以下のレベルの大学で文系学部って必要なの?安倍政権の大学改革で、文系不要論が台頭してるので、文系の皆さんが大騒ぎしてるんだけど、理系の自分としては、複雑な心境になるんだよね。それって自業自得ってところもあるんじゃないの?っていうか。旧帝大クラスの文系でも,ぬるま湯につかりすぎだろって、理系の人間なら誰でも思ってるんじゃないかな。毎年毎年授業は変わり映えもせず、...</description> </item> <item> <title>映画『デスブログ 劇場版』 - シネマトゥデイ</title> ・・・
Atom
<?xml version="1.0" encoding="UTF-8" ?> <feed xmlns="http://purl.org/atom/ns#" version="0.3" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#" xmlns:media="http://search.yahoo.com/mrss" xmlns:opensearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"> <title type="text/plain">はてなブックマーク - 人気エントリー</title> <link rel="alternate" type="text/html" href="http://b.hatena.ne.jp/hotentry" /> <author> <name></name> </author> <tagline type="text/html" mode="escaped">最近の人気エントリー</tagline> <entry> <title type="text/plain">文系学部って必要なの?</title> <link rel="alternate" type="text/html" href="http://anond.hatelabo.jp/20140516005154" /> <content type="text/html" mode="escaped">もうちょっと詳しく言うと、旧帝大クラス以下のレベルの大学で文系学部って必要なの?安倍政権の大学改革で、文系不要論が台頭してるので、文系の皆さんが大騒ぎしてるんだけど、理系の自分としては、複雑な心境になるんだよね。それって自業自得ってところもあるんじゃないの?っていうか。旧帝大クラスの文系でも,ぬるま湯につかりすぎだろって、理系の人間なら誰でも思ってるんじゃないかな。毎年毎年授業は変わり映えもせず、...</content> <content:encoded>。。。</content:encoded> <hatena:bookmarkcount>68</hatena:bookmarkcount> <id>http://anond.hatelabo.jp/20140516005154</id> <issued>2014-05-17T12:27:53+09:00</issued> <modified>2014-05-17T12:27:53+09:00</modified> </entry> <entry> <title type="text/plain">映画『デスブログ 劇場版』 - シネマトゥデイ</title> ・・・
RSSのパース
simplexml_load_stringでXMLを扱えるように変換
RSSParserからフォーマットに応じてそれぞれのXMLをパースできるようにする
RSSParser.php
<?php require_once(Common::ROOT_DIR . '/parser/RSS10Parser.php'); require_once(Common::ROOT_DIR . '/parser/RSS20Parser.php'); require_once(Common::ROOT_DIR . '/parser/AtomParser.php'); class RSSParser { private $rss10Parser; private $rss20Parser; private $atomParser; private $items; public function __construct() { $this->rss10Parser = new RSS10Parser(); $this->rss20Parser = new RSS20Parser(); $this->atomParser = new AtomParser(); $this->items = array(); } public function parse($url) { $content = file_get_contents($url); $rss = simplexml_load_string($content, 'SimpleXMLElement', LIBXML_NOCDATA); # Status codeが200以外は処理しない $statusCode = $http_response_header[0]; if (strpos($statusCode, '200') == false) { fputs(STDERR, "Unexpected status code: $statusCode\n"); return; } $type = $rss->getName(); if ($type == 'RDF') { foreach ($rss->item as $item) { array_push($this->items, $this->rss10Parser->parse($item)); } } else if ($type == 'rss') { foreach ($rss->channel->item as $item) { array_push($this->items, $this->rss20Parser->parse($item)); } } else if ($type == 'feed') { foreach ($rss->entry as $item) { array_push($this->items, $this->atomParser->parse($item)); } } else { fputs(STDERR, "Unexpected type: $type\n"); } return $this->items; } }
RSS10Parser.php
<?php require_once(Common::ROOT_DIR . '/parser/AbstractParser.php'); class RSS10Parser extends AbstractParser { Const DC_NAMESPACE = 'http://purl.org/dc/elements/1.1/'; public function getTitle($item) { return $item->title; } public function getLink($item) { return $item->link; } public function getDescription($item) { if (isset($item->description)) { return $item->description; } else { return null; } } public function getDate($item) { if (isset($item->children(self::DC_NAMESPACE)->date)) { return $item->children(self::DC_NAMESPACE)->date; } else { return null; } } }
RSS20Parser.php
<?php require_once(Common::ROOT_DIR . '/parser/AbstractParser.php'); class RSS20Parser extends AbstractParser { public function getTitle($item) { return $item->title; } public function getLink($item) { return $item->link; } public function getDescription($item) { if (isset($item->description)) { return $item->description; } else { return null; } } public function getDate($item) { if (isset($item->pubDate)) { return $item->pubDate; } else { return null; } } }
AtomParser.php
<?php require_once(Common::ROOT_DIR . '/parser/AbstractParser.php'); class AtomParser extends AbstractParser { public function getTitle($item) { return $item->title; } public function getLink($item) { return $item->link->attribute()->href; } public function getDescription($item) { if (isset($item->content)) { return $item->content; } else { return null; } } public function getDate($item) { if (isset($item->published)) { return $item->published; } else { return null; } } }
Parserの抽象クラス AbstractParser.php
<?php abstract class AbstractParser { abstract protected function getTitle($item); abstract protected function getLink($item); abstract protected function getDescription($item); abstract protected function getDate($item); public function parse($item) { $items['title'] = (string)$this->getTitle($item); $items['link'] = (string)$this->getLink($item); $items['description'] = (string)$this->getDescription($item); $items['date'] = (string)$this->getDate($item); return $items; } }
DBへの書き込み
parseした各情報をDBに書き込む
<?php class DBManager { private $pdo; public function __construct($dbname, $host, $user, $pass) { try { $this->pdo = new PDO( "mysql:dbname=$dbname;host=$host", $user, $pass, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::MYSQL_ATTR_READ_DEFAULT_FILE => '/etc/my.cnf',)); $this->pdo->query("SET NAMES utf8"); } catch (PDOException $e) { die($e->getMessage()); } } public function registerItems($item, $rssUrl) { if($this->itemExists($item['link'])) { // 既に存在していれば更新 $this->updateItems($item, $rssUrl); } else { $this->insertItems($item, $rssUrl); } } public function itemExists($link) { $stmt = $this->pdo->prepare(implode(' ', array( 'SELECT link', 'FROM items', 'WHERE link = ?', 'LIMIT 1', ))); $stmt->execute(array($link)); return (bool)$stmt->fetch(); } public function insertItems($item, $rssUrl) { $this->pdo->beginTransaction(); try { $stmt = $this->pdo->prepare(implode(' ', array( 'INSERT', 'INTO items(link, title, description, date, rss_url, created, modified)', 'VALUES (?, ?, ?, ?, ?, NOW(), NOW())', ))); $stmt->execute(array( $item['link'], $item['title'], $item['description'], $item['date'], $rssUrl)); $id = $this->pdo->lastInsertId(); $this->pdo->commit(); return $id; } catch (Exception $e) { $this->pdo->rollBack(); throw $e; } } public function updateItems($item, $rssUrl) { $this->pdo->beginTransaction(); try { $stmt = $this->pdo->prepare(implode(' ', array( 'UPDATE', 'items SET title=?, description=?, date=?, rss_url=?, modified=NOW()', 'WHERE link = ?' ))); $stmt->execute(array( $item['title'], $item['description'], $item['date'], $rssUrl, $item['link'])); $id = $this->pdo->lastInsertId(); $this->pdo->commit(); return $id; } catch (Exception $e) { $this->pdo->rollBack(); throw $e; } } }
cron実行スクリプト
<?php require_once(Common::ROOT_DIR . '/DBManager.php'); require_once(Common::ROOT_DIR . '/RSSParser.php'); $lines = file_get_contents(Common::ROOT_DIR . '/rssList.txt'); // 改行文字を取り除く $lines = explode("\n", $lines); $rssParser = new RSSParser(); $dbManager = new DBManager(Common::DB_NAME, Common::DB_HOST, Common::DB_USER, Common::DB_PASS); $items = array(); foreach ($lines as $rssUrl) { # 空のurlは処理しない if (empty($rssUrl)) { continue; } // Parse RSS $items = $rssParser->parse($rssUrl); // To DB foreach ($items as $item) { $dbManager->registerItems($item, $rssUrl); } } // Disconnect DB $dbManager = null;
これをcronに設定。1時間に一回スクリプトが実行される設定
0 * * * * /usr/bin/php /var/www/html/rss2db.php >/tmp/crontab.log 2>&1
これで定期的にRSSを取得しDBに保存することができた