Zabbix 5.0でYahoo!天気の天気情報を取得してダッシュボードに表示する

## はじめに

以前に書いた記事 [Zabbix 4.0でlivedoor 天気情報を取得してダッシュボードに表示する](/mandoi/items/bc8778410b85842c7c1b) はlivedoor天気情報のAPIを使って天気情報を取得していたのですが、2020年7月31日でサービス提供終了のお知らせが…悲しい。
このお知らせに気づいたのが7月に入ってからだったので、慌てて別の情報取得手段を探し、結局はYahoo!天気のWebページをスクレイピングして同じようなデータを監視することにしたのでした。

ということで、今回の記事はYahoo!天気を監視します。時期的にZabbix 5.0を使っていますがタイトルに若干偽りあり、Zabbix 5.0ならではの機能は使っていません(ハズ)のでZabbix 4.0でも大丈夫かと思います。

今回はHTTPエージェントではなく外部スクリプトを用いてスクレイピングしたデータをJSONで出力して取り込んでいます。外部スクリプトはとにかく必要な形のJSONを出力できればどんなものでも構いません。(結局、HTTPエージェントから外部チェックに戻っている(苦笑))

なお、自分は以下のポリシーで監視アイテムを設定できるよう、スクリプトを作りました。

* livedoorの天気情報を取得していた既存の監視アイテム(JSONのパスとか取れるデータとか)はできるだけ変更しない。
* どうしても追加が必要になったものは追加する。
* 保存前処理でごちゃごちゃ加工していたところはスクリプト側で先に加工しておく。

## 環境情報

CentOS 7.8.2003 (x86_64) + Zabbix 5.0.2 + Python(json, BeautifulSoup, requests等々のモジュール)

## [準備] データの抽出元を検討する

### どこから監視したいデータを抽出するか

YOLPというAPIの中に気象情報APIもあるみたいですけど、ちょっと大変そうな感じがするし、実は天気のほかに気象警報(土砂災害や河川洪水、避難時指示情報)をYahoo!天気からスクレイピングして監視しているので同じ方法で抽出してしまうことにしました。ぶっちゃけて言えば手抜きというやつです。

とりあえずは https://weather.yahoo.co.jp/weather/ からリンクを辿ることにします。
例えば、東京の天気情報を監視する場合は https://weather.yahoo.co.jp/weather/jp/13/ を経由して最終的に https://weather.yahoo.co.jp/weather/jp/13/4410.html から都市単位の今日明日の天気を抽出すれば良さそうです。

### 都市ごとの天気を抽出する

上記ページを取り込めたら、今日明日の天気から、画像、天気の文字情報、最高気温、最低気温をいただきます。こんな時こそブラウザのデベロッパーツールの出番です。
![class_ForecastCity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208817/4e425432-ecca-87ed-bb32-15972407bd65.png) 
`forecastCity` クラスを指定した `div` タグから情報を拾えば良さそう。
任意の都市の天気を取得できるようにURL内のID部分(東京であれば13)は指定可能にして、これをZabbixのホストマクロから渡せるようにしておくと使い回しが利くでしょう。

```python:抜粋
_div_map = soup.find('div', attrs={'id':'map'})
for _a in _div_map.find_all('a'):
    if (type(_a.dl) is types.NoneType):
        continue

    _city_url = _a['href']
    _city = _a.find('dt', attrs={'class':'name'}).text

    res_city = requests.get(_city_url)
    content_city = res_city.content
    soup_city = BeautifulSoup(content_city, 'html.parser')

    _div_city = soup_city.find('div', attrs={'class':'forecastCity'})
    _td = _div_city.table.tr.find_all('td', recursive=False)

    _w_today_img_url = _td[0].find('p', attrs={'class':'pict'}).img['src']
    _w_today_img_alt = _td[0].find('p', attrs={'class':'pict'}).img['alt']
    _w_today_img_tag = '<img src="' + _w_today_img_url + '" width="75" height="40" alt="' + _w_today_img_alt + '"/>'

    w_today = {
        'image': {
            'tag': _w_today_img_tag,
            'width': 75,
            'height': 40,
            'url' : _w_today_img_url,
            'title': _w_today_img_alt
        },
        'temperature': {
            'min' : {
                'celsius': int(_td[0].find('li', attrs={'class': 'low'}).em.text)
            },
            'max' : {
                'celsius': int(_td[0].find('li', attrs={'class': 'high'}).em.text)
            }
        }
    }
```

(これより前に都道府県別マップ (e.g. https://weather.yahoo.co.jp/weather/jp/13/) のコンテンツを `BeautifulSoup(content, 'html.parser')` でオブジェクト化してあることが前提です)
IDが `map` の `div` タグ配下からひたすら `a` タグをfind_allして、都市ごとのデータではない、 `dl` タグが存在しない(NoneType)箇所は抽出対象から除外します。
さらに `a` タグのリンク先から都市ごとのURL (e.g. https://weather.yahoo.co.jp/weather/jp/13/4410.html) を抽出してはそのURLのコンテンツをさらに取り込みパースするという、ローテク駆使した力技でマップ内に点在する都市のデータをかき集めます。

上記抜粋では `w_today` に今日の天気を入れているところまでですが、findする `td` タグの要素を2番目(1)にして `w_tommorow` に明日の天気をdict化しておきます。

## [Zabbix] 監視設定をおこなう

### 監視ホストを作成

今回も地域(都市)のID単位でホストを作ることにします。前回記事の監視ホストがある場合はそのままマクロを追加しても良いと思います。

| 設定項目 | 設定値 | 説明 |
|--:|:--|:--|
| ホスト名 | 任意 | ユニークなホスト名 |
| 表示名 | 任意 | 識別しやすい任意の表示名を指定する |
| グループ | 任意 | 任意のグループを選択する |
| 有効 | ✔︎ | このホストを有効にする |

ただし、今回はホスト名をIDにせず、マクロでIDを定義することにしました。

| マクロ | 値 | 説明 |
|:--|:--|:--|
| {$PREF_ID} | 13 | 東京のID(スクレイピング対象URLの指定用) |
| {$FILTER_PREF_NAME} | 東京 | jsonpathでフィルタするときに使う都市名 |

### 監視アイテムを作成

外部スクリプトはlivedoorのAPIで取れるデータに似せた形のJSONを返すように作ったので、監視データはこんな形で出力されます。こんな感じで出力できればスクリプトの書き方はどんなでも良いので、いい感じに作ります。(アバウト)

```
{
  "forecasts": [
    {
      "w_today": {
        "image": {
          "url": "https://s.yimg.jp/images/weather/general/next/size150/156_day.png",
          "width": 75,
          "tag": "<img src=\"https://s.yimg.jp/images/weather/general/next/size150/156_day.png\" width=\"75\" height=\"40\" alt=\"晴時々曇\"/>",
          "title": "晴時々曇",
          "height": 40
        },
        "temperature": {
          "max": {
            "celsius": 36
          },
          "min": {
            "celsius": 28
          }
        }
      },
      "pref": "東京",
      "w_tomorrow": {
        "image": {
          "url": "https://s.yimg.jp/images/weather/general/next/size150/313_day.png",
          "width": 75,
          "tag": "<img src=\"https://s.yimg.jp/images/weather/general/next/size150/313_day.png\" width=\"75\" height=\"40\" alt=\"雨のち曇\"/>",
          "title": "雨のち曇",
          "height": 40
        },
        "temperature": {
          "max": {
            "celsius": 33
          },
          "min": {
            "celsius": 27
          }
        }
      }
    },
(snip)
  ],
  "id": 13,
  "ts": 1597620333
}
```

前回のホスト設定やアイテムを活かすため、最低限、最低気温 `.forecasts[].temprature.min`, 最高気温 `.forecasts[].temprature.max`, 天気の画像 `.forecasts[].image.url` は取り出せる形を目指しつつ、あとで都市名のフィルタをかけたいので `.forecasts[].pref` を追加しています。jsonpathで分解してからHTMLタグを作り直すのも面倒なので、ついでに`img`タグを `.forcasts[].image.tag` として追加しました。

### アイテム(1) - 親アイテム

以下のアイテムを作り、今日明日の天気に関する要素をまとめて保存します。

| 設定項目 | 設定値 | 説明 |
|--:|:--|:--|
|名前|気象予報|任意の名前を指定する|
|タイプ|外部チェック|前回はAPIを叩いていたのでHTTPエージェントだったが今回は自作スクリプト|
|キー|notify-weather-forecasts.py[{$PREF_ID}]|スクリプト名、第一引数にIDを渡す変数|
|ホストインターフェース|127.0.0.1:10050|特に使うインターフェースはないのでデフォルトで|
|データ型|テキスト||
|監視間隔|1h|結構更新はされるみたいなので適度な間隔で|
|ヒストリの保存期間|7d|とりあえず7日保存|
|アプリケーション|Weather|任意のアプリケーション名を指定もしくは作成する|
| 有効 | ✔︎ | このアイテムを有効にする |

保存前処理も作ります。

| 設定項目 | 名前 | パラメータ | 失敗時のカスタマイズ | 説明 |
|--:|:--|:--|:--:|:--|
|保存前処理の設定|JSONPath|\$.forecasts[?(@.pref=="{$FILTER_PREF_NAME}")].first()|-|都市名でフィルタしてからデータを保存する|

### アイテム(2)-1 - 今日の天気

| 設定項目 | 設定値 | 説明 |
|--:|:--|:--|
|名前|w_today|任意の名前を指定する|
|タイプ|依存アイテム|親アイテムからデータを分割するので依存アイテムにする|
|キー|notify-weather-forecasts.w_today|任意のキー名を指定する|
|マスターアイテム|ホスト名:気象予報|先に作った親アイテムがマスター|
|データ型|テキスト||
|ヒストリの保存期間|7d|とりあえず7日保存|
|アプリケーション|Weather|任意のアプリケーション名を指定もしくは作成する|
| 有効 | ✔︎ | このアイテムを有効にする |

今日の天気を分割する保存前処理を作ります。

| 設定項目 | 名前 |パラメータ | 説明 |
|--:|:--|:--|:--|
|保存前処理の設定|JSONPath|\$.w_today|「今日」のオブジェクト|

### アイテム(2)-2 - 今日の最低気温

| 設定項目 | 設定値 | 説明 |
|--:|:--|:--|
|名前|w_today_min|任意の名前を指定する|
|タイプ|依存アイテム||
|キー|notify-weather-forecasts.w_today_min|任意のキー名を指定する|
|マスターアイテム|ホスト名:w_today|(2)-1のアイテムを指定する|
|データ型|整数|整数値を返す<br />今回はNullを返してくることはない(「取得不可」になることはない)はず。|
|単位||指定不要|
|ヒストリの保存期間|7d||
|トレンドの保存期間|トレンドを保存しない|トレンドの保存はお好みで|
|値のマッピング||指定不要|
|アプリケーション|Weather|任意のアプリケーション名を指定もしくは作成する|
| 有効 | ✔︎ | このアイテムを有効にする |

親アイテムから最低気温だけを保存するため、「保存前処理」も設定します。

| 設定項目 | 名前 |パラメータ | 説明 |
|--:|:--|:--|:--|
|保存前処理の設定|JSONPath|\$.temperature.min.celsius |前回のw_today_minアイテムと同じパスで取得可能|

### アイテム(2)-3 - 今日の最高気温

| 設定項目 | 設定値 | 説明 |
|--:|:--|:--|
|名前|w_today_max|任意の名前を指定する|
|タイプ|依存アイテム||
|キー|notify-weather-forecasts.w_today_max|任意のキー名を指定する|
|マスターアイテム|ホスト名:w_today|(2)-1のアイテムを指定する|
|データ型|整数|整数値を返す<br />今回はNullを返してくることはない(「取得不可」になることはない)はず。|
|単位||指定不要|
|ヒストリの保存期間|7d||
|トレンドの保存期間|トレンドを保存しない|トレンドの保存はお好みで|
|値のマッピング||指定不要|
|アプリケーション|Weather|任意のアプリケーション名を指定もしくは作成する|
| 有効 | ✔︎ | このアイテムを有効にする |

親アイテムから最高気温だけを保存するため、「保存前処理」も設定します。

| 設定項目 | 名前 |パラメータ | 説明 |
|--:|:--|:--|:--|
|保存前処理の設定|JSONPath|\$.temperature.max.celsius |前回のw_today_minアイテムと同じパスで取得可能|

### アイテム(2)-4 - 今日の天気の画像

| 設定項目 | 設定値 | 説明 |
|--:|:--|:--|
|名前|w_today_image|任意の名前を指定する|
|タイプ|依存アイテム||
|キー|notify-weather-forecasts.w_today_image|任意のキー名を指定する|
|マスターアイテム|w_today|(2)-1のアイテムを指定する|
|データ型|テキスト||
|ヒストリの保存期間|7d||
|アプリケーション|Weather|任意のアプリケーション名を指定もしくは作成する|
| 有効 | ✔︎ | このアイテムを有効にする |

親アイテムから画像のimgタグを保存するため、「保存前処理」も設定します。

| 設定項目 | 名前 |パラメータ | 説明 |
|--:|:--|:--|:--|
|保存前処理の設定|JSONPath|$.image.tag ||

と、ここまでで「今日」の天気情報を取得する監視アイテムができあがりました。 続けて「明日」の天気も作る場合は、w_todayをw_tomorrowに読み替えてアイテムを作ります。

## [Zabbix] お天気監視ダッシュボードを作る

ダッシュボードはほぼいじることがないので前回記事の [お天気監視ダッシュボードを作る](/mandoi/items/bc8778410b85842c7c1b#zabbix-%E3%81%8A%E5%A4%A9%E6%B0%97%E7%9B%A3%E8%A6%96%E3%83%80%E3%83%83%E3%82%B7%E3%83%A5%E3%83%9C%E3%83%BC%E3%83%89%E3%82%92%E4%BD%9C%E3%82%8B) を参照ください。
変更する点はホストパターンを今回作成したホスト名に変更するくらいで大丈夫かと思います。

![zabbix_dashboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208817/cfb22d8c-e317-ee30-4efc-f7db82346def.png)

## おわりに

またこんな記事ですがお読みいただきありがとうございました。
しばらく前にZabbixの設定を行っていて、この記事を書くまでに間が空いてしまったので実は嘘を書いているところがあるかも知れません。嘘を見つけたら随時直していこうと思います。

外部スクリプトもサンプルとして載せた方が良いのでしょうかね。あまり綺麗なものではないのですが。