はてなモノリス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>&#x30D5;&#x30E9;&#x30F3;&#x30B9;&#x8A9E;&#x306B;&#x306F;&#x30AD;&#x30B9;&#x8868;&#x73FE;&#x304C;&#x305F;&#x304F;&#x3055;&#x3093;&#x3042;&#x308B;&#x3068;&#x304B;&#x3002;<br/><a href="/mono/wtmCy6YF4t#/nilab/wtuAF4hSGN"><img src="http://img.f.hatena.ne.jp/images/fotolife/x/nilab/00000000/20120722181440_120.jpg"/></a></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>&#x30D5;&#x30E9;&#x30F3;&#x30B9;&#x8A9E;&#x306B;&#x306F;&#x30AD;&#x30B9;&#x8868;&#x73FE;&#x304C;&#x305F;&#x304F;&#x3055;&#x3093;&#x3042;&#x308B;&#x3068;&#x304B;&#x3002;<br/><a href="/mono/wtmCy6YF4t#/nilab/wtuAF4hSGN"><img src="http://img.f.hatena.ne.jp/images/fotolife/x/nilab/00000000/20120722181440_120.jpg"/></a></content:encoded>
</item>
(以下略)
Ref.
- はてなモノリス - バーコードをスキャンしてモノを共有!
- nilab - プロフィール - はてなモノリス
- JTidy - JTidy
- The ROME Project
- Home - ROME - Confluence
tags: hatenamono java rome jtidy jdom
Posted by NI-Lab. (@nilab)