Posted at
LispDay 14

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

More than 5 years have passed since last update.


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

この記事は、Lisp アドベントカレンダー 2013 12/14 の記事です。


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

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


  • やりたかったことは達成できていません(時間切れ...)

  • lisp そんなに関係ありません(というか Java のありがたいライブラリに大きく依存してますし...)

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


やりたかったこと

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


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

ということです。今回の「特定の」の中身ですが、いろいろ欲しい情報はあるのですがまずは「キロ程」情報をターゲットにします(他にも属性として整理したいことはいろいろあるのですが...)。「キロ程」とは、ある路線での始点からの距離、と思っておけば良いです(例えば米原駅( Wikipedia )によると、東海道本線では445.9km(東京起点)、北陸本線では0.0km(米原起点)となっています)。

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

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


方針

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

[net.sourceforge.htmlcleaner/htmlcleaner "2.2"]

使い方はざっくり言うと、



  1. org.htmlcleaner.HtmlCleaner のインスタンスを作る

  2. いくつかのプロパティを設定する

  3. String な HTML ソースを食わせる

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

(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 に変換

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

(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 (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 なりシーケンスなりの処理、なのでいけそうです。


ところが

世の中、そう甘くはありません。米原駅 のキロ程情報が表示されている付近を見てみましょう。

スクリーンショット 2013-12-14 21.35.20.png

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


すんませんすんません

本当はここから、


  • XML の tree 情報から、キロ程を含む情報をピックアップ

  • あとは適当にリスト操作して目的の情報をゲット!!!

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

なんかしまらない感じになってしまったので、せめてものお詫びに、バナナマン日村 替え歌「ココロオドル」 東海道本線ver の URL を貼っておきます(東海道線の東京から神戸までの全駅を、「ココロオドル」の替え歌で歌っています)。

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

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

(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"))
;; => ...