どうも、お久しぶりです、7yan00です!
今回はgoでシンプルなweb画像収集クローラーを作る話をしたいと思います。
しかし、まだまだ経験不足故、間違っている点、改善点なども多数あるかと思いますので、もし何かあれば僕7yan00にリプライなりコメントなりをしていただければ幸いです。
## スクレイピング
web上の画像を収集するためにはまずその画像のリンク先の値を入手せねばなりません、よってここではまずスクレイピングをします。net/htmlなどを使って取得することもできますが、ここではより簡単にgoqueryを使いたいと思います!
goqueryでお手軽スクレイピング!
Big Sky::Go言語で jQuery ライクな操作が出来る goquery を試した
を参考にさせてもらいました。
goqueryは短い上にシンプルにスクレイピングするコードを記述することができます。
今回はblog.golang.orgをスクレイピングして、imgタグのsrc属性を検出することで画像のリンク先の値を取得するコードを書いてみます。
package main
import (
"fmt"
"github.com/PuerkitoBio/goquery"
)
func GetPage(url string) {
doc, _ := goquery.NewDocument(url)
doc.Find("img").Each(func(_ int, s *goquery.Selection) {
url, _ := s.Attr("src")
fmt.Println(url)
})
}
func main() {
url := "http://blog.golang.org/"
GetPage(url)
}
実行結果は
% go run main2.go
5years/gophers5th.jpg
5years/conferences.jpg
io2014/summerfest.jpg
io2014/booth.jpg
io2014/collage.jpg
docker-outyet.png
とちゃんとblog.golang.orgの画像のリンク先を拾ってきていることが分かります!
でもこのままだと、まだ入力したurlから拾えるリンク先しかとってこれないので、もう一手間加えて、最初に指定したurl以下すべてをクロールできるようにします、
##もう一手間加える
package main
import (
"fmt"
"net/url"
"strings"
"github.com/PuerkitoBio/goquery"
)
var stock = []string{}
var base string = "http://blog.golang.org/"
func main() {
result := makeUrl(base)
results := GetUrl(result)
for len(results) > 0 {
results = GetUrl(results)
}
fmt.Println(results)
}
func GetUrl(urls []*url.URL) []*url.URL {
sourceUrl := []*url.URL{}
L:
for _, item := range urls {
url_string := item.String()
for e := 0; e < len(stock); e++ {
if url_string == stock[e] {
continue L
}
}
if !strings.Contains(url_string, base) {
continue L
}
stock = append(stock, url_string)
results := makeUrl(url_string)
sourceUrl = append(sourceUrl, results...)
}
return sourceUrl
}
func makeUrl(base string) []*url.URL {
doc, _ := goquery.NewDocument(base)
var result []*url.URL
doc.Find("a").Each(func(_ int, s *goquery.Selection) {
target, _ := s.Attr("href")
base, _ := url.Parse(base)
targets, _ := url.Parse(target)
result = append(result, base.ResolveReference(targets))
})
return result
}
*この部分は前に僕が自分のブログで書いたものとほとんど同じです、ただ変数名が理解不能なものを使っていたので、訂正しました。
resultsの要素数まで関数を回し続ける、といったような感じです。なんだかもう少しうまいやり口があるような気もするので、もしよければ指摘してもらえればありがたいです。
L:
for _, item := range urls {
url_string := item.String()
for e := 0; e < len(stock); e++ {
if url_string == stock[e] {
continue L
}
}
同じurlにもう一度リクエストを投げたりなんてすることがないように、一度呼んだurlはスライスに格納して、関数内にマッチするかどうかの条件節を置いて、マッチする、即ち既に呼んだurlならば同じ処理をさせず、リストを使ってもう一度、forループの始点に戻るようにしました。
if !strings.Contains(url_string, base) {
continue L
}
また特定のurl以下を呼ばなければならないので、文字列に特定のurlを含むかどうかを条件として、含まないようであれば、また関数を回してあげることにしました。
以上のような感じでやってはみたのですが、量が多すぎるため、クロールしきれないです、たぶん書き方によってはどうにかなるとはおもうのですが、、、助言などしてもらえれば助かります。
画像の保存
スクレイピングをする手段は分かったので、今度は保存してみましょう。http.GetでとってきたBodyをos.Createで作ったファイルにコピーする、という要領で画像の保存できます。
今回はきたけー (id:kitak)さんの記事を参考にさせていただきました!
golang で URLの画像データを取得して、ローカルのファイルに保存する術
では実際に記述してみましょう、
package main
import (
"io"
"net/http"
"os"
)
func main() {
var url string = "http://blog.golang.org/5years/gophers5th.jpg"
response, err := http.Get(url)
if err != nil {
panic(err)
}
defer response.Body.Close()
file, err := os.Create("gopher.jpg")
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(file, response.Body)
}
これでファイルが保存されていますね!
##この2つを合体する
この2つを合体してスクレイピングしてとってきたリンク先の画像を保存していくツールを作っていきます。
package main
import (
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"sync"
"github.com/PuerkitoBio/goquery"
)
var stock = []string{}
var base = "http://blog.golang.org/"
var i int = 0
var wg = new(sync.WaitGroup)
func main() {
flag.Parse()
fmt.Println("It works!")
doc, _ := goquery.NewDocument(base)
results := makeUrl(doc)
for len(results) > 0 {
results = GetUrl(results)
}
wg.Wait()
}
func containsInStock(value string) bool {
l := len(stock)
for i := 0; i < l; i++ {
if stock[i] == value {
return true
}
}
return false
}
func GetUrl(urls []*url.URL) []*url.URL {
saurceUrl := []*url.URL{}
L:
for _, item := range urls {
url_string := item.String()
if !strings.Contains(url_string, base) {
continue L
}
if containsInStock(url_string) {
continue L
}
fmt.Println(url_string)
stock = append(stock, url_string)
doc, _ := goquery.NewDocument(url_string)
results := makeUrl(doc)
wg.Add(1)
go GetImage(doc)
saurceUrl = append(saurceUrl, results...)
}
return saurceUrl
}
func makeUrl(doc *goquery.Document) []*url.URL {
var result []*url.URL
doc.Find("a").Each(func(_ int, s *goquery.Selection) {
target, _ := s.Attr("href")
base, _ := url.Parse(base)
targets, _ := url.Parse(target)
result = append(result, base.ResolveReference(targets))
})
return result
}
func GetImage(doc *goquery.Document) {
var result []*url.URL
doc.Find("img").Each(func(_ int, s *goquery.Selection) {
target, _ := s.Attr("src")
base, _ := url.Parse(base)
targets, _ := url.Parse(target)
result = append(result, base.ResolveReference(targets))
})
for _, imageUrl := range result {
imageUrl_String := imageUrl.String()
if containsInStock(imageUrl_String) {
continue
}
response, err := http.Get(imageUrl_String)
if err != nil {
panic(err)
}
defer response.Body.Close()
file, err := os.Create(fmt.Sprintf("hoge%d.jpg", i))
i++
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(file, response.Body)
}
wg.Done()
}
ここのポイントとしては、stockにその値があるかどうか調べることが多くなったので、func containsInStock を作って共通化をしました。
func containsInStock(value string) bool {
l := len(stock)
for i := 0; i < l; i++ {
if stock[i] == value {
return true
}
}
return false
}
こんな感じでループが重複するのを防いでいます。
あとはwaitgroupの使い方で、waitgroupを作って
var wg = new(sync.WaitGroup)
あとはfunc mainでwg.Waitしてあげて、func GetImageが終了するまですなわちwg.Doneするまでまってあげる、という形です
これをしないと、func mainが終了した時点で、実行が終了してしまいますよね。
こんな感じですね。waitgroupをうまく使うのがコツっぽかったです。これで完成ですね。ただ先ほどもいった通り、まだまだ改善の余地が多々ありそうなコードです。。。
今回はこんな感じで、golangでweb画像収集クローラーを作るという記事とします!
前回の記事でコミュ二ティの方々に多くの迷惑をかけてしまい、本当に申し訳ないと思っています、、、
でも、なんとか技術力をあげて、皆さんのお力になれる、強いエンジニアになれたらな、と思っています。
ただ、本当にまだまだなので、多々間違いがあると思います、もし何か間違いがあれば、言っていただければ、と思います。