Hatena::Groupbugrammer

蟲!虫!蟲!

Esehara Profile Site (by Heroku) / Github / bookable.jp (My Service)
過去の記事一覧はこちら

なにかあったら「えせはら あっと Gmail」まで送って頂ければ幸いです。
株式会社マリーチでは、Pythonやdjango、また自然言語処理を使ったお仕事を探しています

 | 

2011-09-22

[]Haskellの入門がてら、はてなブックマークを簡易スクレイピングする。 00:51

前書き

 一部で流行っている関数型言語Haskellですが、具体的にHaskellで何ができるの?という段階になると、なかなかイメージがつかめない。むしろ、普段から作っている9行くらいのプログラミングを移植すれば、手続き型と関数型の違いがわかっていいのではないかと。特に自分はWebスクレイピングとかやっているのでちょうどいいかな、と思う次第。

まずはRubyスクレイピングする

 スクレイピングとは英語のscrape="削る"という言葉からきている。この場合は情報を加工して扱いやすいようにする、くらいの意味で使っている。最初に、いわゆる手続き型プログラムだとどういう書き方になるのか、というのを見る。汚いソースだけど参考までに。

require "rubygems"
require "mechanize"

agent = Mechanize.new

for i in 0..50 do
    page = agent.get("http://b.hatena.ne.jp/entrylist?sort=eid&of=" + (i * 20).to_s)
    entrys = page.search("div.entry-body")
    entrys.each{|en|
          if (en.xpath(".//li[@class='users']").text.to_i < 5) then
                print "#{en.children.xpath(".//a[@class='entry-link']")[0]["href"]}'\n"
                print en.children.xpath(".//a")[0].text.sub("<title>","")
                print "#{en.xpath(".//li[@class='users']").text}\n"
                print "<p>#{en.xpath(".//blockquote").text.sub("続きを読む","")}\n"
          end
}
end

 ここで何をやっているか。

 大まかには

  1. ページを加工するためのライブラリを読み込む
  2. ページをダウンロードする
  3. ページが所持する、必要な情報をパースする
  4. 整形し、出力を行う

 という四つの段階が必要になってくる、と予測。さすがにHaskellくらい有名になっているならば、HTML構造を分析するくらいのライブラリだったらあるだろう、というよくわからない信頼でやる。要するに同じ段階を踏めば、同じようなプログラムが出来るだろうと。

第一段階:ライブラリを読み込み、使用する

 Rubyだと"gems"みたいな命令があるし、またPythonなら"easy_install"があるように、Haskellにも何らかのライブラリを読み込むための機能くらいあるだろう。Haskellの場合はCabalがある。つうわけでcabalを導入しましょう……という話になるんだけど、このcabal自体はなかなかのくせものらしい。"依存関係とかぐちゃぐちゃになって手のつけようがない"という報告がちらほら。あるいは"なぜか既にインストールされているバージョンのパッケージを勝手に上書きインストールして、package id がかわってしまうためにそれに依存しているパッケージが壊れてしまう"というのがあったり……後者のサイトでは、それをFixするためのスクリプトを配布している。

 そういう困難も理解しつつ、じゃあcabalのインストール方法はどうするの、といえば、ubuntuだと"sudo apt-get cabal-install"というライブラリもあるんだけど、「せっかくだから最新版を使いたいんだ俺はー!」という人はHaskell Cabal in Ubuntu | Spork Codeをみて導入すればいいのではないでしょうか。

 さて、次に問題になるのは、HaskellでWebスクレイピングを使うためのライブラリということになる。その点に関しては、英語のサイトになるけれどもDrinking TagSoup by Exampleがわかりやすい感じ。まずはHTTPで接続するためのライブラリ"http"が必要なようなので、インストール

 せっかくだから対話型で"ghci"を使用。

 上記のソースコードを一から手入力して、機能をひとつずつ把握する……というのはいいのだけれども、不用意に"hogehoge x = x * x"みたいなのを入力すると"parse error on input"と怒られてしまう。ので、とりあえず「これローカル関数だから」ということでletを使うとすんなりと束縛してくれるようす。(なんかHaskellでは束縛とかいうらしいですよ)。

Prelude Network.HTTP> let openURL x = getResponseBody =<< simpleHTTP (getRequest x)

Prelude Network.HTTP> openURL "http://b.hatena.ne.jp/"

 こうすると、うまくやってくれる。

 吐き出されるHTMLを読んでいると、何やら文字化けしていて読めない。この周りはUTF-8で解決する必要がある。ちょっとここで迂回してしまったのだけど、UTF-8でdecodeしてやる必要がある。

import Network.HTTP
import Codec.Binary.UTF8.String as UTF8

openURL x = getResponseBody =<< simpleHTTP (getRequest x)

urlUTF8 x = do page <- openURL x
               return $ UTF8.decodeString page

main = do putStr =<< urlUTF8 "http://b.hatena.ne.jp/entrylist"

 urlUTF8の部分に関しては、openURLが束縛されているgetResponseBodyがIO Monad型なので(意味不明な人はHaskell入門書を読もう!!)、一度 pageという変数に、入力された文字列を束縛してやることによって、String型に変換。それはdecodeStringが受け取るものがString型であってIO Monad型じゃないため。で、束縛されたあとにputStrがString型を受け付けないので、さらに "=<<"してやって、アクションを渡すということになると思う。たぶん。

 ここまできたら、あとはHTMLを切り刻めばいいだけの話となる。その部分に関しては別のライブラリに登場してもらおう。それはTagSoupだ。TagSoupは、ブラウザエミュレートもしてくれるっぽいので、Haskellスクレイピングしたい人は覚えていて損はないと思う。あとご丁寧にも「スクレイピング入門」もしてくれている。

Drinking TagSoup by Example

 このTagSoup、やたらと高性能なのだが如何せん初心者にとっては意味がわからなかったりするが、英語が苦ではなければとりあえず他の人のソースを読めばいいのではないかと。

 上記のサンプルプログラムでは「はてなブックマーク」の新規エントリーをただ読みこむだけだけれども、例えば最近上がったタイトルを取得したい、という場合には次のようにしてみる。

import Network.HTTP
import qualified Codec.Binary.UTF8.String as UTF8
import Text.HTML.TagSoup

main = scrapeTags "http://b.hatena.ne.jp/entrylist"

openURL x = getResponseBody =<< simpleHTTP (getRequest x)

scrapeTags x = do tags <- fmap parseTags $ getUTF8 x
                  putStr $ unlines $ map(fromAttrib "title") $filter (~== TagOpen "a" [("class","entry-link")]) tags

getUTF8 x = do page <- openURL x
               return $ UTF8.decodeString page

 こうしてやることによって、タイトルだけが表示された。

 Haskellにはいわゆるfor文が無い。とするとリストを操作するためにはどうすればいいのか、という問題が出てくる。例えば上のfilterはリストを返したりするのだが、fromAttribは単一のタグクラス型にしか適応が出来ない。そうする場合、mapが使用できる。mapは全てのリストに対してある関数を適応する、という関数になる。リストは単一の要素を集めたものだから、当然分解すれば単一要素になるわけで、mapを使用。あとはunlinesでリストを結合してやる、putStrで出力すればいい。

 しかし、上記のアプローチは困ったことになる。というのは、スクレイピングする場合、普通は「タイトル」「URL」となるはずだ。例えば……

似非原は臭い

http://esehara.kusai.com/

 みたいな表記になる必要があるのだが、困ったことに、上のようにリストをただ渡すだけだと、このようなミックスが出来ない。合成関数とか色々と使ってみたんだけど、いまいち要領が得なかった。結果として別の関数を用意してやり、そこから head(リストの一番最初を取り出す関数)を使う、というアプローチを取る。はてなブックマークだと、aタグを取得した場合、titleとhrefが埋め込まれている。つまり、一つの要素に対して二つの属性が取り出せる。


import Network.HTTP
import qualified Codec.Binary.UTF8.String as UTF8
import Text.HTML.TagSoup

main = scrapeTags "http://b.hatena.ne.jp/entrylist"

openURL x = getResponseBody =<< simpleHTTP (getRequest x)

scrapeTags x = do tags <- fmap parseTags $ getUTF8 x
                  resultTags $filter (~== TagOpen "a" [("class","entry-link")]) tags

resultTags x = do putStr $ fromAttrib "title" $ head x
                  putStr $ fromAttrib "href" $ head x

getUTF8 x = do page <- openURL x
            return $ UTF8.decodeString page

 しかし、悲しいことにこれだけだと、一つのエントリだけしか表示されないという悲しい事態になってしまう。なので、なんとかループを構築してやる必要がある。前出したように、Haskellにはfor文に値するものが存在しない。したがって再帰を利用してループする必要がある。

 再帰とは、簡単にいってしまえば、自分自身を呼び出す関数のこと。そこでウロボロスの蛇のようにぐるぐると回ってもらおうというわけだ。しかし、同じデータを送りつづけるとCPUがバターになってしまうので、データはちょこっとずつ変更していく。上記の場合ならば、リストの先頭を取り出しているわけだから、リストの先頭を除いたものをリストに渡してあげればいい。それがtail関数になる。

 さて、tail関数を適用して再帰にすればいいんでしょ……とはやる前に、空リストが渡されたときの処理を考えないといけない。当然のことながら、リストが空なのに、先頭の要素を取り出すなんて無茶な話なので、再帰させる前にtailした結果が空リストなら、そこで適当な捨て台詞でもつぶやいてもらって処理をやめてもらう、というのがいい。

 そこで手続き型であるならば"ifを使おう!"という話になるのだけども、せっかくのHaskellだし、いい分岐の方法ないかなーと思って探してみると、「ガード」というのがあったので、これを使う。

 簡単にいっちゃうと、ガードというのは値がパターンにマッチしているかということを調べて分岐させるためのものらしい。ポイントは渡された値が空リストだったら自分を呼び出す前に捨て台詞を吐いてもらうといい、みたいな使い方をするとよさげ。さっそくresultTags関数にガードを呼ぶ。ついでなのでローカル変数みたいなwhereも一緒に使う。これは特定の定義内でのみ使える関数を定義するものらしい。


import Network.HTTP
import qualified Codec.Binary.UTF8.String as UTF8
import Text.HTML.TagSoup

main = scrapeTags "http://b.hatena.ne.jp/entrylist"

openURL x = getResponseBody =<< simpleHTTP (getRequest x)

scrapeTags x = do tags <- fmap parseTags $ getUTF8 x
                  resultTags $filter (~== TagOpen "a" [("class","entry-link")]) tags

resultTags x = do putentry (fromAttrib "title" $ head x) (fromAttrib "href" $ head x)
                  result (tail x)
               where
                  result y
                    | y == [] = putStrLn "end"
                    | otherwise = resultTags y
                  putentry y z
                    | y == "" = putStrLn ""
                    | otherwise = do putStrLn y
                                     putStrLn z 

getUTF8 x = do page <- openURL x
               return $ UTF8.decodeString page

 これを出力してみると、見事に「はてなダイアリー」の注目のエントリリストが表示出来るようになった。ちなみにputentryに関しては、このコードだとゴミが入ってしまうので、それを退けるために使用した。こんな短いコードを書くだけでも十二時間近くの時間を費やしたのでした。ぎゃふん。

おまけ:ghc7をインストールしてHaskellSDKの"Leksah"でもインストールするとよい

 Ubuntuなら、既にapt-getできるパッケージではあるんだが、どうせだからcabal経由でインストールしようとすると、"ghcが7じゃねえよ!"と怒られる。ちなみに、ghcの最新版をインストールする手段は英語だけど、"ここを参考にして"導入できる。のはいいんだけど、現在のghc最新版は7.2.1となっていて、要するにバージョンを逆に越えてしまうという問題が発生する。ので、やはりapt-getに……。

 で、個人的にLeksahを導入して躓いたところを中心に。

 たまにLeksahの初期起動に失敗したさい、Configファイルが生成されない場合がある。そういうときにLeksah-serverが起動するさい、"prefscoll.lkshpが無いよ!"と怒られたりする。デフォルト設定ファイルに関しては、下のところにある。

/usr/share/leksah/data/prefscoll.lkshp

 あとはWorkspace -> Packageの順番で作ってやればいい。Packageはどういう風に設定すればいいのか、についてはneue cc - Haskell用IDE 「Leksah」の紹介と導入方法を参照するといい。

 ただ、Leksahで直接importすると"そんなパッケージ見つからないよ!"と言われたりする。それはdependanceが解決していないから。ここでややこしいのはcabalでインストールしたpackage名を指定しなきゃいけないというところ。とはいえ、globalインストールしてあるのなら、たいていは[Package]->[Edit Package]->[Dependences.]->[Select]でインストールされているパッケージ一覧が見られるので、ちょこちょことselectから選んでいけばいいのではないかとは思う。

反省会:関数型言語を触ってみて

 正直なところ、関数型悪くない!!とは思う。個人的には必要な機能を逆算しながら書いていくというのは、記述スタイルとしていいような気はする。こういうこと言うとHaskellerに怒られそうだが、メソッドチェインみたいな形でガンガンメソッドを繋げていく方法になれている人ならば、逆にいい感じで書けるのではないかと。流れが右から左になったんじゃなくて、左から右になっただけだし。「代入」を使わないで束縛、引数と返り値、あとは再帰とかでなんとかしましょうよ、という感じもわりかし好き。

 むしろ、問題はHaskellの静的型付けというところのほうで、色々と小手先で型をねじ曲げる必要があるのがなかなかしんどい作業だった。エラーが出るたびに「えっ、入力の型がまずいの?出力の型がまずいの?」という基礎的なところから、IO Monadのエラーに至るまで、ゆとりのような俺としては、そこで頭を抱えては終始型の辻褄あわせをするという感じになっていた。正直、そっちのほうが大変なんだけど、それは経験の問題なんだろうな、とは思う。

 そういうことを考えると、必要なときは関数型のスタイルで、という選択肢が増えるのは、それは絶対にアリなんだろうなと思う一方で、Haskellは正直荷が重いかもしれん……ぐぬぬ、という気もしないことはない。こればっかりは日本語の情報が不足していて、「こういう事例がありましたよ」というのがあんまり無いのも原因かもしれない。さすがに最初に「じゃあ関数型をやりましょう」みたいな人もいないだろうし。それで結局、英語のサイトとそこに掲載されているソースコードと睨めっこするみたいなこともやっていた。

 上記の問題に関しては、たぶん体系立った勉強をしていないからの可能性のほうが高いので、適当な参考書でも読んでみるのがいいのかなとは思う。

 何はともあれ、やっていて損はない気がするHaskell。でもやっぱり型で怒られるの疲れたし、むしろScalaのほう……ゲフンゲフン

trwintdejjtrwintdejj2013/07/28 22:43zioarcvhsbnnfs, <a href="http://www.wlnaonhhxt.com/">fvdzhosqud</a> , [url=http://www.pjomjotfaq.com/]ibgrstarht[/url], http://www.towlwzfgik.com/ fvdzhosqud

frgaklkhylfrgaklkhyl2013/07/31 04:18wsmlscvhsbnnfs, <a href="http://www.luvwqykcxg.com/">wxaqbgjsky</a> , [url=http://www.ktwdwfkyui.com/]rkgpdykjmc[/url], http://www.jwkiindajy.com/ wxaqbgjsky

noslldxjyunoslldxjyu2014/06/24 05:53jqjfrcvhsbnnfs, <a href="http://www.zrsfwswpzn.com/">mhpgaekemr</a> , [url=http://www.dxukiyvagv.com/]cylpobfgud[/url], http://www.egjvioikgm.com/ mhpgaekemr

 |