- 投稿日:2020-08-12T22:49:27+09:00
go言語でHTMLをスクレイピングしてそれを送信するslackbotを作成する
はじめに
普段はvueとかtypescriptとか使って、フロントエンドを書いています。
純粋に興味がありgo言語を1から勉強し始め、試しにスクレイピングして取ってきたデータを定時実行で送るslackbotを作成しました。
結構似たようなこと書いてる記事が多いのですがあまり最新の情報がなくメモのためでもあります。
go言語触り始めたばかりなのでコードが理解できず不要なのが混じっているかもしれませんがご了承ください。完成したのはこんな感じです、ローカルで試したときなので時間はおかしいです。
ソースコードはこちらにあります、よかったらstar押していってください!環境、使ったパッケージ
go version go1.14.3
github.com/PuerkitoBio/goquery
slackの設定
他の方の記事にいくらでも乗っているので詳しい説明は省略します、
slackbot 設定
とかで調べれば出てくるはず。このサイトから上位の記事を抜き出します。
サイトの負担を回避するためにローカルにhtmlをダウンロードしてそこからスクレイピングします。ディレクトリ構成
home/
┠repositoty
┗copy.go
┗scraping.go
┠data
┗index.html
┠slack
┗notify.go
┗app.yaml
┗cron.yaml
┗main.gocopy
copy.goのファイルではスクレイピング元のサイトからhtmlをダウンロードしてdata/index.htmlにコピーします。
copy.gofunc Copy() { url := "https://kabutan.jp/info/accessranking/2_1" //urlのサイトのbody部分を取得 res, err := http.Get(url) if err != nil { log.Fatal(err) } defer res.Body.Close() //body部分をbodyに代入 body, err := ioutil.ReadAll(res.Body) if err != nil { log.Fatal(err) } //bodyの内容を./data/index.htmlに出力 ioutil.WriteFile("./data/index.html", body, 0666) }コメントしたとおりです、ローカルにダウンロードしてます。
scraping
scraping.goのファイルではcopyしてきたdata/index.htmlからurlをとってきます。
scraping.gofunc Scraping() (links []string){ //スクレイピングしてとってくるのがurlの後半部分だけなので前にくっつけるために定義 url := "https://kabutan.jp" //./data/index.htmlを読み込む fileInfos, _ := ioutil.ReadFile("./data/index.html") //stringじゃないといけないからstringに変換 stringReader := strings.NewReader(string(fileInfos)) //スクレイピングするファイルを指定 doc, err := goquery.NewDocumentFromReader(stringReader) if err != nil { log.Fatal(err) } doc.Find("table.s_news_list tbody tr td a").Each(func(_ int, s *goquery.Selection) { //leadに上記のタグのhrefの値を順番に代入 lead, _ := s.Attr("href") //リンクを正常に飛ばすためにhttps://kabutan.jpをくっつける link := (url + lead) //[]string型であるlinksに前からlinkを挿入する links = append(links, link) }) return links }これもコメントの通りです。
urlをくっつけるかどうかはスクレイピングしてくるサイトによると思うので、一度fmtとかで出力してみてurlおかしかったら工夫したらいいと思います。notify
notify.goではslackで送るtextを作成します。
repositoryという名前でrepositoryディレクトリをimportしてます。slack/notify.gofunc Createdata() string { text := []string{} links := repository.Scraping() //linksをforで回してlinkに入れる for i, link := range links { //10件までしか取れないようにする if i >= 10 { break } //"[1] newsのURL" のような形にする text = append(text, fmt.Sprintf("[%d] <%s>", i+1, link)) } //改行してわかりやすくする slackpost := strings.Join(text, "\n") return slackpost }おそらくアタッチメントとかもここでやったほうがいいと思うんですがmain.goでやってしまってます、許してください。。。
main
main.goで送信の処理します、絶対notifyで全部やるべきなのはわかってます、ユルシテ。。。
slackという名前でslackディレクトリをimportしてます、あとrepositoryも。main.gotype Payload struct { Text string `json:"text"` Attachments []Attachment `json:"attachments"` } type Attachment struct { Color string `json:"color"` Title string `json:"title"` } func handler(w http.ResponseWriter, r *http.Request) { repository.Copy() urlData := slack.Createdata() //webhookのurlを渡してある webhookurl := os.Getenv("WEBHOOK") //Attachmentの型に沿って指定 attachment := Attachment{ "#FFC0CB", urlData, } //Payloadの型に沿って指定 payload := Payload{ "今日のニュースだよ!!!!!!", []Attachment{attachment}, } //payloadをjsonの形に params, err := json.Marshal(payload) if err != nil { log.Println(err) return } //ここでslackに送信する処理 res, err := http.PostForm( webhookurl, url.Values{"payload": {string(params)}}, ) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } body, err := ioutil.ReadAll(res.Body) if err != nil { log.Println(err) return } defer res.Body.Close() log.Println(string(body)) } func main() { http.HandleFunc("/postmsg", handler) http.ListenAndServe(":8080", nil) }main関数に入っている内容を全部消して、handler関数の中身をmainに入れて動かすとローカルで動作が確認できますが、GAEがurlをgetで呼ぶ形っぽかったのでサーバー立ててます。
WEBHOOKの部分はこちらの記事を参考にwebhookのurlを取得して
export WEBHOOK=hogehoge
とターミナルで打つことで定義できます。(hogehogeにurlを入れる)
アタッチメントについてはいろいろ他にもできるみたいなのでこちらを参考にしてみてください。最後に
バックエンド全くわからず、つまずきまくりましたがなんとか形にすることができました、ベストプラクティスではないと思うのでもっと良い方法があったら教えてくだ去ると嬉しいです。
デプロイまでいけましたがcronジョブでエラーが発生し動きません、解決方法がわかる方おりましたら教えていただけると、、、
- 投稿日:2020-08-12T03:15:23+09:00
Golang 基礎(並列処理) part3
part2の続き
goroutine
goroutine: 並列処理。
go 処理 とかく。処理が終わらなくても、プログラムのコードは終了する。
対策①sync.WaitGroupを使う!package main import ( "fmt" "sync" ) func goroutine(s string, wg *sync.WaitGroup) { defer wg.Done() // 並列処理が終了したと伝える for i := 0; i < 5; i++ { fmt.Println(s) } } func normal(s string) { for i := 0; i < 5; i++ { fmt.Println(s) } } func main() { var wg sync.WaitGroup wg.Add(1) // 処理するべき並列処理を1つ追加する go goroutine("world", &wg) // goroutine 並列処理 normal("hello") wg.Wait() // wg.Addで追加した分,wg.Done()されるまで待機。(コードを終わらせない) }対策②channelを使う!
チャネルを受信するまで、受信する行でコードが止まるから。package main import "fmt" func goroutine(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // sumをc(チャネル)に送信 } func main() { s1 := []int{1, 2, 3, 4, 5} s2 := []int{11, 234, 324, 89382} c := make(chan int) // キューのように送られてきたものから順次入っていき、出ていく。 ex. 89951 15 go goroutine(s1, c) go goroutine(s2, c) x := <-c // cに何か入ってくるまでコードがここで止まる!! fmt.Println("x:", x) y := <-c fmt.Println("y:", y) }buffered channel
channelの中に入ってくるデータ(buffer)の個数を指定したチャネル。
ch := make(chan int, 2) // 最大2個までしか受け入れない! ch <- 100 fmt.Println(len(ch)) // 1 ch <- 200 fmt.Println(len(ch)) // 2channelのrangeとclose
rangeで取り出すときは必ずclose()を一緒に使う!
package main import "fmt" func goroutine(s []int, c chan int) { sum := 0 for _, v := range s{ sum += v c <- sum } close(c) // rangeで取り出す場合には、close()でもうこれ以上データが入ってこないと伝える必要がある! } func main() { s := []int{1, 2, 3, 4, 5} c := make(chan int, len(s)) go goroutine(s, c) // 受信したデータを順次forループで取り出していく for i := range c{ fmt.Println(i) } }producerとconsumer
package main import ( "fmt" "sync" ) func producer(ch chan int, i int) { // Something ex.どこかのサーバーに行ってログを取ってくる ch <- i * 2 } func consumer(ch chan int, wg *sync.WaitGroup) { for i := range ch { func() { defer wg.Done() // 取ってきたデータをもとになにか処理する。 ex.取ってきたログをsaveする fmt.Println("process", i*1000) }() } } func main() { var wg sync.WaitGroup ch := make(chan int) // Producer 複数のgoroutineでなにか処理(データを取ってきたり)する for i := 0; i < 10; i++ { wg.Add(1) go producer(ch, i) } // Consumer Producerで取ってきたデータを順次処理する。 go consumer(ch, &wg) wg.Wait() close(ch) }fan-out fan-in
並列処理を複数段階に分ける! 例えば役割ごとに処理を分けるなど。
package main import "fmt" func producer(first chan int) { defer close(first) for i := 0; i < 10; i++ { first <- i } } func multi2(first <-chan int, second chan<- int) { defer close(second) for i := range first { second <- i * 2 } } func multi4(second <-chan int, third chan<- int) { defer close(third) for i := range second { third <- i * 4 } } func main() { first := make(chan int) second := make(chan int) third := make(chan int) go producer(first) go multi2(first, second) go multi4(second, third) for result := range third{ fmt.Println(result) } }channelとselect
違う並列処理を複数走らせて、お互いにブロッキングしないようにするときに使う。
package main import ( "fmt" "math/rand" "time" ) func goroutine1(ch chan string) { for { // ex.ネットワーク越しにパケットを受信するイメージ ch <- "packet from 1" time.Sleep(3 * time.Second) } } func goroutine2(ch chan int) { for { ch <- rand.Intn(100) time.Sleep(1 * time.Second) } } func main() { c1 := make(chan string) c2 := make(chan int) go goroutine1(c1) go goroutine2(c2) // 違う並列処理を複数走らせて、お互いにブロッキングしないようにするときに使う for { select { case msg := <-c1: fmt.Println(msg) case num := <-c2: fmt.Println(num) } } }Default Selection: チャネルから受信してないときの処理
for selectから抜けるときは下記のOuterLoop(名前は何でも良い)のようにする。package main import ( "fmt" "time" ) func main() { tick := time.Tick(100 * time.Millisecond) boom := time.After(500 * time.Millisecond) OuterLoop: for { select { case <-tick: fmt.Println("tick.") case <-boom: fmt.Println("BOOM!") break OuterLoop // for selectから抜けるときに使う!名前は何でも良い。 default: // 受信してないときはdefaultの処理が行われる fmt.Println(" .") time.Sleep(50 * time.Millisecond) } } fmt.Println("##############") }sync.Mutex
下のようなコードだと、時々並列処理の書き換えがコンフリクトしてエラーが出る。
package main import ( "fmt" "time" ) func main() { c := make(map[string]int) go func() { for i := 0; i < 10; i++ { c["key"] += 1 } }() go func() { for i := 0; i < 10; i++ { c["key"] += 1 } }() time.Sleep(1 * time.Second) fmt.Println(c, c["key"]) }そこでsync.Mutexを以下のように使う!
package main import ( "fmt" "sync" "time" ) type Counter struct { v map[string]int mux sync.Mutex } func (c *Counter) Increment(key string) { c.mux.Lock() defer c.mux.Unlock() c.v[key]++ } func (c *Counter) Value(key string) int { c.mux.Lock() defer c.mux.Unlock() return c.v[key] } func main() { //c := make(map[string]int) c := Counter{v: make(map[string]int)} go func() { for i := 0; i < 10; i++ { //c["key"] += 1 c.Increment("Key") } }() go func() { for i := 0; i < 10; i++ { c.Increment("Key") } }() time.Sleep(1 * time.Second) //fmt.Println(c, c["key"]) fmt.Println(c, c.Value("Key")) }参考