インターネット上を流れるトラフィックは量的な意味では動画データが過半数を占めるようになりつつあるが、依然としてテキストデータの重要性は変わらない。テキストデータは日々ものすごいスピードで増え続けており、一人の人間の目で追い続けるのはもはや不可能である。また、それらのテキストデータは大抵の場合構造化されておらず、そのままでは使えないことが多い。そこでデータ分析が必要になる。テキストデータをデータ分析し、意味ある構造を取り出す行為をテキストマイニングと呼ぶ。
本章では、Common Lispのライブラリを用いて、インターネットをクローリングし、必要な情報を収集した上で構造化するためのいくつかの方法を紹介する。
Webスクレイピングとはウェブサイトから必要な情報を取り出す行為のことである。 Webスクレイピングでは、(1)Webからデータを取得し、(2)これを解析し、(3)そこから必要な情報を探索していくというプロセスを踏む。 Common Lispにはこの各段階について対応したライブラリがあるので、以下ではそのインストール方法と基本的な使用例を解説する。
ウェブサイトからデータを取得するためにはHTTPクライアントのDexadorを使用する。Common Lispには古くからDrakmaというHTTPクライアントがあるが、Dexadorの方が(特に同じホストに複数回アクセスする場合において)速く動作する。
インストールはQuicklispから行なえる。
(ql:quickload :dexador)最も基本的かつ重要なのはdex:getで、GETメソッドで指定のURLからデータを取得する関数である。これは(1)本体データの文字列、(2)ステータスコード、(3)レスポンスヘッダーのハッシュテーブル、(4)URI構造体、(5)読み出し元ソケットのストリームの5つを多値で返す。
(dex:get "http://lisp.org/")
"<HTML>
<HEAD>
<title>John McCarthy, 1927-2011</title>
<STYLE type=\"text/css\">
BODY {text-align: center}
</STYLE>
</HEAD>
<BODY>
<h1>John McCarthy</h1>
<img src=\"jmccolor.jpg\" alt=\"a picture of John McCarthy, from his website\"/>
<h3>1927-2011</h3>
<br><br>
<a href=\"http://www-formal.stanford.edu/jmc/\">John McCarthy's Home Page</a><br>
<a href=\"http://news.stanford.edu/news/2011/october/john-mccarthy-obit-102511.html\">Obituary</a>
</BODY>
</HTML>
"
200
#<HASH-TABLE :TEST EQUAL :COUNT 10 {1003B35F73}>
#<QURI.URI.HTTP:URI-HTTPS https://lisp.org/>
#<CL+SSL::SSL-STREAM for #<FD-STREAM for "socket 192.168.11.253:47632, peer: 144.76.156.38:443" {100361BC23}>>このうち特に重要なのは最初の2つで、データ本体は次節で扱うHTMLパーサへの入力として用い、ステータスコードはデータの取得に成功したか失敗したか、さらにその理由を調べるのに用いる。
ここで紹介するPlumpと次節で紹介するCLSSは同じ作者のプロダクトで、セットで使われる。 PlumpはXML/HTMLデータを文字列として受け取り、CLOSオブジェクトのノードから構成されるDOMの木構造を生成し、その木構造のルートノードのオブジェクトを返すパーサである。
例えば以下のようにして、前述のDexadorのgetの結果をパースすると、ルートノードのオブジェクトが返る。
(plump:parse (dex:get "http://lisp.org/"))
;; => #<PLUMP-DOM:ROOT {1008638843}>各ノードの子ノードのベクタはplump:childrenで、親ノードはplump:parentでそれぞれ得られる。
(defparameter root (plump:parse (dex:get "http://lisp.org/")))
(plump:children root)
;; => #(#<PLUMP-DOM:ELEMENT HTML {1008AE94D3}> #<PLUMP-DOM:TEXT-NODE {1008AF2EF3}>)
(plump:parent (aref (plump:children root) 0))
;; => #<PLUMP-DOM:ROOT {1008AE8F93}>DOMの木構造を走査し、各ノードに対して関数を適用する高階関数plump:traverseが用意されている。 例えば、テキストノードから文字列を取り出して連結して返す関数は以下のようになる。
(defun concat-node-text (node)
(let ((text-list nil))
(plump:traverse node
(lambda (node) (push (plump:text node) text-list))
:test #'plump:text-node-p)
(apply #'concatenate 'string (nreverse text-list))))
(concat-node-text root)
"
John McCarthy, 1927-2011
BODY {text-align: center}
John McCarthy
1927-2011
John McCarthy's Home Page
Obituary
"同様にして、DOM木構造に対して変更を加えることもできる。例えばテキストノードの文字列を全て大文字に変更するには、
(plump:traverse root
(lambda (node)
(setf (plump:text node)
(string-upcase (plump:text node))))
:test #'plump:text-node-p)
(concat-node-text root)
"
JOHN MCCARTHY, 1927-2011
BODY {TEXT-ALIGN: CENTER}
JOHN MCCARTHY
1927-2011
JOHN MCCARTHY'S HOME PAGE
OBITUARY
"このように、DOM木構造から情報を取り出したり変更を加えることができる。
(clss:select “img” (plump:parse “<div><p>A beautiful image: <img src=”//example.com/image.png” alt=”image” /></p></div>”))