読者です 読者をやめる 読者になる 読者になる

はてなブログ、カテゴリー情報を取得するサンプルコード

はじめに

自分のブログ記事の中で、特定カテゴリーのタグが付いている記事一覧が必要になりました。
それぞれの記事について必要な情報は、記事タイトル記事URLだけ。
それっぽい API なんて公式、非公式を問わずググれば見つかるでしょ。

ってことでググるが、あれ、見つからない。。。


調べる

とりあえず、カテゴリー毎のリンクがあるのでそのページを確認する。

“自転車日本一周” - 記事一覧 - プログラミング的な何か、あと自転車日本一周とか

わかったこと

  • カテゴリ一覧ページにほしい情報(タイトル、記事のリンク)が存在する。
  • それぞれの記事リンクには、クラス情報として entry-title が付いている。
  • 記事総数が1ページに入らない(50記事以上?)場合、ページネートが作成されている。
  • ページネートはクエリーパラメータ(page=1, page=2, page=3 ...) で実現されている。
  • ページネートのクエリーパラメータとして、不正(page=-1とか、記事が存在しないインデックス等)な値を入力すると、この期間に記事はありませんと表示される。

ふむふむ。
スクレイピングで問題なく情報を抽出できそう。

実装の流れ

こんな感じで書いてみる。

  1. ブログURL, カテゴリ名を指定する。
  2. 指定カテゴリのページ(page=1)から、class が entry-title のアンカー情報を抽出する。
  3. 次のページ(page=2) から同様に class が entry-title のアンカー情報を抽出する。
  4. page=3, page=4, page=5 ... も同様。取得したアンカー情報が空だったら情報抽出を打ち切る
  5. 取得した情報を扱いやすいフォーマット変換する。

終了判定は、"次のページ" リンクの有無でも問題なさそう。

サンプルコード

Clojure

html のパースに enlive を使用。:dependencies に

[enlive "1.1.1"]

を追加しておきます。

(ns hatena-blog.core
  (require [clojure.java.io :refer [reader]]
           [net.cgrand.enlive-html :refer [select html-resource]]))

(defn category-entries [blog-url category]
  (loop [result [] index 1]
    (let [entries (-> (format "%s/archive/category/%s?page=%d" blog-url category index)
                      reader
                      html-resource
                      (select [:a.entry-title]))]
      (if (empty? entries)
        result
        (recur (concat result entries) (inc index))))))

(defn formatted-entries [blog-url category]
  (let [entries (category-entries blog-url category)]
    (map #(array-map :title (first (:content %))
                     :url (str blog-url (get-in % [:attrs :href]))) entries)))


こんな感じで使用する。

(formatted-entries "http://snufkon.hatenablog.com" "自転車日本一周")

結果として {:title "記事タイトル", :url "記事URL"} を持つ遅延シーケンスが返ってくる。

({:title "【旅120日目 2012/09/26】能登半島の先端へ", :url "http://snufkon.hatenablog.com/ ..."} 
 {:title "【旅119日目 2012/09/25】富山から能登半島へ", :url "http://snufkon.hatenablog.co ..."}
 {:title "【旅118日目 2012/09/24】日本一の落差、称名滝へヒルクライム", :url "http://snufko ..."} 
 {:title "【旅117日目 2012/09/23】雨で停滞、とくになし", :url "http://snufkon.hatenablog. ..."} 
 {:title "【旅116日目 2012/09/22】青い海と空、そして再会。", :url "http://snufkon.hatenabl ...})


Ruby

html パーサとして nokogiri を使用。

# -*- coding: utf-8 -*-
require 'uri'
require 'open-uri'
require 'nokogiri'

def category_entries(blog_url, category, index=1)
  url = URI.escape("#{blog_url}/archive/category/#{category}?page=#{index}")
  doc = Nokogiri::HTML(open(url))

  nodes = doc.css('a.entry-title')
  if nodes.size == 0
    nodes
  else
    nodes + category_entries(blog_url, category, index+1)
  end
end

def formatted_entries(blog_url, category)
  entries = category_entries(blog_url, category)
  entries.map do |entry|
    {title: entry.text, url: blog_url + entry['href']}
  end
end


こんな感じで使用する。

entries = formatted_entries("http://snufkon.hatenablog.com", "自転車日本一周")

entries に {:title => "記事タイトル", :url "記事URL"} を持つ配列が格納される。

[{:title=>"【旅120日目 2012/09/26】能登半島の先端へ", :url=>"http://snufkon.hatenablog.com/..."}
{:title=>"【旅119日目 2012/09/25】富山から能登半島へ", :url=>"http://snufkon.hatenablog.com..."}
{:title=>"【旅118日目 2012/09/24】日本一の落差、称名滝へヒルクライム", :url=>"http://snufkon...."}
{:title=>"【旅117日目 2012/09/23】雨で停滞、とくになし", :url=>"http://snufkon.hatenablog.co..."}
{:title=>"【旅116日目 2012/09/22】青い海と空、そして再会。", :url=>"http://snufkon.hatenablo..."}]

終わりに

Clojure のほうが Ruby よりいけてる! って言いたかったけそうでもなかったという残念な結果に。
もっとスマートなコードを書けるようになりたい。