はじめに
なんか面白そうなAPIとか探していたら、NHK番組表APIなるものを発見しました。
NHKっていったら、紅白、高校野球、朝ドラ、大河、天才テレビくんとか忍たま乱太郎とか、昔からそんなイメージ。
最近だと、キングダムとか進撃の巨人なんかも放送してたり、昔のイメージを覆しはじめてますねー。
ちなみに最近見つけた僕のイチオシは「香川照之の昆虫すごいぜ!」です。
ちなみに、NHKって不定期放送多くないですかね?
LIFE!とか、見逃しちゃうんですよね〜。
ウッチャンのコント番組見ると、笑う犬思い出しちゃいますよね〜。ミル姉さんとか。
ということで、今回はLambda使ってサーバーレスな感じで、見たい番組・好きなタレントが出ている番組をSlackに通知する、お手軽アプリケーション作ろうかと思います。
言語はGo(勉強中)です。
環境など
- AWS Lambda
- Amazon CloudWatch Events(cron式)
- 言語:Go
- NHK番組表API(Program List API)
NHK番組表APIについて
APIを使用するにあたって、こちらから、ユーザ登録が必要です。
メールアドレスの登録をした後、アプリの登録をしてAPIキーを発行する必要があります。
今回使用するProgram List APIの仕様確認やAPIの試打は、こちらから。
Goの実装
ディレクトリ構成
以下のような構成にしました。
% tree
.
└── nhk_api_test
├── README.md
├── cmd
│ └── nhk_api_test
│ └── main.go // main関数
├── go.mod
├── go.sum
├── payload.go // Slackのwebhookで使う構造体
├── proglamList.go // 番組表APIの構造体
└── webhook.go // メインロジック
それぞれの実装について、ざっくり説明します。
Go触ってて、学んだところとか。
main.go
package main
import (
"github.com/aws/aws-lambda-go/lambda"
nhk "github.com/tkl4230/nhk_api_test"
)
func webhook() {
nhk.Webhook()
}
func main() {
lambda.Start(webhook)
}
特に主だった処理はしていません。
Lambdaの開始とwebhook.go
のWebhook()
を呼び出しているだけです。
goでは、javaとかみたくインスタンス.関数()
ではなくpackage.関数()
で呼び出します。
外部パッケージの関数を呼び出す時は、その関数の頭文字は大文字で宣言する必要があります。
関数名が小文字の場合は、外部パッケージからは見えなくなり、同一パッケージからの呼び出しのみ可能となります。
同一パッケージ内の関数呼び出しの場合関数名
で呼び出せます。
あと、importで宣言しているlambdaライブラリを使用するためには以下のコマンド叩いておく必要があります。
go get github.com/aws/aws-lambda-go/lambda
proglamList.go
Program List APIで返ってくるjsonをマッピングするための構造体を宣言してます。
package nhk_api_test
import "time"
type Program struct {
List List `json:"list"`
}
type List struct {
E1 []E1 `json:"e1"`
}
type E1 struct {
ID string `json:"id"`
EventID string `json:"event_id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Area Area `json:"area"`
Service Service `json:"service"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Content string `json:"content"`
Act string `json:"act"`
Genres []string `json:"genres"`
}
type Area struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Service struct {
ID string `json:"id"`
Name string `json:"name"`
LogoS Logo `json:"logo_s"`
LogoM Logo `json:"logo_m"`
LogoL Logo `json:"logo_l"`
}
type Logo struct {
URL string `json:"url"`
Width string `json:"width"`
Height string `json:"height"`
}
各フィールドの後ろでjson:"xxx"
と記述することで、jsonを解析した時にjsonのプロパティとgoのフィールドを紐づけてくれます。
payload.go
Webhookに設定するPayloadの構造体を宣言して、Slackに通知するメッセージ内容の作成と設定をしています。
package nhk_api_test
import (
"bytes"
"fmt"
"text/template"
"time"
)
const timeLayout = "15:04"
// Payload.Text用のテンプレート
var tpl = `
{{ range $key, $val := . -}}
日時: {{ $key }}
{{ range $index, $var := $val }}
番組: {{ $var.Title }}
出演者: {{ $var.Act }}
時間: {{ timeFmt $var.StartTime }} 〜 {{ timeFmt $var.EndTime }}
{{ end }}
------------------------------
{{ end }}
`
type payload struct {
Channel string `json:"channel"`
UserName string `json:"username,omitempty"`
Text string `json:"text"`
IconUrl string `json:"icon_url,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
}
// Payload.Textの作成と設定
func (p *payload) setText(m map[string][]E1) {
// テンプレート内で使用する関数の設定
fMap := template.FuncMap{
"timeFmt": timeFmt,
}
// テンプレートの割当
tpl := template.Must(template.New("tpl").Funcs(fMap).Parse(tpl))
// テンプレートからデータ出力
var buffer bytes.Buffer
if err := tpl.Execute(&buffer, m); err != nil {
fmt.Println(err)
}
// 出力したデータを文字列化
p.Text = buffer.String()
}
// テンプレートに渡す関数。
// 番組表APIの日時の形式を hh:mm にしてくれる。
func timeFmt(date time.Time) string {
return date.Format(timeLayout)
}
goにはクラスという概念がないため、構造体に属するメソッドを定義する時は、以下のように書き、レシーバ型に構造体を指定します。
func (レシーバ値 レシーバ型) 関数名()
上記の書き方で、クラス内のメソッドを宣言するように、構造体に属したメソッドを宣言することになります。
なので、このファイルのfunc (p *payload) setText()
はpayload構造体に属したメソッドの宣言になります。
普通のfunc 関数名()
は、パッケージに属する感じですかね。
呼び出す時もパッケージ名.関数()
ではなく、構造体を生成して、構造体.関数()
になります(具体的にはこちらの3-a(詳細はロジック内)を参照)。
あと*
は、ポインタ渡すかどうかです。Javaでいう、参照渡しに近いかと思います(要勉強)。
webhook.go
内容としては、主な処理を書いてます。
処理に必要な値は環境変数として外出ししているものもあります。
処理の流れとしては、以下の通りです。
- 現在日取得
- TZがUTCなので注意です。
- 取得するjsonがyyyy--mm-dd.jsonなので、そこで使用します。
- 以下の処理はループとなり、現在日を基準に環境変数:TERMに設定した回数、翌日以降のデータ取得を行います。
- NHK番組表APIをコール
- 取得したjsonの解析
- jsonデータの中に環境変数:PERFORMER, PROGRAM_NAMEに部分一致する番組があれば、その番組データをmapに格納。
- Webhookに設定するpayload構造体の生成。
- mapに番組データがある場合、テンプレートを元にメッセージ生成
- Webhook実行
package nhk_api_test
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
var (
// 番組表APIのAPIキー
apiKey = os.Getenv("API_KEY")
// 探したい出演者(カンマ区切り)
performer = os.Getenv("PERFORMER")
// 探したい番組名(カンマ区切り)
programName = os.Getenv("PROGRAM_NAME")
// 探したい期間(Max7日)
term = os.Getenv("TERM")
// WebhookのURL
webhookURL = os.Getenv("WEBHOOK_URL")
// Slackのチャンネル名
channel = os.Getenv("CHANNEL")
)
const (
// 日付のフォーマットProgram List APIのjson名で使用
dateLayout = "2006-01-02"
// URL
programListURL = "https://api.nhk.or.jp/v2/pg/list/130/e1/%s.json?key=%s"
)
// Webhook メインロジック
func Webhook() {
// 今日の日付取得
// LambdaはUTCなので、9H補正
now := time.Now()
now = now.Add(time.Duration(9) * time.Hour)
// 期間設定を数値に変換
term, _ := strconv.Atoi(term)
// 見たい番組表格納用マップ
// K: 日付、V: E1構造体
programMap := make(map[string][]E1)
// API実行(3日先まで)
for i := 0; i < term; i++ {
// URIに渡す日付を計算
d := now.AddDate(0, 0, i)
date := d.Format(dateLayout)
// 番組表APIの呼び出し
reqURL := fmt.Sprintf(programListURL, date, apiKey)
resp, err := http.Get(reqURL)
if err != nil {
log.Println("エラー発生")
log.Println(fmt.Sprintf("Error:%s", err))
log.Println(fmt.Sprintf("Request:%s", reqURL))
return
}
if resp.StatusCode != 200 {
log.Println("ステータスコード異常")
log.Println(fmt.Sprintf("Status code:%v", resp.StatusCode))
log.Println(fmt.Sprintf("Request:%s", reqURL))
return
}
defer resp.Body.Close()
// 番組表APIレスポンスのjson解析
var p Program
if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
log.Println("json解析でエラー発生")
log.Println(err)
return
}
// 見たい番組名・出演者があれば、pListに格納
pList := make([]E1, 0)
for _, e1 := range p.List.E1 {
if isNoticeTarget(e1) {
pList = append(pList, e1)
}
}
// 見たい番組名・出演者がなければ、翌日へ。
l := len(pList)
if l == 0 {
log.Printf("%vに、探してる番組・出演者はありませんでした。\n", date)
continue
}
log.Printf("%vに、%v件の番組が見つかりました。\n", date, l)
// マップに格納
programMap[date] = pList
}
// payloadの生成
payload := payload{
Channel: channel,
UserName: "nhk番組取得お知らせ",
IconEmoji: ":ghost:",
Text: "探している番組は見つかりませんでした。",
}
// 見たい番組が見つかれば、メッセージ(Text)を更新。
if len(programMap) > 0 {
payload.setText(programMap)
}
// jsonのエンコード
jsonByte, err := json.Marshal(payload)
if err != nil {
log.Println("jsonのエンコードでエラー発生")
log.Println(err)
return
}
// Webhookの呼び出し
_, err = http.PostForm(
webhookURL,
url.Values{"payload": {string(jsonByte)}},
)
if err != nil {
log.Println("Webhookの実行でエラー発生")
log.Println(err)
return
}
}
// Webhookで通知する対象か判定
func isNoticeTarget(e1 E1) bool {
// 出演者のチェック
performerList := strings.Split(performer, ",")
for _, v := range performerList {
if strings.Contains(e1.Act, v) {
return true
}
}
// 番組のチェック
programNameList := strings.Split(programName, ",")
for _, v := range programNameList {
if strings.Contains(e1.Title, v) {
return true
}
}
return false
}
所々出てくるアンスコのところは戻り値で不要なものを読み捨ててます。
変数宣言のところでos.Getenv("XXX")
の記載がありますが、こちらで環境変数を読み込んでいます。
AWSの設定
デプロイパッケージの作成
こちら、Lambdaの方に設定します。
GOOS=linux go build -o webhook main.go
zip webhook.zip webhook
Lambda
nhk_program_webhookという関数名にしました。 トリガーにCloudWatch Eventsを設定しています。 ハンドラはwebhookとしています。 先ほど作成したデプロイパッケージはこちらからアップロードします。 環境変数6つ。 なんとなく思いついたものを設定してみました。CloudWatch Events
画面はLambdaの管理画面ですけど、CloudWatch Eventsの設定です。 毎日22:10(日本で7:10)に動くようにしてます。動かしてみた
CloudWatchではなく、テスト実行ですが、動きました。
(CloudWatchのトリガーからも動くことは確認済みです)
なんと、昆虫すごいぜ! 再放送してます!!
すでに4回目っていうことで、乗り遅れてますが、要チェックです!!
ということで、NHK番組表API叩いてSlack通知する、でした。
ありがとうございました。