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"]
使い方はざっくり言うと、
-
org.htmlcleaner.HtmlCleaner
のインスタンスを作る - いくつかのプロパティを設定する
- 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 に変換
ところが、得られた node
は org.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 なりシーケンスなりの処理、なのでいけそうです。
ところが
世の中、そう甘くはありません。米原駅 のキロ程情報が表示されている付近を見てみましょう。
キロ程には、路線の情報とその路線における数値情報がセットで必要ですが、<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"))
;; => ...