サンプル・プログラムの実行例
サンプル・プログラムのダウンロード
| jmaWeeklyWeather.php | サンプル・プログラム本体。 |
| jmaweatherspots.xml | 予報地点情報ファイル。「PHPで天気予報を求める」参照。 |
| jmaWeatherInit.php | 予報地点情報ファイル作成プログラム。「PHPで天気予報を求める」参照。 |
| pahooWeather.php | 気象情報に関わるクラス pahooWeather。 気象情報に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
| pahooCache.php | キャッシュ処理に関わるクラス pahooCache。 キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
| pahooTwitterAPI.php | Twitter APIに関わるクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooBlueskyAPI.php | Bluesky APIに関わるクラス pahooBlueskyAPI。 使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooScraping.php | スクレイピング処理に関わるクラス pahooScraping。 スクレイピング処理に関わるクラスの使い方は「PHPでDOMDocumentを使ってスクレイピング」を参照。include_path が通ったディレクトリに配置すること。 |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.5.0 | 2024/06/23 | Twitter(現・X)ボタンを "X" に変更 |
| 2.4.0 | 2023/02/11 | pahooInputDataクラス導入 |
| 2.3 | 2021/07/26 | ツイート機能を追加 |
| 2.2 | 2021/04/02 | キャッシュ・システム導入:pahooCacheクラス |
| 2.12 | 2021/03/15 | 取得時刻によって,情報無し項目がゼロになったり,表示日がズレる不具合を修正 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 3.1.0 | 2023/03/18 | 地域気象観測所一覧のフォーマット変更に対応 |
| 3.0.0 | 2023/02/11 | アメダスのページから自動ダウンロード対応 |
| 2.2 | 2022/03/12 | 気象庁防災情報XMLのhttps化に対応, キャッシュ・システム導入:pahooCacheクラス |
| 2.11 | 2021/03/23 | bug-fix,地域観測所一覧(アメダス)を最新に |
| 2.1 | 2021/03/03 | 天気予報(2~3日予報)に対応 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 5.7.0 | 2025/08/09 | readEnvWBGTinfoSpots(), readEnvWBGTforecast(), getWBGTcolor() 追加 |
| 5.6.2 | 2025/04/10 | readJmaSpots() -- bug-fix |
| 5.6.1 | 2025/04/08 | getMyscriptPathURL() -- bug-fix |
| 5.6.0 | 2025/02/23 | getJmaNearSpot() -- 引数 $distanceMax 追加 |
| 5.5.0 | 2025/02/01 | 予報地点情報ファイルを1週間毎に再作成する |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 4.5.1 | 2025/05/31 | deg2ddmm(), deg2hhmm() 不具合修正 |
| 4.5.0 | 2024/03/17 | ヒジュラ暦メソッドを追加 |
| 4.4.1 | 2024/03/17 | getCabinetOfficeHolidayTable() -- bug-fix |
| 4.4.0 | 2024/02/25 | 内閣府の祝日表を参照できるようにした |
| 4.3.2 | 2023/02/11 | getSolarTerm72() 表記改訂:水澤腹堅→水沢腹堅 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.2.0 | 2025/09/06 | cLoad() HTTPヘッダを送信できるようにした |
| 1.1.3 | 2025/08/10 | var→public |
| 1.1.2 | 2023/07/22 | bug-fix |
| 1.1.1 | 2023/02/11 | コメント追記 |
| 1.1 | 2021/04/08 | simplexml_load()メソッド追加 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 5.6.0 | 2025/08/13 | .pahooEnv導入 |
| 5.5.1 | 2024/11/23 | __construct() -- PHP8.4における応急処置 |
| 5.5.0 | 2024/06/21 | TwitterOAuth 7.0.0 対応 |
| 5.4.0 | 2024/05/18 | twitter.com → x.com 変更対応 |
| 5.3.0 | 2023/08/15 | tweet3() -- メディアのシャフル機能 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.7.0 | 2025/08/17 | reductImage,uploadBlob仕様変更←画像に余計な空白が入らないようにするため |
| 2.6.0 | 2025/08/14 | .pahooEnv 導入 |
| 2.5.1 | 2025/08/10 | uploadBlob() -- bug-fix |
| 2.5.0 | 2025/08/02 | getOGPInformation() -- og:imageがないページに対応 |
| 2.4.0 | 2025/07/19 | atruri2postURL() 追加, uploadBlob() 修正 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.2.1 | 2024/10/31 | __construct() 文字化け対策 |
| 1.2.0 | 2024/09/29 | getValueFistrXPath() 属性値でない指定に対応 |
| 1.1.0 | 2023/10/15 | getValueFistrXPath() 追加 |
| 1.0.1 | 2023/09/29 | __construct() bug-fix |
| 1.0.0 | 2023/09/18 | 初版 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.0.1 | 2025/08/11 | getParam() bug-fix |
| 2.0.0 | 2025/08/11 | pahooLoadEnv() 追加 |
| 1.9.0 | 2025/07/26 | getParam() 引数に$trim追加 |
| 1.8.1 | 2025/03/15 | validRegexPattern() debug |
| 1.8.0 | 2024/11/12 | validRegexPattern() 追加 |
目次
気象庁防災情報XMLフォーマット
https://www.data.jma.go.jp/developer/xml/data/yyyymmddhhmmss_番号_電文コード_連番.xml府県週間天気予報情報は、長期フィードの定時の中にある電文コード VPFW50 に入っている。府県天気予報情報は、同じく長期フィードの定時の中にある電文コード VPFD51 に入っている。そこで、フィードから配信日時 yyyymmddhhmmss が最も大きく、VPFW50 または VPFD51 を含むURLを取り出せば、それが目指す情報XMLとなる。
VPFW50 は、当日を含む6~7日間の天気予報・降水確率・最低・最高気温などを提供しているが、予報地点が全国約70箇所と少ない。また、情報を取得する時間帯によっては、当日の予報情報が欠けていることがある。
一方、VPFD51 は、当日を含む2~3日間だけの情報だが、予報地点は全国約170箇所と多く、当日の予報情報も含まれている。
そこで、VPFW50 と VPFD51 の両方の情報を参照して7日間の天気予報情報を表示する「週間予報」と、VPFD51 だけ参照する「天気予報」の2種類の予報を選択できるようにした。メインプログラムの変数 $forecast の値によって切り替える。
VPFW50の構造
注意すべきは、1つのXMLファイルの中に1つまたは複数の予報地点が含まれていること。さらに、天気予報(アイコン画像を含む)と降水確率は「地方」に紐付いているが、最低気温・最高気温は「予報地点」に紐付いているということである。地方と予報地点は1対N対応のため、後述する予報値地点データファイルに用意しておくことにした。
また、TimeDefine タグに示される7日分の情報が格納されているのだが、XML情報の取得時間帯によって、1つ目(refID=1)の情報が、今日になるか明日になるか分かれる。そこで、TimeDefine を取得して、現在日時(PCの内蔵時計)と比較して決定することにした。
降水確率や気温は、XML情報の取得タイミングによっては、1つ目(refID=1)~3つ目(refID=3)が「値なし」になっていることがある。そこで、後述する情報XML VPFW50 から、今日~明後日(または明日~明後日)の情報を取り出し、補完することにする。
VPFD51の構造
注意すべきは、VPFW50 と同じで1つのXMLファイルの中に1つまたは複数の予報地点が含まれていること、VPFW50 の予報地方と異なる場合があること。
また、天気予報(アイコン画像を含む)は1日単位で3回分だが、降水確率は6時間毎の値で6回分、気温は12時間毎の値で4回分と、各々の予報間隔が異なっている。いちいち TimeDefine を取得して、本日(PCの内蔵時計)と比較していく必要がある。
降水確率や気温は、XML情報の取得タイミングによっては、1つ目(refID=1)~3つ目(refID=3)が「値なし」になっていることがある。
準備:各種定数とユーザー定義クラス
jmaWeeklyWeather.php
49: //ツイート・ボタン TRUE:有効,FALSE:無効
50: define('TWITTER', FALSE);
51:
52: //画像化したいオブジェクト
53: define('TARGET', 'target');
54:
55: //表示幅(ピクセル)
56: define('WIDTH', 700);
57:
58: //地方セレクタ(北海道地方,東北地方‥‥)を使うかどうか
59: // 0 = 使わない
60: // 1 = 使う
61: define('REGION', 1);
62:
63: //天気予報の種類(初期値)
64: // 0 = 天気予報
65: // 1 = 週間予報
66: define('DEF_FORECAST', 1);
67:
68: //天気予報の表示列数
69: define('COLUMNS', 7);
70:
71: //予報の開始日:デフォルト値(0:今日,1:明日‥‥6)
72: define('DEF_START', 0);
73:
74: //予報の終了日:デフォルト値(0:今日,1:明日‥‥6)
75: define('DEF_GOAL', 6);
76:
77: //参照する電文コード【変更不可】
78: $ForecastCodes = array('VPFD51', 'VPFW50');
79:
80: //キャッシュ保持時間(分) 0:キャッシュしない
81: //気象庁へのアクセス負荷軽減のため,60分以上のキャッシュ保持をお勧めします.
82: define('LIFE_CACHE', 120);
83:
84: //キャッシュ・ディレクトリ
85: //書き込み可能で,外部からアクセスされないディレクトリを指定してください.
86: //最大150Mバイトを消費します.
87: define('DIR_CACHE', './pcache/');
88:
89: //気象情報クラス:include_pathが通ったディレクトリに配置
90: require_once('pahooWeather.php');
91:
92: //キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
93: require_once('pahooCache.php');
94:
95: //データ入力に関わる関数群:include_pathに配置すること
96: require_once('pahooInputData.php');
97:
98: //Twitterクラス:include_pathが通ったディレクトリに配置
99: if (TWITTER) {
100: require_once('pahooTwitterAPI.php');
101: }
102:
103: // サブルーチン ==============================================================
週間予報は最大7日分を取得できるが、横表示の日数を定数 COLUMNS に設定している。
7なら1行で、ここを4にすると4日分と3日分の2行表示となる。
その他、「変更不可」以外の定数は自由に変更できる。
出力結果を Twitter(現・X) に投稿することができる。投稿機能を有効化するときは、定数 TWITTER を TRUE にする。ユーザー定義クラス pahooTwitterAPI を利用するので、"pahooTwitterAPI.php" をinclude_pathが通ったディレクトリに配置すること。また、事前にアプリケーションの登録が必要になる。詳しくは「PHPでTwitter(現・X)に投稿(ツイート)する」を参照してほしい。
出力結果を Bluesky に投稿することができる。投稿機能を有効化するときは、定数 BLUESKY を TRUE にする。ユーザー定義クラス pahooBlueskyAPI を利用するので、"pahooBlueskyAPI.php" をinclude_pathが通ったディレクトリに配置すること。また、事前にアプリケーションの登録が必要になる。詳しくは「PHPでPHPでBlueskyに投稿する」を参照してほしい。
Twitter(現・X) や Bluesky のボタン・アイコンについては、「HTMLとCSSでさまざまなアイコンを表示する」を参照して欲しい。
解説:キャッシュ・システム
- Atomフィード(長期フィード:定時)
- VPFW50
- VPFD51
たとえば120分(=2時間)を指定した場合、気象庁防災情報XMLへのアクセスは
1日24時間÷2=12回
だけ行えばいい。VPFW50やVPFD51のXMLファイルのサイズは約16Kバイト。VPFW50の対象地点が70箇所、VPFD51の対象地点が170箇所であることから、1日あたりのアクセス量の最大値は
4.0M×12+16K×70×12+16×170×12=141Mバイト
となる。
pahooCache.php
24: /**
25: * コンストラクタ
26: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
27: * @param int $life キャッシュ保持時間(分)(省略可能)
28: * @param string $dir キャッシュ・ディレクトリ(省略可能)
29: * @param array $httpHeader httpヘッダに渡す配列(省略可能)
30: * USER AGENT偽装に用いることを想定
31: * (例)
32: * array(
33: * 'User-Agent: Mozilla/5.0(Windows NT 10.0; Win64; x64) pahooAppy/pahoo.org AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
34: * 'Accept-Language: ja-JP'
35: * );
36: * @return なし
37: */
38: function __construct($life=self::LIFE_CACHE, $dir=self::DEF_DIRCACHE, $httpHeader=NULL) {
39: if ($life < 0) {
40: $life = 0;
41: }
42: if (preg_match('/\/$/ui', $dir) == 0) {
43: $dir = $dir . '/';
44: }
45: $this->error = FALSE;
46: $this->errmsg = '';
47: $this->debug = '';
48: $this->lifeCache = $life;
49: $this->dirCache = $dir;
50: $this->httpHeader = $httpHeader;
51:
52: // PHP5以上であることを調べる.
53: if (! $this->isphp5over()) {
54: $this->error = TRUE;
55: $this->errmsg = '動作にはPHP5以上が必要です';
56: return;
57: }
58:
59: // キャッシュ・ディレクトリが無ければ作成する.
60: if (! is_dir($this->dirCache)) {
61: $res = mkdir($this->dirCache, 0744);
62: if ($res == FALSE) {
63: $this->error = TRUE;
64: $this->errmsg = 'キャッシュ・ディレクトリ "' . $this->$dirCache . '" の作成に失敗しました';
65: return;
66: }
67: }
68: }
キャッシュ保持時間とキャッシュ・ファイルを保存するディレクトリはコンストラクタで指定する。ディレクトリが無い場合、コンストラクタ内で生成するようにしてある。
pahooCache.php
104: /**
105: * 指定したキャッシュ・ファイルを削除する.
106: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
107: * @param string $pat 削除するファイル名(正規表現指定可能)(省略可能)
108: * @param int $life キャッシュ保持時間(分)(省略可能)
109: * @return int 削除したファイル数
110: */
111: function delete($pat='.+', $life=self::LIFE_CACHE) {
112: $fullname = $this->dirCache . '*';
113:
114: $cnt = 0;
115: foreach (glob($fullname) as $name) {
116: // ファイルかつパターンにマッチしたファイルが削除対象
117: if (is_file($name) && (preg_match($pat, basename($name)) > 0)) {
118: $ft = filemtime($name);
119: // $lifeより古いファイルが削除対象
120: if ((time() - $ft) > $life * 60) {
121: if (unlink($name) == TRUE) {
122: $cnt++;
123: }
124: }
125: }
126: }
127:
128: return $cnt;
129: }
pahooCache.php
131: /**
132: * cURLを利用して指定したURLのコンテンツを取得する.
133: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
134: * @param string $url URL
135: * @return mixed コンテンツ/FALSE:読み込み失敗
136: */
137: function cLoad($url) {
138: $curl = curl_init($url);
139: curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); //結果を文字列で返す
140: curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); // サーバ証明書検証をスキップ
141: curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); // 〃
142: curl_setopt($curl, CURLOPT_FOLLOWLOCATION, TRUE); //リダイレクトをたどる
143: curl_setopt($curl, CURLOPT_AUTOREFERER, TRUE); // 同上
144: curl_setopt($curl, CURLOPT_TIMEOUT, 30); //タイムアウト時間
145:
146: // HTTPヘッダを渡す
147: if ($this->httpHeader !== NULL) {
148: curl_setopt($curl, CURLOPT_HTTPHEADER, $this->httpHeader);
149: // HTTPヘッダは不要
150: } else {
151: curl_setopt($curl, CURLOPT_HEADER, FALSE);
152: }
153:
154: // コンテンツ取得
155: $contents = curl_exec($curl);
156: if (($contents == FALSE) || (curl_errno($curl) != 0)) {
157: $this->error = TRUE;
158: $this->errmsg = '"' . $url . '" が取得できません';
159: return FALSE;
160: }
161: $arr = curl_getinfo($curl);
162:
163: if ($arr['http_code'] != 200) {
164: $this->error = TRUE;
165: $this->errmsg = '"' . $url . '" が取得できません';
166: return FALSE;
167: }
168: curl_close($curl);
169:
170: return $contents;
171: }
pahooCache.php
173: /**
174: * 指定したURLからコンテンツを読み込む.
175: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
176: * @param string $url URL
177: * @return mixed コンテンツ/FALSE:読み込み失敗
178: */
179: function forceLoad($url) {
180: $fname = $this->dirCache . md5($url); // キャッシュするファイル名
181:
182: $res = $this->cLoad($url);
183: if ($res == FALSE) {
184: return FALSE;
185: }
186: $ret = file_put_contents($fname, $res);
187: if ($ret == FALSE) {
188: $this->error = TRUE;
189: $this->errmsg = 'キャッシュファイル "' . $fname . '" の書き込み失敗しました';
190: return FALSE;
191: }
192: $this->debug .= $url . "\n";
193:
194: return $res;
195: }
pahooCache.php
197: /**
198: * 指定したキャッシュまたはURLからコンテンツを読み込む.
199: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
200: * @param string $url URL
201: * @return mixed コンテンツ/FALSE:読み込み失敗
202: */
203: function load($url) {
204: // キャッシュ有効
205: if ($this->lifeCache > 0) {
206: $this->delete('/[0-9a-f]+/ui', $this->lifeCache); // 古いキャッシュを削除
207: $fname = $this->dirCache . md5($url); // キャッシュするファイル名
208: // キャッシュが存在する
209: if (is_file($fname)) {
210: $res = file_get_contents($fname);
211: if ($res == FALSE) {
212: $this->error = TRUE;
213: $this->errmsg = 'キャッシュファイル "' . $fname . '" の読み込みに失敗しました';
214: return FALSE;
215: }
216: $this->debug .= $fname . "\n";
217: // ネットから取得
218: } else {
219: $res = $this->forceLoad($url);
220: }
221:
222: // キャッシュ無効
223: } else {
224: $res = $this->forceLoad($url);
225: }
226:
227: return $res;
228: }
キャッシュが有効の時は、まず、delete メソッドを使って古いキャッシュを削除する。filemtime 関数を使ってファイルのタイムスタンプを取得し、それを現在日時と比較することで削除するかどうかを判定している。
ッシュへ書き込む。
キャッシュ・ファイル名は、URLからハッシュ関数 md5 を使って生成する。
キャッシュが存在すれば、 file_get_contents を使って読み込む。
無ければ、forceLoad メソッドを使ってネットから読み込み、キャッシュへ書き込む。ネットからの読み込みは cLoad メソッドであるが、cURL関数を使って読み込んでいる。
pahooCache.php
230: /**
231: * キャッシュ・システムを利用したSimple XMLロード.
232: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
233: * @param string $url XMLファイル名(URL指定)
234: * @return bool TRUE:取得成功/FALSE:失敗
235: */
236: function simplexml_load($url) {
237: // キャッシュ・システムを使ったロード
238: $contents = $this->load($url);
239:
240: // 失敗したらネットから強制ロード
241: if ($this->iserror()) {
242: $this->error = FALSE;
243: $this->errmsg = '';
244: $contents = $this->forceLoad($url);
245: if ($this->iserror()) {
246: return FALSE;
247: }
248: }
249:
250: // XMLロード
251: $xml = @simplexml_load_string($contents);
252:
253: // キャッシュが壊れている可能性
254: if ($xml == FALSE) {
255: $contents = $this->forceLoad($url);
256: $xml = @simplexml_load_string($contents);
257: if ($xml == FALSE) {
258: $this->error = TRUE;
259: $this->errmsg = '"' . $url . '" が取得できません';
260: return FALSE;
261: }
262: }
263:
264: return $xml;
265: }
まず、前述の load メソッドを使ってキャッシュ・システムからコンテンツをロードする。
ここで失敗したら、キャッシュ・ファイルが破損している可能性があるので、forceLoad メソッドを使ってネットから取得する。
取得したコンテンツを simplexml_load_string に渡してXML解釈させるのだが、ここでエラーが発生した場合も、キャッシュ・ファイルが破損している可能性があるので、同様にネットからXMLファイルを取り直して、再度、 simplexml_load_string へ流し込む。
解説:予報地点情報ファイルを読み込む
pahooWeather.php
174: /**
175: * 予報地点情報ファイルをプロパティに読み込む.
176: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
177: * @param なし
178: * @return bool TRUE:読込成功/FALSE:失敗
179: */
180: function readJmaSpots() {
181: // 予報地点情報ファイルの更新要否を判断する
182: if (empty($_SERVER['SCRIPT_NAME']) ||
183: (basename($_SERVER['SCRIPT_NAME']) !== self::URL_JMPWEATHERINIT)) {
184: $ti = $this->readJmaSpotsTimeDiff();
185: if ($ti > self::INTERVAL_READJMASPOTS) {
186: $this->execJmaWeatherInit();
187: }
188: }
189:
190: // 予報地点情報ファイルをプロパティに読み込む
191: $cnt = 0;
192: $xml = @simplexml_load_file(self::FILE_JMASPOTS);
193: // レスポンス・チェック
194: if (isset($xml->spot) == FALSE) {
195: $res = FALSE;
196: $this->error = TRUE;
197: $this->errmsg = '予報地点ファイル "' . self::FILE_JMASPOTS . '" がありません';
198: // バージョン・チェック
199: } else if ($xml->version != self::FILE_VERSION) {
200: $res = FALSE;
201: $this->error = TRUE;
202: $this->errmsg = '予報地点ファイル "' . self::FILE_JMASPOTS . '" のバージョンが違います';
203: } else {
204: // 必要な情報を配列へ格納
205: foreach ($xml->spot as $spot) {
206: $this->spots[$cnt]['code'] = (string)$spot->code;
207: $this->spots[$cnt]['page'] = (string)$spot->page;
208: $this->spots[$cnt]['regionCode'] = (string)$spot->regionCode;
209: $this->spots[$cnt]['regionName'] = (string)$spot->regionName;
210: $this->spots[$cnt]['prefCode'] = (string)$spot->prefCode;
211: $this->spots[$cnt]['prefName'] = (string)$spot->prefName;
212: $this->spots[$cnt]['areaCode'] = (string)$spot->areaCode;
213: $this->spots[$cnt]['areaName'] = (string)$spot->areaName;
214: $this->spots[$cnt]['stationName'] = (string)$spot->stationName;
215: $this->spots[$cnt]['stationCode'] = (string)$spot->stationCode;
216: $this->spots[$cnt]['longitude'] = (float)$spot->longitude;
217: $this->spots[$cnt]['latitude'] = (float)$spot->latitude;
218: $this->spots[$cnt]['location'] = (float)$spot->location;
219: $cnt++;
220: }
221: if ($cnt == 0) {
222: $res = FALSE;
223: $this->error = TRUE;
224: $this->errmsg = '予報地点ファイル "' . self::FILE_JMASPOTS . '" がありません';
225: } else {
226: $res = TRUE;
227: }
228: }
229:
230: return $res;
231: }
後述する予報地点情報ファイルを読み込むメソッド readJmaSpots はコンストラクタから呼ばれる。
予報地点情報ファイルは不定期に更新しなければならないため、本メソッドの冒頭で後述する readJmaSpotsTimeDiff メソッドを使って、予報地点情報ファイルの更新日が1週間(この値はクラス・プロパティ INTERVAL_READJMASPOTS に設定)より古ければ、execJmaWeatherInit メソッドを予報地点情報ファイルを再作成する。
解説:気象庁防災情報XMLから最新の週間天気予報情報URLを取得
pahooWeather.php
420: /**
421: * ページ番号を指定して気象庁防災情報XMLから最新の天気予報情報URLを求める.
422: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
423: * @param int $page ページ番号
424: * @return array(VPFD51, VPFW50)/FALSE:取得失敗
425: */
426: function jmaGetWeatherForecastURL($page) {
427: // URLパターン
428: $vpfd51 = sprintf('/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VPFD51\_%06d\.xml/ui', $page);
429: $vpfw50 = sprintf('/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)VPFW50\_%06d\.xml/ui', $page);
430:
431: $xml = $this->pcc->simplexml_load(self::FEED_REGULAR_L);
432: // レスポンス・チェック
433: if ($this->pcc->iserror()) {
434: $this->error = TRUE;
435: $this->errmsg = $this->pcc->errmsg;
436: return FALSE;
437: } else if (! isset($xml->entry)) {
438: $this->error = TRUE;
439: $this->errmsg = '"' . self::FEED_REGULAR_L . '" の解釈に失敗しました';
440: return FALSE;
441: }
442: $this->xmlfile[0] = self::FEED_REGULAR_L;
443:
444: // フィード(XMLファイル)解析
445: $vpfd51_url = $vpfd51_dt = '';
446: $vpfw50_url = $vpfw50_dt = '';
447: $res = FALSE;
448: foreach ($xml->entry as $node) {
449: // 日時がより新しいURLを採用
450: if (preg_match($vpfd51, $node->id, $arr) > 0) {
451: if ($arr[1] > $vpfd51_dt) {
452: $vpfd51_url = $arr[0];
453: $vpfd51_dt = $arr[1];
454: $res = TRUE;
455: }
456: } else if (preg_match($vpfw50, $node->id, $arr) > 0) {
457: if ($arr[1] > $vpfw50_dt) {
458: $vpfw50_url = $arr[0];
459: $vpfw50_dt = $arr[1];
460: $res = TRUE;
461: }
462: }
463: }
464:
465: // エラー・チェック
466: if (! $res) {
467: $this->error = TRUE;
468: $this->errmsg = '気象庁防災情報XMLから週間天気予報情報を取得できません';
469: return FALSE;
470: }
471:
472: $this->xmlfile[1] = $vpfw50_url;
473: $this->xmlfile[2] = $vpfd51_url;
474:
475: return array($vpfd51_url, $vpfw50_url);
476: }
URLを正規表現で分解し、配信日時 yyyymmddhhmmss が最も大きく、VPFW50 または VPFD51 を含むURLを返す。
解説:天気予報情報を読み込む
pahooWeather.php
478: /**
479: * 予報地点コード$stationを指定し,天気予報情報を配列$itemsに格納する.
480: * 気象庁防災情報XMLを利用する.
481: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
482: * @param array $items 天気予報を格納する配列
483: * @param string $station 予報地点コード
484: * @param int $forecast 0:天気予報,1:週間天気予報(省略時:1)
485: * 0のとき‥‥jmaWeeklyWeather[0](本日)~[6](6日後)に代入
486: * 1のとき‥‥jmaWeeklyWeather[0](本日)~[2](2日後)に代入
487: * @return bool TRUE:成功/FALSE:失敗
488: */
489: function jmaGetWeatherForecast(&$items, $station, $forecast=1) {
490: // 電文コード
491: $code = ($forecast == 0) ? 'VPFD51' : 'VPFW50';
492: // 名前空間
493: define('JMX_EB', 'http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
494: // 曜日
495: static $week_name = array('日', '月', '火', '水', '木', '金', '土');
496: // マッチングパターン
497: $pat11 = '/([0-9]+)\-([0-9]+)\-([0-9]+)/ui'; // 年月日
498: $pat12 = '/([0-9]+)\-([0-9]+)\-([0-9]+)T([0-9]+)\:/ui'; // 年月日時
499: $pat21 = '/から/ui'; // 降水確率
500: $pat31 = '/最低気温/ui'; // 最低気温
501: $pat32 = '/^日中の最高気温/ui'; // 最高気温
502:
503: // 予報地点コードの取得
504: $res = FALSE;
505: foreach ($this->spots as $id=>$spot) {
506: if (($spot['code'] == $code) && ($spot['stationCode'] == $station)) {
507: $res = TRUE;
508: break;
509: }
510: }
511: if ($res == FALSE) {
512: $this->error = TRUE;
513: $this->errmsg = '予報地点コードが見つかりません';
514: return FALSE;
515: }
516:
517: // 初日のみ初期化
518: $i = 0;
519: $dt = new DateTime(date('Y-m-d'));
520: $this->jmaWeeklyWeather[$i]['year'] = date('Y');
521: $this->jmaWeeklyWeather[$i]['month'] = date('n');
522: $this->jmaWeeklyWeather[$i]['day'] = date('j');
523: $this->jmaWeeklyWeather[$i]['week'] = (string)$week_name[date_format($dt, 'w')];
524: $this->jmaWeeklyWeather[$i]['stationName'] = $this->spots[$id]['stationName'];
525: $this->jmaWeeklyWeather[$i]['weather'] = '';
526: $this->jmaWeeklyWeather[$i]['image'] = '';
527: $this->jmaWeeklyWeather[$i]['rainy'] = '';
528: $this->jmaWeeklyWeather[$i]['temp_max'] = '';
529: $this->jmaWeeklyWeather[$i]['temp_min'] = '';
530:
531: // 最新の週間天気予報情報URLを取得
532: $page = $this->spots[$id]['page'];
533: $area = $this->spots[$id]['areaCode'];
534: $station = $this->spots[$id]['stationCode'];
535:
536: list($vpfd51, $vpfw50) = $this->jmaGetWeatherForecastURL($page);
537: if ($this->pcc->iserror()) {
538: $this->error = TRUE;
539: $this->errmsg = $this->pcc->geterror();
540: return FALSE;
541: }
542: if ($this->error) return FALSE;
543: if ($this->pcc->error) return FALSE;
544:
545: $this->jmaWeeklyWeather['stationCode'] = $this->spots[$id]['stationCode'];
546: $this->jmaWeeklyWeather['stationName'] = $this->spots[$id]['stationName'];
547: $this->jmaWeeklyWeather['location'] = $this->spots[$id]['location'];
548:
549: // VPFW51(府県週間天気予報)の解析
550: if ($forecast == 1) {
551: $xml = $this->pcc->simplexml_load($vpfw50);
552: // レスポンス・チェック
553: if ($this->pcc->iserror()) {
554: $this->error = TRUE;
555: $this->errmsg = $this->pcc->errmsg;
556: return FALSE;
557: } else if (! isset($xml->Body->MeteorologicalInfos)) {
558: $this->error = TRUE;
559: $this->errmsg = '"' . $vpfw50 . '" の解釈に失敗しました';
560: return FALSE;
561: }
562:
563: // 年月日取得
564: foreach ($xml->Body->MeteorologicalInfos->TimeSeriesInfo->TimeDefines->TimeDefine as $TimeDefine) {
565: if (preg_match($pat11, $TimeDefine->DateTime, $arr) > 0) {
566: // 最初の情報は何時か
567: $i = (int)$TimeDefine['timeId'];
568: if ($i == 1) {
569: // $dd = ((date('j') == (int)$arr[3])) ? (-1) : 0;
570: // v.5.02
571: $ss2 = date('Y-m-d');
572: if ($ss2 < $arr[0]) $dd = 0; // 明日
573: else if ($ss2 == $arr[0]) $dd = (-1); // 今日
574: else $dd = (-2); // 昨日
575: }
576: $i += $dd;
577: if ($i < 0) continue; // v.5.02
578: // 予報1日分の初期化
579: $dt = new DateTime($arr[0]);
580: $this->jmaWeeklyWeather[$i]['year'] = (int)$arr[1];
581: $this->jmaWeeklyWeather[$i]['month'] = (int)$arr[2];
582: $this->jmaWeeklyWeather[$i]['day'] = (int)$arr[3];
583: $this->jmaWeeklyWeather[$i]['week'] = (string)$week_name[date_format($dt, 'w')];
584: $this->jmaWeeklyWeather[$i]['weather'] = '';
585: $this->jmaWeeklyWeather[$i]['image'] = '';
586: $this->jmaWeeklyWeather[$i]['rainy'] = '';
587: $this->jmaWeeklyWeather[$i]['temp_max'] = '';
588: $this->jmaWeeklyWeather[$i]['temp_min'] = '';
589: }
590: }
591:
592: foreach ($xml->Body->MeteorologicalInfos as $info) {
593: // 天気・降水確率の取得
594: if ((string)$info['type'] == '区域予報') {
595: foreach ($info->TimeSeriesInfo->Item as $item) {
596: if ((string)$item->Area->Code != $area) continue;
597: foreach ($item->Kind as $kind) {
598: if ((string)$kind->Property->Type == '天気') {
599: $node = $kind->Property->WeatherPart->children(JMX_EB);
600: foreach ($node->Weather as $Weather) {
601: $id = (int)$Weather->attributes()->refID + $dd;
602: if ($id < 0) continue; // v.5.02
603: $this->jmaWeeklyWeather[$id]['weather'] = $this->jmaShortWeather((string)$Weather);
604: }
605: $node = $kind->Property->WeatherCodePart->children(JMX_EB);
606: foreach ($node->WeatherCode as $WeatherCode) {
607: $id = (int)$WeatherCode->attributes()->refID + $dd;
608: if ($id < 0) continue; // v.5.02
609: // 当日夜間
610: if (($id == 0) && (date('H') >= 18)) {
611: $this->jmaWeeklyWeather[$id]['image'] = $this->jmaTelop2url((int)$WeatherCode, 1);
612: } else {
613: $this->jmaWeeklyWeather[$id]['image'] = $this->jmaTelop2url((int)$WeatherCode, 0);
614: }
615: }
616: } else if ((string)$kind->Property->Type == '降水確率') {
617: $node = $kind->Property->ProbabilityOfPrecipitationPart->children(JMX_EB);
618: foreach ($node->ProbabilityOfPrecipitation as $rainy) {
619: $id = (int)$rainy->attributes()->refID + $dd;
620: if ($id < 0) continue; // v.5.02
621: $this->jmaWeeklyWeather[$id]['rainy'] = (string)$rainy;
622: }
623: }
624: }
625: }
626: // 最低気温・最高気温
627: } else if ((string)$info['type'] == '地点予報') {
628: foreach ($info->TimeSeriesInfo->Item as $item) {
629: if ((int)$item->Station->Code != $station) continue;
630: foreach ($item->Kind->Property as $property) {
631: if ((string)$property->Type == '最低気温') {
632: $node = $property->TemperaturePart->children(JMX_EB);
633: foreach ($node->Temperature as $Temperature) {
634: $id = (int)$Temperature->attributes()->refID + $dd;
635: if ($id < 0) continue; // v.5.02
636: $this->jmaWeeklyWeather[$id]['temp_min'] = (string)$Temperature; // v.5.02
637: }
638: } else if ((string)$property->Type == '最高気温') {
639: $node = $property->TemperaturePart->children(JMX_EB);
640: foreach ($node->Temperature as $Temperature) {
641: $id = (int)$Temperature->attributes()->refID + $dd;
642: if ($id < 0) continue; // v.5.02
643: $this->jmaWeeklyWeather[$id]['temp_max'] = (string)$Temperature; // v.5.02
644: }
645: }
646: }
647: }
648: }
649: }
650: }
651:
652: // VPFD51(府県天気予報 R1)の解析
653: // v.5.03修正(ここから)
654: $code = 'VPFD51';
655: // 予報地点コードの取得
656: $res = FALSE;
657: foreach ($this->spots as $id=>$spot) {
658: if (($spot['code'] == $code) && ($spot['stationCode'] == $station)) {
659: $res = TRUE;
660: break;
661: }
662: }
663: if ($res == FALSE) {
664: $this->error = TRUE;
665: $this->errmsg = '予報地点コードが見つかりません';
666: return FALSE;
667: }
668: $area = $this->spots[$id]['areaCode'];
669: // v.5.03修正(ここまで)
670:
671: $xml = $this->pcc->simplexml_load($vpfd51);
672: // レスポンス・チェック
673: if ($this->pcc->iserror()) {
674: $this->error = TRUE;
675: $this->errmsg = $this->pcc->errmsg;
676: return FALSE;
677: } else if (! isset($xml->Body->MeteorologicalInfos)) {
678: $this->error = TRUE;
679: $this->errmsg = '"' . $vpfd51 . '" の解釈に失敗しました';
680: return FALSE;
681: }
682:
683: // 初期化
684: $rain_table = array('-', '-', '-', '-', '-', '-', '-', '-');
685: $temp_table = array(0, 0, 0, 0);
686:
687: foreach ($xml->Body->MeteorologicalInfos as $info) {
688: if ((string)$info['type'] == '区域予報') {
689: foreach ($info->TimeSeriesInfo as $TimeSeriesInfo) {
690: // 日時
691: foreach ($TimeSeriesInfo->TimeDefines->TimeDefine as $TimeDefine) {
692: if (preg_match($pat21, $TimeDefine->Name, $arr) > 0) {
693: preg_match($pat12, $TimeDefine->DateTime, $arr);
694: $ss1 = $arr[1] . '-' . $arr[2] . '-' . $arr[3];
695: // 最初の情報は何時か
696: $i = (int)$TimeDefine['timeId'];
697: if ($i == 1) {
698: // $dd = ((date('j') == (int)$arr[3])) ? (-1) : 0;
699: // v.5.02
700: $ss2 = date('Y-m-d');
701: if ($ss2 < $ss1) $dd = 0; // 明日
702: else if ($ss2 == $ss1) $dd = (-1); // 今日
703: else $dd = (-2); // 昨日
704: $d2 = $arr[4] / 6; // 6時間毎
705: // v.5.02
706: if ($dd == (-2)) $d2 = $d2 - 4 + 1;
707: // 年月日代入(天気予報の場合)
708: if ($forecast == 0) {
709: $dt = new DateTime($arr[1] . '-' . $arr[2] . '-' . $arr[3]);
710: for ($j = 0; $j < 3; $j++) {
711: $this->jmaWeeklyWeather[$j]['year'] = (int)date_format($dt, 'Y');
712: $this->jmaWeeklyWeather[$j]['month'] = (int)date_format($dt, 'n');
713: $this->jmaWeeklyWeather[$j]['day'] = (int)date_format($dt, 'j');
714: $this->jmaWeeklyWeather[$j]['week'] = (string)$week_name[date_format($dt, 'w')];
715: date_add($dt, date_interval_create_from_date_string('1 days'));
716: }
717: }
718: }
719:
720: } else if (preg_match($pat11, $TimeDefine->DateTime, $arr) > 0) {
721: // 最初の情報は今日か明日か
722: $i = (int)$TimeDefine['timeId'];
723: if ($i == 1) {
724: // $dd = ((date('j') == (int)$arr[3])) ? (-1) : 0;
725: // v.5.02
726: $ss2 = date('Y-m-d');
727: if ($ss2 < $arr[0]) $dd = 0; // 明日
728: else if ($ss2 == $arr[0]) $dd = (-1); // 今日
729: else $dd = (-2); // 昨日
730:
731: }
732: }
733: }
734: // 地方コードは上何桁で判定するか
735: $flag = FALSE;
736: $n = 5;
737: for ($n = 5; $n >= 1; $n--) {
738: foreach ($TimeSeriesInfo->Item as $item) {
739: if (substr((string)$item->Area->Code, 0, $n) != substr($area, 0, $n)) continue;
740: $flag = TRUE;
741: }
742: if ($flag) break;
743: }
744: // 天気予報
745: foreach ($TimeSeriesInfo->Item as $item) {
746: foreach ($item->Kind as $kind) {
747: if (substr((string)$item->Area->Code, 0, $n) != substr($area, 0, $n)) continue;
748: if ((string)$kind->Property->Type == '天気') {
749: $node = $kind->Property->WeatherPart->children(JMX_EB);
750: foreach ($node->Weather as $Weather) {
751: $id = (int)$Weather->attributes()->refID + $dd;
752: if ($id < 0) continue; // v.5.02
753: $this->jmaWeeklyWeather[$id]['weather'] = $this->jmaShortWeather((string)$Weather);
754: }
755: $node = $kind->Property->WeatherCodePart->children(JMX_EB);
756: foreach ($node->WeatherCode as $WeatherCode) {
757: $id = (int)$WeatherCode->attributes()->refID + $dd;
758: if ($id < 0) continue; // v.5.02
759: if (($id == 0) && (date('H') >= 18)) {
760: $this->jmaWeeklyWeather[$id]['image'] = $this->jmaTelop2url((int)$WeatherCode, 1);
761: } else {
762: $this->jmaWeeklyWeather[$id]['image'] = $this->jmaTelop2url((int)$WeatherCode, 0);
763: }
764: }
765: // 降水確率
766: } else if ((string)$kind->Property->Type == '降水確率') {
767: $node = $kind->Property->ProbabilityOfPrecipitationPart->children(JMX_EB);
768: foreach ($node->ProbabilityOfPrecipitation as $rainy) {
769: $id = (int)$rainy->attributes()->refID + $dd + $d2;
770: if ($id < 0) continue; // v.5.02
771: $rain_table[$id] = (string)$rainy;
772: }
773: }
774: }
775: }
776: }
777: } else if ((string)$info['type'] == '地点予報') {
778: foreach ($info->TimeSeriesInfo as $TimeSeriesInfo) {
779: // 日時
780: foreach ($TimeSeriesInfo->TimeDefines->TimeDefine as $TimeDefine) {
781: if (preg_match($pat11, $TimeDefine->DateTime, $arr) > 0) {
782: // 最初の情報は何時か
783: $i = (int)$TimeDefine['timeId'];
784: if ($i == 1) {
785: // $dd = ((date('j') == (int)$arr[3])) ? (-1) : 0;
786: // v.5.02
787: $ss2 = date('Y-m-d');
788: if ($ss2 < $arr[0]) $dd = 0; // 明日
789: else if ($ss2 == $arr[0]) $dd = (-1); // 今日
790: else $dd = (-2); // 昨日
791: $temp_table[$i] = 0;
792: $day0 = (int)$arr[3];
793: } else {
794: $temp_table[$i] = ($day0 == (int)$arr[3]) ? 0 : 1;
795: $day0 = (int)$arr[3];
796: }
797: }
798: }
799: // 最低気温・最高気温
800: $id = 1 + $dd;
801: foreach ($TimeSeriesInfo->Item as $item) {
802: if ((int)$item->Station->Code != $station) continue;
803: foreach ($item->Kind->Property as $property) {
804: if (preg_match($pat31, (string)$property->Type) > 0) {
805: $node = $property->TemperaturePart->children(JMX_EB);
806: $id += $temp_table[(int)$node->Temperature->attributes()->refID];
807: if ($id < 0) continue; // v.5.02
808: $this->jmaWeeklyWeather[$id]['temp_min'] = (string)$node->Temperature; // v.5.02
809: } else if (preg_match($pat32, (string)$property->Type) > 0) {
810: $node = $property->TemperaturePart->children(JMX_EB);
811: $id += $temp_table[(int)$node->Temperature->attributes()->refID];
812: if ($id < 0) continue; // v.5.02
813: $this->jmaWeeklyWeather[$id]['temp_max'] = (string)$node->Temperature; // v.5.02
814: }
815: }
816: }
817: }
818: }
819: }
820: // 降水確率を天気予報情報へ
821: foreach ($rain_table as $key=>$val) {
822: // v.5.02
823: if (($dd == (-2)) && ($key >= 4)) break;
824: $id = floor($key / 4);
825: if (! isset($this->jmaWeeklyWeather[$id]['rainy'])) {
826: $this->jmaWeeklyWeather[$id]['rainy'] = '';
827: }
828: // 週間予報で取得した降水確率があれば上書き
829: if (isset($this->jmaWeeklyWeather[$id]['rainy']) && is_numeric($this->jmaWeeklyWeather[$id]['rainy'])) {
830: $this->jmaWeeklyWeather[$id]['rainy'] = '';
831: }
832: $this->jmaWeeklyWeather[$id]['rainy'] .= (string)$val;
833: if ($key % 4 != 3) $this->jmaWeeklyWeather[$id]['rainy'] .= '/';
834: }
835: // 不明要素を空文字で埋める(VPFD51の場合)
836: if ($forecast == 0) {
837: for ($i = 0; $i < 3; $i++) {
838: if (! isset($this->jmaWeeklyWeather[$i]['weather'])) {
839: $this->jmaWeeklyWeather[$i]['weather'] = '';
840: }
841: if (! isset($this->jmaWeeklyWeather[$i]['image'])) {
842: $this->jmaWeeklyWeather[$i]['image'] = '';
843: }
844: if (! isset($this->jmaWeeklyWeather[$i]['rainy'])) {
845: $this->jmaWeeklyWeather[$i]['rainy'] = '';
846: }
847: if (! isset($this->jmaWeeklyWeather[$i]['temp_min'])) {
848: $this->jmaWeeklyWeather[$i]['temp_min'] = '';
849: }
850: if (! isset($this->jmaWeeklyWeather[$i]['temp_max'])) {
851: $this->jmaWeeklyWeather[$i]['temp_max'] = '';
852: }
853: }
854: }
855:
856: // 配列へ代入
857: $items = $this->jmaWeeklyWeather;
858: return TRUE;
859: }
曜日(月,火,水‥‥)は情報XMLにないので、 date_format 関数によって曜日番号を計算し、あらかじめ用意した曜日名テーブル $week_name を引き当てる。
要素 Weather にある天気予報(日本語)は文字列として長いので、ユーザー・メソッド jmaShortWeather を使って平仮名等を省略することで短縮する。
要素 WeatherCode にある天気予報テロップ番号は、以前は天気予報アイコン画像ファイルと1対1対応だったのだが、気象庁サイト・リニューアルにともない、アイコンがSVGファイルとなり、対応もN対1となった。そこで、気象庁週間予報ホームページを解析し、天気予報テロップ番号から対応するSVGを引き当てるユーザー・メソッド jma_telop2url を用意した。
VPFD51 の解析では、VPFD51の構造で述べたとおり、天気予報(アイコン画像を含む)は1日単位で3回分だが、降水確率は6時間毎の値で6回分、気温は12時間毎の値で4回分と、各々の予報間隔が異なっている。いちいち要素 TimeDefine の値を取得して、本日と比較するよう工夫している。
天気予報(VPFD51 のみ取得)の場合は、このループ処理で年月日も代入しておく。
VPFD51 では、地方コードが VPFW50 と異なる場合がある。そこで、VPFW50 と VPFW50 の各々の地方コード(5桁)を、上位5桁、4桁、3桁‥‥と順々に減らし、一致したコードを同じ地方と判断するようにした。
前述の通り、降水確率は6時間毎で開始位置が他の要素とは異なるため、いったん、配列 $rain_table に格納し、最後に配列 $jma_WeeklyWeather に代入するよう工夫した。
さらに、情報取得の時間帯によっては予報情報が入っていない場合があるので、最後に不明要素を空文字で埋めるようにした。
解説:週間天気予報表を作成する
jmaWeeklyWeather.php
527: /**
528: * 指定した週間天気予報情報などから週間天気予報表(HTML)を作成する.
529: * @param array $items 週間天気予報情報を格納した配列
530: * @param int $start 作成開始オフセット(0~6)
531: * @param int $goal 作成終了オフセット(0~6)
532: * @param int $columns 天気予報の表示列数(初期値:COLUMNS)
533: * @param int $forecast 0:天気予報,1:週間天気予報(省略時:1)
534: * @return int 配列に格納した情報件数/0=失敗
535: */
536: function makeWeeklyWeather($items, $start, $goal, $columns=COLUMNS, $forecast=1) {
537: //引数のベリファイ
538: if ($start < 0 || $start > 6) return FALSE;
539: if ($goal < 0 || $goal > 6) return FALSE;
540: if ($columns < 0 || $columns > 7) return FALSE;
541: if ($start > $goal) return FALSE;
542: if ($forecast == 0) {
543: if ($start > 2) return FALSE;
544: if ($goal > 2) return FALSE;
545: if ($columns > 3) return FALSE;
546: }
547:
548: $outstr =<<< EOT
549: <table class="weather">
550: <caption>{$items['stationName']}の天気予報</caption>
551:
552: EOT;
553:
554: $i = $start;
555: while ($i <= $goal) {
556: //日付の行
557: $j = 0;
558: while ($j < $columns) {
559: if ($j == 0) $outstr .= "<tr>\n";
560: if (($i <= $goal) && isset($items[$i]['week'])) {
561: $mmddww = sprintf("%02d/%02d<span class=\"wsmall\">(%s)</span>", $items[$i]['month'], $items[$i]['day'], $items[$i]['week']);
562: $outstr .=<<< EOT
563: <td class="dt">{$mmddww}</td>
564:
565: EOT;
566: } else {
567: $outstr .= "<td class=\"dt\"> </td>\n";
568: }
569: $i++;
570: $j++;
571: if ($j == $columns) $outstr .= "</tr>\n";
572: }
573: //天気予報の行
574: $i -= $columns;
575: $j = 0;
576: while ($j < $columns) {
577: //新しい行
578: if ($j == 0) $outstr .= "<tr>\n";
579: if ($i <= $goal) {
580: $mmddww = sprintf("%02d/%02d<span class=\"wsmall\">(%s)</span>", $items[$i]['month'], $items[$i]['day'], $items[$i]['week']);
581: $temp_max = ((string)$items[$i]['temp_max'] != '') ? $items[$i]['temp_max'] . '℃' : '-';
582: $temp_min = ((string)$items[$i]['temp_min'] != '') ? $items[$i]['temp_min'] . '℃' : '-';
583: $outstr .=<<< EOT
584: <td class="info">
585: {$items[$i]['weather']}<br />
586: <img class="wicon" src="{$items[$i]['image']}" /><br />
587: <span class="wsmall">
588: {$items[$i]['rainy']}%<br />
589: {$temp_max}/{$temp_min}
590: </span>
591: </td>
592:
593: EOT;
594: //情報なし列
595: } else {
596: $outstr .= "<td class=\"info\"> </td>\n";
597: }
598: $i++;
599: $j++;
600: if ($j == $columns) $outstr .= "</tr>\n";
601: }
602: }
603: $outstr .= "</table>\n";
604:
605: return $outstr;
606: }
解説:表示とURLパラメータ
jmaWeeklyWeather.php
718: //パラメータを取得する.
719: $id = (int)getParam('id', FALSE, 0);
720: $region = (string)sprintf('%02d', (int)getParam('region', FALSE, 0));
721: $pref = (string)sprintf('%02d', (int)getParam('pref', FALSE, 0));
722: $station = (string)sprintf('%05d', (int)getParam('station', FALSE, 0));
723: $outenc = (string)getParam('charset', FALSE, INTERNAL_ENCODING);
724: $start = (int)getParam('start', FALSE, DEF_START);
725: $goal = (int)getParam('goal', FALSE, DEF_GOAL);
726: $forecast = (int)getParam('forecast', FALSE, DEF_FORECAST);
727: $columns = COLUMNS;
jmaWeeklyWeather.php?id=1&forecast=1&station=49142のようにすることで、ある地点の天気予報のみを表示させることができる。つまり、このスクリプトをホームページやブログの一部として組み込むことで、週間天気予報を表示するパーツになる。
station は予報地点コードで、予報地点情報ファイルを参照されたい。
また、出力はHTML文のみとなり、スタイルシートは本体ページの方で用意していただきたい。必要なclassは次の通り。
jmaWeeklyWeather.php
280: <style>
281: /* エラー表示 */
282: p.werror {
283: color: red;
284: }
285: /* 天気予報表 */
286: table.weather {
287: width: {$width}px;
288: border:solid 1px #000000;
289: border-collapse:collapse;
290: margin-top:10px;
291:
292: }
293: /* 天気予報表:月日表示部 */
294: table.weather td.dt {
295: width: {$width}px;
296: border:solid 1px #000000;
297: border-collapse: collapse;
298: padding:4px;
299: white-space:nowrap;
300: text-align:center;
301: }
302: /* 天気予報表:予報表示部 */
303: table.weather td.info {
304: width: {$width}px;
305: border:solid 1px #000000;
306: border-collapse: collapse;
307: padding:4px;
308: white-space:nowrap;
309: text-align:center;
310: }
311: /* 天気予報表:予報アイコン */
312: img.wicon {
313: width: 60px;
314: }
315: /* 天気予報表:小さい文字 */
316: span.wsmall {
317: font-size: small;
318: }
319: /* Tweetボタン */
320: .x_tweet_button {
321: color: #FFFFFF;
322: background-color: #000000;
323: font-size: 70%;
324: font-weight: bold;
325: text-align: center;
326: border-radius: 4px;
327: padding: 4px 8px 4px 8px;
328: border: none;
329: }
330: </style>
jmaWeeklyWeather.php?id=1&forecast=1&station=49142&charset=SJISとすると、甲府の週間天気予報をシフトJISで出力することができる。
jmaWeeklyWeather.php?region=02&pref=04のようにすることで、あらかじめプルダウン選択を指定することができる。
ここでは、地方を「東北地方」(region=02)に、都道府県を「宮城県」(pref=04)に指定している。
jmaWeeklyWeather.php?start=1&goal=6のように指定することで、予報開始日と終了日を指定することができる。
start は予報開始日(0:今日,1:明日‥‥6)、goal は予報終了日(0:今日,1:明日‥‥6)である。
予報地点情報ファイル
予報地点情報ファイルは、同梱のプログラム jmaWeatherInit.php を使って自動生成することができる。
予報地点の緯度・経度は、気象庁の「地域気象観測システム(アメダス)」ページにある地域気象観測所一覧(ZIP圧縮形式)をダウンロードし、解凍して得られるCSVファイルから自動取得する。
jmaWeatherInit.php
610: /**
611: * 指定した気象庁防災情報XMLから各地点の最新の府県週間天気予報情報URLを取得する.
612: * 気象庁防災情報XMLのデータコード$codeを指定し,URLを配列$itemsに格納する.
613: * @param object $pwt pahooWeatherオブジェクト
614: * @param string $code データコード
615: * @param array $items 府県週間天気予報情報URLを格納する配列
616: * [page]['url'] URL
617: * [page]['dt'] 日時
618: * @param string $errmsg エラーメッセージを格納
619: * @return int 格納したURLの数/FALSE:エラー発生
620: */
621: function getWeatherUrls($pwt, $code, &$items, &$errmsg) {
622: //マッチするURLパターン
623: $pat1 = sprintf('/https?\:\/\/www\.data\.jma\.go\.jp\/developer\/xml\/data\/([0-9\_]+)%s\_([0-9]+)\.xml/ui', $code);
624:
625: //気象庁防災情報XML:長期フィード - 定時配信
626: $errmsg = '';
627: unknown_certificate();
628: $xml = @simplexml_load_file($pwt::FEED_REGULAR_L);
629: //レスポンス・チェック
630: if (! isset($xml->entry)) {
631: $errmsg = '気象庁防災情報XMLにアクセスできません';
632: return FALSE;
633: }
634:
635: //フィードを解析
636: $items = array();
637: $cnt = 0;
638: foreach ($xml->entry as $entry) {
639: if (preg_match($pat1, (string)$entry->id, $arr) > 0) {
640: $dt = (string)$arr[1];
641: $page = (string)$arr[2];
642: //登録済み
643: if (isset($items[$page])) {
644: //より新しければ要素を上書き
645: if ($dt > $items[$page]['dt']) {
646: $items[$page]['dt'] = (string)$dt;
647: $items[$page]['url'] = (string)$entry->id;
648: }
649: //未登録
650: } else {
651: $items[$page]['dt'] = (string)$dt;
652: $items[$page]['url'] = (string)$entry->id;
653: $cnt++;
654: }
655: }
656: }
657: //ソート
658: ksort($items);
659:
660: return $cnt;
661: }
jmaWeatherInit.php
663: /**
664: * 指定した府県週間天気予報情報URLから予報地点情報を求める.
665: * 府県週間天気予報情報URLを$urlに指定し,予報地点情報を配列$itemsに格納する.
666: * @param string $url 府県週間天気予報情報URL
667: * @param array $items 予報地点情報を格納する配列
668: * @param string $errmsg エラーメッセージを格納
669: * @return int 格納した予報地点の数/FALSE:エラー発生
670: */
671: function getWeatherSpotsSub($url, &$items, &$errmsg) {
672: $errmsg = '';
673: unknown_certificate();
674: $xml = @simplexml_load_file($url);
675: //レスポンス・チェック
676: if (! isset($xml->Body->MeteorologicalInfos)) {
677: $errmsg = '府県週間天気予報情報を取得できません';
678: return FALSE;
679: }
680:
681: $flag1 = $flag2 = FALSE;
682: foreach ($xml->Body->MeteorologicalInfos as $MeteorologicalInfos) {
683: //地方名
684: if (!$flag1 && $MeteorologicalInfos['type'] == '区域予報') {
685: $flag1 = TRUE;
686: $cnt = 0;
687: foreach ($MeteorologicalInfos->TimeSeriesInfo->Item as $item) {
688: $items[$cnt]['areaName'] = (string)$item->Area->Name;
689: $items[$cnt]['areaCode'] = (string)$item->Area->Code;
690: $cnt++;
691: }
692: //予報地点名
693: } else if (!$flag2 && $MeteorologicalInfos['type'] == '地点予報') {
694: $flag2 = TRUE;
695: $cnt = 0;
696: foreach ($MeteorologicalInfos->TimeSeriesInfo->Item as $item) {
697: $items[$cnt]['stationName'] = (string)$item->Station->Name;
698: $items[$cnt]['stationCode'] = (string)$item->Station->Code;
699: $cnt++;
700: }
701: }
702: }
703: //余分な予報地点を削除
704: /**
705: foreach ($items as $key=>$item) {
706: if ($key >= $cnt) {
707: unset($items[$key]['stationName']);
708: unset($items[$key]['stationCode']);
709: }
710: }
711: **/
712:
713: return $cnt;
714: }
jmaWeatherInit.php
764: /**
765: * 予報地点情報に場所・緯度・経度を代入する:VPFD51用
766: * @param array $amedas 地域観測所一覧
767: * @param array $spot 予報地点情報
768: * @return なし
769: */
770: function addLocation($amedas, &$spot) {
771: //地域観測所を探索
772: foreach ($amedas as $item) {
773: if ($spot['stationCode'] == $item['stationCode']) {
774: $spot['location'] = $item['location'];
775: $spot['latitude'] = $item['latitude'];
776: $spot['longitude'] = $item['longitude'];
777: return;
778: }
779: }
780: }
jmaWeatherInit.php
744: /**
745: * 予報地点情報に地方名を代入する:VPFD51用
746: * @param array $spot 予報地点情報
747: * @return なし
748: */
749: function addAreaName(&$spot) {
750: global $TableStation;
751:
752: foreach ($TableStation as $pref=>$arr1) {
753: foreach ($arr1 as $areaName=>$arr2) {
754: foreach ($arr2 as $stationName) {
755: if (($spot['prefName'] == $pref) && ($spot['stationName'] == $stationName)) {
756: $spot['areaName'] = (string)$areaName;
757: return;
758: }
759: }
760: }
761: }
762: }
予報地点名から地方名を逆引きして配列に代入する関数が addAreaName である。
pahooWeather.php
123: /**
124: * 前回,予報地点情報ファイルを読み込んだ日時と現在日時の差分を返す
125: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
126: * @param なし
127: * @return int 日時差分(単位:時)
128: */
129: function readJmaSpotsTimeDiff() {
130: $res = 9999999;
131: $t1 = @filemtime(self::FILE_JMASPOTS);
132: if ($t1 !== FALSE) {
133: $t0 = time();
134: if ($t0 - $t1 > 0) {
135: $res = (int)(($t0 - $t1) / (60 * 60));
136: }
137: }
138: return $res;
139: }
pahooWeather.php
141: /**
142: * 現在実行しているスクリプトのURLパスを取得する.
143: * @param なし
144: * @return なし
145: */
146: function getMyscriptPathURL() {
147: $protocol = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
148: $host = (! empty($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : 'www.pahoo.org';
149: $scriptDir = (! empty($_SERVER['SCRIPT_NAME'])) ? dirname($_SERVER['SCRIPT_NAME']) : '/e-soul/webtech/php06/program';
150:
151: return $protocol . $host . $scriptDir . '/';
152: }
pahooWeather.php
154: /**
155: * "jmaWeatherInit.php" を実行する.
156: * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
157: * @param なし
158: * @return なし
159: */
160: function execJmaWeatherInit() {
161: $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
162: $options = [
163: 'http' => [
164: 'method' => 'GET',
165: 'header' => "Referer: {$referer}\n"
166: ]
167: ];
168: $context = stream_context_create($options);
169:
170: $url = $this->getMyscriptPathURL() . self::URL_JMPWEATHERINIT;
171: $res = @file_get_contents($url, FALSE, $context);
172: }
主な変更点:バージョン1.x→2.x
jmaWeeklyWeather.php
- 表示用CSSは関数 makeHeader に集約した。
- 天気予報の種類を定数 DEF_FORECAST で指定。従来の週間予報は予報地点が約70箇所と少ない、という声をいただき、約170箇所ある2~3日予報と選べるようにした。
- 地方セレクタ(北海道地方,東北地方‥‥)を使うかどうかを定数 REGION で指定。
- 地方・都道府県・予報地点のセレクタは、動作を軽くするため、JavaScriptで実装する形に変更した。
- 週間予報を表示するために、その都度、気象庁防災情報XMLの3つのXMLファイルを読み込むように変更した。
- とくに定数 FEED_REGULAR_L で指定するXMLファイルが大きく、もし読み込みエラーや動作がもっさりするようであれば、状況を添えてお問い合わせいただきたい。
- データ構造を変更したため、差し替えること。
- データ構造を変更したため、差し替えること。
活用例
また、サイドバーにコンパクトな形で天気予報を掲示している。
ご活用いただき、ありがとうございます。
質疑応答
天気予報プログラムのプルダウンから地域変更しても表示がされないようですが。【回答】
ホームページのサンプル・プログラムは表示します。
ご自身のプログラムが正常に動かないときは、キャッシュディレクトリ(定数 DIR_CACHE で指定するもの)は、ディレクトリごと消去してから走らせてみてください。キャッシュに古い(http)URLが残っている場合があるためです。
また、予報地点が更新されました。また、予報地点ファイル(定数 FILE_JMASPOTS で指定するもの)を差し替えてください。
【質問】 しゅんすけ様
天気予報プログラムを実行したところ下記のエラーが表示されました。【回答】
エラー:気象庁防災情報XMLから週間天気予報情報を取得できません
気象庁防災情報XMLがhttps化したためでした。
対応プログラムをアップしました。クラスファイル "pahooWeather.php" を差し替えれば動くようになります。お試しください。
【質問】
https://www.benricho.org/weather_japan/ のサイトように特定の地域(例えば東京都・東京)の天気のみを表示させるにはどのように記述すればよいのですか?【回答】
URLパラメータの station(予報地点コード)に整数を設定して呼び出してください。たとえば東京都・東京であれば、下記のようにして呼び出します。
jmaWeeklyWeather.php?station=44132
また、プログラムに直接値を代入したいのであれば、メイン・プログラムの初期化にある $pref, $city に直接値を代入してください。→「解説:表示とURLパラメータ」参照
【質問】
pahooWeather.php の 583~589行目(// 月の補正 の部分)を下記のように修正したところ月日は一致したのですが、今日の天気(当日の天気)が出てきません。【回答】
date_add($dt, date_interval_create_from_date_string('-1 month'));
↓
date_add($dt, date_interval_create_from_date_string('1 day'));
"pahooWeather.php" version 4.18 では、当該行はコメントアウトしており機能していません。ご確認ください。
【質問】
サイト内に天気予報と今日は何の日(https://www.pahoo.org/e-soul/webtech/php05/php05-16-01.shtm)とを同時に表示さたところPHPで競合が起こりエラーが発生してしまいました。これらを解決する良い方法があれば教えてください。【回答】
「競合」というのは、具体的にどのようなことが起きているのか、表示されるメッセージなどをお知らせください。
【質問】
サイト内に天気予報と今日は何の日を同時に表示させた際のエラーは次のようなものでした。【回答】
Cannot redeclare ~ (previously declared in ~)in ~
サイト内で、"jmaWeeklyWeather.php" と "DayToday.php" をマージするか、includeしていませんか。この2つプログラムは別々に配置することを想定しています。
【質問】
天気予報が表示されなくなりました。【回答】
気象庁の表示が変わったためと思われますが、全国の天気予報(https://www.jma.go.jp/bosai/forecast/)からjmaweatherspots.xmlの書き換えを行うことで表示することは可能でしょうか。
冒頭に記載の通り、2021年(令和3年)2月24日、気象庁ホームページがリニューアルしたため、pahooWeather::__jma_readWeeklyWeather() メソッドが機能していません。jmaweatherspots.xml の書き換えだけでは対応できません。
プログラムを全面的に書き直しました。「主な変更点:バージョン1.x→2.x」をご覧いただき、ご利用ください。
【質問】
PHPで天気予報を求める(3)で、指定した予報地点の天気の画像が実際の気象庁の予報地点の画像と合っていない地点が多くあります。最高温度と最低温度は指定した予想地点と気象庁の予報地点とは完全一致しています。【回答】
画像が合っていないのは、何らかのループ処理がなされ、その結果、つまり最後の値が読み込まれるために、対象地域の最後の地点の画像が読み込まれているようです。例えば、長野県であれば、長野市、松本市、飯田市の3地点がありますが、ループ処理の結果、長野市の画像が最後の地点(飯田市)の画像に置き換わっているようです。これは長野県に限らず全国的にそのような現象が見られます。
気象庁ホームページがリニューアルしたため、pahooWeather::__jma_readWeeklyWeather() メソッドが機能しなくなりました。
との記載がありますが、修正にはかなりの時間がかかるとのことでしょうか。
冒頭に記載の通り、プログラムを全面的に書き直しました。「主な変更点:バージョン1.x→2.x」をご覧いただき、ご利用ください。
【質問】
「PHPで天気予報を求める」で3月6日の修正版のリリース有難うございます。大変助かっております。【回答】
3日間の天気予報(週間予報ではない)において、深夜0時以降、前日、その日、その翌日の天気の表示となりますが、その日の天気情報が前日の天気情報として表示されてしまいます。言い換えれば、例えば3月20日の天気情報が、3月19日の情報として表示されているようです。
ただ、午前5時以降は、気象庁の最新の情報を拾うので、表示は正しくなります。つまり深夜0時から午前5時までの表示に問題があるように思われます。
深夜0時以降、日付が自動的に次の日に変われば、問題は解消されるのかと思われますが、そのような対応で根本的に解決されるのか、そこは検証していただければ幸いです。
冒頭にあるとおり、改良版をリリースしました。お試しください。
【質問】
修正されたとのメッセージはいただきましたが、本日3月21日深夜0時以降に確認した所、以下のことが分かりました。【回答】
週間予報の方は、深夜0時以降は新しい日付に切り替わるために正しく表示されますが、天気予報の方は、天気情報は翌日分が正しく表示されますが、日付が前日のままとなっているため、日付と天気情報が合わなくなったままです。ですから、深夜0時に日付が変わった段階で、次の日に切り替わった日付が表示されれば、天気情報と合うために問題が解消される思うのですが。
恐れ入りますが、修正をお願いできますでしょうか。
調べたところ、時間帯によるものではなく、次の不具合によるようです。
札幌、長崎など複数の地点は、VPFW51(府県週間天気予報)とVPFD51(府県天気予報 R1)とで割り当てられている地方コード(areaCode)が異なっており、このため違う地方のデータを取り込んでしまっていました。
違いを正しく処理するよう、"pahooWeather.php" を改良しました。「v.5.03」を記した箇所です。
また、参照している地域観測所一覧(アメダス)に「福島」が載っていませんでした。このため、福島の天気が会津のデータを読み込んでいました。あたらしい地域観測所一覧をダウンロードし、"jmaweatherspots.xml" を再作成しました。
お試しください。
参考サイト
- 気象庁防災情報XMLフォーマット 情報提供ページ
- 週間天気予報:気象庁
- PHPで地図で指定した場所の週間カレンダーを表示:ぱふぅ家のホームページ
- C++ で週間天気予報を表示する:ぱふぅ家のホームページ

気象庁は、RSSやWebAPIを使って天気予報情報を提供していないのだが、気象庁防災情報XMLによって PULL型配信している。そこで今回は、気象庁防災情報XMLから各地の週間天気予報情報(天気、降水確率、最高・最低気温)を取得し、一覧表に表示するPHPプログラムを作ってみることにする。
(2025年2月1日)予報地点情報ファイルを1週間毎に再作成するようにした.
(2024年11月9日)予報地点情報ファイルを更新
(2024年10月7日)Bluesky投稿機能を追加
(2024年6月23日)TwitterOAuth 7.0.0 対応,Twitter(現・X)ボタンを "X" に変更
(2024年4月22日)京都市,熱海市など3日間しか表示されなくなった地点があり,予報地点情報ファイルを更新した.
プログラムを実行する