Help us understand the problem. What is going on with this article?

scala-scraperによるWebスクレイピング

More than 3 years have passed since last update.

scala-scraperによるWebスクレイピング

by harry0000
1 / 26

これなに?

2016/11/16(水)開催のScala勉強会第185回 in 本郷の発表資料です。

Scala勉強会 in 本郷 とは


自己紹介

harry0000

http://qiita.com/harry0000
https://github.com/harry0000

  • 艦これ 甲7提督

  • 艦これアーケード 乙提督
    敵泊地に突入せよ!! 乙作戦 4周

  • Scala歴は10ヵ月程度

  • @cakesolutionsにブロックされる程度のScala能力1

※ 艦これ = 「艦隊これくしょん-艦これ-」。軍艦を擬人化したカードゲーム。


そうだ、スクレイピングしよう (動機)

  • 艦これアーケード攻略Wikiの2つの表に差分があって困る

  • 見せ方が違うだけで、本来は同じデータ

    • 片方に編集漏れが生じると差分ができてしまう
    • 編集者は2つの表を一緒に編集しなければならない(つらい)
  • とりあえず、逆引きの表から海域別の表を生成し、差分を解消することに

    • Webスクレイピング!

※ 海域 = ゲームのステージ。
※ 艦娘 = かんむす。ゲームのキャラクター。


scala-scraper とは

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

最初、使用経験のあったSeleniumを使っていましたが、
遅い & 静的なページのスクレイピングなので、こちらに切り替えました。


Webスクレイピングの流れ

  1. ページ(HTML)の取得
  2. 要素のパース
    • データ取得
    • バリデーション
  3. 好きなようにデータをゴニョゴニョする

ページ(HTML)の取得 (1 / 2)

scala-scraperではBrowserでページにアクセスすると、Documentオブジェクトを取得できます。

val doc: Document = JsoupBrowser().get("https://www.google.co.jp/")

Browserは以下の2つが用意されています。

https://github.com/ruippeixotog/scala-scraper#browsers

  • JsoupBrowser

    • jsoupを使用したBrowser
    • 静的なページ用
      JavaScriptを実行しない
    • 速い
  • HtmlUnitBrowser

    • HtmlUnitを使用したGUI-less Browser
    • 動的なページ用
      JavaScriptを実行する (実行するとは言っていない)

ページ(HTML)の取得 (2 / 2)

BrowserからDocumentを取得する方法は3種類あります。

// 1. Web上のページから
browser.get(url)
browser.post(url, form)

// 2. HTMLファイルから
browser.parseFile(file)
browser.parseInputStream(stream, charset)
browser.parseResource(name, charset)

// 3. 文字列から
browser.parseString(html)
  • parseResource()は、ResourceをInputStreamで開いてparseInputStream()を呼ぶ
  • 詳細はBrowser.scalaでご確認ください

Document

ブラウザ(JavaScript)でのdocumentオブジェクトに相当するtrait。
以下のようなメソッドがあります。

location: String

root: Element

title: String = root.select("title").headOption.fold("")(_.text.trim)

head: Element = root.select("head").head

body: Element = root.select("body").head

toHtml: String

Element

ブラウザ(JavaScript)でのElementインターフェイスに相当するtrait。
以下のようなメソッドがあります。

tagName: String

parent: Option[Element]

children: Iterable[Element]

siblings: Iterable[Element]

attrs: Map[String, String]

hasAttr(name: String): Boolean

attr(name: String): String

text: String

innerHtml: String

outerHtml: String

select(query: String): ElementQuery

ElementQueryIterable[Element]を継承しているtraitです。

trait ElementQuery extends Iterable[Element]

要素の取得

とりあえず黙って以下をimport

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

基本形(イメージ)

Element >> Extractor

Element >?> Extractor
Element ::= (Document | Element | Functor[Element])
Functor ::= (Option | List | Either | ...other functor)
  • 暗黙の型変換やらなんやらが色々絡むため、正確な表現ではなく、あくまでイメージ
  • 引数のExtractorは1~3個渡すことが可能で2、2個以上渡した場合は結果がタプルで返ってきます

val main: Element = doc >> element("#main")

ContentExtractors

これだけ覚えておけば大丈夫な感じのExtractor

  • 要素取得
element(query): Element
elements(query): ElementQuery
elementList(query): List[Element]
  • テキスト取得
text(query): String
texts(query): Iterable[String]
allText(query): String
  • 属性取得
attr(name)(query): String
attrs(name)(query): Iterable[String]
  • フォーム(input)データ取得
formData(query): Map[String, String]
formDataAndAction(query): (Map[String, String], String)

>>

単純に要素を取得する。

  • 要素が存在しなかった場合
    • 単一要素を取得するExtractorは、NoSuchElementExceptionが発生
    • 複数要素を取得時は、黙ってNilを返してくる。。。


単一要素取得: element()text()attr()
複数要素取得: elements()texts()attrs()など


>> の使用例

val doc = JsoupBrowser().get("http://weather.yahoo.co.jp/weather/")

for {
  area <- doc >> elements("#map > ul > li > a > dl")
  (name, weather) = area >> (text("dt"), texts("dd"))
} println(s"$name - ${weather.mkString(" ")}")

Yahoo!天気・災害 - 天気予報 / 防災情報から地域ごとの天気を取得。


>?>

Optionに包んで値を返してくれる。

  • 要素が存在しなかった場合
    • 単一の要素を取得するExtractor: Noneを返す。
    • 複数の要素を取得するExtractor: Some(Nil)を返す。。。

>?> の使用例

val doc = JsoupBrowser().get("http://b.hatena.ne.jp/ctop/it")

for {
  main     <- doc >?> element("#main div")
  topEntry <- main >?> element("div[data-track-section=hotentry] > ul > li.entry-unit")
  entries  <- main >?> elementList("div > ul[data-track-section=hotentry] > li.entry-unit")
  entry    <- topEntry +: entries
  users    <- entry >?> text("ul.users")
  contents <- entry >?> element("div.entry-contents h3")
  url      <- contents >?> attr("href")("a")
  title = contents.text
} {
  println(s"$users,$title,$url")
}

はてなブックマークのテクノロジーカテゴリの記事をカンマ区切りで取得。

Optionなのでfor式が使えます。


要素のバリデーション

Content Validation
https://github.com/ruippeixotog/scala-scraper#content-validation

Element ~/~ validator(<extractor>)(<matcher>)

<extractor>で抽出した要素が<matcher>を満たすかチェックする。

~/~scala.util.Eitherを返すので、for式で使えないこともないです。

match式のために、VSuccessVFailureが用意されています。


~/~ の使用例

val doc = JsoupBrowser().get("https://www.google.co.jp/")

doc ~/~ validator(text("#hplogo"))(_.nonEmpty) match {
  case VSuccess(d) => println("いつものGoogleトップページ?")
  case VFailure(_) => println(s"今日は何の日? : ${(doc >> attrs("alt")("img")).mkString}")
}

google.co.jpのlogoのテキストが空でないことをチェックしています。
普段logoには「日本」とテキストが入っていますが、
何か特別な日だった場合、画像などに差し変わってる場合があります。

2016/11/13
いつものGoogleトップページ?

2016/11/14
今日は何の日? : フレデリック バンティング 生誕125周年


Tips: >>>?>~/~は続けて書くことができる

ElementFunctor[Element]であればこれらのメソッドが使えるため、続けて書くことができます。

試しにいっぱい繋げてみると…

val rowTexts = doc ~/~ validator(text("#account"))(_ == "Logged in") >?> element("#main") >?> elementList("table tbody tr") >> texts("td")

このときrowTextsの型は以下の通りです。

val rowTexts: Either[Unit, Option[Option[List[Iterable[String]]]]]

match式では…

rowTexts match {
  case VSuccess(Some(Some(rows))) if rows.nonEmpty => rows.map(texts => texts.mkString(",")).foreach(println)
  case VSuccess(_) => println("Logged in. But ...")
  case VFailure(_) => println("Not logged in.")
}

Tips: 複数の要素を取得してNilが返ってきた場合、エラーにしたい

>?>でOptionが返るので、Option.filter(_.nonEmpty)Noneに変換します。
Option.toRight()Eitherにもできます。

for {
  rows <- (doc >?> elementList("#body table tbody tr")).filter(_.nonEmpty).toRight("Could not find table rows.").right
} yield {
  // do something.
}

Tips: queryを設定ファイルから読み込む

queryをコードにベタ書きすると、ちょっとしたページのデザイン変更などで
コード修正と再コンパイルが必要になってしまいます。

scala-scraperにはqueryをconfファイルから読み込むためのメソッドが用意されています。

https://github.com/ruippeixotog/scala-scraper#integration-with-typesafe-config

ただしベータ版の機能で、仕様が変更される可能性があります。

NOTE: this feature is in a beta stage. Please expect API changes in future releases.

※ メソッドの詳細は以下のソースをご参照ください


動的なページのスクレイピングは…?

試しにHtmlUnitBrowserでAngularJS(v1.4.0)が導入されているページを取得してみると...

https://kancolle-arcade.net/ac/#/place

11 12, 2016 6:27:02 午後 com.gargoylesoftware.htmlunit.javascript.StrictErrorReporter runtimeError
重大: runtimeError: message=[An invalid or illegal selector was specified (selector: '*,:x' error: Invalid selector: :x).] sourceName=[https://kancolle-arcade.net/ac/scripts/vendor.59dd5e26.js] line=[24] lineSource=[null] lineOffset=[0]
11 12, 2016 6:27:03 午後 com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
警告: Obsolete content type encountered: 'text/javascript'.

ダメみたいですね(諦観)


HtmlUnitがサポートしているJSフレームワーク

http://htmlunit.sourceforge.net/#JavaScript_Support

  • jQuery 1.8.2: Full support
  • MochiKit 1.4.1: Full support
  • GWT 2.5.0: Full support
  • Sarissa 0.9.9.3: Full support
  • MooTools 1.2.1: Full support
  • Prototype 1.7.1 (1.6.0, 1.6.1): Very good support
  • Ext JS 2.2: Very good support
  • Dojo 1.0.2: Good support
  • YUI 2.3.0: Good support

古い…
そこでSeleniumですよ!(本末転倒)


Webスクレイピングのすすめ

  • Webスクレイピングで身近な問題を解決できる場合があります

  • 簡単なアプリならScalaの入門や勉強に最適

  • Scalaで作りたいアプリがなくて困ってる人は、とりあえずWebスクレイピングしてみては?

    • まずは普段見てるページから収集するとよさげな情報を探すところから
    • 画像URLを収集してダウンロードしまくったり etc.

参考資料


  1. フォローしたらブロックされました。Twitter上でのからみは一切なかったので理由は不明。 

  2. 3つのメソッドが用意されていて、それぞれExtractorを1~3個受け取るため。4個以上渡したい場合はプルリクを出しましょう! 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした