はてなモノリスAPIが欲しい。。。でも、無いのでしょうがなくなんとかする。

はてなモノリスにて、自分が投稿したデータを取得したい。

はてなモノリスのRSSには最新の投稿10件分が載っている。


http://mono.hatena.ne.jp/ユーザーID/rss

そこに page=1303205041 という感じのよくわからないパラメータを付けると、もう10件古い投稿データが取得できる。


http://mono.hatena.ne.jp/ユーザーID/rss?page=1342339765

この page パラメータはユーザーのプロフィールページ(自分の投稿が並んでいるページ)


http://mono.hatena.ne.jp/ユーザーID/

とか、プロフィールページの2ページ目、3ページ目


http://mono.hatena.ne.jp/ユーザーID/?page=1342339765

とか、これらのページの rel="next" を持つ HTML の要素にある href 属性から取得できる。


<a href="?page=1342339765" rel="next">

なので、プロフィールページをスクレイピングして page パラメータを取得しつつ、その page パラメータを付加した RSS フィードから投稿データを取得していく。

以下、Java による処理を行うソースコード。外部ライブラリとして、HTMLパーサのJTidyと、フィードパーサのROMEを使っている。


import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
 
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
 
import org.w3c.dom.Document;
import org.w3c.tidy.Tidy;
 
import org.jdom.Element;
 
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 HatenaMonolith {
 
  public static void main(String[] args) throws Exception {
    HatenaMonolith mono = new HatenaMonolith("nilab");
    // 全部取得
    while(true){
      Entry[] entries = mono.nextEntries();
      if(entries == null){
        break;
      }
      for(Entry entry : entries){
        System.out.println(entry.title);
      }
      System.out.println(mono.getPage());
      Thread.sleep(1000); // BANされないように1秒スリープ
    }
  }
  
  private final String username;
  private String page;
  private boolean end = false;
 
  /**
   * HatenaMonolith コンストラクタ。
   * @param username はてなのユーザーID
   */
  public HatenaMonolith(String username){
    this.username = username;
    this.page = null;
  }
 
  /**
   * HatenaMonolith コンストラクタ。
   * @param username はてなのユーザーID
   * @param page page パラメータ
   */
  public HatenaMonolith(String username, String page){
    this.username = username;
    this.page = page;
  }
  
  public String getPage(){
    return page;
  }
 
  /**
   * JTidy を使って org.w3c.dom.Document を取得する。
   * @param url 対象WebページのURL
   * @return org.w3c.dom.Document オブジェクト
   * @throws IOException
   */
  private static Document getHtmlDoccument(String url) throws IOException {
    URLConnection con = new URL(url).openConnection();
    Tidy tidy = new Tidy();
    tidy.setShowErrors(0);
    tidy.setShowWarnings(false);
    tidy.setQuiet(true);
    Document doc = tidy.parseDOM(con.getInputStream(), null);
    return doc;
  }
  
  /**
   * ROME を使ってフィードのエントリを取得する。
   * @param feedUrl 対象フィードのURL
   * @return エントリのリスト
   * @throws Exception
   */
  private static SyndEntry[] getFeedEntries(String feedUrl) throws Exception {
    URL url = new URL(feedUrl);
    SyndFeedInput input = new SyndFeedInput();
    SyndFeed feed = input.build(new XmlReader(url));
    List entries = feed.getEntries();
    return (SyndEntry[])entries.toArray(new SyndEntry[entries.size()]);
  }
  
  /**
   * はてなモノリスの page パラメータを取得する。
   * @param username はてなのユーザーID
   * @param page page パラメータ
   * @return page パラメータ
   * @throws Exception
   */
  private static String getNextPage(String username, String page) throws Exception {
    // http://mono.hatena.ne.jp/nilab/
    String url = "http://mono.hatena.ne.jp/" + username + "/";
    if(page != null){
      // http://mono.hatena.ne.jp/nilab/?page=1342339765
      url += "?page=" + page;
    }
    // はてなモノリスのWebページをスクレイピングしてpageパラメータを取得
    Document doc = getHtmlDoccument(url);
    XPathFactory xpf = XPathFactory.newInstance();
    XPath xpath = xpf.newXPath();
    // <a href="?page=1342339765" rel="next"> から抽出
    String href = (String) xpath.evaluate("//*[@rel='next']/@href", doc, XPathConstants.STRING);
    if(!href.equals("")){
      return href.split("=")[1]; // ?page=1342657559 の右側を抽出
    }else{
      return null;
    }
  }
  
  /**
   * はてなモノリスのフィードエントリを取得する。
   * @param username はてなのユーザーID
   * @param page pageパラメータ
   * @return エントリのリスト
   * @throws Exception
   */
  private static SyndEntry[] getEntries(String username, String page) throws Exception {
    //  http://mono.hatena.ne.jp/nilab/rss
    String url = "http://mono.hatena.ne.jp/" + username + "/rss";
    if(page != null){
      // http://mono.hatena.ne.jp/nilab/rss?page=1342339765
      url += "?page=" + page;
    }
    SyndEntry[] entries = getFeedEntries(url);
    return entries;
  }
  
  /**
   * はてなモノリスの投稿エントリ情報を返す。
   * @return 投稿エントリ情報のリスト(これ以上情報が無い場合はnullを返す)
   * @throws Exception
   */
  public Entry[] nextEntries() throws Exception {
    if(end){
      return null;
    }
    ArrayList<Entry> result = new ArrayList<Entry>();
    SyndEntry[] entries = getEntries(username, page);
    for(SyndEntry entry : entries){
      Entry me = createEntry(entry);
      result.add(me);
    }
    page = getNextPage(username, page);
    if(page == null){
      // 最後のページまで到達
      end = true;
    }
    return result.toArray(new Entry[result.size()]);
  }
  
  private static Entry createEntry(SyndEntry se){
    Entry me = new Entry();
    me.rdfAbout = se.getUri();
    me.link = se.getLink();
    me.description = se.getDescription().getValue();
    me.title = se.getTitle();
    me.dcCreator = se.getAuthor();
    me.dcDate = se.getPublishedDate().toString();
    // org.jdom.Element
    List<Element> fmlist = (List<Element>)se.getForeignMarkup();
    for(Element de : fmlist){
      String name = de.getQualifiedName();
      String value = de.getValue();
      if("hatena:imageurl".equals(name)){
        me.hatenaImageUrl = value;
      }else if("hatena:imageurlmedium".equals(name)){
        me.hatenaImageUrlMedium = value;
      }else if("hatena:imageurlsmall".equals(name)){
        me.hatenaImageUrlSmall = value;
      }
    }
    return me;
  }
  
  public static class Entry {
    
    String rdfAbout;
    String link;
    String dcDate;
    String description;
    String dcCreator;
    String title;
    String hatenaImageUrl;
    String hatenaImageUrlMedium;
    String hatenaImageUrlSmall;
    
    public String toString(){
      StringBuilder sb = new StringBuilder();
      sb.append(getClass().toString());
      sb.append("\n");
      sb.append("rdfAbout=" + rdfAbout);
      sb.append("\n");
      sb.append("link=" + link);
      sb.append("\n");
      sb.append("dcDate=" + dcDate);
      sb.append("\n");
      sb.append("description=" + description);
      sb.append("\n");
      sb.append("dcCreator=" + dcCreator);
      sb.append("\n");
      sb.append("title=" + title);
      sb.append("\n");
      sb.append("hatenaImageUrl=" + hatenaImageUrl);
      sb.append("\n");
      sb.append("hatenaImageUrlMedium=" + hatenaImageUrlMedium);
      sb.append("\n");
      sb.append("hatenaImageUrlSmall=" + hatenaImageUrlSmall);
      sb.append("\n");
      return sb.toString();
    }
  }
  
}

参考までに、以下にはてなモノリスのRSSフィードのサンプル。


<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns="http://purl.org/rss/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel rdf:about="http://mono.hatena.ne.jp/nilab/">
    <link>http://mono.hatena.ne.jp/nilab/</link>
    <description/>
    <title>nilab - プロフィール - はてなモノリス</title>
    <items>
      <rdf:Seq>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtmCy6YF4t#/nilab/wtuAF4hSGN"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtmCy6YF4t#/nilab/wtmCy6ZAMC"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtmqsnmuFQ#/nilab/wtmCxqjcTj"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtuABVDwHt#/nilab/wtuABVEnuJ"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtmCtcGXNG#/nilab/wtmCtcKiyV"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtubGE5yjS#/nilab/wtuAzNvYya"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtmca9hwnM#/nilab/wtuAa9uNiW"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtucdwNVuw#/nilab/wtuAa5x5zV"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtm7tqDyaU#/nilab/wtmBbNrSSJ"/>
        <rdf:li rdf:resource="http://mono.hatena.ne.jp/mono/wtt4yVthpR#/nilab/wtuziapw7b"/>
      </rdf:Seq>
    </items>
  </channel>
  <item rdf:about="http://mono.hatena.ne.jp/mono/wtmCy6YF4t#/nilab/wtuAF4hSGN">
    <link>http://mono.hatena.ne.jp/mono/wtmCy6YF4t#/nilab/wtuAF4hSGN</link>
    <dc:date>2012-07-22T09:14:38Z</dc:date>
    <description>&amp;#x30D5;&amp;#x30E9;&amp;#x30F3;&amp;#x30B9;&amp;#x8A9E;&amp;#x306B;&amp;#x306F;&amp;#x30AD;&amp;#x30B9;&amp;#x8868;&amp;#x73FE;&amp;#x304C;&amp;#x305F;&amp;#x304F;&amp;#x3055;&amp;#x3093;&amp;#x3042;&amp;#x308B;&amp;#x3068;&amp;#x304B;&amp;#x3002;&lt;br/&gt;&lt;a href="/mono/wtmCy6YF4t#/nilab/wtuAF4hSGN"&gt;&lt;img src="http://img.f.hatena.ne.jp/images/fotolife/x/nilab/00000000/20120722181440_120.jpg"/&gt;&lt;/a&gt;</description>
    <dc:creator>nilab</dc:creator>
    <title>パリ愛してるぜ〜 / じゃんぽ〜る西 Jean-Paul NISHI</title>
    <hatena:imageurl>http://img.f.hatena.ne.jp/images/fotolife/x/nilab/00000000/20120722181440_120.jpg</hatena:imageurl>
    <hatena:imageurlmedium>http://img.f.hatena.ne.jp/images/fotolife/x/nilab/00000000/20120722181440_120.jpg</hatena:imageurlmedium>
    <hatena:imageurlsmall>http://img.f.hatena.ne.jp/images/fotolife/x/nilab/00000000/20120722181440_120.jpg</hatena:imageurlsmall>
    <content:encoded>&amp;#x30D5;&amp;#x30E9;&amp;#x30F3;&amp;#x30B9;&amp;#x8A9E;&amp;#x306B;&amp;#x306F;&amp;#x30AD;&amp;#x30B9;&amp;#x8868;&amp;#x73FE;&amp;#x304C;&amp;#x305F;&amp;#x304F;&amp;#x3055;&amp;#x3093;&amp;#x3042;&amp;#x308B;&amp;#x3068;&amp;#x304B;&amp;#x3002;&lt;br/&gt;&lt;a href="/mono/wtmCy6YF4t#/nilab/wtuAF4hSGN"&gt;&lt;img src="http://img.f.hatena.ne.jp/images/fotolife/x/nilab/00000000/20120722181440_120.jpg"/&gt;&lt;/a&gt;</content:encoded>
  </item>
(以下略)

Ref.

tags: hatenamono java rome jtidy jdom

Posted by NI-Lab. (@nilab)