気象庁と言えば天気予報ということで、今回は天気予報の表示ページを作成してみます
apiのURL
天気予報のデータは、https://www.jma.go.jp/bosai/forecast/data/forecast/{officeCode}.json に格納されています。
{officeCode}の部分は都道府県ごとの地域コードで、その一覧は https://www.jma.go.jp/bosai/common/const/area.json に格納されています(たとえば宮城県の場合は「150000」)
今回は使用しませんが、気象庁ホームページでは天気予報の地域(一次細分区域)と気温予想地点との対応付けのためのデータ https://www.jma.go.jp/bosai/forecast/const/forecast_area.json も同時に読み込まれていますので、本家のように天気予報と気温を対応付けたい場合は参考にしてください
apiの構造
大きく前半(/0)と後半(/1)に分かれており、前半に天気予報、後半に週間予報が格納されています
天気予報部分
天気予報の電文に限らず、気象庁ホームページの内部apiの構造は、気象庁防災情報XMLの構造によく似たものとなっています。
このため、気象庁防災情報XMLの技術資料のページに掲載されている解説資料の内容が参考になることが多いです
天気予報の場合は「府県天気予報,地域時系列予報(R1)」の解説資料が参考になります
| 要素名 | 説明 |
|---|---|
| 0.publishingOffice | 発表官署名 |
| 0.reportDatetime | 発表時刻 |
| 0.timeSeries | 予報 |
予報部はさらに情報の時間の区切り方によって3つに分かれており
| 要素名 | 説明 |
|---|---|
| 0.timeSeries.0 | 天気予報(日単位) |
| 0.timeSeries.1 | 降水確率(6時間単位) |
| 0.timeSeries.2 | 気温 |
となっています
天気予報
時系列を定義したtimeDefinesと、データが入るareasの2つの部分に分かれています
| 要素名 | 説明 |
|---|---|
| 0.timeSeries.0.timeDefines | 時系列部 |
| 0.timeSeries.0.areas | 予報部 |
timeDefinesには予報対象時刻が配列で格納されています。
たとえば3月19日11時発表の予報だと、
["2026-03-19T11:00:00+09:00","2026-03-20T00:00:00+09:00","2026-03-21T00:00:00+09:00"]
のようになっており、それぞれ19日、20日、21日の予報であることを示しています
時の部分は当日は発表時刻、翌日・翌々日は常に0時となります。
5時(~10時)発表の予報ではあさっての予報はありません。
発表時刻は1時間単位に丸められます。23時台後半~翌日4時台前半に発表される予報は、運用上1つ前に発表した予報の訂正として発表されます
areasは予報地域ごとに5つの要素に分かれます
| 要素名 | 説明 |
|---|---|
| 0.timeSeries.0.areas.{i}.area | 地域名・地域コード |
| 0.timeSeries.0.areas.{i}.weatherCodes | 天気予報テロップ番号 |
| 0.timeSeries.0.areas.{i}.weathers | 予報文 |
| 0.timeSeries.0.areas.{i}.winds | 風の予報 |
| 0.timeSeries.0.areas.{i}.waves | 波の予報(ない地域もある) |
地域名・コード以外は
["くもり","晴れ","くもり 時々 晴れ"]
のように配列形式となっており、それぞれが時系列部の1番目、2番目、3番目の日の予報であることを示しています
天気予報テロップ番号は、天気予報を3桁の番号で表したもので、天気のマークを判定する際などに使われています。
一覧表は気象庁防災情報XML技術資料の電文毎の解説資料のうち、府県天気予報・府県週間天気予報の解説資料付録に記載されています
降水確率
| 要素名 | 説明 |
|---|---|
| 0.timeSeries.1.timeDefines | 時系列部 |
| 0.timeSeries.1.areas | 予報部 |
| 0.timeSeries.1.areas.{i}.area | 地域名・コード |
| 0.timeSeries.1.areas.{i}.pops | 降水確率 |
timeDefinesとareasに分かれているのは天気予報と同様ですが、timeDefinesは
["2026-03-19T12:00:00+09:00","2026-03-19T18:00:00+09:00","2026-03-20T00:00:00+09:00",…]
のように6時間刻みになっています
降水確率を表すpops部は
["20","50","20",…]
のように配列になっており、それぞれ、timeDefinesの1番目の時刻から6時間の降水確率、2番目の時刻から6時間の降水確率…を表します
気温
| 要素名 | 説明 |
|---|---|
| 0.timeSeries.2.timeDefines | 時系列部 |
| 0.timeSeries.2.areas | 予報部 |
| 0.timeSeries.2.areas.{i}.area | 地域名・コード |
| 0.timeSeries.2.areas.{i}.temps | 気温 |
timeDefinesとareasに分かれているのは天気予報と同様ですが、areaは地域ではなく地点となっているのが特徴です。また、timeDefinesは日中(5時~16時)と夜(17時~23時)で形式が異なり、日中の予報では
["2026-03-19T09:00:00+09:00", "2026-03-19T00:00:00+09:00", "2026-03-20T00:00:00+09:00", "2026-03-20T09:00:00+09:00"]
のようになっており、それぞれ きょう日中(9時~18時)の最高気温、きょう全日の最高気温、あす朝(0時~9時)の最低気温、あす日中の最高気温を表します
夜の予報では
["2026-03-20T00:00:00+09:00", "2026-03-20T09:00:00+09:00"]
のようになっており、それぞれ あす朝の最低気温、あす日中の最高気温を表します
週間予報部分
天気予報と似たような構造となっているため、特徴的な部分を中心に示します
| 要素名 | 説明 |
|---|---|
| 1.timeSeries.0. | 領域予報(天気・降水確率) |
| 1.timeSeries.0.areas.{i}.weatherCodes | 天気予報テロップ番号 |
| 1.timeSeries.0.areas.{i}.pops | 降水確率 |
| 1.timeSeries.0.areas.{i}.reliabilities | 信頼度 |
信頼度は、予報に雨の表現が付くか付かないかが今後変わる可能性をA~Cの3段階で表したもので、Aが信頼度が高いことを表します
| 要素名 | 説明 |
|---|---|
| 1.timeSeries.1.areas.{i}.tempsMin | 最低気温 |
| 1.timeSeries.1.areas.{i}.tempsMinUpper | 最低気温(上限) |
| 1.timeSeries.1.areas.{i}.tempsMinLower | 最低気温(下限) |
| 1.timeSeries.1.areas.{i}.tempsMax | 最高気温 |
| 1.timeSeries.1.areas.{i}.tempsMaxUpper | 最高気温(上限) |
| 1.timeSeries.1.areas.{i}.tempsMaxLower | 最高気温(下限) |
週間予報では最低気温、最高気温は幅を持って予報されており、上限と下限のデータが含まれています
| 要素名 | 説明 |
|---|---|
| 1.tempAverage.areas.{i}.min | 最低気温の平年値(4日後の値) |
| 1.precipAverage.areas.{i}.max | 最高気温の平年値(4日後の値) |
| 1.precipAverage.areas.{i}.min | 降水量の平年値の下限(7日間の合計) |
| 1.precipAverage.areas.{i}.max | 降水量の平年値の上限(7日間の合計) |
表示用のコード
ここまでの内容を表に起こしていきます。
天気概況と違って色々とこだわりようはあると思いますので、あくまで参考としてお考えください
天気画像を表示するための対応表を用意しておきます(気象庁ホームページ内のソースコードから拝借してきました)(週間予報の文字情報表示にも使います)
const weatherIcons = {"100":["100.svg","500.svg","100","晴","CLEAR"],"101":["101.svg","501.svg","100","晴時々曇","PARTLY CLOUDY"],…
おそらく、[昼の天気画像のファイル名、夜の天気画像のファイル名、地図表示する際の塗りつぶしの色、予報表現(日本語)、予報表現(英語)]だと思われます
都道府県の選択肢を作成します。例によって気象庁ホームページの地域情報ファイルから都道府県名を取得します。
天気概況と同様、帯広測候所と名瀬測候所は予報を発表しないため、読み飛ばす処理を加えます
function getGlobals(){
fetch("https://www.jma.go.jp/bosai/common/const/area.json")
.then((response) => response.json())
.then((response) => {
areas = response;
makeOfficeSelect();
});
}
function makeOfficeSelect(){
let officeSelect = "";
let officeCodes = Object.keys(areas['offices']);
officeCodes.sort((a,b)=>{ return a-b;});
for( let officeCode of officeCodes){
let name = areas['offices'][officeCode]['name'];
if( officeCode=="014030" || officeCode=="460040"){
continue;
}else if( officeCode=="014100"){
name = "釧路・根室・十勝地方";
}else if( officeCode=="460100"){
name = "鹿児島県";
}
officeSelect += "<option value='" + officeCode + "'>" + name + "</option>";
}
document.getElementById("officeSelect").innerHTML = officeSelect;
get();
}
予報時刻を表示する部分は、天気予報/降水確率/気温それぞれで時間の刻みが異なるため、分岐する処理を加えます
for( let j=0; j<timeDefines.length; j++){
let timeDefine = Temporal.ZonedDateTime.from( timeDefines[j]+"[Asia/Tokyo]");
if( i==0){
out += "<th>" + dateFormat(timeDefine,'D日') + "</th>";
}else if( i==1){
out += "<th>" + dateFormat(timeDefine,'D日h時~') + "</th>";
}else if( i==2){
const tempTitles = {"2":["朝の最低","日中の最高"],"4":["日中の最高","の最高","朝の最低","日中の最高"]};
out += "<th>" + dateFormat(timeDefine,'D日') + tempTitles[timeDefines.length][j] + "</th>";
}
}
天気アイコンは気象庁ホームページから拝借し、予報発表時刻が17時以降かつ当日の予報では夜用のアイコンに読み替えるようにします
if( reportDatetime.hour>=17 && j==0){
weatherIcon = weatherIcons[weatherCode][1];
}else{
weatherIcon = weatherIcons[weatherCode][0];
}
out += "<td><img src='https://www.jma.go.jp/bosai/forecast/img/" + weatherIcon + "'><br>";
表示ページ全体のソースコード
2026/03/31 Safariでも動くよう、Temporal APIに替えてDate APIを使用した版を追加しました(サンプルページのみ)
サンプルページ
サンプルページ(Date APIを使用した版)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>天気予報</title>
<style>
*{ font-family:sans-serif;}
table, tr, td, th{ border-collapse:collapse; text-align:center;}
th,td{ padding:2px 8px; border-style:solid; border-width:1px; border-color:#d8d8db;}
th{ background-color:#f1f1f4;}
</style>
</head>
<body>
<h1>天気予報</h1>
<div id="menu">
<select id="officeSelect"></select>
<button id="update">表示</button>
</div>
<div id="out"></div>
<script>
"use strict";
let areas = {};
const weatherIcons = {"100":["100.svg","500.svg","100","晴"],"101":["101.svg","501.svg","100","晴時々曇"],"102":["102.svg","502.svg","300","晴一時雨"],"103":["102.svg","502.svg","300","晴時々雨"],"104":["104.svg","504.svg","400","晴一時雪"],"105":["104.svg","504.svg","400","晴時々雪"],"106":["102.svg","502.svg","300","晴一時雨か雪"],"107":["102.svg","502.svg","300","晴時々雨か雪"],"110":["110.svg","510.svg","100","晴後時々曇"],"111":["110.svg","510.svg","100","晴後曇"],"112":["112.svg","512.svg","300","晴後一時雨"],"113":["112.svg","512.svg","300","晴後時々雨"],"114":["112.svg","512.svg","300","晴後雨"],"115":["115.svg","515.svg","400","晴後一時雪"],"116":["115.svg","515.svg","400","晴後時々雪"],"117":["115.svg","515.svg","400","晴後雪"],"118":["112.svg","512.svg","300","晴後雨か雪"],"160":["104.svg","504.svg","400","晴一時雪か雨"],"170":["104.svg","504.svg","400","晴時々雪か雨"],"181":["115.svg","515.svg","400","晴後雪か雨"],"200":["200.svg","200.svg","200","曇"],"201":["201.svg","601.svg","200","曇時々晴"],"202":["202.svg","202.svg","300","曇一時雨"],"203":["202.svg","202.svg","300","曇時々雨"],"204":["204.svg","204.svg","400","曇一時雪"],"205":["204.svg","204.svg","400","曇時々雪"],"206":["202.svg","202.svg","300","曇一時雨か雪"],"207":["202.svg","202.svg","300","曇時々雨か雪"],"210":["210.svg","610.svg","200","曇後時々晴"],"211":["210.svg","610.svg","200","曇後晴"],"212":["212.svg","212.svg","300","曇後一時雨"],"213":["212.svg","212.svg","300","曇後時々雨"],"214":["212.svg","212.svg","300","曇後雨"],"215":["215.svg","215.svg","400","曇後一時雪"],"216":["215.svg","215.svg","400","曇後時々雪"],"217":["215.svg","215.svg","400","曇後雪"],"218":["212.svg","212.svg","300","曇後雨か雪"],"260":["204.svg","204.svg","400","曇一時雪か雨"],"270":["204.svg","204.svg","400","曇時々雪か雨"],"281":["215.svg","215.svg","400","曇後雪か雨"],"300":["300.svg","300.svg","300","雨"],"301":["301.svg","701.svg","300","雨時々晴"],"302":["302.svg","302.svg","300","雨時々止む"],"303":["303.svg","303.svg","400","雨時々雪"],"304":["300.svg","300.svg","300","雨か雪"],"308":["308.svg","308.svg","300","雨で暴風を伴う"],"309":["303.svg","303.svg","400","雨一時雪"],"311":["311.svg","711.svg","300","雨後晴"],"313":["313.svg","313.svg","300","雨後曇"],"314":["314.svg","314.svg","400","雨後時々雪"],"315":["314.svg","314.svg","400","雨後雪"],"316":["311.svg","711.svg","300","雨か雪後晴"],"317":["313.svg","313.svg","300","雨か雪後曇"],"340":["400.svg","400.svg","400","雪か雨"],"361":["411.svg","811.svg","400","雪か雨後晴"],"371":["413.svg","413.svg","400","雪か雨後曇"],"400":["400.svg","400.svg","400","雪"],"401":["401.svg","801.svg","400","雪時々晴"],"402":["402.svg","402.svg","400","雪時々止む"],"403":["403.svg","403.svg","400","雪時々雨"],"406":["406.svg","406.svg","400","風雪強い"],"407":["406.svg","406.svg","400","暴風雪"],"409":["403.svg","403.svg","400","雪一時雨"],"411":["411.svg","811.svg","400","雪後晴"],"413":["413.svg","413.svg","400","雪後曇"],"414":["414.svg","414.svg","400","雪後雨"]};
getGlobals();
function getGlobals(){
fetch("https://www.jma.go.jp/bosai/common/const/area.json")
.then((response) => response.json())
.then((response) => {
areas = response;
makeOfficeSelect();
});
}
function makeOfficeSelect(){
let officeSelect = "";
let officeCodes = Object.keys(areas['offices']);
officeCodes.sort((a,b)=>{ return a-b;});
for( let officeCode of officeCodes){
let name = areas['offices'][officeCode]['name'];
if( officeCode=="014030" || officeCode=="460040"){
continue;
}else if( officeCode=="014100"){
name = "釧路・根室・十勝地方";
}else if( officeCode=="460100"){
name = "鹿児島県";
}
officeSelect += "<option value='" + officeCode + "'>" + name + "</option>";
}
document.getElementById("officeSelect").innerHTML = officeSelect;
get();
}
document.getElementById("update").addEventListener("click",function(e){
get();
});
function get(){
let officeCode = document.getElementById("officeSelect").value;
fetch("https://www.jma.go.jp/bosai/forecast/data/forecast/" + officeCode + ".json")
.then((response) => response.json())
.then((fcst) => {
display( fcst);
});
}
function display( fcst){
let out = "";
out += "<h2>予報</h2>";
let reportDatetime = Temporal.ZonedDateTime.from( fcst[0]['reportDatetime']+"[Asia/Tokyo]");
out += "<p>";
out += dateFormat( reportDatetime, 'y/m/d h時 ');
out += fcst[0]['publishingOffice'] + "発表";
out += "</p>";
for( let i=0; i<fcst[0]['timeSeries'].length; i++){
let timeDefines = fcst[0]['timeSeries'][i]['timeDefines'];
const infoTypes = ["天気","降水確率","気温"];
out += "<p><table>";
out += "<tr><th>" + infoTypes[i] + "</th>";
for( let j=0; j<timeDefines.length; j++){
let timeDefine = Temporal.ZonedDateTime.from( timeDefines[j]+"[Asia/Tokyo]");
if( i==0){
out += "<th>" + dateFormat(timeDefine,'D日(w)') + "</th>";
}else if( i==1){
out += "<th>" + dateFormat(timeDefine,'D日h時~') + "</th>";
}else if( i==2){
const tempTitles = {"2":["朝の最低","日中の最高"],"4":["日中の最高","の最高","朝の最低","日中の最高"]};
out += "<th>" + dateFormat(timeDefine,'D日') + tempTitles[timeDefines.length][j] + "</th>";
}
}
out += "</tr>";
for( let area of fcst[0]['timeSeries'][i]['areas']){
let areaName = area['area']['name'];
out += "<tr><th>" + areaName + "</th>";
for( let j=0; j<timeDefines.length; j++){
if( i==0){
let weatherCode = area['weatherCodes'][j], weatherIcon;
if( reportDatetime.hour>=17 && j==0){
weatherIcon = weatherIcons[weatherCode][1];
}else{
weatherIcon = weatherIcons[weatherCode][0];
}
out += "<td><img src='https://www.jma.go.jp/bosai/forecast/img/" + weatherIcon + "'><br>";
out += area['weathers'][j].replace(/ 後 /g," のち ").replace(/ 所により/g,"<br>所により").replace(/ を/g,"を").replace(/ で/g,"で").replace(/ から/g,"から").replace(/ まで/g,"まで") + "</td>";
}else if( i==1){
out += "<td>" + area['pops'][j] + "</td>";
}else if( i==2){
out += "<td>" + area['temps'][j] + "</td>";
}
}
out += "</tr>"
}
out += "</table></p>";
}
out += "<h2>週間予報</h2>";
let reportDatetimeWeek = Temporal.ZonedDateTime.from( fcst[1]['reportDatetime']+"[Asia/Tokyo]");
out += "<p>";
out += dateFormat( reportDatetimeWeek, 'y/m/d h時 ');
out += fcst[1]['publishingOffice'] + "発表";
out += "</p>";
for( let i=0; i<fcst[1]['timeSeries'].length; i++){
let timeDefines = fcst[1]['timeSeries'][i]['timeDefines'];
const infoTypes = ["天気","気温"];
out += "<p><table>";
out += "<tr><th>" + infoTypes[i] + "</th>";
for( let j=0; j<timeDefines.length; j++){
let timeDefine = Temporal.ZonedDateTime.from( timeDefines[j]+"[Asia/Tokyo]");
out += "<th>" + dateFormat(timeDefine,'D日(w)') + "</th>";
}
out += "</tr>";
for( let area of fcst[1]['timeSeries'][i]['areas']){
let areaName = area['area']['name'];
out += "<tr><th>" + areaName + "</th>";
for( let j=0; j<timeDefines.length; j++){
if( i==0){
let weatherCode = area['weatherCodes'][j], weatherIcon;
weatherIcon = weatherIcons[weatherCode][0];
out += "<td><img src='https://www.jma.go.jp/bosai/forecast/img/" + weatherIcon + "'><br>";
out += weatherIcons[weatherCode][3].replace(/晴/g,"晴れ").replace(/曇/g,"くもり").replace(/後/g,"のち") + "<br>";
out += area['pops'][j] + "<br>";
out += area['reliabilities'][j] + "</td>";
}else if( i==1){
out += "<td>";
out += "最低 " + area['tempsMin'][j] + "(" + area['tempsMinLower'][j] + "~" + area['tempsMinUpper'][j] + ")<br>";
out += "最高 " + area['tempsMax'][j] + "(" + area['tempsMaxLower'][j] + "~" + area['tempsMaxUpper'][j] + ")";
out += "</td>";
}
}
out += "</tr>"
}
out += "</table></p>";
}
document.getElementById("out").innerHTML = out;
}
function dateFormat( t, f='y-m-dTh:n:s', is24=false){
const j=["","月","火","水","木","金","土","日"];const e=["","Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
if(is24&&t.hour==0){t.substract({days:1});f=f.replace(/h/g,"24").replace(/H/g,24);}
const y=t.year,m=t.month,d=t.day,h=t.hour,n=t.minute,s=t.second,w=t.dayOfWeek;
f=f.replace(/y/g,("000"+y).slice(-4)).replace(/Y/g,y).replace(/m/g,("0"+m).slice(-2)).replace(/M/g,m).replace(/d/g,("0"+d).slice(-2)).replace(/D/g,d);
f=f.replace(/w/g,j[w]).replace(/W/g,e[w]);
f=f.replace(/h/g,("0"+h).slice(-2)).replace(/H/g,h).replace(/n/g,("0"+n).slice(-2)).replace(/N/g,n).replace(/s/g,("0"+s).slice(-2)).replace(/S/g,s);
return f;
}
</script>
</body>
</html>
愚痴
apiの構造を記事で表現するの難しいですね……