2022/11/12 地震による電気火災対策「感震ブレーカー」を投稿しました

日々の安心した生活は防災から!
「もしも」に備えるための防災グッズやアプリ、お役立ち情報などを発信中!

Pythonで火山カメラ画像をキャプチャする(4)

副産物

こんにちは、管理人のアカツキです。プログラミング言語Pythonを使用して作成した火山カメラキャプチャについて、その処理部分の解説を行っています。今まで投稿した記事はこちらです。

第四回目は引き続きブロック3の解説です。while文を使用した無限ループの構成を考えていきます。

スポンサーリンク

(再掲)火山カメラ・キャプチャ コード

まずは火山キャプチャの全体コードおよびその機能について再掲します。

"""
JMA火山監視カメラキャプチャ Ver.1.01
・JMAの火山監視カメラ画像をキャプチャする
・連続キャプチャを実施する(重複はスキップ)
・火山名をリストから選択する
・最初のリストはよくアクセスすると思われる火山
・火山リストをアップデート(20220727更新分)
"""
# ブロック1
from selenium import webdriver
from selenium.webdriver.chrome import service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
import os
import requests
import time

# ブロック2
dir_path = "(適当なディレクトリ)/volcano_camera/"

fav_volcano = {1:'十勝岳 白金模範牧場',
               2:'阿蘇山 草千里',
               3:'桜島 牛根',
               4:'諏訪之瀬島 寄木',
               5:'その他'}

area_volcano = {1:'北海道',
                2:'東北地方',
                3:'関東・中部地方',
                4:'伊豆・小笠原諸島',
                5:'九州地方',
                6:'戻る'}

hokkaido_volcano = {1: 'アトサヌプリ 北東山麓', 2: 'アトサヌプリ 硫黄山駐車場北',
                    3: 'アトサヌプリ 屈斜路湖南', 4: '雌阿寒岳 上徹別',
                    5: '雌阿寒岳 阿寒富士北', 6: '大雪山 忠別湖東',
                    7: '大雪山 旭岳姿見2', 8: '十勝岳 白金模範牧場',
                    9: '十勝岳 避難小屋南東', 10: '樽前山 別々川',
                    11: '倶多楽 414m山', 12: '倶多楽 地獄谷',
                    13: '有珠山 東湖畔', 14: '有珠山 月浦',
                    15: '北海道駒ヶ岳 鹿部公園南東', 16: '北海道駒ヶ岳 赤井川',
                    17: '北海道駒ヶ岳 剣ヶ峯', 18: '恵山 高岱',
                    19: '恵山 火口原', 20: '戻る'}

tohoku_volcano = {1: '岩木山 百沢東', 2: '八甲田山 大川原', 3: '八甲田山 地獄沼',
                  4: '十和田 銀山', 5: '秋田焼山 栂森', 6: '岩手山 柏台',
                  7: '岩手山 黒倉山', 8: '鳥海山 上郷2', 9: '栗駒山 大柳',
                  10: '栗駒山 展望岩頭', 11: '蔵王山 遠刈田温泉', 12: '蔵王山 上山金谷',
                  13: '蔵王山 刈田岳', 14: '蔵王山 御釜北', 15: '吾妻山 上野寺',
                  16: '安達太良山 若宮', 17: '安達太良山 鉄山', 18: '磐梯山 剣ケ峯',
                  19: '磐梯山 櫛ヶ峰', 20: '戻る'}

kanto_tyubu_volcano = {1: '那須岳 湯本ツムジケ平', 2: '那須岳 日の出平北', 3: '日光白根山 歌ヶ浜',
                       4: '日光白根山 上小川', 5: '草津白根山 奥山田', 6: '草津白根山 逢ノ峰山頂',
                       7: '草津白根山 草津', 8: '浅間山 鬼押', 9: '浅間山 追分',
                       10: '新潟焼山 宇棚', 11: '弥陀ヶ原 芦峅', 12: '焼岳 中尾峠',
                       13: '乗鞍岳 乗鞍高原', 14: '御嶽山 三岳黒沢', 15: '御嶽山 奥の院',
                       16: '白山 白峰', 17: '富士山 萩原', 18: '箱根山 宮城野',
                       19: '箱根山 大涌谷', 20: '箱根山 箱根峠', 21: '伊豆東部火山群 大原',
                       22: '伊豆東部火山群 大崎', 23: '戻る'}

izu_ogasawara_volcano = {1: '伊豆大島 北西外輪', 2: '伊豆大島 中央火孔北', 3: '新島 式根',
                         4: '神津島 前浜南東', 5: '三宅島 坪田', 6: '三宅島 神着',
                         7: '三宅島 山頂火口北西', 8: '八丈島 楊梅ヶ原', 9: '青ヶ島 手取山', 
                         10: '戻る'}

kyusyu_volcano = {1: '鶴見岳・伽藍岳 塚原無田', 2: '九重山 上野', 3: '九重山 星生山北尾根',
                  4: '九重山 飯田大原', 5: '阿蘇山 草千里', 6: '阿蘇山 車帰',
                  7: '阿蘇山 南阿蘇村', 8: '雲仙岳 野岳', 9: '雲仙岳 垂木台地南',
                  10: '霧島山 猪子石(新燃岳)', 11: '霧島山 猪子石(御鉢)', 12: '霧島山 御鉢火口南縁',
                  13: '霧島山 高原西麓', 14: '霧島山 八久保', 15: '霧島山 韓国岳',
                  16: '霧島山 えびの高原', 17: '霧島山 硫黄山南', 18: '桜島 牛根',
                  19: '桜島 東郡元', 20: '桜島 垂水荒崎', 21: '桜島 中央港新町',
                  22: '薩摩硫黄島 岩ノ上', 23: '口永良部島 本村西', 24: '口永良部島 屋久島吉田',
                  25: '諏訪之瀬島 寄木', 26: '諏訪之瀬島 キャンプ場', 27:'戻る'}

area_volcano2 = {'北海道':hokkaido_volcano,
                '東北地方':tohoku_volcano,
                '関東・中部地方':kanto_tyubu_volcano,
                '伊豆・小笠原諸島':izu_ogasawara_volcano,
                '九州地方':kyusyu_volcano}

# ブロック3
# メニューを行き来するためのフラグを設定
Flag = True
while Flag:
    for vkey1, vvalue1 in fav_volcano.items():
        print(f"{vkey1} : {vvalue1}")
    
    select_vkey1 = input("火山を選択してください [No.?] >>> ")
    select_vkey1 = int(select_vkey1)
    select_vvalue1 = fav_volcano[select_vkey1]
    
    # 1~6を選んだ場合の処理→値を取得してループ離脱
    if not select_vvalue1 == "その他":
        select_volcano = select_vvalue1
        break
    
    elif select_vvalue1 == "その他":
        
        # もう一つwhileを入れる
        while Flag:
            for vkey2, vvalue2 in area_volcano.items():
                print(f"{vkey2} : {vvalue2}")

            select_vkey2 = input("地域を選択してください [No.?] >>> ")
            select_vkey2 = int(select_vkey2)
            select_vvalue2 = area_volcano[select_vkey2]
            
            if select_vvalue2 == "戻る":
                # 再度前の画面に戻る
                break

            elif select_vvalue2 != "戻る":
                for vkey3,vvalue3 in area_volcano2[select_vvalue2].items():
                    print(f"{vkey3} : {vvalue3}")
             
                select_vkey3 = input("火山を選択してください [No.?] >>> ")
                select_vkey3 = int(select_vkey3)
                select_vvalue3 = area_volcano2[select_vvalue2][select_vkey3]
                
                if not select_vvalue3 == "戻る":
                    # 火山名を取得してループを抜ける
                    select_volcano = select_vvalue3
                    Flag = False

                elif select_vvalue3 == "戻る":
                    # 地域選択画面に戻る
                    pass

print(select_volcano,"を選びました")
print(select_volcano,"にアクセスします")

# ブロック4
chrome_driver = "(適当なディレクトリ)/chromedriver.exe"
chrome_service = service.Service(executable_path=chrome_driver)
driver = webdriver.Chrome(service=chrome_service)
actions = ActionChains(driver)
wait = WebDriverWait(driver=driver, timeout=10)

driver.implicitly_wait(5)
driver.set_window_position(50,50)
driver.set_window_size(1400,1300)

# JMAの監視カメラ画像のページに直接アクセスする
driver.get("https://www.data.jma.go.jp/vois/data/tokyo/volcam/volcam.php")
# wait.until(EC.presence_of_all_elements_located)

# 選択された火山カメラを開く
volcam = driver.find_element(By.LINK_TEXT,select_volcano)
volcam.click()

# wait.until(EC.presence_of_all_elements_located)

# スライダを左端に移動
driver.find_element(By.CSS_SELECTOR,"img[src='./icon/player/oldest.png']").click()

# 早送りボタンを取得
forward = driver.find_element(By.CSS_SELECTOR,"img[src='./icon/player/next-frame.png']")

# ブロック5
# 画像を保存する(繰り返し)
# 撮影枚数を取得
img_num = driver.find_element(By.XPATH,"//*[@id='main']/form/table[2]/tbody/tr[2]/td/table/tbody/tr[3]/td[5]/input[3]")
img_num = img_num.get_attribute("value")
time.sleep(1)

for i in range(int(img_num)):
    img_tag = driver.find_element(By.NAME,"myImg")
    img_url = img_tag.get_attribute("src")
    
    img = requests.get(img_url)
    file_name = os.path.basename(img_url)
    
    # 保存ディレクトリの作成
    # 取得した火山名を使う
    save_dir = os.path.join(dir_path,select_volcano)
    
    # if not文で判断→フォルダがない→False→if notはTrue→フォルダを作る
    if not os.path.isdir(save_dir):
        os.makedirs(save_dir)
    
    # 撮影の重複をチェック
    check_file = os.path.join(save_dir,file_name)
    
    # if notで判定する
    # 条件が真ならif notは偽を返す
    # 条件が偽ならif notは真を返す
    # ファイルがない=撮影していない=if notがTrue=Trueの処理=キャプチャする
    if not os.path.isfile(check_file):
        with open(check_file,"wb") as f:
            f.write(img.content)
    
    # 次の時間に移動
    forward.click()
    time.sleep(1)

driver.quit()
print("終わりました")

プログラムの機能は次のようになっています。

JMA火山監視カメラキャプチャ Ver.1.01(volcano_capture.py)

  • 気象庁(JMA)が提供している火山監視カメラ画像について、
    任意の火山画像をキャプチャ(保存)する
  • キャプチャする火山名はリストから選択する(番号を入力して指定)
  • 画像を保存するフォルダ・ファイル名は自動で生成する
  • 火山リストを更新(2022.7.27 カメラ追加のプレスリリース)

全体コードにはその役割ごとにブロック番号を付けています。

ブロック一覧とその役割

  1. プログラムで使用するモジュールの呼び出し
  2. ファイル保存や火山名の選択に使う変数やデータ
  3. 火山選択メニュー
  4. 選択された火山カメラをブラウザで読み込む
  5. キャプチャした画像の保存処理

今回はブロック3について続きの解説となります。

ブロック3・火山選択メニュー

# ブロック3
# メニューを行き来するためのフラグを設定
Flag = True
while Flag:
    for vkey1, vvalue1 in fav_volcano.items():
        print(f"{vkey1} : {vvalue1}")
    
    select_vkey1 = input("火山を選択してください [No.?] >>> ")
    select_vkey1 = int(select_vkey1)
    select_vvalue1 = fav_volcano[select_vkey1]
    
    # 1~6を選んだ場合の処理→値を取得してループ離脱
    if not select_vvalue1 == "その他":
        select_volcano = select_vvalue1
        break
    
    elif select_vvalue1 == "その他":
        
        # もう一つwhileを入れる
        while Flag:
            for vkey2, vvalue2 in area_volcano.items():
                print(f"{vkey2} : {vvalue2}")

            select_vkey2 = input("地域を選択してください [No.?] >>> ")
            select_vkey2 = int(select_vkey2)
            select_vvalue2 = area_volcano[select_vkey2]
            
            if select_vvalue2 == "戻る":
                # 再度前の画面に戻る
                break

            elif select_vvalue2 != "戻る":
                for vkey3,vvalue3 in area_volcano2[select_vvalue2].items():
                    print(f"{vkey3} : {vvalue3}")
             
                select_vkey3 = input("火山を選択してください [No.?] >>> ")
                select_vkey3 = int(select_vkey3)
                select_vvalue3 = area_volcano2[select_vvalue2][select_vkey3]
                
                if not select_vvalue3 == "戻る":
                    # 火山名を取得してループを抜ける
                    select_volcano = select_vvalue3
                    Flag = False

                elif select_vvalue3 == "戻る":
                    # 地域選択画面に戻る
                    pass

print(select_volcano,"を選びました")
print(select_volcano,"にアクセスします")

while文で無限ループを作る

ブロック3の役割は、ユーザーがどの火山カメラにアクセスするのかを決定する処理、つまり火山選択メニューであることは前回お示ししました。そして処理の流れを示すフロー図は次の通りです。

ブロック3のフロー図
ブロック3のフロー図

フロー図の赤線緑線の矢印、これが今回のポイントになります。

どちらの矢印についても、Yes側に沿っていくと前に実施していた処理に向かっています。仮に再び処理を進め、矢印の分岐に辿り着いたとします。

もしそこでYesになるような入力を行った場合、また同じ処理に戻されることが了解されると思います。そして再度分岐処理になるのですが、ここでNoになるような入力が無い限り、またもや処理を戻されることは明らかです。

このように、ある条件を満たさない限りは同じ処理を繰り返すことを無限ループと呼称します。これは火山選択メニューであるというブロック3の役割を考えるとそうならざるを得ないと言えるでしょう。

なぜならここでカメラ画像を見たい火山が決まらないとブロック4以降の処理が何もできないからです。ここで火山名を確定する必要があるために無限ループになっているということです。

その無限ループを実現する構文がwhile文ということになります。

# while文
# ループを作る
while 条件式:
    ループ処理

# 条件式がTrue(真)の場合、ループ内の処理を繰り返す
# 条件式がFalse(偽)になればループを抜けることができる
# ループ処理にbreakを入れると強制的にループを脱出できる

# 絶対に許してもらう無限ループ
# フラグ変数でループを制御
Flag = True 
while Flag:
    answer = input("ゆるしてくれよ!な!な! [いいえ:n、はい:y]")
    if answer == "y":
        Flag = False

print("ありがてえ!あんたのことは わすれねえよ!じゃあな!")

上がその例文になります。while文は最初に設定した条件式が満たされている場合(この状態はTrueになります)にwhileブロック内の処理を繰り返す構文です。Trueの間中ずっと処理を繰り返すのですから無限ループということになります。

ループを脱出する

ではループから抜けられないのか?というとそんなことはありません。条件式が満たされていればループになるのですから、それが満たされない、つまりFalse(偽)になれば抜けられます。

これは言わばルールに則った正当なループ離脱方法です。正当ということはイレギュラーなやり方もあり、それがbreak文です。これを処理ブロックに記述すれば条件式が真であっても強制的にループを脱出します。

breakはイレギュラーなやり方と述べましたが、レギュラーと言えるくらい使う頻度が高い処理だと思います。実際本プログラムでもループ脱出にはほぼこれを使っています。

そして先の例文ではフラグ変数を利用してループ脱出の判定をしています。フラグとはフラッグ(Flag)、つまりのことです。俗に言われるフラグが立つ、〇〇フラグというアレのことですね。条件分岐のための条件を司るものになります。

Flag = True 
while Flag:

このようにwhile文の前にFlagにTrueを代入しておいて、whileの条件に指定してあげます。その後、ループ内処理でループを脱出するに値する条件が満たされたときに

Flag = False

と値を入れ替えます。Flag自体はTrueかFalseを入れる変数ですので、この変更は自由にできます。これを処理ブロックに仕込んでやれば、その処理が終了して再びループを開始する時にwhile条件にFalseが渡され、ループを終えることができるというからくりです。

赤と緑のループを構築する

以上をベースにして、while文がどのように本プログラムに使われているのかを確認していきます。まずはフロー図の赤線部分からです。

図から、赤線の矢印はどこから出ているのかを見てみましょう。すると二セット目の分岐処理から出ていることが分かります。矢印はその後、プログラムの開始部分を指しています。

つまり、プログラムの最初から二番目の条件分岐までをwhileブロックに入れればフローを満足するループになりそうです。

続いて緑線を見てみましょう。こちらは最後の三セット目の分岐処理が矢印の始点になっており、二セット目の処理部に入っています。

緑線については二番目の処理部から最後の条件分岐部分をwhileブロックにすることで記述できそうです。

ところで、緑線にもう一度着目してください。途中で赤線を飛び越えていますよね?そして二番目の処理部に入っていますが、この部分って赤線whileブロックに含まれていませんか?

ということは、whileの中にもう一つのwhileが入っており、これはループとループが混ざり合っているプログラム構造になります。そのような意味でこのブロック3は多重ループになっています(一般的にはループの中に丸ごと別のループが入っている(入れ子、ネスト)ものを多重ループと言うと思います。本プログラムでは各ループが一部重なった形になっていますので、そこまで厳密な意味での多重ループとは言えないかも知れません。ただコード上はwhileの中にwhileが入っていてネストされているように見えています)。

結果として

  • 赤線のループ範囲
    プログラム開始部分から二セット目の分岐処理まで
  • 緑線のループ範囲
    二セット目の処理ブロックから三セット目の条件分岐処理まで

となることが分かりました。それでは実際のコードに当てはめてみましょう。またこの中で、前回後回しにしたif文の処理についてもまとめて述べていきます。

まずwhile文ですが、次のように4行目と20行目に置かれています。

# 3行目:Flag変数(赤、緑両ループに適用)
# 4行目:赤線に対応したwhile文
Flag = True
while Flag:

# 20行目:緑線に対応したwhile文        
        while Flag:

3行目にFlag変数が設定されていますね。これは後で解説しますが、このFlag一つでと緑両方のループの真偽をコントロールしています。

そしてここからが少しややこしい所です。まず1セット目の分岐です(13~17行目)。

# ここのbreakは赤ループを抜けるためのもの
    if not select_vvalue1 == "その他":
        select_volcano = select_vvalue1
        break
    
    elif select_vvalue1 == "その他":

ここはすでに赤ループの範囲内に入っています。ですので、火山名が確定している場合はループを脱出する必要があります。それが最初のifブロックにあるbreakの役割です。

火山名が確定しない場合はelif文以下の処理に進み、先ほど20行目にあったwhileブロックに入っていきます。この時点で緑両方のループに入っていることに注意してください。

続いて2セット目の分岐処理です。28~32行目が該当するコードです。

# ここのbreakは赤ループ離脱ではありません
     if select_vvalue2 == "戻る":
                # 再度前の画面に戻る
                break

            elif select_vvalue2 != "戻る":

最初のifでは、もう一度お気に入りリストを表示する時の処理を示しています。ですのでこれは赤ループを最初からやり直すことになります。

ですが、最後の処理にbreak文が付いています。赤ループをやり直すのに?と思われたかも知れませんが、先ほども申し上げたように、この処理ブロックはすでに両方のループに入っています。ですので、ここのbreak緑のループを脱出するものです。そしてもう一つ重要なことがあります。それは

breakは、それが含まれている直近のwhileブロックを脱出することができる

ということです。そうでなければこのbreakが実行された時点で火山選択メニューが終了し、火山名が取得できずにブロック4の処理が行われ、何らかのエラーが発生することでしょう。

ここの多重ループでは、の中にが入っている構造になっています。ですのでがキャンセルされて赤ループのみになるということです。

そして最後の三セット目の分岐処理です。40~47行目のコードが対応します。

# Flag変数を書き換えて赤と緑のループを一気に抜ける
                if not select_vvalue3 == "戻る":
                    # 火山名を取得してループを抜ける
                    select_volcano = select_vvalue3
                    Flag = False

                elif select_vvalue3 == "戻る":
                    # 地域選択画面に戻る
                    pass

最初のif文で火山名が確定した時の処理を行っています。フロー図にある通り、ブロック4に向かいます。つまりループを離脱しなければなりません。ここでFlag変数の出番ということになります。

最後の処理でFlagの中身がFalseに変更されています。これによってプログラムは緑のループを抜け、赤ループwhileブロックに入ります。

この後の処理はありませんので、プログラムは赤ループを繰り返すかどうかの判定に入ります。ところが、どちらのループにおいてもwhileの条件にはFlagを使用していました。

緑ループ内で書き換えたFlagの効果が赤ループにまで及んでいたということです。これが先ほどFlag一つで赤と緑両方のループの真偽をコントロールしていると述べた理由です。

その結果、プログラムは赤ループも離脱してフロー図右側の処理に移行、一連の火山選択が完了します。

一方でやっぱり別の地域から選びたい!というワガママ願望にはelif文でお応えしています。それがpass文です。何も処理を行わない、という処理を行います。この行以降はコードがありませんからプログラムは再度緑ループの始点、つまり地域の選択リストを出す部分に戻ります。

ですので、言ってしまえばここのelif文は別に無くてもプログラムは動くはずです。値のふるい分けは先のif notで完了していますから。

ではなぜわざわざelif文を書いたのか?ということですが、フロー図に沿って分岐処理を行っていることをはっきりさせる目的で記述しています。ならばelifの処理を何も書かなければ良いのでは?と思われるかも知れませんが、何も書かないと実行時に次のエラーが発生してしまうのです。

IndentationError: expected an indented block

インデントされた(indented)ブロック(block)を期待したのですが…というエラーです。Pythonではある処理を行うコードの一群をブロックとして扱っており、その判断をインデント(字下げ)されているかで行います。

つまり、elif文の処理ブロックを探したのだけど見つからなかった、と返してきているんですね。それで、何もすることがないけれど、とりあえず処理はしている体を装いたい…passはそのような意味もあって記述しています。

まとめです

火山カメラキャプチャプログラムについて、ブロック3の解説を二回に渡って行いました。if文やwhile文など、プログラムにおいて非常に使用頻度が高い構文が出てきましたが、やや入り組んだ部分もありていねいに解説をしたつもりですが、まだまだ至らないところがあると思います。

また、このブロックでは例外処理というものを全く考えていません。例外処理とは、受け取った値が不正な場合に起こり得るエラーに対応するための処理を指します。

特に火山名選択というユーザーから入力を受け付ける部分において非常に重要です。プログラムの仕様を見て頂ければ分かりますが、ユーザーは火山名などを番号で入力することになっています。ところが番号は一ケタだけでなくて二ケタの場合もあります。

もし、25とキーを叩いたつもりが35になってしまったら?もし半角ではなく全角で入力してしまったら?入力されたのがアルファベットや平仮名だったら?

…と、必ずしもプログラムが期待した値をユーザーが入力してくれる保証がどこにも無いのですね。自分一人でこそこそ使うならともかく、それを多くの人が使うとなると例外も発生する確率が上がることが予想され、予期せぬエラーが発生した場合に何かしらの影響が出るかも知れません。

Pythonではそのような例外処理対策としてtryexceptが用意されています。

本プログラムでもそれを取り入れようとしたのですが、whileループの中でどのように書くのか考えがまとまらず割り切って例外処理を外しました。それ以前にwhileループの構築でかなり悩み、何度も失敗していたこともあって諦めたというのが正しいでしょうか。ですが大事な作業ですので、またいつか、チャレンジしたいと思います。

次はブロック4に入ります。実際にブラウザを操作してデータを取得するwebスクレイピングについて解説予定です。



タイトルとURLをコピーしました