概要

RSSをパースする用事があったのでJavaのソレ系ライブラリのROMEを使ってみた。

一口にRSSと言っても、世の中にはRSS0.9、RSS1.0、RSS2.0、Atomなどいろんなものが存在する。単一の形式であれば自前でXMLをパースするのもありだけど、いろんなサイトに対応しようとする時はこうしたライブラリを使った方が現実的。

@CretedDate 2012/01/23
@Versions Rome1.0, Java6

情報源

公式サイトはjava.netにもあるみたいだけど、そっちはだいぶ荒んだ状態になっていた。下記のページが一番素性が良さそうに見える。

Home - ROME - Confluence
https://rometools.jira.com/wiki/display/ROME/Home

上記URLによると、ROMEは以下の形式に対応しているらしい。

RSS 0.90, RSS 0.91 Netscape, RSS 0.91 Userland, RSS 0.92, RSS 0.93, RSS 0.94, RSS 1.0, RSS 2.0, Atom 0.3, Atom 1.0

これだけ揃っていれば十分だろう。ライセンスもApache 2.0 licenseと比較的使いやすいものになっている。

使い方はチュートリアル的なものがある

Rome05Tutorials
http://wiki.java.net/bin/view/Javawsxml/Rome05Tutorials

導入

まず、下記のサイトからROMEのjarを落とす。

Home - ROME - Confluence
https://rometools.jira.com/wiki/display/ROME/Home

次にJDOMに依存しているらしいので下記のサイトからJDOMのjarも落とす。

JDOM: Binaries
http://www.jdom.org/downloads/index.html

上記2つのjarをクラスパスに通せば利用可能。

Mavenを利用する場合は、ちゃんとセントラルレポジトリに登録されているらしい。

Maven Repository: rome
http://mvnrepository.com/artifact/rome/rome

RSSをパースしてみる

サンプルとしてgihyo.jpのRSSフィードを使ってみる。gihyo先生はRSSフィードをRSS1.0、RSS2.0、Atomの3種類用意している。

http://gihyo.jp/feed

これを利用して、3つの形式を同じようにパースできるか調べてみる。

import java.net.URL;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

public class RomeSample {
    public static void main(String[] args) throws Exception {
        String url = "http://rss.rssad.jp/rss/gihyo/feed/rss1";
        // String url = "http://rss.rssad.jp/rss/gihyo/feed/rss2";
        // String url = "http://rss.rssad.jp/rss/gihyo/feed/atom";

        URL feedUrl = new URL(url);
        SyndFeedInput input = new SyndFeedInput();

        SyndFeed feed = input.build(new XmlReader(feedUrl));
        // サイトのタイトル
        System.out.println(feed.getTitle());
        // サイトのURL
        System.out.println(feed.getLink());

        for (Object obj : feed.getEntries()) {
            SyndEntry entry = (SyndEntry) obj;
            // 記事タイトル
            System.out.println(entry.getTitle());
            // 記事のURL
            System.out.println(entry.getLink());
            // 記事の詳細
            System.out.println(entry.getDescription().getValue());
        }
    }
}

上記のコードを実行すると、3つの違う形式のXMLが同じ結果を返した。

ところでTutorialのサンプルコードでは上記のようにXmlReaderをcloseしてなかったけど、良いのだろうか。

試しにlocalhostを対象に10万回ほど上記処理をループしてみたところ、メモリの使用量が増えたりすることもなく無事に処理が終了したけど、実際使う時は気分的にcloseしておこうと思う。

SyndFeedから取れる情報

Romeを使ってRSSをパースした結果は、サイト全体の情報はSyndFeed、各エントリーの情報はSyndEntryに入って渡されるらしい。

試しにはてなYahooニュースのRSS(インド新聞)をパースしてみたところ、SyndFeedには以下の情報が入っていた。

URL feedUrl = new URL("http://b.hatena.ne.jp/entrylist?sort=hot&mode=rss");
SyndFeedInput input = new SyndFeedInput();
SyndFeed feed = input.build(new XmlReader(feedUrl));

System.out.println("author : " + feed.getAuthor());
  //=> null
System.out.println("authors : " + feed.getAuthors());
  //=> []
System.out.println("categories : " + feed.getCategories());
  //=> []
System.out.println("contributors : " + feed.getContributors());
  //=> []
System.out.println("description : " + feed.getDescription());
  //=> Yahoo! JAPANのニュースに掲載されている記事の最新の見出しを提供しています。
System.out.println("encoding : " + feed.getEncoding());
  //=> null
System.out.println("feedType : " + feed.getFeedType());
  //=> rss_2.0
System.out.println("image : " + feed.getImage().getTitle());
  //=> Yahoo! ニュース
System.out.println("image : " + feed.getImage().getUrl());
  //=> http://i.yimg.jp/images/news/yjnews_s.gif
System.out.println("language : " + feed.getLanguage());
  //=> ja
System.out.println("title : " + feed.getTitle());
  //=> Yahoo!ニュース - 海外 - インド新聞
System.out.println("link : " + feed.getLink());
  //=> http://news.search.yahoo.co.jp/search?to=2&rkf=1&ei=...(長いので略
System.out.println("uri : " + feed.getUri());
  //=> null

SyndEntryで取れる情報

次にSyndEntry(各エントリー)の情報。うちのブログのRSSをパースしてみたところ、以下のような情報が取れた。

URL feedUrl = new URL("http://blog.mwsoft.jp/index.rdf");
SyndFeedInput input = new SyndFeedInput();
SyndFeed feed = input.build(new XmlReader(feedUrl));
SyndEntry entry = (SyndEntry) feed.getEntries().get(0);

System.out.println("author : " + entry.getAuthor());
  //=> MW
System.out.println("link : " + entry.getLink());
  //=> http://blog.mwsoft.jp/article/53192188.html
System.out.println("title : " + entry.getTitle());
  //=> scala.swingを試してみる
System.out.println("uri : " + entry.getUri());
  //=> http://blog.mwsoft.jp/article/53192188.html
System.out.println("contributors : " + entry.getContributors());
  //=> []
System.out.println("enclosures : " + entry.getEnclosures());
  //=> []
System.out.println("links : " + entry.getLinks());
  //=> []
System.out.println("publishedDate : " + entry.getPublishedDate());
  //=> Fri Jan 20 00:35:19 JST 2012
System.out.println("updatedDate : " + entry.getUpdatedDate());
  //=> null

SyndContent contents = (SyndContent)entry.getContents().get(0);
System.out.println("contentType : " + contents.getType());
  //=> html
System.out.println("contentValue : " + contents.getValue());
  //=> 持ち運び可能な簡易なGUIアプリを作る必要があったので...(以下略

SyndContent description = (SyndContent)entry.getDescription();
System.out.println("descriptionType : " + description.getType());
  //=> text/plain
System.out.println("descriptionValue : " + description.getValue());
  //=> 持ち運び可能な簡易なGUIアプリを作る必要があったので...(以下略

RomeFetcherで結果をキャッシュする

RSSは最新情報を一定間隔で確認しに行くものなので、lastModifiedとかを確認して更新がなければ取得しないようなキャッシュ機能が欲しくなる。

そうした動作はRomeFetcherを使えばなんとかなるという話を聞いた。RSSの取得状況をキャッシュして、2回目にリクエストする時はIf-Modified-Sinceしたり、If-None-MatchでETagを渡したりしてくれるらしい。

https://rometools.jira.com/browse/FETCHER/

RomeFetcherもRomeと同じでMavenのセントラルレポジトリに登録されている。

http://mvnrepository.com/artifact/rome/rome-fetcher

簡易なサンプルコード。

FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getInstance();
HttpURLFeedFetcher feedFetcher = new HttpURLFeedFetcher(feedInfoCache);
SyndFeed feed = feedFetcher.retrieveFeed(new URL("http://rss.rssad.jp/rss/gihyo/feed/rss1"));

上記のようにretrieveFeedで実行すると、リクエストしたことがあるURLに対しては、If-Modified-SinceやIf-None-Matchを指定してリクエストし、「304 Not Modified」が帰ってきた場合はキャッシュから結果を引き出す。

この子をMavenで入れたらcommons-loggingとかcommons-httpclientあたりの大きめのアプリを作る時に競合しやすそうな子が入ってしまった。この辺は使わなくてもなんとかなるので、個人的にはexclusion指定して使っている。

    <dependencies>
        <dependency>
            <groupId>rome</groupId>
            <artifactId>rome-fetcher</artifactId>
            <version>1.0</version>
            <exclusions>
                <exclusion>
                    <groupId>xerces</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>commons-httpclient</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

304が返った場合は処理をスキップするだけで良ければ、lastModifiedとETagだけ自前でキャッシュして304が返ったらExceptionするようなコードを自前で書いてしまっても良いような気がする。

以下のようなイメージで。

HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setIfModifiedSince(lastModified);
conn.setRequestProperty("If-None-Match", etag);

if (conn.getResponseCode() == 304) {
  conn.getInputStream().close();
  throw new MyException("304 : " + url);
}

XmlReader reader = new XmlReader(conn.getInputStream());
SyndFeed feed = input.build(reader);

その他

Tutorialには指定のフォーマットを別のフォーマットにコンバートするとか、Servletで結果を返すとかも載っている。けど、個人的にその辺の機能は使う予定はないのでノータッチ。

詳細はチュートリアル参照。

Rome05Tutorials
http://wiki.java.net/bin/view/Javawsxml/Rome05Tutorials