Clojure で HTML スクレイピングしてみる

## Clojure で HTML スクレイピングしてみる

この記事は、[Lisp アドベントカレンダー 2013](http://qiita.com/advent-calendar/2013/lisp) 12/14 の記事です。

### まずはじめにお断り...

まずはじめにお断りさせていただきます...

* やりたかったことは達成できていません(時間切れ...)
* lisp そんなに関係ありません(というか Java のありがたいライブラリに大きく依存してますし...)

というわけで内容も薄い記事になっちゃいますが、期限ぎりぎりなのでもう公開してしまいます。しばらくは引き続きやりたいことを継続することでしょう(きっと)。

### やりたかったこと

スクレイピング、と書けばかなり曖昧な表現なのですが、実はやりたかったことはかなり具体的だったりします。それは、

* Wikipedia の駅のページから、特定の情報を抜き出す

ということです。今回の「特定の」の中身ですが、いろいろ欲しい情報はあるのですがまずは「キロ程」情報をターゲットにします（他にも属性として整理したいことはいろいろあるのですが...）。「キロ程」とは、ある路線での始点からの距離、と思っておけば良いです(例えば米原駅( [Wikipedia](http://ja.wikipedia.org/wiki/%E7%B1%B3%E5%8E%9F%E9%A7%85) )によると、東海道本線では445.9km（東京起点）、北陸本線では0.0km（米原起点）となっています)。

整理された公開されたデータベースがあればよいのですが、どうやらなさそうなので Wikipedia に頼ってみよう、というわけです。

(それ以外にも、とある船のオンラインゲームに必要(?)な艦船の情報も集めてみたい、とかやりたいことは他にもあるので、勉強しておく価値はあると思いました)

### 方針

自分は `Clojure` がいちばん楽なのでその前提で考えた時、まずは使えそうなライブラリを探してみます。HTML をお手軽に読み込めそうな(良さげな)ものとして [HTML Cleaner](http://htmlcleaner.sourceforge.net/) が良さそうです。Maven リポジトリにも公開されているので、`project.clj` に以下を追加します。

```clj
[net.sourceforge.htmlcleaner/htmlcleaner "2.2"]
```
使い方はざっくり言うと、

1. `org.htmlcleaner.HtmlCleaner` のインスタンスを作る
2. いくつかのプロパティを設定する
3. String な HTML ソースを食わせる

という感じになります。Clojure で書くと

```clj
(defn html->node
  [cleaner html-src]
  (doto (.getProperties cleaner)
    (.setOmitComments true)        ;; HTML のコメントは無視する
    (.setPruneTags "script,style") ;; <script>, <style> タグは無視する
    (.setOmitXmlDeclaration true))
  (.clean cleaner html-src)) ;; cleaner.clean(string) でパース

(html->node (HtmlCleaner.) page-src)
;; => node オブジェクト(org.htmlcleaner.TagNode)
```

こんな感じです。

### Clojure で扱いやすいように XML に変換

ところが、得られた `node` は `org.htmlcleaner.TagNode` クラスのインスタンスを返します。中身は読み込んだ HTML の構造に沿った形の tree 構造になっています。Java のオブジェクトをいちいち手繰っていくのは面倒なので、どうにかして Clojure で単純にアクセスしたいと思います。ここは一旦 XML に変換してからその XML を Clojure で読み込むことにしましょう。

```clj
(defn node->xml
  [cleaner node]
  (let [props (.getProperties cleaner)
        xml-serializer (CompactXmlSerializer. props)]
    (-> (.getAsString xml-serializer node) ;; node を XML の String に変換
        java.io.StringReader.
        org.xml.sax.InputSource.
        xml/parse)))                    ;; clojure.xml/parse で Clojure 内部表現に変換
```

### ここまでのまとめ

エラー処理等一切気にしなければ、

```clj
(defn test01
  [url]
  (let [cleaner (HtmlCleaner.)
        page-src (slurp url)
        node (html->node cleaner page-src)
        xml (node->xml cleaner node)]
    ;; ここで xml の処理...
    xml
  ))
(def x (test01 "http://qiita.com/advent-calendar/2013/lisp"))
x
;; => {:tag :html, :attrs nil, :content [{:tag :head, :attrs nil, :content [{:tag :meta, :attrs {:charset "UTF-8"}, :content nil} ...
```

あとは map なりシーケンスなりの処理、なのでいけそうです。

### ところが

世の中、そう甘くはありません。[米原駅](http://ja.wikipedia.org/wiki/%E7%B1%B3%E5%8E%9F%E9%A7%85) のキロ程情報が表示されている付近を見てみましょう。

![スクリーンショット 2013-12-14 21.35.20.png](https://qiita-image-store.s3.amazonaws.com/0/3208/281fd568-c0cd-aeae-e4b0-021841dcaadb.png)

キロ程には、路線の情報とその路線における数値情報がセットで必要ですが、`<table>` でレイアウトされ単純に **キロ程** というキーワード **付近** の情報だけを抜く、というのはあまり単純には書けません(それでもちょっとがんばれば書けるのですが...)。

### すんませんすんません

本当はここから、

* XML の tree 情報から、キロ程を含む情報をピックアップ
* あとは適当にリスト操作して目的の情報をゲット!!!

というところを書きたかったのですが、時間切れしてしまいました。ここから先がほんとうは面白いのですが...また時間を見つけて挑戦してみます（もちろんツッコミも歓迎します）。

なんかしまらない感じになってしまったので、せめてものお詫びに、[バナナマン日村　替え歌「ココロオドル」　東海道本線ver の URL ](http://www.youtube.com/watch?v=2GY6zxV666A) を貼っておきます(東海道線の東京から神戸までの全駅を、「ココロオドル」の替え歌で歌っています)。

最後に全体のソースを貼っておきます。

それでは良いクリスマスを!!!

```clj
(ns html-parser.core
  (:require [clojure.xml :as xml])
  (:import [org.htmlcleaner HtmlCleaner CompactXmlSerializer]))

;; 参考 https://gist.github.com/sids/391818

(defn html->node
  [cleaner html-src]
  (doto (.getProperties cleaner)
    (.setOmitComments true)        ;; HTML のコメントは無視する
    (.setPruneTags "script,style") ;; <script>, <style> タグは無視する
    (.setOmitXmlDeclaration true))
  (.clean cleaner html-src)) ;; cleaner.clean(string) でパース

(defn node->xml
  [cleaner node]
  (let [props (.getProperties cleaner)
        xml-serializer (CompactXmlSerializer. props)]
    (-> (.getAsString xml-serializer node) ;; node を XML の String に変換
        java.io.StringReader.
        org.xml.sax.InputSource.
        xml/parse)))                    ;; clojure.xml/parse で Clojure 内部表現に変換

(defn test01
  [url]
  (let [cleaner (HtmlCleaner.)
        page-src (slurp url)
        node (str->node cleaner page-src)
        xml (node->xml cleaner node)]
    ;; ここで xml の処理...
    xml
  ))

;; (def x (test01 "http://qiita.com/advent-calendar/2013/lisp"))
;; => ...
```