Haskell で Web スクレイピング

このエントリーをはてなブックマークに追加
どうも。開発の_fpです。
『7つの言語7つの世界』が題材の社内勉強会に参加して Haskell を担当したところ、意外にハマってしまいました。

今回は Haskell で Web スクレイピングということで、シンプルなリンクの抽出と、表形式のデータの取得をやってみたいと思います。
Web 上のライブなデータを扱うことができるので、Haskell をこれから学習する人にとっても面白いのではないかと思います。

一般に Web スクレイピングといえば、Python の BeautifulSoup などが定番かと思いますが、 Haskell にも TagSoup というモジュールがあります。(同名の Java のものとは無関係のようです)

開発環境は Haskell-Platform です。
事前に、いくつか必要なモジュールをインストールしておきます。
> cabal update
> cabal install utf8-string tagsoup
まずは、 ghci を起動して Web のコンテンツ取得を試してみます。
> import Network.HTTP
> getResponseBody =<< simpleHTTP (getRequest "http://www.shanon.co.jp")
HTMLのソースが表示されます。
ついでに以下もそれぞれ試してみると良いでしょう。
> (getRequest "http://www.shanon.co.jp/")
> simpleHTTP (getRequest "http://www.shanon.co.jp")
ここからは、TagSoupを使っていきます。
ソースファイルを作成して、ghciからロードします。
-- hrefs.hs
import Network.HTTP
import Text.HTML.TagSoup

openURL :: String -> IO String
openURL x = getResponseBody =<< simpleHTTP (getRequest x)

hrefs :: IO [String]
hrefs = do
tags <- fmap parseTags $ openURL "http://www.shanon.co.jp/"
let attrs = [head i | (TagOpen "a" i) <- tags]
return [j | ("href", j) <- attrs]
ghciで以下のコマンドを実行します。
> :load hrefs.hs
> hrefs
実行すると、URLのリストが表示されます。
parseTags の戻り値はTagのオープンとクローズのリストなので、まずは a タグをリスト内包表記で取り出し、次にアトリビュートのリストから href の値を取り出してリストとして返しています。

次に、tableタグのデータを抽出してみます。
今回は、個人的に引越し先として検討している千葉県船橋市の小学校のデータを抽出してみます。
元のサイトはこちらです。


さて、先ほどのaタグと違って、今度は開きタグと閉じタグの対応関係を見なくてはなりません。
何度もパターンマッチすればできそうですが、ちょっと面倒です。
そこで、今回は TagSoup.Tree を使って木構造からデータを取得してみます。
-- schools.hs
import Network.HTTP
import Text.HTML.TagSoup
import Text.HTML.TagSoup.Tree
import Data.Char (isSpace)
import qualified Codec.Binary.UTF8.String as U

openURL :: String -> IO String
openURL x = getResponseBody =<< simpleHTTP (getRequest x)

-- |先頭と末尾の空白を取り除く
trim :: String -> String
trim = f . f
where f = reverse . dropWhile isSpace

getSchools :: IO [[[String]]]
getSchools = do
tags <- fmap parseTags $ openURL "http://www.city.funabashi.chiba.jp/kodomo/school/0004/p008678.html"
-- 木構造を取得
let tree = tagTree tags
-- tr を取り出す (先頭2つと末尾は除く)
let schools = map doRow (init $ drop 2 $ [x | (TagBranch "tr" _ x) <- universeTree tree])
return schools
where
-- |セルの文字列を取り出す
doCell :: [TagTree String] -> [String]
doCell xs = [U.decodeString $ trim $ fromTagText t | t@(TagText _) <- flattenTree xs]

-- |td を取り出す
doRow :: [TagTree String] -> [[String]]
doRow xs = map doCell [x | (TagBranch "td" _ x) <- xs]

showSchools = do
s <- getSchools
mapM doSchool s
return ()
where
doSchool xs = do
-- インデックス1の要素(学校名)を取り出す
let sname = head $ xs !! 1
putStr (sname ++ "\n")
-- 残りを表示
putStr ((show $ drop 2 xs) ++ "\n")
ghci
> :load schools.hs
> showSchools
実行結果
船橋
[["3"],["87","(2)"],["3"],["86",""],["3"],["84","(2)"],["3"],["78","(1)"],["3"],["86","(1)"],["2"],["74","(3)"],["17","(2)"],["495","(9)"]]
湊町
[["2"],["69","(3)"],["3"],["79","(2)"],["3"],["81","(6)"],["2"],["71","(3)"],["2"],["75","(2)"],["3"],["83","(4)"],["15","(3)"],["458","(20)"]]
...
一旦、木構造に変換してtrを抜き出し、そこからさらにtdを抜き出しています。最終的にセルの値は flattenTree でリスト構造に戻して取り出します。詳しくはコメントを参照して下さい。

補足すると、doCellのTagTextのリスト内包表記はAs-patternというもので、
パターンの前に識別子と@をつけるとマッチしたタプル全体を参照できます。

空白の除去には、Wikipediaで紹介されている trim 関数をそのまま使いました。
いかにも Haskell らしくていい感じです。

また、サイトから取得した文字列が文字化けしてしまうので、デコードしています。
出力はターミナルの文字コードがUTF-8なら日本語で表示されると思いますが、WindowsではデフォルトがCP932などになっているので注意が必要です。

以上、簡単に説明してみました。
Haskell というと学術的なイメージがありますが、このように実用的なプログラムも簡潔に書けることが分かると思います。
私もまだまだ始めたばかりですが、面白いサンプルがあれば紹介していければと思っています。
次の記事
« Prev Post
前の記事
Next Post »
Related Posts Plugin for WordPress, Blogger...