scala-scraperを使ってスクレイピングしてみた時のメモ

  • 13
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

スクレイピングツール色々あるんですが、最近検索してみたら scala-scraper というものが見つかったので、そちらを試してみた時のメモです。

scala-scraper Github Repository
https://github.com/ruippeixotog/scala-scraper

今回検証用に書いたコードはここにあります。
https://github.com/ara-ta3/scala-scraper-getting-started

インストールはbuild.sbtに下記を記述するだけです。

libraryDependencies += "net.ruippeixotog" %% "scala-scraper" % "1.0.0"

検証した環境は

  • scala version 2.11.8
  • sbt version 0.13.11

です。

特徴

ファイル・文字列・HTTPで取得したhtmlからDOMを取得できる。

GET/POSTアクセス・ローカルのファイル・プログラム上に定義された文字列からDOMを取得することができます。

例はREADMEの quick-start の所にあります。
https://github.com/ruippeixotog/scala-scraper#quick-start

今回はプログラム上にダミーのHTMLを用意して、色々試してみました。
用意したHTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>dummy</title>
  </head>
  <body>
    <h1>h1tag</h1>
    <div class="contents">
      <ul>
        <li>a</li>
        <li>b</li>
        <li>c</li>
      </ul>
    </div>
    <div id="aaa">
    </div>
  </body>
  <script>
    var span  = document.createElement("span");
    span.innerText= "aaa";
    document.getElementById("aaa").appendChild(span)
  </script>
</html>

CSS Selectorの文法でDOMを取得できる

どこまで出来るかとか検証していないですが、 document.querySelector やjQueryのquery selectorと同じようにDOMを取得できます。

http://www.w3schools.com/cssref/css_selectors.asp

試したScalaコード

package com.ru.waka

import net.ruippeixotog.scalascraper.browser._
import net.ruippeixotog.scalascraper.dsl.DSL._
import net.ruippeixotog.scalascraper.dsl.DSL.Extract._

object Main {
  val dummy = """
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
        <title>dummy</title>
      </head>
      <body>
        <h1>h1tag</h1>
        <div class="contents">
          <ul>
            <li>a</li>
            <li>b</li>
            <li>c</li>
          </ul>
        </div>
        <div id="aaa">
        </div>
      </body>
      <script>
        var span  = document.createElement("span");
        span.innerText= "aaa";
        document.getElementById("aaa").appendChild(span)
      </script>
    </html>
  """
  val jsoupBrowser = JsoupBrowser()
  def main(args: Array[String]): Unit = {
    val doc = jsoupBrowser.parseString(dummy)
    println("=== JsoupBrowser ===")
    println(doc >> text("title"))
    println(doc >> text("h1"))
    println(doc >> elementList("li"))
    println(doc >> attr("charset")("meta"))
    println(doc >?> attr("charset")("title"))
    println(doc >?> text("#aaa span"))
  }
}

これを実行するとこんな感じになります。

[info] Running com.ru.waka.Main 
=== JsoupBrowser ===
dummy
h1tag
List(JsoupElement(<li>a</li>), JsoupElement(<li>b</li>), JsoupElement(<li>c</li>))
UTF-8
None
None

主に使うものは >>, >?>, text, elementList, attr あたりです。

>>>?> はparseしたHTML文書からDOMを取得するときに使うのですが、 >> はCSSセレクタに対応するDOMが存在しない時に java.util.NoSuchElementException をthrowするのに対し、 >?> はNoneを返すという違いがあります。

println(doc >?> attr("charset")("title")) // 存在しないのでNoneが返る
println(doc >> attr("charset")("title")) // 同じく存在しないが、 `java.util.NoSuchElementException` がthrowされる。

text は textの引数のCSSセレクタに対応するDOMのinnerTextを返します。
もしCSSセレクタが複数のDOMに対応する場合、マッチした先頭のDOMが対象となります。

println(doc >> text("title")) // -> dummy
println(doc >> text("li") // // -> a

elementList は引数のCSSセレクタに対応するDOMのListを返します。

println(doc >> elementList("li")) // -> List(JsoupElement(<li>a</li>), JsoupElement(<li>b</li>), JsoupElement(<li>c</li>))
println((doc >> elementList("li")).map(_.text)) // -> List(a, b, c)

attr はattr(アトリビュート名)(CSSセレクタ)の様に利用して、DOMに設定された属性を取得します。

// <meta charset="UTF-8">
println(doc >> attr("charset")("meta")) // -> UTF-8

2種類のブラウザ実装が使える

HtmlUnitの場合JavaScriptも実行してくれるので、JavaScript実行後のDOMもいじることができます。
上記のdummyHTMLにはDOMを追加するscriptを記述しています。
なので、JavaScriptが実行されていれば、idがaaaのDOMの中にaaaという文字列を含んだspanタグが存在するはずです。

package com.ru.waka

import net.ruippeixotog.scalascraper.browser._
import net.ruippeixotog.scalascraper.dsl.DSL._
import net.ruippeixotog.scalascraper.dsl.DSL.Extract._

object Main {
  val dummy = """
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
        <title>dummy</title>
      </head>
      <body>
        <h1>h1tag</h1>
        <div class="contents">
          <ul>
            <li>a</li>
            <li>b</li>
            <li>c</li>
          </ul>
        </div>
        <div id="aaa">
        </div>
      </body>
      <script>
        var span  = document.createElement("span");
        span.innerText= "aaa";
        document.getElementById("aaa").appendChild(span)
      </script>
    </html>
  """
  val jsoupBrowser = JsoupBrowser()
  val htmlUnitBrowser = HtmlUnitBrowser()
  def main(args: Array[String]): Unit = {
    println("=== JsoupBrowser ===")
    val doc = jsoupBrowser.parseString(dummy)
    println(doc >?> text("#aaa span"))

    println("=== HtmlUnitBrowser ===")
    val doc2 = htmlUnitBrowser.parseString(dummy)
    println(doc2 >?> text("#aaa span"))
  }
}

実行結果

[info] Running com.ru.waka.Main 
=== JsoupBrowser ===
None
=== HtmlUnitBrowser ===
Some(aaa)

という感じでした。
JavaScriptのフロントサイドフレームワークが使われている場合など、HtmlUnit製の方を使えばScraping出来るかもしれないですね。

まとめ

  • scala-scraper というライブラリがある
  • 現時点だと2種類の実装(JsoupBrowser, HtmlUnitBrowser)がある
  • HtmlUnitBrowserではJavaScriptも実行可能
  • DOMを取得する部分ではCSSセレクタが利用できて楽