LoginSignup
4
7

More than 1 year has passed since last update.

気象庁の1日毎の過去の気象データをPythonでスクレイピングしてPostgreSQLにINSERTしてみた

Last updated at Posted at 2022-08-21

はじめに

やること

  • 気象庁の1日ごとの気象データをスクレイピングする
  • 取得したデータをPostgreSQLのDBにINSERTする
  • 取得するデータは2007年~2022年の「今日」の一日前まで、全国の観測地点でデータがあるところ

環境等

  • プログラミング言語: Python3.10
  • DBサーバ: Amazon AWS RDB
  • 実行環境: Amazon AWS EC2
  • スクレイピングに使うライブラリ: BeautifulSoup

参考にした情報

制作にあたり、以下の記事と記載のコードを参考にさせていただきました。

やろうと思ったきっかけ

私は写真が趣味でして、撮影するのに天候は重要なわけですが、気象庁などの天気予報はせいぜい1~2ヶ月先がいいところ。しかし、撮影の計画は数ヶ月前から練るもので、この日にしようと思った日がたまたま撮影に適さない天候だったら悲しいわけです。

そこで、最終的な目標を「未来の任意の日付の日地点における天候が、撮影に適しているかを評価する」としまして、まずはそのバックデータを取得することが必要だったわけです。

というわけで、そのバックデータとなる気象データをローカルのDBに落としてこなければならないな、とおもいこんなことを考えました。

...という事もありますが、最近流行のビッグデータでAIで機械学習がどうのこうの、とか、AWSとかいろいろなモノの勉強も兼ねてます。

準備

スクレイピングについて

気象庁の気象データは、なんかをチョチョっと操作すればCSVとかでドガっとデータが得られる...というふうに都合良く行かないので、正攻法では手動でポチポチする必要があります。この辺の話はこちらの記事にわかりやすくまとめてあります。

データが無いならスクレイピングすればいいじゃん!!ってことで、そのやり方もわかったので光明がみえてきました。

データベースについて

スクレイピングしたデータはどこかに仕舞っておく必要がありますね。最近はやりのNoSQLみたいなやつもありますが、昔ながらのリレーショナルデータベースでいきます。
後でやる(予定の)計算とかもRDB側のクエリでなんとかしたいな、って言う思いがあるので、PostgreSQLでやってます。

実行環境について

実行環境は、Amazon RDSとEC2です。オンプレでも仮想環境でもラズパイでも動かせると思います。

取得するデータ

取得する気象データは、今回は1日ごとのデータです。気象庁のサイトから取得できるデータは、次の通りでした。日本語の部分は気象庁のWEBサイトでの表記で、アルファベットの部分は私が勝手に英訳したものです。テーブル名とかになってます。

  • 日付: date
  • 気圧(hPa)[現地]: pressure_ground
  • 気圧(hPa)[海面]: pressure_sea
  • 降水量(mm)[合計]: rainfall_amount
  • 降水量(mm)[最大(1時間)] : rainfall_max_one_hour
  • 降水量(mm)[最大(10分間)]: rainfall_ten_minute
  • 気温(℃)[平均]: temperature_ave
  • 気温(℃)気温[最高]: temperature_max
  • 気温(℃)気温[最低]: temperature_min
  • 湿度(%)[平均]: humidity_average
  • 湿度(%)[最小]: humidity_min
  • 風向・風速(m/s)[平均風速]: windspeed_ave
  • 風向・風速(m/s)[最大風速_風速]: windspeed_max_value
  • 風向・風速(m/s)[最大風速_風向]: windspeed_max_direction
  • 風向・風速(m/s)[最大瞬間風速_風速]: windspeed_max_instant_value
  • 風向・風速(m/s)[最大瞬間風速_風向]: windspeed_max_instant_direction
  • 日照時間(h): sunshine_hour
  • 雪(cm)[降雪合計]: snowfall_amount
  • 雪(cm)[最深積雪]: snow_depth
  • 天気概況(昼)[06:00-18:00]: weather_overview_daytime
  • 天気概況(夜)[18:00-翌日06:00]: weather_overview_night

観測地点に関するデータ

これに加えて、場所を表すコードを使います。観測地点の場所を表しているのだと思います。
観測地点に関する情報は、こちらのサイトにあるものを参考にさせていただきました。

更新されてから時間が経っているようですが、そんなに頻繁にかわるものでもないので、まあ問題ないでしょう。

どこかに最新版ないかな...

テーブル構造

このデータを格納するテーブルの構造を考えます。上記の取得データは1日毎のデータです。これを1レコードとして考えます。地点を表すコードも入れておきます。
地点コードは数字だけで構成されています。地点コードだけだと場所が分かりませんが、別テーブルに地名(と座標)を入れておいて、あとで結合すれば良いでしょう。

というわけで、テーブルを作成するクエリです。

  • 主キーは登録順の整数シリアル値で(MySQLでいうAUTO_INCREMENT)、INSERTするごとに自動インクリメントになります。
  • 温度、気温、気圧などは小数点を扱える型にします
  • 風向き(wind*_direction)は、気象庁のデータでは"北"とか"南南西"とか言う表記になっています。あとで機械学習するときにこれだと不便かもしれないので、北を0度とした360度表記にするので整数型にしています。
  • 地点コードは、整数型か文字列を扱える型にするか迷いましたが整数型にしました。たぶん、ソートとかするときラクそうです。
  • weather_overview* は、天気概況です。「晴時々曇り」とかそういうのです。文字列として表記してます。試しにスクレイピングしたところ、表記のバリエーションがかなり多いし、長い文字列になる場合もあるので余裕をみておきます。
create_table.sql
CREATE TABLE weather_table(
    id SERIAL PRIMARY KEY ,
    date          DATE,
    prec_no			 INTEGER,
    block_no		 INTEGER,
    pressure_ground  REAL,
    pressure_sea     REAL,
    rainfall_amount  REAL,
    rainfall_max_one_hour  REAL,
    rainfall_ten_minute    REAL,
    temperature_ave  REAL,
    temperature_max  REAL,
    temperature_min  REAL,
    humidity_average  REAL,
    humidity_min      REAL,
    windspeed_ave     REAL,
    windspeed_max_value    REAL,
    windspeed_max_direction  INTEGER,
    windspeed_max_instant_value   REAL,
    windspeed_max_instant_direction  INTEGER,
    sunshine_hour  REAL,
    snowfall_amount  REAL,
    snow_depth   REAL,
    weather_overview_daytime   VARCHAR(200),
    weather_overview_night     VARCHAR(200)
);

その他DB設定

AWS RDBで作成したインスタンスは、デフォルトのユーザが管理者権限をもっています。アプリ(Python)側からこの権限を使うのは危ないので、アプリから接続する専用のユーザを作っておきます。

ちなみに、PostgresSQLでは、ユーザのことをロール(ROLE)と呼ぶそうです。

CREATE ROLE username LOGIN PASSWORD 'password';

作成したテーブルにSELECTとINSERTの権限を付与します。

GRANT SELECT, INSERT ON weather_table TO username;

SERIALのデータ型を使っているので、それを使うための権限も付与します。

GRANT USAGE ON SEQUENCE weather_table_id_seq TO username;

テーブルが出来たので、スクレイピングしたデータを受け入れる準備が整いました。

コードの前に、実行結果

実行例

こんな感じでスクレイピングとデータのINSERTが進んでいきます。

$ python3 weather_scraper.py
weather_scraper.py started!!
fetch failed for 三森山!! wait 10 second.__________________________________________________________.

INSERTしたデータはこんな感じになりました
image.png

どれくらい時間がかかるか

どの地点を選択するかにもよりますが、こちらのサイトで紹介されている地点をすべて網羅すると、全行程で3~5日かかるっぽいです。

サーバからBANされないか

コードの所々にsleep()を入れています。sleep()入れないで実行すると、実行途中にサーバ側から接続切られることが多々ありました。とりあえずですが、10秒くらいsleep()入れておけば、サーバ側から切られることが無くなりました。
もしかすると、もうちょっと長くsleep()した方が良いかもしれませんが...

コードの説明

実際に動いているコードを記載します。

部分毎の説明

説明とともにコードを掲載します。(全文通したのは後述)

  • 使用するライブラリなど import
.py
from bs4 import BeautifulSoup  

import csv
import json
import requests
import psycopg2
from datetime import datetime 
import codecs
import time

-スクレイピングするURLとよく使う処理

.py
# URLで年と月ごとの設定ができるので%sで指定した英数字を埋め込めるようにします。
base_url = "http://www.data.jma.go.jp/obd/stats/etrn/view/daily_s1.php?prec_no=%s&block_no=%s&year=%s&month=%s&day=1&view=p1"


# 取ったデータをfloat型に返還する。変換できない文字は0.0で代用。
def str2float(str):
    try:
        return float(str)
    except:
        return 0.0


#場所データのcsvファイル名
place_csv_file_name = './place.csv'

#logfile名
logfile_name = "./weather_scraper.log"

#ログファイルに記録する
def printlog(label_str, body_str):
    print(datetime.now(), ',' ,label_str, ',', body_str,
                            file =  codecs.open(logfile_name, 'a', 'utf-8'))

#端末に印字する(文字が流れていかないように1行で上書きする)
def printmsg(msg):
    print('\r___________________________________________________________________________________________________' , end='')
    print('\r%s' % msg, end='')


  • DBMSに接続する処理など
.py
#SQLサーバの情報
dbms_hostname = 'xxxxxxxxx.rds.amazonaws.com'
dbms_port = 12345
dbms_database_name ='postgres'

#SQLサーバに接続するためのユーザ名とパスワード情報を保存したファイル名
db_auth_filename = 'auth.json'

#DBの認証情報をファイルから読み取る
def read_auth(filename):
    file = open(filename, "r")
    auth = json.load(file)
    file.close()

    return auth

#DBMSに接続する
def connect_dbms():
    auth = read_auth("auth.json")
    dbms_username = auth['username']
    dbms_password = auth['password']

    con = psycopg2.connect(
        host = dbms_hostname,
        port = dbms_port,
        database=dbms_database_name,
        user= dbms_username, 
        password=dbms_password)
    
    return con
  • 方角を0度~360度で表す(北を0度とする)
  • 風向きは「北」とか「北北東」とかの文字列で取得されるので、0度を北としたときの角度で表記する
def kanji_direction_to_degree(kanji_direction):
    direction = -1

    match   kanji_direction:
        case '北北東':
            direction = 23
        case '東北東':
            direction = 68           
        case '東南東':
            direction = 113
        case '南南東':
            direction = 158
        case '南南西':
            direction = 203
        case '西南西':
            direction = 248
        case '西北西':
            direction = 293
        case '北北西':
            direction = 335
        case '北東':
            direction = 45
        case '南東':
            direction = 135
        case '南西':
            direction = 225
        case '北西':
            direction = 315
        case '北':
            direction = 0
        case '東':
            direction = 90
        case '南':
            direction = 180
        case '西':
            direction = 270
        case _:
            direction = -1
    return direction
  • プログラムを開始
  • 最初に、地点データをCSVから読み込んでくる
  • ちなみに、CSVはこんな感じ
No.,i,地点名,prec_no,block_no,緯度(度)経度(度、分)標高(m),緯度(分),経度(度)標高(m),経度(分)標高(m),標高(m)
1,1 ,稚内,11 ,47401 ,45,24.9 ,141 ,40.7 ,2.8 
2,2 ,沓形,11 ,2 ,45,10.7 ,141 ,8.3 ,14.0 
3,3 ,浜頓別,11 ,3 ,45,7.5 ,142 ,21.0 ,18.0 
4,4 ,北見枝幸,11 ,47402 ,44,56.4 ,142 ,35.1 ,6.7 
  • コードはこれ
.py
if __name__ == "__main__":
    #場所データを格納する配列を初期化
    place_code_prec_no = []
    place_code_block_no = []
    place_name = []

    print('weather_scraper.py started!!')

    #場所データをcsvから読み込み
    csv_file = open(place_csv_file_name, 'r', encoding='utf-8', errors='', newline='' )
    f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)
    
    header = next(f)
    for row in f:
        place_code_prec_no.append(int(row[3].strip(' ')))   #地域コード
        place_code_block_no.append(int(row[4].strip(' ')))  #地点コード
        place_name.append(row[2].strip(' '))  #地点名
    
    csv_file.close()
  • DBMSに接続する
.py
   #DBMSに接続
   connector = connect_dbms()
   
   #cursorの取得
   cursor = connector.cursor()
  • 一番外側のループで地点を設定する
  • ログファイルに進行状況も記録します
.py
    #各地点(地域・場所)をスクレイピングする
    for place in place_name:
        exception_place_count = 0  #その地点(地域・場所)での例外カウンタをクリア        

        place_count = place_count + 1

        printlog('NEW_PLACE_FETCH_START', place)

        fail_flag = False   #失敗フラグ初期化
        
        index = place_name.index(place)
  • 年ごとにデータを取得するためのループ
  • 後の方にあるループで、データ取得に失敗していたときに、その地点を諦めるかどうかを判断ためにfail_flagを利用しています。
.py
        # データを取得する年、とりあえず15年前まで
        years = [2022, 2021,
                 2020, 2019, 2018, 2017, 2016, 2015, 2014, 2013, 2012, 2011, 
                 2010, 2009, 2008, 2007]
        for year in years: 
            
            #取得に失敗していたら、この地点は諦める
            if(fail_flag == True):
                printmsg('fetch failed for %s!! wait 10 second.' % place)
                printlog('FETCH_FAILED', place)
                            
                time.sleep(10)    #サーバへの気遣いsleep
                break  #次の地点へ

  • 月ごとのデータを扱うループ。「今年」の場合は、「今月」までの取得とする
.py
            #各月のデータを取得する
            month = 1 #1月から開始
            month_max = 12 #最終月

            #今年の場合は、今月が最終月
            if(year == datetime.now().year):
                month_max = datetime.now().month

            while(month <= month_max):
                # インクリメントは、ループの終わりでやっている。(例外発生時にその月をやり直すため)

                printmsg("fetching.  area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
                printlog("FETCHING", "area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
  • ここまで来れば、取得するべきURLができあがる。
  • URLを指定してデータを取得する
  • データを取得出来なかったときは例外が発生するとおもうので、ある程度リトライして、それでも取得出来なければあきらめる。
.py

                #URLを指定してデータを取得(fetch)する
                try:
                    # 2つの地点コードと年と月を当てはめる。
                    r = requests.get(base_url % (
                        place_code_prec_no[index], place_code_block_no[index], year, month))
                except:
                    printmsg("exception. wait 30 second. After wait, retry fetching.")
                    printlog('EXCEPTION', 'fetch retry.')


                    exception_place_count = exception_place_count + 1
                    exception_amount_count = exception_amount_count + 1

                    #例外の合計回数が10回以上の場合
                    if(exception_amount_count >= 10):
                        #サーバから嫌われたと思って、プログラム終了する
                        printmsg("too many exception!! terminated.")
                        printlog('TERMINATED','')
                        exit(-1)  #プログラム終了

                    #その地点での例外の回数が3回以上の場合、残りの月を放棄してその地点を諦める
                    if(exception_place_count >=3):
                        printmsg("skip this area due to exception")
                        printlog('AREA_SKIPPED','area: %s' % place)
                        #この地点はあきらめる
                        fail_flag = True
                        time.sleep(30)  #30秒待つ。サーバへの気遣い
                        break
 
                    #規定回数未満の場合は、この月をやりなおす
                    time.sleep(30)  #30秒待つ。サーバへの気遣い
                    continue

                #データ取得終了
                printmsg("fetched.  area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
                printlog("FETCHED", "area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))

                #取得に成功したので、失敗フラグクリア
                fail_flag = False
  • 取得したデータをBeautifulSoupに渡して解析してもらう
  • 指定した地点・年月のデータがないときは、その地点は諦める
  • (新しくできた地点だと、あまり昔のデータは登録されていないかもしれない。直近の年月から昔に遡って取得しているので、なるべく多くのデータを取得できるはず。)
.py
                r.encoding = r.apparent_encoding

                #BeautifulSoupに取得したデータを渡す
                soup = BeautifulSoup(r.text, 'html.parser')

                # findAllで条件に一致するものをすべて抜き出します。
                # 今回の条件はtrタグでclassがmtxになってるものです。
                rows = soup.findAll('tr', class_='mtx')

                # 表の最初の1~4行目はカラム情報なのでスライスする。(indexだから初めは0だよ)
                rows = rows[4:]
                
                #データがない場合は、その地点は諦める
                if(len(rows) == 0):
                    printmsg("fetched but no data found.  area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
                    printlog("FETCHED_NO_DATA", "area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
                    fail_flag = True
                    break
  • ここまでで、1ヶ月分のデータが取得できている。
  • データはテーブルに入っている。
  • 1日が1行となっている。
  • 列が各項目になっている。これらを分解して、DBMSに登録できる表記に変換する
.py
                # 1日〜最終日までの1行を網羅し、取得します。
                for row in rows:
                    # 今度はtrのなかのtdをすべて抜き出します
                    data = row.findAll('td')
          
                    #日時(yyyy/mm/dd), date
                    date = str(year) + "/" + str(month) + "/" + str(data[0].string);
                                        
                    # 地点コードA, prec_no
                    prec_no = place_code_prec_no[index]
                    
                    # 地点コードB, block_no
                    block_no = place_code_block_no[index]
                                        
                    # 気圧(hPa)[現地], pressure_ground
                    pressure_ground = str2float(data[1].string)

                    # 気圧(hPa)[海面], pressure_sea
                    pressure_sea = str2float(data[2].string)

                    # 降水量(mm)[合計], rainfall_amount
                    rainfall_amount = str2float(data[3].string)

                    # 降水量(mm)[最大(1時間)] , rainfall_max_one_hour
                    rainfall_max_one_hour = str2float(data[4].string)

                    # 降水量(mm)[最大(10分間)], rainfall_ten_minute
                    rainfall_ten_minute = str2float(data[5].string)
                    
                    # 気温(℃)[平均], temperature_ave
                    temperature_ave = str2float(data[6].string)

                    # 気温(℃)気温[最高], temperature_max
                    temperature_max = str2float(data[7].string)

                    # 気温(℃)気温[最低], temperature_min
                    temperature_min = str2float(data[8].string)

                    # 湿度(%)[平均], humidity_average
                    humidity_average = str2float(data[9].string)

                    # 湿度(%)[最小], humidity_min
                    humidity_min = str2float(data[10].string)

                    # 風向・風速(m/s)[平均風速], windspeed_ave
                    windspeed_ave = str2float(data[11].string)

                    # 風向・風速(m/s)[最大風速_風速], windspeed_max_value
                    windspeed_max_value = str2float(data[12].string)

                    # 風向・風速(m/s)[最大風速_風向], windspeed_max_direction
                    if(len(data[13])>0):
                        windspeed_max_direction = kanji_direction_to_degree(data[13].string.string.strip(' ').strip(' ').strip(']').strip(')'))
                    else:
                        windspeed_max_direction = -1

                    # 風向・風速(m/s)[最大瞬間風速_風速], windspeed_max_instant_value
                    windspeed_max_instant_value = str2float(data[14].string)

                    # 風向・風速(m/s)[最大瞬間風速_風向], windspeed_max_instant_direction
                    if(len(data[15])>0): #データが取れていなかったら -1 にする
                        windspeed_max_instant_direction = kanji_direction_to_degree(data[15].string.strip(' ').strip(' ').strip(']').strip(')'))
                    else:
                        windspeed_max_instant_direction = -1
                    # 日照時間(h), sunshine_hour
                    sunshine_hour = str2float(data[16].string)

                    # 17番(降雪量合計)は、記録なしのときは"--"となるみたいなので、0になおす
                    snowfall_amount = 0
                    if(len(data[17]) > 0):
                        if (data[17].string != '--'):  # 雪(cm)[降雪合計], snowfall_amount
                            snowfall_amount =str2float(data[17].string)
                        
                    
                    # 雪(cm)[最深積雪], snow_depth
                    snow_depth = str2float(data[18].string)

                    # 天気概況(昼)[06:00-18:00], weather_overview_daytime
                    
                    if( len(data[19]) > 0):
                        weather_overview_daytime = data[19].string.replace(' ','').replace(' ','').replace(')','').replace(']','')
                    else:
                        weather_overview_daytime = "-"
                    
                    # 天気概況(夜)[18:00-翌日06:00], weather_overview_night
                    if( len(data[20]) > 0):
                        weather_overview_night = data[20].string.replace(' ','').replace(' ','').replace(')','').replace(']','')
                    else:
                        weather_overview_night = "-"

                    
  • 1レコードのデータができた
  • しかし、なぜか「今月」の場合も最終日までデータがあるようです。この場合、全てのデータが0になっているっぽい。気圧データは0になるってことはあり得ないので、気圧が0ならDBにINSERTしないことにします。
  • INSERTするクエリをつくったあと、クエリを実行します
  • ログファイルにも、使ったクエリ文を記録しておきます
.py


                    #データがなかったら(気圧が取れてないのはデータが取れてないと言うこと)
                    if( (pressure_ground > 0 ) and (pressure_sea) > 0 ):
                        printmsg("inserting.  area: %s,  year: %s, month: %s, records_inserted: %s, place_count: %s." % (place, year, month, inserted_count, place_count))

                        #クエリを実行して追加
                        cursor.execute('INSERT INTO weather_table ( date, prec_no, block_no, pressure_ground, \
                                pressure_sea, rainfall_amount, rainfall_max_one_hour, rainfall_ten_minute, \
                                temperature_ave, temperature_max, temperature_min, humidity_average, \
                                humidity_min, windspeed_ave, windspeed_max_value, windspeed_max_direction, \
                                windspeed_max_instant_value, windspeed_max_instant_direction, sunshine_hour, \
                                snowfall_amount, snow_depth, weather_overview_daytime, weather_overview_night) \
                                VALUES( %s, %s,  %s, %s, %s,   %s,   %s,   %s,   %s, %s, \
                                        %s,     %s,      %s,   %s, %s, %s, %s, %s, %s, %s, \
                                        %s,    %s, %s)' , (
                                date, prec_no, block_no, pressure_ground, 
                                pressure_sea, rainfall_amount, rainfall_max_one_hour, rainfall_ten_minute, 
                                temperature_ave, temperature_max, temperature_min, humidity_average, 
                                humidity_min, windspeed_ave, windspeed_max_value, windspeed_max_direction, 
                                windspeed_max_instant_value, windspeed_max_instant_direction, sunshine_hour, 
                                snowfall_amount, snow_depth, weather_overview_daytime, weather_overview_night)
                                ) 
                        
                        #反映する
                        connector.commit()
                    
                        printmsg("inserted.  area: %s,  year: %s, month: %s, records_inserted: %s, place_count: %s." % (place, year, month, inserted_count, place_count))
                        printlog('INSERTED', 'area: %s,  year: %s, month: %s, \"INSERT INTO weather_table ( ' 
                                'date, prec_no, block_no, pressure_ground, '
                                'pressure_sea, rainfall_amount, rainfall_max_one_hour, rainfall_ten_minute, '
                                'temperature_ave, temperature_max, temperature_min, humidity_average, '
                                'humidity_min, windspeed_ave, windspeed_max_value, windspeed_max_direction, '
                                'windspeed_max_instant_value, windspeed_max_instant_direction, sunshine_hour, '
                                'snowfall_amount, snow_depth, weather_overview_daytime, weather_overview_night) '
                                'VALUES( %s, %s, %s,  %s, %s, %s, %s, %s, %s, %s, '
                                        '%s, %s, %s,  %s, %s, %s, %s, %s, %s, %s, '
                                        '%s, %s, %s)\"' % (
                                place, year, month, date, prec_no, block_no, pressure_ground, 
                                pressure_sea, rainfall_amount, rainfall_max_one_hour, rainfall_ten_minute, 
                                temperature_ave, temperature_max, temperature_min, humidity_average, 
                                humidity_min, windspeed_ave, windspeed_max_value, windspeed_max_direction, 
                                windspeed_max_instant_value, windspeed_max_instant_direction, sunshine_hour, 
                                snowfall_amount, snow_depth, weather_overview_daytime, weather_overview_night))

                        #挿入済みカウンタをインクリメント
                        inserted_count = inserted_count + 1
                    

コードの全文

コードの全文を掲載します

weather_scraper.py

from bs4 import BeautifulSoup  

import csv
import json
import requests
import psycopg2
from datetime import datetime 
import codecs
import time
from urllib.error import HTTPError
from urllib.error import URLError
import sys

# URLで年と月ごとの設定ができるので%sで指定した英数字を埋め込めるようにします。
base_url = "http://www.data.jma.go.jp/obd/stats/etrn/view/daily_s1.php?prec_no=%s&block_no=%s&year=%s&month=%s&day=1&view=p1"


# 取ったデータをfloat型に返還する。変換できない文字は0.0で代用。
def str2float(str):
    try:
        return float(str)
    except:
        return 0.0


#場所データのcsvファイル名
place_csv_file_name = './place.csv'

#logfile名
logfile_name = "./weather_scraper.log"

#ログファイルに記録する
def printlog(label_str, body_str):
    print(datetime.now(), ',' ,label_str, ',', body_str,
                            file =  codecs.open(logfile_name, 'a', 'utf-8'))

#端末に印字する(文字が流れていかないように1行で上書きする)
def printmsg(msg):
    print('\r___________________________________________________________________________________________________' , end='')
    print('\r%s' % msg, end='')


#SQLサーバの情報
dbms_hostname = 'xxxxxxxxxxxx.rds.amazonaws.com'
dbms_port = 12345
dbms_database_name ='postgres'

#SQLサーバに接続するためのユーザ名とパスワード情報を保存したファイル名
db_auth_filename = 'auth.json'

#DBの認証情報をファイルから読み取る
def read_auth(filename):
    file = open(filename, "r")
    auth = json.load(file)
    file.close()

    return auth

#DBMSに接続する
def connect_dbms():
    auth = read_auth("auth.json")
    dbms_username = auth['username']
    dbms_password = auth['password']

    con = psycopg2.connect(
        host = dbms_hostname,
        port = dbms_port,
        database=dbms_database_name,
        user= dbms_username, 
        password=dbms_password)
    
    return con



#方角を整数値に変換、
def kanji_direction_to_degree(kanji_direction):
    direction = -1

    match   kanji_direction:
        case '北北東':
            direction = 23
        case '東北東':
            direction = 68           
        case '東南東':
            direction = 113
        case '南南東':
            direction = 158
        case '南南西':
            direction = 203
        case '西南西':
            direction = 248
        case '西北西':
            direction = 293
        case '北北西':
            direction = 335
        case '北東':
            direction = 45
        case '南東':
            direction = 135
        case '南西':
            direction = 225
        case '北西':
            direction = 315
        case '':
            direction = 0
        case '':
            direction = 90
        case '':
            direction = 180
        case '西':
            direction = 270
        case _:
            direction = -1
    return direction



if __name__ == "__main__":
    #場所データを格納する配列を初期化
    place_code_prec_no = []
    place_code_block_no = []
    place_name = []

    print('weather_scraper.py started!!')

    #場所データをcsvから読み込み
    csv_file = open(place_csv_file_name, 'r', encoding='utf-8', errors='', newline='' )
    f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)
    
    header = next(f)
    for row in f:
        place_code_prec_no.append(int(row[3].strip(' ')))   #地域コード
        place_code_block_no.append(int(row[4].strip(' ')))  #地点コード
        place_name.append(row[2].strip(' '))  #地点名
    
    csv_file.close()
        

    #DBMSに接続
    connector = connect_dbms()
    
    #cursorの取得
    cursor = connector.cursor()

    inserted_count = 0     #何レコード挿入したか
    place_count = 0      #何カ所廻ったか
    exception_amount_count = 0 #例外が合計何回発生したか


    #各地点(地域・場所)をスクレイピングする
    for place in place_name:
        exception_place_count = 0  #その地点(地域・場所)での例外カウンタをクリア        

        place_count = place_count + 1

        printlog('NEW_PLACE_FETCH_START', place)

        fail_flag = False   #失敗フラグ初期化
        
        index = place_name.index(place)
        # データを取得する年、とりあえず15年前まで
        years = [2022, 2021,
                 2020, 2019, 2018, 2017, 2016, 2015, 2014, 2013, 2012, 2011, 
                 2010, 2009, 2008, 2007]
        for year in years: 
            
            #取得に失敗していたら、この地点は諦める
            if(fail_flag == True):
                printmsg('fetch failed for %s!! wait 10 second.' % place)
                printlog('FETCH_FAILED', place)
                            
                time.sleep(10)    #サーバへの気遣いsleep
                break  #次の地点へ

            #各月のデータを取得する
            month = 1 #1月から開始
            month_max = 12 #最終月

            #今年の場合は、今月が最終月
            if(year == datetime.now().year):
                month_max = datetime.now().month

            while(month <= month_max):
                # インクリメントは、ループの終わりでやっている。(例外発生時にその月をやり直すため)

                printmsg("fetching.  area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
                printlog("FETCHING", "area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))

                #URLを指定してデータを取得(fetch)する
                try:
                    # 2つの地点コードと年と月を当てはめる。
                    r = requests.get(base_url % (
                        place_code_prec_no[index], place_code_block_no[index], year, month))
                except:
                    printmsg("exception. wait 30 second. After wait, retry fetching.")
                    printlog('EXCEPTION', 'fetch retry.')


                    exception_place_count = exception_place_count + 1
                    exception_amount_count = exception_amount_count + 1

                    #例外の合計回数が10回以上の場合
                    if(exception_amount_count >= 10):
                        #サーバから嫌われたと思って、プログラム終了する
                        printmsg("too many exception!! terminated.")
                        printlog('TERMINATED','')
                        exit(-1)  #プログラム終了

                    #その地点での例外の回数が3回以上の場合、残りの月を放棄してその地点を諦める
                    if(exception_place_count >=3):
                        printmsg("skip this area due to exception")
                        printlog('AREA_SKIPPED','area: %s' % place)
                        #この地点はあきらめる
                        fail_flag = True
                        time.sleep(30)  #30秒待つ。サーバへの気遣い
                        break
 
                    #規定回数未満の場合は、この月をやりなおす
                    time.sleep(30)  #30秒待つ。サーバへの気遣い
                    continue

                #データ取得終了
                printmsg("fetched.  area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
                printlog("FETCHED", "area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))

                #取得に成功したので、失敗フラグクリア
                fail_flag = False

                r.encoding = r.apparent_encoding

                #BeautifulSoupに取得したデータを渡す
                soup = BeautifulSoup(r.text, 'html.parser')

                # findAllで条件に一致するものをすべて抜き出します。
                # 今回の条件はtrタグでclassがmtxになってるものです。
                rows = soup.findAll('tr', class_='mtx')

                # 表の最初の1~4行目はカラム情報なのでスライスする。(indexだから初めは0だよ)
                rows = rows[4:]
                
                #データがない場合は、その地点は諦める
                if(len(rows) == 0):
                    printmsg("fetched but no data found.  area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
                    printlog("FETCHED_NO_DATA", "area: %s,  year: %s, records_inserted: %s, place_count: %s." % (place, year, inserted_count, place_count))
                    fail_flag = True
                    break

                # 1日〜最終日までの1行を網羅し、取得します。
                for row in rows:
                    # 今度はtrのなかのtdをすべて抜き出します
                    data = row.findAll('td')
          
                    #日時(yyyy/mm/dd), date
                    date = str(year) + "/" + str(month) + "/" + str(data[0].string);
                                        
                    # 地点コードA, prec_no
                    prec_no = place_code_prec_no[index]
                    
                    # 地点コードB, block_no
                    block_no = place_code_block_no[index]
                                        
                    # 気圧(hPa)[現地], pressure_ground
                    pressure_ground = str2float(data[1].string)

                    # 気圧(hPa)[海面], pressure_sea
                    pressure_sea = str2float(data[2].string)

                    # 降水量(mm)[合計], rainfall_amount
                    rainfall_amount = str2float(data[3].string)

                    # 降水量(mm)[最大(1時間)] , rainfall_max_one_hour
                    rainfall_max_one_hour = str2float(data[4].string)

                    # 降水量(mm)[最大(10分間)], rainfall_ten_minute
                    rainfall_ten_minute = str2float(data[5].string)
                    
                    # 気温(℃)[平均], temperature_ave
                    temperature_ave = str2float(data[6].string)

                    # 気温(℃)気温[最高], temperature_max
                    temperature_max = str2float(data[7].string)

                    # 気温(℃)気温[最低], temperature_min
                    temperature_min = str2float(data[8].string)

                    # 湿度(%)[平均], humidity_average
                    humidity_average = str2float(data[9].string)

                    # 湿度(%)[最小], humidity_min
                    humidity_min = str2float(data[10].string)

                    # 風向・風速(m/s)[平均風速], windspeed_ave
                    windspeed_ave = str2float(data[11].string)

                    # 風向・風速(m/s)[最大風速_風速], windspeed_max_value
                    windspeed_max_value = str2float(data[12].string)

                    # 風向・風速(m/s)[最大風速_風向], windspeed_max_direction
                    if(len(data[13])>0):
                        windspeed_max_direction = kanji_direction_to_degree(data[13].string.string.strip(' ').strip(' ').strip(']').strip(')'))
                    else:
                        windspeed_max_direction = -1

                    # 風向・風速(m/s)[最大瞬間風速_風速], windspeed_max_instant_value
                    windspeed_max_instant_value = str2float(data[14].string)

                    # 風向・風速(m/s)[最大瞬間風速_風向], windspeed_max_instant_direction
                    if(len(data[15])>0): #データが取れていなかったら -1 にする
                        windspeed_max_instant_direction = kanji_direction_to_degree(data[15].string.strip(' ').strip(' ').strip(']').strip(')'))
                    else:
                        windspeed_max_instant_direction = -1
                    # 日照時間(h), sunshine_hour
                    sunshine_hour = str2float(data[16].string)

                    # 17番(降雪量合計)は、記録なしのときは"--"となるみたいなので、0になおす
                    snowfall_amount = 0
                    if(len(data[17]) > 0):
                        if (data[17].string != '--'):  # 雪(cm)[降雪合計], snowfall_amount
                            snowfall_amount =str2float(data[17].string)
                        
                    
                    # 雪(cm)[最深積雪], snow_depth
                    snow_depth = str2float(data[18].string)

                    # 天気概況(昼)[06:00-18:00], weather_overview_daytime
                    
                    if( len(data[19]) > 0):
                        weather_overview_daytime = data[19].string.replace(' ','').replace(' ','').replace(')','').replace(']','')
                    else:
                        weather_overview_daytime = "-"
                    
                    # 天気概況(夜)[18:00-翌日06:00], weather_overview_night
                    if( len(data[20]) > 0):
                        weather_overview_night = data[20].string.replace(' ','').replace(' ','').replace(')','').replace(']','')
                    else:
                        weather_overview_night = "-"
                    
                    #データがなかったら(気圧が取れてないのはデータが取れてないと言うこと)
                    if( (pressure_ground > 0 ) and (pressure_sea) > 0 ):
                        printmsg("inserting.  area: %s,  year: %s, month: %s, records_inserted: %s, place_count: %s." % (place, year, month, inserted_count, place_count))

                        #クエリを実行して追加
                        cursor.execute('INSERT INTO weather_table ( date, prec_no, block_no, pressure_ground, \
                                pressure_sea, rainfall_amount, rainfall_max_one_hour, rainfall_ten_minute, \
                                temperature_ave, temperature_max, temperature_min, humidity_average, \
                                humidity_min, windspeed_ave, windspeed_max_value, windspeed_max_direction, \
                                windspeed_max_instant_value, windspeed_max_instant_direction, sunshine_hour, \
                                snowfall_amount, snow_depth, weather_overview_daytime, weather_overview_night) \
                                VALUES( %s, %s,  %s, %s, %s,   %s,   %s,   %s,   %s, %s, \
                                        %s,     %s,      %s,   %s, %s, %s, %s, %s, %s, %s, \
                                        %s,    %s, %s)' , (
                                date, prec_no, block_no, pressure_ground, 
                                pressure_sea, rainfall_amount, rainfall_max_one_hour, rainfall_ten_minute, 
                                temperature_ave, temperature_max, temperature_min, humidity_average, 
                                humidity_min, windspeed_ave, windspeed_max_value, windspeed_max_direction, 
                                windspeed_max_instant_value, windspeed_max_instant_direction, sunshine_hour, 
                                snowfall_amount, snow_depth, weather_overview_daytime, weather_overview_night)
                                ) 
                        
                        #反映する
                        connector.commit()
                    
                        printmsg("inserted.  area: %s,  year: %s, month: %s, records_inserted: %s, place_count: %s." % (place, year, month, inserted_count, place_count))
                        printlog('INSERTED', 'area: %s,  year: %s, month: %s, \"INSERT INTO weather_table ( ' 
                                'date, prec_no, block_no, pressure_ground, '
                                'pressure_sea, rainfall_amount, rainfall_max_one_hour, rainfall_ten_minute, '
                                'temperature_ave, temperature_max, temperature_min, humidity_average, '
                                'humidity_min, windspeed_ave, windspeed_max_value, windspeed_max_direction, '
                                'windspeed_max_instant_value, windspeed_max_instant_direction, sunshine_hour, '
                                'snowfall_amount, snow_depth, weather_overview_daytime, weather_overview_night) '
                                'VALUES( %s, %s, %s,  %s, %s, %s, %s, %s, %s, %s, '
                                        '%s, %s, %s,  %s, %s, %s, %s, %s, %s, %s, '
                                        '%s, %s, %s)\"' % (
                                place, year, month, date, prec_no, block_no, pressure_ground, 
                                pressure_sea, rainfall_amount, rainfall_max_one_hour, rainfall_ten_minute, 
                                temperature_ave, temperature_max, temperature_min, humidity_average, 
                                humidity_min, windspeed_ave, windspeed_max_value, windspeed_max_direction, 
                                windspeed_max_instant_value, windspeed_max_instant_direction, sunshine_hour, 
                                snowfall_amount, snow_depth, weather_overview_daytime, weather_overview_night))

                        #挿入済みカウンタをインクリメント
                        inserted_count = inserted_count + 1
                        
                # 次の月へ
                month = month + 1
                #ひと月ごとに10秒休む(WEBサーバへの気遣い)
                time.sleep(10)

            #1年ごとにさらに10秒の休止
            time.sleep(10)

                                      
                                 

以上

4
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
7