Apitore blog

Apitoreを運営していた元起業家のブログ

JavaでRSSをパースするためにRomeを使ってみた

はじめに

最近話題なので、ちょっとキュレーションサイトでも作ろうと思います。TechCrunchみたいな情報サイトを自分好みに組み合わせるキュレーションです。情報サイトはRSSフィードを提供しているので、今回はRomeを使ってRSSフィードを使いやすい形にパースしました。

関連情報

ライブラリ

Romeは2017年3月16日時点で最新のライブラリを使いました。httpclientは場合によっては他のライブラリでバージョン違いが使われていることがまれによくあるので注意してください。

<dependency>
  <groupId>com.rometools</groupId>
  <artifactId>rome</artifactId>
  <version>1.7.1</version>
</dependency>
<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.5.3</version>
</dependency>

使ってみた

ソースコードは最後にシェアします。ちゃんと動作させるまでに結構苦労しました。特徴は以下の3点です。

  1. キャッシュが効いてる(はず)ので軽快
  2. リダイレクト(301,302)しててもちゃんと動作
  3. CDATAにUnicodeが入ってる場合はそこだけ取り除く

1 キャッシュ対応

下に抜粋したコードを載せます。このコードでキャッシュを効かせています(たぶん)。RSSフィードに更新がなければキャッシュを表示するので、軽快に動作します(たぶん)。

CloseableHttpClient client = HttpClients.createMinimal();
HttpUriRequest method = new HttpGet(rss);
CloseableHttpResponse response = client.execute(method);
InputStream stream = response.getEntity().getContent();
SyndFeedInput input = new SyndFeedInput();
feed = input.build(new XmlReader(stream));

2 リダイレクト対応

たまーに、httpからhttpsみたいなリダイレクトが発生します。Romeはリダイレクトは対応してくれないので、以下のコードでリダイレクト先のURLを取得します。こちらのブログを参考にさせていただきました。

private String getRedirectUrl(String rss) throws UnknownHostException, IOException {
  URL url = new URL(rss);
  String host = url.getHost();
  int port = url.getPort();
  if(port < 0) port = 80;
  try(
      Socket soc = new Socket(host, port);
      OutputStream os = soc.getOutputStream();
      PrintWriter pw = new PrintWriter(os);
      InputStream is = soc.getInputStream();
      InputStreamReader isr = new InputStreamReader(is);
      BufferedReader bur = new BufferedReader(isr);
      ){
    pw.printf("GET %s HTTP/1.1\r\n", url.getPath());
    pw.printf("Host: %s:%d\r\n", host, port);
    pw.print("\r\n");
    pw.flush();
    String line = bur.readLine();
    if(line == null) return rss;
    String[] status = line.split(" ");
    if(status.length < 2) return rss;
    switch(status[1]){
    case "301":
    case "302":
      break;
    default:
      return rss;
    }
    String result = null;
    while((line = bur.readLine()) != null){
      int pos = line.indexOf(':');
      if(pos < 0) break;
      String name = line.substring(0, pos);
      String value = line.substring(pos + 1);
      if(value.length() < 1) continue;
      value = value.substring(1);
      switch(name){
      case "Location":
        result = value;
        break;
      case "Last-Modified":
        break;
      default:
      }
    }
    if (result == null)
      result = rss;
    return result;
  }
}

3 CDATAに入るUnicode対応

たまーに、RSSフィードにCDATAタグが使われることがあって、たまーにUnicodeが混入します。Romeはこれにも対応していないので、Unicodeは除外します。こちらのブログを参考にさせていただきました。

private StringReader getTrimmedUnicodeXML(String rss) throws IOException {
  URL url = new URL(rss);
  HttpURLConnection urlconn = (HttpURLConnection)url.openConnection();
  urlconn.setRequestProperty("Accept-Charset", "UTF-8");
  urlconn.connect();
  BufferedReader reader = new BufferedReader(new InputStreamReader(urlconn.getInputStream(), "UTF-8"));
  String xml = "";
  while (true){
    String line = reader.readLine();
    if ( line == null ){
      break;
    }
    xml += line;
  }
  reader.close();
  urlconn.disconnect();
  xml = xml.replaceAll("[\\00-\\x08\\x0a-\\x1f\\x7f]", "");
  StringReader sreader = new StringReader(xml);
  return sreader;
}

ソースコード

全体のソースコードはこちらです。処理は3段構えになっています。最初は普通にRomeを使います。エラーが出たら、リダイレクトかどうかチェックします。まだ失敗するようなら、Unicodeが入っているかチェックします。それでも失敗するなら諦めます。一応、100個くらいのサイトのRSSでチェックしてエラーは出なかったので、下のコードはある程度ロバストだと思います。

public void getRss(String rss) {
  SyndFeed feed = null;
  try {
    CloseableHttpClient client1 = HttpClients.createMinimal();
    HttpUriRequest method1 = new HttpGet(rss);
    CloseableHttpResponse response1 = client1.execute(method1);
    InputStream   stream1  = response1.getEntity().getContent();
    SyndFeedInput input   = new SyndFeedInput();
    try {
      feed                  = input.build(new XmlReader(stream1));
    } catch (FeedException e) { //失敗したので、リダイレクトを疑う
      rss = getRedirectUrl(rss);
      CloseableHttpClient client2 = HttpClients.createMinimal();
      HttpUriRequest method2 = new HttpGet(rss);
      CloseableHttpResponse response2 = client2.execute(method2);
      InputStream   stream2  = response2.getEntity().getContent();
      try {
        feed                  = input.build(new XmlReader(stream2));
      } catch (FeedException e1) {//失敗したので、Unicodeを疑う
        try {
          feed                  = input.build(getTrimmedUnicodeXML(rss));
        } catch (FeedException e2) {//失敗したので、あきらめる
          System.err.println("FeedException");
          System.err.println(rss);
          return;
        }
      }
      stream2.close();
      response2.close();
      client2.close();
    }
    stream1.close();
    response1.close();
    client1.close();
  } catch (ClientProtocolException e) {
    System.err.println("ClientProtocolException");
    System.err.println(rss);
    return;
  } catch (IOException e) {
    System.err.println("IOException");
    System.err.println(rss);
    return;
  } catch (IllegalArgumentException e) {
    System.err.println("IllegalArgumentException");
    System.err.println(rss);
    return;
  }
  //成功
  System.out.println(feed.getTitle());
  System.out.println(feed.getLink());
  for( Object entry : feed.getEntries() )
  {
    SyndEntry e     = (SyndEntry ) entry;
    System.out.println(e.getAuthor());
    System.out.println(e.getTitle());
    System.out.println(e.getLink());
    if (e.getDescription() != null)
      System.out.println(e.getDescription().getValue());
    System.out.println(e.getPublishedDate());
  }
}
private String getRedirectUrl(String rss) throws UnknownHostException, IOException {
  URL url = new URL(rss);
  String host = url.getHost();
  int port = url.getPort();
  if(port < 0) port = 80;
  try(
      Socket soc = new Socket(host, port);
      OutputStream os = soc.getOutputStream();
      PrintWriter pw = new PrintWriter(os);
      InputStream is = soc.getInputStream();
      InputStreamReader isr = new InputStreamReader(is);
      BufferedReader bur = new BufferedReader(isr);
      ){
    pw.printf("GET %s HTTP/1.1\r\n", url.getPath());
    pw.printf("Host: %s:%d\r\n", host, port);
    pw.print("\r\n");
    pw.flush();
    String line = bur.readLine();
    if(line == null) return rss;
    String[] status = line.split(" ");
    if(status.length < 2) return rss;
    switch(status[1]){
    case "301":
    case "302":
      break;
    default:
      return rss;
    }
    String result = null;
    while((line = bur.readLine()) != null){
      int pos = line.indexOf(':');
      if(pos < 0) break;
      String name = line.substring(0, pos);
      String value = line.substring(pos + 1);
      if(value.length() < 1) continue;
      value = value.substring(1);
      switch(name){
      case "Location":
        result = value;
        break;
      case "Last-Modified":
        break;
      default:
      }
    }
    if (result == null)
      result = rss;
    return result;
  }
}
private StringReader getTrimmedUnicodeXML(String rss) throws IOException {
  URL url = new URL(rss);
  HttpURLConnection urlconn = (HttpURLConnection)url.openConnection();
  urlconn.setRequestProperty("Accept-Charset", "UTF-8");
  urlconn.connect();
  BufferedReader reader = new BufferedReader(new InputStreamReader(urlconn.getInputStream(), "UTF-8"));
  String xml = "";
  while (true){
    String line = reader.readLine();
    if ( line == null ){
      break;
    }
    xml += line;
  }
  reader.close();
  urlconn.disconnect();
  xml = xml.replaceAll("[\\00-\\x08\\x0a-\\x1f\\x7f]", "");
  StringReader sreader = new StringReader(xml);
  return sreader;
}

おわりに

RSSフィードのパースはRomeを使うと便利ですね。細かい調整は必要でしたが、これで色んなサイトの情報を自由に組み合わせられそうです。 このあとどうするかですが、まずは「RSS2Json」のAPIを出します。RSSを入力にJsonを出力するものです。かつてはGoogleがRSS2JsonのAPIを提供していたみたいですが、なぜかやめちゃいました。Jsonで扱えたほうがラクな場合も多いと思うので、便利に使ってください。 次に、せっかく100個のサイトのRSSを集めたので、適当にジャンル分けして、ジャンル毎にまとめたフィードAPIを出します。有名人ブログ、2ちゃんまとめ、テック、証券、旅行、デザイン、ゲーム、ニュースの8ジャンルです。 ここまでは比較的すぐにAPIを提供できると思います。少し時間がかかると思いますが、最後は機械学習でニュース仕分けをやろうと思います。イメージとしては「俺専用、俺の好みを熟知した、情報収集担当の(美人)秘書API」です。ゆくゆくは皆さんにも公開して、皆さん個々人専用の(イケメン/美人)秘書APIを提供したいです。お楽しみに。

参考