Python Code Notes

Information by python engineer

Python LINEボットで遊ぶ!天気予報Webアプリを作ろう② BeautifulSoupでwebスクレイピング

以前作成したオウム返しするLINEボットに、天気予報機能を搭載させていきます。 前回は、使用する天気予報サービスと、モジュールについてまとめました。

  • 前回

今回は、天気予報を取得するスクリプトの作成をします。今回作成するスクリプトファイルをLINEボットに読み込ませることで、LINEボットを通して天気予報を取得できるようになります。

目次

詳細設計

まずは、天気予報アプリに必要になる機能をまとめておきます。以下の3つの機能があれば良さそうです。

  1. 全国地点定義表を読み込み、都市名とそのidを取得
  2. 入力した都市名が定義表内にあれば天気予報情報を取得
  3. 天気予報を表示

それぞれの機能に対し、役割に合ったクラス名とメソッドを割り当てておきます。

No. クラス名 メソッド
1 GetPlaceData get_city
2 GetWeatherData get_weather
3 GetWeatherData show_weather

2つのクラスに分けるのは細かすぎるかもしれませんが、今回はこれで行きます。

全体的なコードの流れは、以下の通りにしていきます。

  1. GetPlaceDataオブジェクトを生成し、get_cityメソッドで全国地点定義表から都市名とそのidを取得
  2. 入力された都市名が全国地点定義表内にあればGetWeatherDataオブジェクトを生成し、get_weatherメソッドで天気予報情報を収集
  3. GetWeatherDataオブジェクトのshow_weatherメソッドで、天気予報情報を見やすく表示

早速、上記の設計をもとに、ドラフトとなるコードを書いて保存しておきましょう。ファイル名は、「weathergetter.py」としました。

import requests
import bs4

class GetPlaceData():
    def __init__(self):
        pass

    def get_city(self):
        pass

class GetWeatherData():
    def __init__(self):
        pass
    
    def get_weather(self):
        pass
    
    def show_weather(self):
        pass

if __name__ == "__main__":
    pass

最初に、web上からのデータ収集に必要になるrequestsモジュールと、収集したデータを解析するbs4モジュールをインポートします。 次に、GetPlaceDataクラスとGetWeatherDataクラスを定義します。

都市データ収集用 GetPlaceDataクラス

このクラスでは、全国地点定義表を読み込み、都市名とそのidを取得する機能を持たせます。

オブジェクト生成時に全国地点定義表を読込み、get_cityメソッドで、都市名をキー、都市idをバリューとした辞書を返すようにします。

コード

class GetPlaceData():
    '''
    全国地点定義表から、都市名とidを取得するクラス
    '''
    def __init__(self):
        '''
        全国地点定義表の読み込み
        '''
        path = 'http://weather.livedoor.com/forecast/rss/primary_area.xml'  #1
        self.res = requests.get(path)   #2
        
    def get_city(self):
        '''
        都市名とidを辞書型で取得
        '''
        city_list = {}  #3
        b = bs4.BeautifulSoup(self.res.text, 'xml').select('city')  #4

        for city in b:
            city_list[city.get('title')] = city.get('id')   #5
        
        return(city_list)   #6

コードの解説

  1. 全国地点定義表のアドレス
  2. 全国地点定義表を取得
  3. 都市名とid保管用に、空の辞書を準備
  4. BeautifulSoupで、取得した全国地点定義表から「city」タグを収集
  5. 「city」タグ内から、.get('title')で都市名、```.get('id')で都市idを取得し、辞書のキー・バリューとして保管
  6. 都市名:都市idの辞書を返す

以上で、GetPlaceDataオブジェクトを生成し、get_cityメソッドを実行すれば、都市名と都市idを辞書型で取得できるようになりました。

天気予報データ収集用 GetWeatherDataクラス

このクラスでは、idを入力した都市の天気予報情報を取得する機能を持たせます。

オブジェクト生成時にidを渡してその地点の天気予報情報を読込ませ、get_weatherメソッドで天気、最高気温、最低気温を取得します。

また、show_weatherメソッドで天気予報の結果を出力させます。

コード

class GetWeatherData():
    '''
    livedoorのAPIから、天気予報データを取得するクラス
    '''
    url = 'http://weather.livedoor.com/forecast/webservice/json/v1'   #1

    def __init__(self, city_id):
        place = {'city' : city_id}   #2
        self.weather_data = requests.get(self.url, place).json()   #3

    def get_weather(self):
        self.weather = []                  #天気(晴れ、雨など)
        self.temperature_max = []   #最高気温
        self.temperature_min = []   #最低気温
        
        for w in self.weather_data['forecasts']:   #4
            self.weather.append(w['telop'])   #5

            try:
                self.temperature_max.append(' max ' + w['temperature']['max']['celsius'] + '℃ ')   #6
            except:
                self.temperature_max.append(' max - ℃ ')
            
            try:
                self.temperature_min.append(' min ' + w['temperature']['min']['celsius'] + '℃ ')   #7
            except:
                self.temperature_min.append(' min - ℃ ')

    def show_weather(self):   #8
        self.get_weather()
        date = ['今日 ', '明日 ', '明後日 ']
        r = ''
        for i in range(3):
            try:
                r = r + date[i] + self.weather[i] + self.temperature_max[i] + self.temperature_min[i] + '\n'
            except:
                continue
        return(r)

コードの解説

  1. 天気予報APIの基礎アドレス
  2. 「city」をキー、「都市id」をバリューとした辞書を定義
  3. 天気予報APIの基礎アドレスに、2の辞書を連結させて都市の天気予報を読み込む(JSON形式)
  4. 読み込んだ天気予報情報から、「forecasts」キーで3日間の天気予報を抜き取る
  5. 「telop」キーで天気(晴れ、雨など)を取得
  6. 「temperature」「max」「celsius」キーで、最高気温を℃で取得(データがない場合は「-」にする)
  7. 「temperature」「min」「celsius」キーで、最低気温を℃で取得(データがない場合は「-」にする)
  8. get_weatherメソッドを実行し、結果を見やすくして返すメソッド

以上で、GetWeatherDataに都市idを渡してオブジェクトを生成すれば、get_weatherメソッドを用いて天気予報を取得できるようになりました。

動作テスト

最後に、if __name__ == "__main__"部にテスト用のコードを書いて、実際に動作させてみます。

例として、ユーザーが「函館」と入力したときに天気予報が取得できるか確認します。

コード

if __name__ == "__main__":
    input_text = '函館'   #1
    city_data = GetPlaceData()   #2
    city_dict = city_data.get_city()   #3

    if input_text in city_dict:   #4
        r = GetWeatherData(city_dict[input_text])   #5
        reply_text = r.show_weather()   #6
        print(reply_text)   #7

コードの解説

  1. ユーザーの入力。
  2. GetPlaceDataオブジェクトを生成
  3. 都市とidを辞書型で取得
  4. ユーザーの入力した都市が、取得した辞書内にあるか確認
  5. GetWeatherDataオブジェクトを生成
  6. 入力した都市の天気予報情報を取得
  7. 天気予報を表示

完成したコードを実行して動作確認をしてみましょう。

事前にpipenv shellを実行して仮想環境に入った後、python weathergetter.pyを実行します。

以下の様な結果が出力されれば成功です。当たり前ですが、天気予報取得のタイミングで天気予報と気温は変化します。

今日 晴のち曇 max - ℃  min - ℃
明日 曇のち晴 max 6℃  min 0℃
明後日 晴時々曇 max - ℃  min - ℃

次は、Herokuへデプロイしてアプリを完成させます。

コード全文

import bs4
import requests

class GetPlaceData():
    '''
    全国の地点定義表から、都市名とidを取得するクラス
    '''
    def __init__(self):
        '''
        全国地点定義表を読み込む
        '''
        path = 'http://weather.livedoor.com/forecast/rss/primary_area.xml'
        self.res = requests.get(path)

    def get_city(self):
        '''
        都市名とidを辞書型で取得
        '''
        city_list = {}
        b = bs4.BeautifulSoup(self.res.text, 'xml').select('city')

        for city in b:
            city_list[city.get('title')] = city.get('id')
        
        return(city_list)

class GetWeatherData():
    '''
    livedoorのAPIから、天気予報データを取得するクラス
    '''
    url = 'http://weather.livedoor.com/forecast/webservice/json/v1'

    def __init__(self, city_id):
        place = {'city' : city_id}
        self.weather_data = requests.get(self.url, place).json()

    def get_weather(self):
        self.weather = []           #天気(晴れ、雨など)
        self.temperature_max = []   #最高気温
        self.temperature_min = []   #最低気温
        
        for w in self.weather_data['forecasts']:
            self.weather.append(w['telop'])

            try:
                self.temperature_max.append(' max ' + w['temperature']['max']['celsius'] + '℃ ')
            except:
                self.temperature_max.append(' max - ℃ ')
            
            try:
                self.temperature_min.append(' min ' + w['temperature']['min']['celsius'] + '℃ ')
            except:
                self.temperature_min.append(' min - ℃ ')
    
    def show_weather(self):
        self.get_weather()
        date = ['今日 ', '明日 ', '明後日 ']
        r = ''
        for i in range(3):
            try:
                r = r + date[i] + self.weather[i] + self.temperature_max[i] + self.temperature_min[i] + '\n'
            except:
                continue
        return(r)


if __name__ == "__main__":
    input_text = '函館'
    city_data = GetPlaceData()
    city_dict = city_data.get_city()

    if input_text in city_dict:
        r = GetWeatherData(city_dict[input_text])
        reply_text = r.show_weather()
        print(reply_text)