20200812のGoに関する記事は2件です。

go言語でHTMLをスクレイピングしてそれを送信するslackbotを作成する

 はじめに

普段はvueとかtypescriptとか使って、フロントエンドを書いています。
純粋に興味がありgo言語を1から勉強し始め、試しにスクレイピングして取ってきたデータを定時実行で送るslackbotを作成しました。
結構似たようなこと書いてる記事が多いのですがあまり最新の情報がなくメモのためでもあります。
go言語触り始めたばかりなのでコードが理解できず不要なのが混じっているかもしれませんがご了承ください。

完成したのはこんな感じです、ローカルで試したときなので時間はおかしいです。
スクリーンショット 2020-08-12 14.58.03.png
ソースコードはこちらにあります、よかったら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.go

copy

copy.goのファイルではスクレイピング元のサイトからhtmlをダウンロードしてdata/index.htmlにコピーします。

copy.go
func 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.go
func 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.go
func 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.go
type 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ジョブでエラーが発生し動きません、解決方法がわかる方おりましたら教えていただけると、、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)) // 2

channelの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"))
}

参考

現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む