Help us understand the problem. What is going on with this article?

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"))
;; => ...
ponkore
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away