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

goでスクレイピングするのにgoquery + bluemonday が最強な件

More than 3 years have passed since last update.

goでスクレイピングをしていて、この2つを使用してスクレイピングをしたらとてもとても捗った。

参考にしたサイト

環境

  • MacOSX (Yosemite, ElCapitan)
    • go 1.5.2

goquery

PuerkitoBio/goqueryでも書かれている通り、JQueryと同様の機能をもったライブラリ

  • Selector
  • Attibute
  • manipulation
  • traversal

などJQueryで馴染みのものが一通り使えます。

使い方

まずはいつも通り

go get github.com/PuerkitoBio/goquery

リンクを抽出してみる

/path/to/hoge.go
package main

import (
    "github.com/PuerkitoBio/goquery"
    "fmt"
)

func main() {
    doc, err := goquery.NewDocument("https://github.com/PuerkitoBio/goquery")
    if err != nil {
        fmt.Print("url scarapping failed")
    }
    doc.Find("a").Each(func(_ int, s *goquery.Selection) {
          url, _ := s.Attr("href")
          fmt.Println(url)
    })
}

リポジトリのコードだけのリンクを抽出

jQueryのセレクタがそのまま使える
なのでスクレイピングしたいページにアクセスしてコンソールで

$('table > tbody > tr > td.content > span > a')

とした値と等価なDOM要素が取得できる

/path/to/hoge.go
package main

import (
    "github.com/PuerkitoBio/goquery"
    "fmt"
)

func main() {
    doc, err := goquery.NewDocument("https://github.com/PuerkitoBio/goquery")
    if err != nil {
        fmt.Print("url scarapping failed")
    }
    doc.Find("table > tbody > tr > td.content > span > a").Each(func(_ int, s *goquery.Selection) {
          url, _ := s.Attr("href")
          fmt.Println(url)
    })
}

保存されたHTMLファイルからデータを抽出する

スクレイピングしすぎると相手先に悪いのでファイルからアクセスする。
まずはこれで適当な場所に保存して

/path/to/hoge.go
ackage main

import (
    "github.com/PuerkitoBio/goquery"
    "fmt"
    "io/ioutil"
    "os"
)

func main() {
    doc, err := goquery.NewDocument("https://github.com/PuerkitoBio/goquery")
    if err != nil {
        fmt.Print("url scarapping failed")
    }
    res, err := doc.Find("body").Html()
    if err != nil {
        fmt.Print("dom get failed")
    }
    ioutil.WriteFile("/path/to/goquery.html", []byte(res), os.ModePerm)
}

保存したファイルから読み込む

/path/to/hoge.go
package main

import (
    "github.com/PuerkitoBio/goquery"
    "fmt"
    "io/ioutil"
    "strings"
)

func main() {
    fileInfos, _ := ioutil.ReadFile("/path/to/goquery.html")
    stringReader := strings.NewReader(string(fileInfos))
    doc, err := goquery.NewDocumentFromReader(stringReader)
    if err != nil {
        fmt.Print("url scarapping failed")
    }
    doc.Find("table > tbody > tr > td.content > span > a").Each(func(_ int, s *goquery.Selection) {
          url, _ := s.Attr("href")
          fmt.Println(url)
    })
}

色々なメソッド

jQueryで使えるメソッドは一通りあるみたいです。(どのメソッドがあるかは直接リポジトリをみたほうが早いです)

  • Next()
    • 指定したDOM要素の次の要素を選択する
s := doc.Find("table > tbody > tr > td.content").Next()
// DOMのまま出力
fmt.Print(s.Html())
  • Parent()
    • 選択したDOMの親要素を取得
s := doc.Find("table > tbody > tr > td.content").Parent()
// DOMの要素の中身を出力
fmt.Print(s.Text())
  • 特定のattibuteを指定してDOMを選択
ret, _ := doc.Find("svg[role=img]").Html()
fmt.Print(ret)

とまあ一通り楽ちんできる機能があります。

さあ、次に取得したHTMLのデータクレンジングが必要ですよね

スクレイピングしたデータをそのまま使うことはほぼ皆無なので、取得したHTMLからデータクレンジングが必要になります。
またHTML等をそのままDBにぶっ込む場合はサニタイズも必要になりますよね。
goqueryだけで頑張ってもいいのですが、もう少し楽にクレンジング作業をしてみましょう。

そこでmicrocosm-cc/bluemondayです。

まずはREADME.mdにかかれている通りの alert('hoge') をサニタイズしてみましょう。

package main

import (
    "fmt"

    "github.com/microcosm-cc/bluemonday"
)

func main() {
    p := bluemonday.UGCPolicy()
    html := p.Sanitize(
        `<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
    )

    // Output:
    // <a href="http://www.google.com" rel="nofollow">Google</a>
    fmt.Println(html)
}

おお、素敵。
綺麗にサニタイズしてくれます。

string, byte, io.Readerの型に対応しているみたいです。

p.Sanitize(string) string
p.SanitizeBytes([]byte) []byte
p.SanitizeReader(io.Reader) bytes.Buffer

bluemondayのNewPolicy()がデータクレンジングに最強なんですよ

bluemondayのNewPolicy()メソッドってのがありまして、例えば下記のようなタグ

<ul>
<li class="toclevel-2 tocsection-2"><a href="#.E5.AD.97.E6.BA.90"><span class="tocnumber">1.1</span> <span class="toctext">字源</span></a></li>
<li class="toclevel-2 tocsection-3"><a href="#.E9.9F.B3"><span class="tocnumber">1.2</span> <span class="toctext">音</span></a></li>
<li class="toclevel-2 tocsection-4"><a href="#.E6.84.8F.E7.BE.A9"><span class="tocnumber">1.3</span> <span class="toctext">意義</span></a>
</ul>

うおう・・・リストの構造だけ綺麗にとって他のattibuteいらない・・・・
とかって時に便利なんです。

<ul> <li> だけ抽出する

bluemonday.NewPolicy() の後に
許可するElementだけを AllowElements() に記載するだけです。

package main

import (
    "fmt"

    "github.com/microcosm-cc/bluemonday"
)

func main() {
    p := bluemonday.NewPolicy()

    p.AllowElements("li").AllowElements("ul")

    html := p.Sanitize(
        `<ul>
<li class="toclevel-2 tocsection-2"><a href="#.E5.AD.97.E6.BA.90"><span class="tocnumber">1.1</span> <span class="toctext">字源</span></a></li>
<li class="toclevel-2 tocsection-3"><a href="#.E9.9F.B3"><span class="tocnumber">1.2</span> <span class="toctext">音</span></a></li>
<li class="toclevel-2 tocsection-4"><a href="#.E6.84.8F.E7.BE.A9"><span class="tocnumber">1.3</span> <span class="toctext">意義</span></a>
</ul>
`,
    )

    fmt.Println(html)
}

結果

go run hoge.go
<ul>
<li>1.1 字源</li>
<li>1.2 音</li>
<li>1.3 意義
</ul>

おお。望み通りになった。

一般的なプロトコルだけ抽出する

例えば scp://wiki.jp/hoge みたいな一般的ではない(scpプロトコルは一般的ですが)hrefが存在したとします。

package main

import (
    "fmt"

    "github.com/microcosm-cc/bluemonday"
)

func main() {
    p := bluemonday.NewPolicy()

    p.AllowElements("li").AllowElements("ul")
    p.AllowStandardURLs()
    p.AllowAttrs("href").OnElements("a")
    html := p.Sanitize(
        `<ul>
<li class="toclevel-2 tocsection-2"><a href="scp://wiki.jp/hoge"><span class="tocnumber">1.1</span> <span class="toctext">字源</span></a></li>
<li class="toclevel-2 tocsection-3"><a href="scp://wiki.jphoge"><span class="tocnumber">1.2</span> <span class="toctext">音</span></a></li>
<li class="toclevel-2 tocsection-4"><a href="https://google.com"><span class="tocnumber">1.3</span> <span class="toctext">意義</span></a></li>
</ul>
`,
    )

    fmt.Println(html)
}

実行してみましょう

go run hoge.go
<ul>
<li>1.1 字源</li>
<li>1.2 音</li>
<li><a href="https://google.com" rel="nofollow">1.3 意義</a></li>
</ul>

おお。httpスキーマだけ綺麗に抽出してくれました。

AllowElements()を使えば、不必要なDOM要素を除去できます。

こんな感じで例えばdivの入れ子がいっぱい複雑でメンドイ・・・とかも上手くAllowElements()を使えば、
綺麗なデータ構造のHTML Nodeを保持できるかと思います。
(元々の用途とは若干違うような気もしますが・・・・)

最後に

一生懸命HTMLNodeを正規表現、またはnet/htmlで頑張ろうと思ったのですが、bluemonday見つけて楽にできて本当に助かった・・・・

ryurock
認定スクラムマスター 認定プロダクトオーナー http://www.scrumalliance.org/community/profile/rkimura2
https://github.com/ryurock
visasq
ビザスクは「知見と、挑戦をつなぐ」をミッションに、世界で1番のナレッジプラットフォームをつくっています。 様々なニーズにつなぐことで、実際に経験したことで得られた知識や意見を、知見として価値最大化します。組織、世代、地域を超えて、知見を集めつなぐことで、世界中のイノベーションに貢献します。
https://visasq.co.jp
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