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実行スクリプト

定期的に実行するためのスクリプト作成 rss2db.php

<?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に保存することができた