Scalaの標準機能を使って、HTMLをスクレイピングしてみようと思っていろいろ試したメモ。手探り状態なので、かなりうだうだとしています。Scala2.9前書きScalaは標準でXPathっぽくXMLを扱う機能が用意されている。Webスクレイピングという用途にはわりと向いていると思う。フルセットのXPathのような多様な指定ができるわけではないので、専用のライブラリを使った方がかゆいところに手は届く部分も多いけど、細かいライブラリの使い方を勉強しなくても、普段使ってるScalaのCollectionみたいな気分で要素を操作できるのは、なかなかに心地良い。ScalaのXML操作の基本ScalaでXMLを扱う場合は、scala.xml配下のXML、Elem、Node、NodeSeqあたりのクラスのお世話になることが多い。scala.xml.XMLは下記コードのような感じで、文字列やファイルをElemクラスに変換してくれる。ElemからXML文字列を出力することもできる。<parent><child>a</child><child>b</child></parent>次に、Elem、Node、NodeSeqの3つについて。これらは名前の通り要素を扱うクラス。NodeSeqという継承関係になっているので、3つとも大枠は同じようなものとして扱える。こいつらはSeqLikeとかTraversableも継承しているので、Listなんかを使う時のお馴染みの機能も実行可能になっている。この3つには「\」や「\\」という関数が用意されている。「\」や「\\」は指定した要素名や属性名にマッチする要素を抽出する、スクレイピングで重宝する機能。下記のようなXMLがあったとする。"child"」と指定すると、parentの直下の要素から、child要素を抽出してくれる。「\」は直下だけを見るので、childrenの下にいるchild(B)は取れず、AとCのchildだけが取れる。"child"」と指定した場合は、再帰的に子要素を見に行く。A〜Cすべての要素が取れる。id="c">C</child>)「\」や「\\」は連結して書ける。childrenの下のchildのように指定したい場合は、こんな風に書く。id="b">B</child>)@を指定すると、属性の値を取ってくる。id属性の値を取りたい場合は以下のような書き方になる。NodeSeq(b)すべての要素を指定する場合は、要素名とかを指定していたところに「_」を指定する。childこんな感じで、ScalaではXMLを割と気軽に処理できる。HTMLをパースするにはValidなXHTMLであればScalaの機能でさらっとパースができるわけだけど、世の中に整形式になってるサイトなんて滅多に存在しないし、HTMLだってパースしないといけない。Webページのパースは専用のライブラリに任せる。Java製のHTMLパーサはいくつか存在している。Parserあたりが有名だろうか。Firefox4のHTML5パーサとして採用されていたり、Liftの中でも利用されているなど、割と実績がある子。Parserhttp://about.validator.nu/htmlparser/2011/6/17現在、最新バージョンはMavenやSBTでは落とせないらしい。1個前のバージョンなら取得可能。nu.validator.htmlparserhttp://mvnrepository.com/artifact/nu.validator.htmlparser/htmlparserということで、さっそくValidator.nuさんを使って、XML文書をScalaのNodeクラスに変換してみる。setNamePolicyにXmlViolationPolicy.ALLOWを設定しておくと、たいていのHTMLはなんとか解釈してくれる。設定しないと「崩れてるぞ」といって例外を起こす。saxer.rootElem}これでNodeが取れる。引数は文字列で渡しているけど、InputSourceはInputStreamも引数に取れるので、直接InputStreamを渡すことの方が多いかもしれない。試しにHTMLを解析してみる試しに、はてなブックマークのトップページから、一覧に出ているエントリーのタイトルを取得してみる。class="entry-body"」要素の下の、「h3」の「a」の下にタイトルは入っているっぽい。先程の関数を使ってNodeを取る。toNode(body)普通はSourceを後で閉じるけど割愛。次に「class="entry-body"」が指定されたdiv要素を取る。属性はText型で返ってくるので、Text(entry-body)で比較してあげる。Text("entry-body"))最後にentryBodyの下のh3の下のaを取る。上の行と繋げて1行で書いても良かったのだけど、折り返すと見づらくなるので分割した。"a"これでtitlesの中にはエントリーの一覧が入る。見た感じ、attributeの判定が少し面倒。そこをXPath使っていい感じにやりたければ、Javaのライブラリを使う必要がある。conversionで後付けしても十分な気もする。CollectionのようにXMLを使ってみるElemとNodeとNodeSeqはSeqLikeとTraversableを継承しているので、Listを使うときにお馴染みの機能も利用できるようになっている。dropとかdropRightで、要素をSeqから落とすことができる。table配下のtrを取ってきた時に、一番上のヘッダとか一番下のフッタを落とす表記が割と綺麗に書ける。<tr><td>2</td></tr>)あとは要素の数をカウントするのが楽だったり。4最初とか最後の要素を取得したり。<td>ふった</td>map関数でListを作ったり。ふった)2つのテーブルをUnionしてみたり。"td")と、まぁ、なかなかに自由に振る舞える。HTMLと戯れてみる実例が少ないので、もう少しいろいろやってみる。こんなHTML(の一部)があったとする。項目3→baz)のように変換したい場合はどう書くと良いだろう。div配下を取ってみる。いう時はchild関数を使っても取れるけど、childは空白とかをPCDataとして取ってきたりするのでイマイチ。PCDataやTextはisAtomがtrueなので簡単にフィルタリングはできる。spanの順で3つ取れている。このくらい規則正しいHTMLだったら、for文で3ステップずつ進みながらyieldすれば楽な気がする。baz)でも、世の中こんなに素直なHTMLは稀なので、ちゃんと要素名を見たりしてチェックしながら入れていった方が良いのだろうな。Scalaにはせっかく要素に対してもパターンマッチが使えるので、そいつを使ってみるとこんな感じとか。baz)なんかもっといい書き方があるような気もする。HTMLのパースはどのみち表記揺れに悩まされていろいろゴニョゴニョすることになるので、このくらいで良いか。value)という関数で、「指定属性名=値」でフィルタをかけられるようにしてみる。value)という属性フィルタ付きの\\も実装してみる。}}試しに使ってみる。id="a">A</bar>まぁ、それなりに書きやすくはある。"bar[@id=b]"」とか書きたいという欲求はあるけど。試しに手書きでclassFilter(HTMLのclass指定用のフィルタ)とか、regexFilter(正規表現で判定するフィルタ)とか作ってみたら、少ないコード量の割には便利と便利に使えた。こんなコードhttps://github.com/mwsoft/sample/blob/master/xmlhelper/src/main/scala/jp/mwsoft/sample/xmlhelper/XmlFilter.scalaこんな感じで呼び出すhttps://github.com/mwsoft/sample/blob/master/xmlhelper/src/test/scala/jp/mwsoft/sample/xmlhelper/XmlFilterTest.scalaScalaでXPathを使う方法を考えるJavaのSDKにはXPathを扱う機能も入っている。その辺りを使えば特にライブラリとかは使わずにこんな風に書ける。Aけど、scalaのElemとJavaのElementは構造からして違うものなので、上の記述とScalaのXMLを連携させるとパフォーマンス的にあまり現実的じゃないことになりそう。いってJavaの機能そのままでガリガリ書くくらいなら、ScalaのXML使った方が楽。それならJavaのXPathImplとかを参考にScalaでXPathを実装しちゃえYO!みたいな気持ちになるかもしれないけど、XPathの実装なんてどんだけ面倒だか分からない。com.sun.org.apache.xpathを読んだだけでお腹いっぱい。スクレイピング目的なら、jQueryっぽい方向で攻めるライブラリを考えた方が良いかもと思ったりもした。そっちの方が実装は楽そうだし、知名度高いし。