PHPで天気予報を求める(その2)

(1/1)
PHP で天気予報を求める」では、livedoor の WeatherHacks を用いて天気予報を表示している。ところが、この API では今日を含めて 3 日間の天気予報しか表示しない。一方、livedoor 天気情報 では、RSS 形式で 1週間分の各地の予報を配信している。
そこで今回は、この RSS を取得し、任意の都市の週間天気予報を表示するプログラムを作ってみることにする。

download プログラムを実行する

サンプル・プログラムの実行例

PHPで天気予報を求める(その2)

「livedoor天気情報」の天気予報情報

livedoor 天気情報は、RSS 2.0 形式で天気予報情報を配信している。
各地点の RSS の URL は「全国の地点定義表(RSS)」に示されている URL である。

サンプル・プログラムのダウンロード

圧縮ファイルの内容
WeeklyWeather.phpサンプル・プログラム本体。

解説:表示列数など

0030: //天気予報の表示列数
0031: define('COLUMNS', 7);

天気予報は最大 7 日分を取得できるが、横表示の日数を COLUMNS に設定している。
7 なら 1行で、ここを 4 にすると 4 日分と 3 日分の 2行表示となる。

解説:天気予報情報の取り出し

0227: /**
0228:  * livedoor天気情報RSSの title から都市名・月・日・曜日を取り出す
0229:  * @param string $title  livedoor天気情報RSSの title
0230:  * @return array(都市名,月,日,曜日) / FALSE=失敗
0231: */
0232: function getMonthDay($title) {
0233:     $pat = '/\[([0-9 ]+)日\((.*)\)の天気 \] (.*) \- (.*) \- 最高気温([0-9.\-]*)℃ - ([0-9 ]+)月([0-9 ]+)日\((.*)\)/u';      //Ver.1.1修正箇所
0234: 
0235:     preg_match($pat$title$arr);
0236:     if (! isset($arr[8]))    return FALSE;
0237: 
0238:     return array($arr[3]$arr[6]$arr[7]$arr[8]);
0239: }
0240: 
0241: /**
0242:  * livedoor天気情報RSSの description から最高・最低気温を取り出す
0243:  * @param string $descripttion  livedoor天気情報RSSの description
0244:  * @return array($max, $min) / FALSE=失敗
0245: */
0246: function getTemperature($description) {
0247:     $pat1 = '/最高気温は([0-9 \.\-]+)℃/u';
0248:     $pat2 = '/最低気温は([0-9 \.\-]+)℃/u';
0249:     $temp1 = '-';
0250:     $temp2 = '-';
0251: 
0252:     //最高気温
0253:     if (preg_match($pat1$description$arr) > 0) {
0254:         if (isset($arr[1])) $temp1 = $arr[1];
0255:     }
0256: 
0257:     //最低気温
0258:     if (preg_match($pat2$description$arr) > 0) {
0259:         if (isset($arr[1])) $temp2 = $arr[1];
0260:     }
0261: 
0262:     return array($temp1$temp2);
0263: }
0264: 
0265: /**
0266:  * 週間天気予報情報を取得する
0267:  * @param string $rss   livedoor天気情報RSSを示すURL
0268:  * @param array  $items 週間天気予報情報を格納する配列
0269:  * @return int 配列に格納した情報件数/0=失敗
0270: */
0271: function getWeeklyWeather($rss, &$items) {
0272:     //PHP4用; DOM XML利用
0273:     if (isphp5over() == FALSE) {
0274:         $dom = @read_xml($rss);
0275:         $cnt = 0;
0276:         if ($dom != FALSE) {
0277:             //DOMから必要な情報を配列へ
0278:             $node = $dom->get_elements_by_tagname('rss');
0279:             $node = $node[0]->get_elements_by_tagname('item');
0280:             foreach ($node as $node1) {
0281:                 $node2 = $node1->get_elements_by_tagname('title');
0282:                 $title = (string)$node2[0]->get_content();
0283:                 if (getMonthDay($title) != FALSE) {
0284:                     list($items[$cnt]['city'], $items[$cnt]['month'],
0285:                         $items[$cnt]['day'], $items[$cnt]['week'])
0286:                         = getMonthDay($title);
0287:                     $node2 = $node1->get_elements_by_tagname('image');
0288:                     $node3 = $node2[0]->get_elements_by_tagname('title');
0289:                     $items[$cnt]['weather'] = (string)$node3[0]->get_content();
0290:                     $node3 = $node2[0]->get_elements_by_tagname('url');
0291:                     $items[$cnt]['image'] = (string)$node3[0]->get_content();
0292:                     $node2 = $node1->get_elements_by_tagname('description');
0293:                     $description = (string)$node2[0]->get_content();
0294:                     if (getTemperature($description) != FALSE) {
0295:                         list($items[$cnt]['temp_max'], $items[$cnt]['temp_min']) = getTemperature($description);
0296:                     } else {
0297:                         $items[$cnt]['temp_max'] = '-';
0298:                         $items[$cnt]['temp_min'] = '-';
0299:                     }
0300:                     $cnt++;
0301:                 }
0302:             }
0303:         }
0304:     //PHP5用; SimpleXML利用
0305:     } else {
0306:         $node_rss = simplexml_load_file($rss);
0307:         //レスポンス・チェック
0308:         if (isset($node_rss->channel) == FALSE)  return FALSE;
0309:         //必要な情報を配列へ
0310:         $cnt = 0;
0311:         foreach ($node_rss->channel->item as $item) {
0312:             $title = (string)$item->title;
0313:             if (getMonthDay($title) != FALSE) {
0314:                 list($items[$cnt]['city'], $items[$cnt]['month'],
0315:                     $items[$cnt]['day'], $items[$cnt]['week'])
0316:                         = getMonthDay($title);
0317:                 $items[$cnt]['weather'] = (string)$item->image->title;
0318:                 $items[$cnt]['image']   = (string)$item->image->url;
0319:                 $description = (string)$item->description;
0320:                 if (getTemperature($description) != FALSE) {
0321:                     list($items[$cnt]['temp_max'], $items[$cnt]['temp_min']) = getTemperature($description);
0322:                 } else {
0323:                     $items[$cnt]['temp_max'] = '-';
0324:                     $items[$cnt]['temp_min'] = '-';
0325:                 }
0326:                 $cnt++;
0327:             }
0328:         }
0329:     }
0330:     return $cnt;
0331: }

入力処理については、「PHP で天気予報を求める」とほぼ同じである。

WebAPI の代わりに、RSS から情報を取り出すユーザー関数 getWeeklyWeather を用意した。
RSS 2.0 の仕様にしたがって、情報を配列変数に格納している。

解説:出力処理

0504: //ForecastMap(全国地点定義表)の解釈
0505: if ($id == 0) {
0506:     $n = parseForecastMap($ForecastTable);
0507:     if ($n == 0)    $errmsg = 'ForecastMapが見つかりません.';
0508: }
0509: 
0510: //検索処理
0511: if ($city != '' && isset($ForecastTable[$pref][$city])) {
0512:     $url = getWeeklyWeatherURL($ForecastTable[$pref][$city]);
0513:     $cnt = getWeeklyWeather($url$items);
0514: else if ($id != 0) {
0515:     $url = getWeeklyWeatherURL($id);
0516:     $cnt = getWeeklyWeather($url$items);
0517: }
0518: 
0519: $HtmlBody1 = makeCommonBody($items$ForecastTable$pref$city$url$errmsg);
0520: $HtmlBody2 = (count($items) == 0) ? '' : makeWeeklyWeather($items, 0, $cnt);
0521: 
0522: //表示処理
0523: if ($id == 0) {
0524:     echo $HtmlHeader;
0525:     echo $HtmlBody1;
0526:     echo $HtmlBody2;
0527:     echo $HtmlFooter;
0528: else  {
0529:     echo mb_convert_encoding($HtmlBody2$outencINTERNAL_ENCODING);
0530: }
0531: 

今回は、画面に出力するコンテンツを $HtmlHeader, $HtmlBody1, $HtmlBody2, $HtmlFooter の 4 つに分割し、必要に応じて表示するようにした。週間予報の table が含まれているのは $HtmlBody2 である。

なぜ、このようなロジックにしたかというと、コマンドラインで

WeeklyWeather.php?id=63


のように ID を指定することで、ある地点の天気予報 table のみを表示させたかったからである。つまり、このスクリプトをホームページやブログの一部として組み込むことで、今日から 1週間分の天気予報を表示するガジェットになる。

また、親となるホームページやブログの文字コードセットにあわせ、charset 変数で出力文字コードを変更できるようにした。たとえば

WeeklyWeather.php?id=6&charset=SJIS


とすると、東京の週間天気予報をシフト JIS で出力することができる。

解説:https化処理

2018 年(平成 30 年)8 月現在、livedoor 天気情報は https 化していない。このため、画面に表示している予報アイコンが https通信のままで、Chrome ブラウザでは「保護された通信」とならない。

そこで、予報アイコンを自サイトに読み込み、再出力することで、自サイトの証明書を使って https 化することにする。

0027: //image2base64()の有効化(出力をすべてhttps化する)
0028: define('IMAGE2BASE64', TRUE);

0110: /**
0111:  * imageコンテンツをBASE64に変換
0112:  * @param string $url イメージURL
0113:  * @return string BASE64
0114: */
0115: function image2base64($url) {
0116:     if (! IMAGE2BASE64)  return $url;
0117: 
0118:     $curl = curl_init() ;
0119:     curl_setopt($curlCURLOPT_URL , $url);
0120:     curl_setopt($curlCURLOPT_HEADER, 1) ; 
0121:     curl_setopt($curlCURLOPT_SSL_VERIFYPEER , FALSE);  //証明書は無視
0122:     curl_setopt($curlCURLOPT_RETURNTRANSFERTRUE);        //結果を文字列で
0123:     $res = curl_exec($curl);
0124:     $info = curl_getinfo($curl);
0125:     curl_close($curl);
0126:     $header = substr($res, 0, $info['header_size']);
0127:     $arr = preg_split("/\n/ui", $header);
0128:     $body = substr($res$info['header_size']);
0129:     $outstr = '';
0130:     if (preg_match('/^image/ui', $info['content_type']) > 0) {
0131:         $base64 = base64_encode($body);
0132:         $outstr = 'data:' . $info['content_type'] .';base64,' . $base64;
0133:     }
0134:     return $outstr;
0135: }

まず、定数 IMAGE2BASE64 を設定する。これが TRUE の場合、ユーザー関数 image2base64 が有効になる。
ユーザー関数 image2base64 は、指定された URL にある image コンテンツを読み込み、BASE64 変換し、img タグに渡す。
これによって https 対応していない予報アイコンを、PHP プログラムが動作しているサイトの証明書を使って https通信する。
ただし、いちいちアイコン画像を読み込むことになるので、サーバ負荷が大きくなることに留意されたい。

https コンテンツを https 化する方法については、「HTTPS のページで外部の非SSL ページの画像を表示させても警告にならないようにする」のようなリバースプロキシを使った方法もあるが、踏み台にされるリスクがあるため、プログラム側で表示コンテンツを制御できる、今回のような方法をとることにした。

活用例

《全国の天気》今日・明日の天気と全国概況一覧 & 週間予報」(みんなの知識 ちょっと便利帳)では、このサンプル・プログラムを活用し、都市名を検索しやすいページを提供している。
また、サイドバーにコンパクトな形で天気予報を掲示している。

また、「なにしろパソコン.com」では、サイドバーにコンパクトな形で天気予報を掲示している。

ご活用いただき、ありがとうございます。

質疑応答

【質問】

週間天気予報を表示するプログラムを利用させていただいておりましたが、2018 年 9 月 2 日に確認したところ、明日・明後日の天気が表示されずに、それ以降の日からの情報しか(3 日分)表示されなくなってしまいました。それまでは正しく動作しておりました。8 月 5 日版の修正バージョンを試させていただきましたが、同様の現象となりました。公開されておられる週間天気ではなく、本日・明日・明後日の天気取得プログラムも 3 日間の情報が取得できずにエラーとなります。
livedoor 側での api の変更がされたのでしょうか?
そのほか何かお気づきのことはございますでしょうか?


【回答】

プログラム実行時の「リクエスト URL」や「livedoor 天気情報」をご覧になれば分かるとおり、2018 年 9 月 2 日(日)~9 月 3 日(月)の提供でデータが抜け落ちています。本プログラムは取得初日のデータをスキップするため、9 月 4 日(火)をスキップして、9 月 5 日(水)以降の予報しかできなくなっています。
これは短期的な障害だとは思います。

参考サイト

(この項おわり)
header