テレビ番組をキーワード検索してSlack通知するならYahoo!テレビが最強（golang+goquery）

特定のキーワード（キャラクターや俳優やサービス等）の番組がいつ放送されるかを抑えたい！という人は結構いらっしゃるんじゃないでしょうか。
私もその一人で、自社サービスに関連する市場のニュースは抑えておきたいなぁと思い、テレビ番組をキーワード検索してSlackに流したいなと思っていました。

↓こんな感じで通知したいなぁと。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/106949/2ea6c6a0-4ce5-0f8d-9c45-d793a05156b0.png)


# テレビ番組表のAPIやRSSはない。Yahoo!テレビを使おう。

そこで、テレビ番組表のAPIやRSSを探してみましたが、、該当するものは以下の通りでした。

- NHK番組表API（地上波の全放送局をカバーできない）
- Gガイド.テレビ王国（RSSがあるらしいがSo-net会員になる必要あり）
- Livedoor RSS（2008年にサービス終了）

[テレビの番組表(アニメ)取得APIについて色々](https://qiita.com/kanineshi/items/016da47744adb291a275)というエントリーを拝見しましたが、やはりAPIやRSSで一発で地上波の番組を網羅するのは難しいようです。

そこで、スクレイピング前提でテレビ番組表のサービスを見ていたら、[Yahoo!テレビ.Gガイド](https://tv.yahoo.co.jp/search/?q=IT)のキーワード検索が非常に使い勝手がよいと分かりました。

あなたが神か

`https://tv.yahoo.co.jp/search/?q=IT`のようにGETパラメータだけで、放送予定の番組名と番組内容からキーワード検索してくれます。
もちろん地上波の全放送局をカバー。地域設定も可能で、BS/CSの番組も検索対象に含めることができます。
goo番組表もキーワード検索できるのですが、過去の番組が多くヒットするので、今回は利用しないことにしました。

あとはスクレイピングするだけです。

# golang+goqueryでスクレイピング

Yahoo!テレビ.Gガイドでは、`ul.programlist > li`で取得した各要素をEachすれば番組情報をさくっと取り出せます。(2018年3月時点)
今回は、golangの有名なスクレイピング用ライブラリのgoqueryを使ってみました。
jQueryと同様にセレクタを指定すれば要素を抜き出すことができます。

goqueryの基本的な使い方などは[goqueryでお手軽スクレイピング！](https://qiita.com/yosuke_furukawa/items/5fd41f5bcf53d0a69ca6)を参照していただくとよいかと。
`go get`するなり何なりして利用します。

```
$ go get github.com/PuerkitoBio/goquery
```

とりあえず、以下のように動かすだけで、番組情報をテキスト化することができます。
とっても簡単です。

```golang:main.go
package main

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

func main() {
    doc, err := goquery.NewDocument("https://tv.yahoo.co.jp/search/?q=キーワード")
    if err != nil {
        fmt.Print("document not found. ")
        os.Exit(1)
    }

    program := ""
    doc.Find(".programlist > li").Each(func(_ int, s *goquery.Selection) {
        program += s.Text() + "\n"
    })

    fmt.Print(program)
}
```

私が実際に使うときには、こんな感じに情報を絞って取り出しています。

- 放送日時
- 放送局
- 番組名
- 番組内容
- Yahoo!テレビ内の番組詳細へのリンク

```golang
    postText := ""
    linkUrl := ""
    doc.Find(".programlist > li").Each(func(_ int, s *goquery.Selection) {
        postText += "_"
        s.Find(".leftarea > p > em").Each(func(_ int, em *goquery.Selection) {
            postText += em.Text() + " "
        })
        atag := s.Find(".rightarea > p > a").First()
        postText += s.Find(".rightarea > p > span").First().Text()
        postText += "_"
        postText += " *[" + atag.Text() + "]* :"
        linkUrl, _ = atag.Attr("href")
        postText += s.Find(".rightarea > p").Filter(":not(:has(a,span))").Text()

        postText += "\n" + "> https://tv.yahoo.co.jp/" + linkUrl + "\n"
    })
```

# slack通知までやってみよう

このソースをマルコピでそれなりに動くかと思います。
実行時は `$ go run main.go "キーワード" "https://hooks.slack.com/services/hogefuga/foobar"` のように検索キーワードとwebhookのURLを指定してください。

```golang:main.go
package main

func main() {
    if len(os.Args) != 3 {
        fmt.Print("specify keyword and webhook url.")
        os.Exit(1)
    }
    var keyword string = os.Args[1]
    var webhook string = os.Args[2]

    doc, err := goquery.NewDocument("https://tv.yahoo.co.jp/search/?q=" + keyword)
    if err != nil {
        fmt.Print("document not found. ")
        os.Exit(1)
    }

    postText := ""
    linkUrl := ""
    doc.Find(".programlist > li").Each(func(_ int, s *goquery.Selection) {
        postText += "_"
        s.Find(".leftarea > p > em").Each(func(_ int, em *goquery.Selection) {
            postText += em.Text() + " "
        })
        atag := s.Find(".rightarea > p > a").First()
        postText += s.Find(".rightarea > p > span").First().Text()
        postText += "_"
        postText += " *[" + atag.Text() + "]* :"
        linkUrl, _ = atag.Attr("href")
        postText += s.Find(".rightarea > p").Filter(":not(:has(a,span))").Text()

        postText += "\n" + "> https://tv.yahoo.co.jp/" + linkUrl + "\n"
    })

    if postText == "" {
        fmt.Print("program not found. ")
        os.Exit(0)
    }

    jsonStr := "{\"text\":\"" + postText + "\"}"
    fmt.Print(jsonStr)
    req, _ := http.NewRequest(
        "POST",
        webhook,
        bytes.NewBuffer([]byte(jsonStr)),
    )
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Print(err)
    }
    defer resp.Body.Close()

}
```

cronで毎晩キーワード検索を仕掛けるといいんじゃないかと思います。！
以上です。