🍣

過去の気象データをCSVファイルとしてダウンロードし保存する

に公開

概要

pytestを使って過去の(地点)気象データ・ダウンロードからCSVファイルを取得する。
https://www.data.jma.go.jp/risk/obsdl/index.php

前提

  • python3.12以降(dataclassが使えれば同じ挙動になるので、python3.7以降なら動くと思われる)
  • pytestをインストール済み

CSVファイルの出力内容について

公式ページに書いてある
https://www.data.jma.go.jp/risk/obsdl/top/help3

指定するパラメータについて

画面操作時にブラウザの開発者ツールからHTTP要求として参照できる
地点番号は気象庁で共通のようなので、(名前が似ているが別ページの)過去の気象データ検索のURLから拾うと良い。次のURLは地点として「八戸」を選んだ場合で、「block_no」から47581を読む。
https://www.data.jma.go.jp/stats/etrn/select/prefecture.php?prec_no=31&block_no=&year=&month=&day=&view=

CSVダウンロードのコード

pytest設定

pytest.ini
[pytest]
# テストファイルのパターン
python_files = test_*.py
python_classes = Test*
python_functions = test_*

# 詳細な出力
addopts = -v --tb=short

本体

HTTPのステータスコードを確認しつつダウンロードする想定により、pytestのテストケースとして作成した。

  • session情報を取得するために一度index.phpページを表示する
  • データ取得urlに対して、JSONでパラメータを渡す
  • うまくパラメータ指定できるとCSVを取得できるので保存する
test_download_csv.py
import pytest
import json
import requests
import time
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import List, Any, Tuple

ROOT_URL = 'https://www.data.jma.go.jp/risk/obsdl/index.php'
SHOW_URL = 'https://www.data.jma.go.jp/risk/obsdl/show/table'


@dataclass
class WeatherDataPayload:
    """気象データダウンロード用のペイロード"""
    stationNumList: List[str] = field(default_factory=lambda: ["s47581"]) # s47581:八戸
    aggrgPeriod: int = 1 # 1:日別 2:(暦日)半旬別 4:旬別 5:月別 6:3か月別 812:2日別(813:3日別を3日おき出力, 823:3日別を毎日連続出力, ...28日別まで同様) 9:時別
    elementNumList: List[List[str]] = field(default_factory=lambda: [["201", ""], ["101", ""]])
    interAnnualFlag: int = 1   # 1:連続期間 2:年毎の同時期(特定の月日を複数年分)
    ymdList: List[str] = field(default_factory=lambda: ["2022", "2022", "9", "10", "30", "1"]) # 2022/9/30-2022/10/1
    optionNumList: List[Any] = field(default_factory=list)
    downloadFlag: str = 'true' # CSVファイルとしてダウンロードする
    rmkFlag: int = 1           # 品質情報を含める
    disconnectFlag: int = 1    # 統計切断情報を含める
    youbiFlag: int = 0         # 1:曜日を含める
    fukenFlag: int = 0         # 1:府県を含める
    kijiFlag: int = 0          # 1:発生時刻(起時)を含める
    huukouFlag: int = 0        # 1:風向を含める
    csvFlag: int = 1           # 1:CSV形式(カンマ区切り)にする
    jikantaiFlag: int = 0      # 1:時別について時間帯を限定する
    jikantaiList: List[Any] = field(default_factory=list) # ["2", "5"] で2時〜5時
    ymdLiteral: int = 1        # 1:日付をyyyy/mm/dd形式にする

    def to_post_data(self) -> dict:
        """POSTリクエスト用のデータに変換"""
        data = {}
        for key, value in asdict(self).items():
            if isinstance(value, list):
                data[key] = json.dumps(value)
            else:
                data[key] = value
        return data


@pytest.fixture(scope="session")
def output_dir():
    """
    CSV保存用ディレクトリを作成(全体で1度だけ実行)
    """
    output_path = Path("csv_output")
    output_path.mkdir(exist_ok=True)

    print(f"\n✓ Created output directory: {output_path.absolute()}")

    return output_path


@pytest.mark.parametrize("payload,filename", [
    pytest.param(
        WeatherDataPayload(),
        "daily_s47581.csv",
        id="daily_station"
    ),
    pytest.param(
        WeatherDataPayload(
            aggrgPeriod=2,
            ymdList=["2022", "2022", "10", "11", "6", "1"] # 2022/10 第6半旬 - 2022/11 第1半旬
        ),
        "mb5_s47581.csv",
        id="mb5_station" # montly based 5days
    ),
    pytest.param(
        WeatherDataPayload(
            aggrgPeriod=4,
            ymdList=["2022", "2022", "10", "11", "3", "1"] # 2022/10 下旬(第3旬) - 2022/11 上旬(第1旬)
        ),
        "mb10_s47581.csv",
        id="mb10_station" # montly based 10days
    ),
    pytest.param(
        WeatherDataPayload(
            aggrgPeriod=5,
            ymdList=["2022", "2022", "10", "11", "", ""] # 2022/10-2022/11
        ),
        "monthly_s47581.csv",
        id="monthly_station"
    ),
    pytest.param(
        WeatherDataPayload(
            aggrgPeriod=6,
            ymdList=["2022", "2022", "10", "11", "", ""] # 2022/10-2022/11
        ),
        "3month_s47581.csv",
        id="3month_station"
    ),
    pytest.param(
        WeatherDataPayload(
            aggrgPeriod=812,
            ymdList=["2022", "2022", "10", "11", "30", "1"] # 2022/10/30-2022/11/1
        ),
        "2days_s47581.csv",
        id="2days_station"
    ),
    pytest.param(
        WeatherDataPayload(aggrgPeriod=9),
        "hourly_s47581.csv",
        id="hourly_station"
    ),
    pytest.param(
        WeatherDataPayload(
            stationNumList=["a0179"], # a0179:三戸
            aggrgPeriod=1,
            ymdList=["2022", "2022", "10", "10", "30", "31"]
        ),
        "daily_a0179.csv",
        id="daily_amedas"
    ),
    pytest.param(
        WeatherDataPayload(
            stationNumList=["s47581"],
            aggrgPeriod=1,
            elementNumList=[["201", ""], ["101", ""], ["301", ""]],
            ymdList=["2022", "2022", "10", "10", "10", "31"]
        ),
        "daily_multiple_elements.csv",
        id="daily_multiple_elements"
    ),
])
def test_download_csv(output_dir, payload, filename):
    """CSVダウンロード共通のテスト関数"""
    session = requests.Session()
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    })

    top = session.get(ROOT_URL)
    time.sleep(2)
    assert top.status_code == 200

    csv = session.post(
        SHOW_URL,
        data=payload.to_post_data(),
        headers={'Referer': ROOT_URL}
    )
    time.sleep(2)
    assert csv.status_code == 200
    assert len(csv.content) > 0

    output_file = output_dir / filename
    with open(output_file, 'wb') as f:
        f.write(csv.content)

    print(f"\n✓ CSV saved: {filename} ({len(csv.content)} bytes)")

動かし方

pytest test_download_csv.py

動かすとoutput_dirフィクスチャに従ってcsv_outputディレクトリを作成し、ダウンロードファイルを保存する。

実行例
% pytest test_basic.py 
====================================================================================================================== test session starts ======================================================================================================================
platform darwin -- Python 3.12.9, pytest-8.4.0, pluggy-1.5.0 -- /opt/homebrew/Caskroom/miniforge/base/bin/python
cachedir: .pytest_cache
rootdir: /Users/issei/programing/risk
configfile: pytest.ini
collected 9 items                                                                                                                                                                                                                                               

test_basic.py::test_download_csv[daily_station] PASSED                                                                                                                                                                                                    [ 11%]
test_basic.py::test_download_csv[mb5_station] PASSED                                                                                                                                                                                                      [ 22%]
test_basic.py::test_download_csv[mb10_station] PASSED                                                                                                                                                                                                     [ 33%]
test_basic.py::test_download_csv[monthly_station] PASSED                                                                                                                                                                                                  [ 44%]
test_basic.py::test_download_csv[3month_station] PASSED                                                                                                                                                                                                   [ 55%]
test_basic.py::test_download_csv[2days_station] PASSED                                                                                                                                                                                                    [ 66%]
test_basic.py::test_download_csv[hourly_station] PASSED                                                                                                                                                                                                   [ 77%]
test_basic.py::test_download_csv[daily_amedas] PASSED                                                                                                                                                                                                     [ 88%]
test_basic.py::test_download_csv[daily_multiple_elements] PASSED                                                                                                                                                                                          [100%]

====================================================================================================================== 9 passed in 39.18s =======================================================================================================================

留意事項

言うまでもなく、提供元サーバへ過度の負荷を掛けることは望ましく有りません。
この実装では、sleepを用いてダウンロード要求の頻度を調整しています。

Discussion