文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

はてなブログAPIで全記事取得とはてブ詳細を取得できたのでソースを公開!

結構みんなやろうとしてたりしますかね〜?


f:id:treeapps:20170818174241p:plain

【はてな】はてなブログのURL一覧とはてブ数を取得する方法【API】 - 文系プログラマによるTIPSブログ
以前こんな記事を書きました。この時は物理的に可能だ、という話で終わってしまったので、今回は実際にやってみました。

結論から言うと、できました!せっかくなのでソースを公開します!

レシピ

必要なもの

はてなブログのエントリー一覧を取得するためのはてなブログAtomPubですが、これははてなブログの所有者に与えられる、AtomPubのAPIキーが必要になります。このキーは管理画面→設定→詳細設定タブ→ AtomPub→APIキーから確認できます。

これはつまり他人のエントリは取得できないことを意味します。このAPIは編集もできるので、他人が触れたらエントリを書き換えたり削除できたりしてしまいますね。

サンプルソース

結構沢山書くので、頑張っていきましょう!サンプルはjavaですが、他の言語でも実装できます。

pom.xml

まずは必要なライブラリです。以下を追加しましょう。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>16.0.1</version>
</dependency>
<dependency>
    <groupId>com.google.http-client</groupId>
    <artifactId>google-http-client</artifactId>
    <version>1.17.0-rc</version>
</dependency>
<dependency>
    <groupId>com.google.http-client</groupId>
    <artifactId>google-http-client-xml</artifactId>
    <version>1.17.0-rc</version>
</dependency>
<dependency>
    <groupId>com.google.http-client</groupId>
    <artifactId>google-http-client-jackson2</artifactId>
    <version>1.17.0-rc</version>
</dependency>
<dependency>
    <groupId>xpp3</groupId>
    <artifactId>xpp3</artifactId>
    <version>1.1.4c</version>
</dependency>
IParser.java
public interface IParser {
    /**
     * パースする
     * @param clazz パースした結果をバインドするクラス
     * @param url リクエスト先のURL
     * @param parseType パース種別
     * @return パースした結果をバインドするクラス
     */
    public <T> T parse(Class<T> clazz, String url, ParseType parseType);
}
ParseType.java

後述するGenericParser.javaで指定する、パースの種別定数クラスです。

public enum ParseType {
    xml, json, text
}
HatebuAtomPub.java

はてなブログAtomPubの結果をバインドするクラスです。

import java.util.List;

import com.google.api.client.util.Key;

/**
 * はてなブログAtomPubのレスポンスオブジェクトです。
 * @see http://developer.hatena.ne.jp/ja/documents/blog/apis/atom
 * @author tree
 */
public class HatebuAtomPub {

    @Key
    public String title;
    @Key("link")
    public List<Link> links;
    @Key("entry")
    public List<Entry> entries;

    public static class Link {
        @Key("@rel")
        public String rel;
        @Key("@href")
        public String href;
    }

    public static class Entry {
        @Key
        public String title;
        @Key("link")
        public List<Link> links;
    }
}
HatebuEntry.java

はてなブックマークエントリー情報取得APIの結果をバインドするクラスです。

import java.util.List;

import com.google.api.client.util.Key;

/**
 * はてなブックマークエントリー情報取得APIのレスポンスオブジェクトです。
 * @see http://developer.hatena.ne.jp/ja/documents/bookmark/apis/getinfo
 * @author tree
 */
public class HatebuEntry {

    @Key("bookmarks")
    public List<Bookmarks> bookmarks;

    public static class Bookmarks {
        @Key
        public String comment;
        @Key
        public String timestamp;
        @Key
        public String user;
        @Key("tags")
        public List<String> tags;
    }

    @Key
    public int count;
    @Key
    public String eid;
    @Key("entry_url")
    public String entryUrl;
    @Key
    public String screenshot;
    @Key
    public String title;
    @Key
    public String url;
}
AbstractParser.java

このクラスはパラメータを調整するための抽象クラスです。

import com.google.api.client.http.HttpResponse;
import com.google.api.client.xml.XmlNamespaceDictionary;
import com.google.common.base.Strings;

public abstract class AbstractParser implements IParser {
    /** XMLネームスペース */
    private final XmlNamespaceDictionary namespace = new XmlNamespaceDictionary();
    /** HttpResponse */
    private HttpResponse response;
    /** basic認証のユーザ名 */
    private String username;
    /** basic認証のパスワード */
    private String password;
    /** 接続タイムアウト */
    private int connectTimeout = 0;
    /** 読み込みタイムアウト */
    private int readTimeout = 0;

    /**
     * XMLのネームスペースを設定する
     * @param key キー
     * @param value バリュー
     */
    protected void setNamespace(String key, String value) {
        if (!Strings.isNullOrEmpty(key) && !Strings.isNullOrEmpty(value))
            namespace.set(key, value);
    }

    /**
     * basic認証を設定する
     * @param username
     * @param password
     */
    protected void setBasicAuth(String username, String password) {
        this.username = username;
        this.password = password;
    }

    protected void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }
    protected void setReadTimeout(int readTimeout) {
        this.readTimeout = readTimeout;
    }
    public HttpResponse getResponse() {
        return response;
    }
    public void setResponse(HttpResponse response) {
        this.response = response;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public XmlNamespaceDictionary getNamespace() {
        return namespace;
    }
    public int getConnectTimeout() {
        return connectTimeout;
    }
    public int getReadTimeout() {
        return readTimeout;
    }
}
GenericParser.java

このクラスはgoogle-http-java-clientでhttpリクエストし、レスポンスであるjson・xmlを指定したクラスにバインドするためのクラスです。

import static com.google.common.base.Strings.isNullOrEmpty;

import java.io.IOException;

import com.google.api.client.http.BasicAuthentication;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.apache.ApacheHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.xml.XmlObjectParser;

public class GenericParser extends AbstractParser {

    @Override
    public <T> T parse(Class<T> clazz, String url, ParseType parseType) {
        try {
            HttpRequestFactory factory = getHttpRequestFactory(parseType);
            HttpRequest request = factory.buildGetRequest(new GenericUrl(url));
            HttpResponse response = request.execute();
            setResponse(response);
            T t = response.parseAs(clazz);
            return t;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * HttpRequestFactoryを取得する
     * @param parseType パース種別
     * @return HttpRequestFactory
     */
    private HttpRequestFactory getHttpRequestFactory(final ParseType parseType) {
        ApacheHttpTransport transport = new ApacheHttpTransport();
        HttpRequestFactory factory = transport.createRequestFactory(new HttpRequestInitializer() {

            public void initialize(HttpRequest request) throws IOException {
                request.setConnectTimeout(getConnectTimeout());
                request.setReadTimeout(getReadTimeout());
                // basic認証
                if (!isNullOrEmpty(getUsername()) && !isNullOrEmpty(getPassword()))
                    request.setInterceptor(new BasicAuthentication(getUsername(), getPassword()));
                switch (parseType) {
                case xml:
                    request.setParser(new XmlObjectParser(getNamespace()));
                    break;
                case json:
                    request.setParser(new JacksonFactory().createJsonObjectParser());
                    break;
                case text:
                    break;
                }
            }
        });
        return factory;
    }
}
HatenaBlogClient.java

このクラスははてブのエントリ一覧を取得し、各エントリのURLをキーにはてブ詳細を取得するためのクラスです。このクラスのmainメソッドを実行する事で処理を実行できます。はてなブログAtomPubはレスポンスがxmlで、はてなブックマークエントリー情報取得APIのレスポンスはjsonです。google-http-java-clientを使うと、jsonとxmlのバインドがほぼ同じコードで書くことができます。
HatenaBlogClient

import static com.google.common.base.Strings.isNullOrEmpty;
import hatebu.bind.HatebuAtomPub;
import hatebu.bind.HatebuAtomPub.Entry;
import hatebu.bind.HatebuAtomPub.Link;
import hatebu.bind.HatebuEntry;
import hatebu.bind.HatebuEntry.Bookmarks;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class HatenaBlogClient {

    public static void main(String[] args) {
        new HatenaBlogClient().test();
    }

    public void test() {
        String atomPubUrl = "https://blog.hatena.ne.jp/treeapps/treeapps.hatenablog.com/atom/entry";
        AbstractParser parser = new GenericParser();
        parser.setBasicAuth("はてなIDを指定します", "AtomPubのAPIキーを指定します");
        String nextPage = "";

        loop: while (true) {
            // 次のページが見つかった場合は次のページを設定
            String nextUrl = isNullOrEmpty(nextPage) ? atomPubUrl : atomPubUrl + "?page=" + nextPage;
            HatebuAtomPub hatebuAtomPub = parser.parse(HatebuAtomPub.class, nextUrl, ParseType.xml);

            // 次のページを取得する
            System.out.println("######################");
            System.out.println("nextUrl = " + nextUrl);
            System.out.println("nextPage = " + nextPage);
            boolean hasNext = false;
            for (Link link : hatebuAtomPub.links) {
                if ("next".equals(link.rel)) {
                    Matcher matcher = Pattern.compile(".*page=([0-9]+)").matcher(link.href);
                    if (!matcher.find())
                        throw new RuntimeException("正規表現が間違っています!");
                    nextPage = matcher.group(1);
                    hasNext = true;
                }
            }
            // nextタグが無い場合は最終ページなので処理終了
            if (!hasNext)
                break loop;

            // エントリーを取得する
            String hatebuEntryUrl = "http://b.hatena.ne.jp/entry/jsonlite/?url=";
            for (Entry entry : hatebuAtomPub.entries) {
                for (Link link : entry.links) {
                    if (!"alternate".equals(link.rel))
                        continue;
                    // エントリーのはてなブックマーク詳細を取得する
                    System.out.println("------------");
                    String url = hatebuEntryUrl + link.href;
                    HatebuEntry hatebuEntry = parser.parse(HatebuEntry.class, url, ParseType.json);
                    System.out.println("count=" + hatebuEntry.count + ", title=" + entry.title);
                    if (hatebuEntry.bookmarks == null)
                        continue;
                    for (Bookmarks bk : hatebuEntry.bookmarks) {
                        System.out.println("user=" + bk.user + ", timestamp=" + bk.timestamp + ", comment="
                                + bk.comment);
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }
    }
}
実行結果
######################
nextUrl = https://blog.hatena.ne.jp/treeapps/treeapps.hatenablog.com/atom/entry
nextPage = 
------------
count=1, title=【ES9018】MY435-ES9018v3とp-700uを導入しました!【DSD】
user=chatarun, timestamp=2014/03/18 08:58:44, comment=
------------
count=1, title=【USB】macのchromeがサウンドデバイスを変更できない件を解決する【optical】
user=kumoi_zangetu, timestamp=2014/03/16 14:32:00, comment=
------------
count=1, title=【最強】プログラミングのフォントはRicty Diminishedで決まり【さらばSource Code Pro】
user=y-kobayashi, timestamp=2014/03/15 11:50:53, comment=
------------
count=1, title=【java】GAE/Jでメール送信する【Play Framework1.2】
user=chatarun, timestamp=2014/03/13 08:26:03, comment=
------------
count=0, title=【即動く】googlemapでroadMapとstreetViewを同期しペグマンを自動復帰させる【サンプル】
------------
count=0, title=【過酷なノルマ】佐川急便の悪評と実態について【体育会系】
------------
count=0, title=【微妙】linuxからコマンドラインでSQLServer2008に接続する【tsql】
------------
count=0, title=【ブログ】NAVERまとめに掲載されるとPV数が増えやすい【SEM】
------------
count=0, title=【レビュー】luxman p-1uからp-700uに変更しました【ヘッドホンアンプ】
------------
count=0, title=【キャッシュ】はてなブックマークウィジェットを自力で実装する【高速化】
######################
nextUrl = https://blog.hatena.ne.jp/treeapps/treeapps.hatenablog.com/atom/entry?page=1392912372
nextPage = 1392912372
------------
count=0, title=【非ブロック化】adsenseスクリプトを非同期に変更して高速化【コンバージョンに影響】
・・・・・・以降は次のページが無くなるまで無限ループします・・・・・・

注意点

AtomPubのpageパラメータの値について

pageパラメータですが、なんと1,2,3,といった連番ではないのです。
この規則性不明なpageパラメータを指定しないと次のページにいけないので、取得する方法を知る必要があります。pageは、AtomPubの以下の部分から取得する事ができます

<link rel="next" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry?page=1377584217" />

ここから取得できます。

最終ページにはnext属性が無い

最終ページかどうかは前述の1行が無い事がサインとなります。

必ずスリープ処理を挟みましょう

明確なスリープのインターバルは定義されていませんが、一般的にweb apiは1秒1リクエストが制限となる場合が多いので、今回1リクエストに付き1秒のインターバルを挟んでいます。

雑感

このコードを使う事で、はてブ詳細だけでなく、記事の編集も行えます。例えばブログのテーマを変えて、全記事のcssのセレクタやタグの置換を行いたい場合にも使えます。

処理結果をDBに保存すれば、統計を取ったり記事の最適化をすべき記事を探し出したり、結構有用なデータになりそうですね!