ある地域の天気予報を取得するスクリプトを書いてみた

はじめに

ちょっと天気予報データを集めることになったので、スクリプトを組んでみました。Python(v2.6.5)で。

BeautifulSoupのインストール

PythonでHTMLをパースする方法について調べてるとBeautifulSoupというライブラリが良さげだったので、これを使ってスクリプトを書くことにしました。

まずはアーカイブをダウンロードして展開します。

 % wget http://www.crummy.com/software/BeautifulSoup/download/3.x/BeautifulSoup-3.0.8.1.tar.gz
 % tar zxf BeautifulSoup-3.0.8.1.tar.gz
 % ls BeautifulSoup-3.0.8.1
BeautifulSoup.py  BeautifulSoupTests.py  PKG-INFO  setup.py

無事に展開できたらsetup.pyを使ってインストールするか、以下のスクリプトと同じ階層にBeautifulSoup.pyを置いてください。

parser.py

以下のスクリプトをparser.pyという名前で保存してください。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import re
import urllib2
from BeautifulSoup import BeautifulSoup
import unicodedata

def p (unicodeStr):
    '''
    unicode文字列をutf-8にエンコードして出力する。
    '''
    print unicodeStr.encode("utf-8")


def parseWeatherForecast (_htmlData):
    '''
    引数で受け取った天気予報のHTMLをパースして返す
    '''

    # HTMLパースの準備
    soup = BeautifulSoup(_htmlData)

    # 対象となる地方や都道府県名の取得
    m = re.match(u"天気予報\n: (.*)", soup.find("h1").contents[0])
    regionName = m.group(1)
    #p(regionName)    # 例:"沖縄本島地方"
    
    # 発表日時、気象台名、天気概況を取り出す
    textframeObj = soup.find("pre", "textframe")
    tmp = textframeObj.contents[0].split("\n")[1]
    # 例:平成22年9月26日04時41分 沖縄気象台発表
    pattern = u"平成([0-9]+)年([0-9]+)月([0-9]+)日([0-9]+)時([0-9]+)分 (.*)発表"
    m = re.match(pattern, tmp)
    tmp = m.groups()  # 例:(u"22", u"9", u"26", u"04", u"41", u"沖縄気象台")
    observatoryName = tmp[-1]   # 気象台の名前を取り出す(例:u"沖縄気象台")
    # 発表日時の全角数字を半角に変換する
    ymdhmArray = [int(unicodedata.normalize("NFKC", i)) for i in tmp[:-1]]
    ymdhmArray[0] += 1988       # 和暦(平成[\d+]年)を西暦に変換
    announceTime  = "/".join([str(x) for x in ymdhmArray[:3]]) + " "
    announceTime += ":".join([str(x) for x in ymdhmArray[3:]])
    #print announceTime         # 例:"2010/9/26 4:41"
    # 天気概況を取り出す
    summary = unicode(textframeObj.renderContents(), "utf-8")
    #p(summary)

    # 各地域の気象予報データをまとめたtableオブジェクトを取り出す
    tableObj = soup.find("table", id="forecasttablefont")
    # tableタグの子要素であるtrタグのリストを作成
    trList = []
    [trList.append(child) for child in tableObj if child != "\n"]  # 改行文字だけの要素は無視
    # 地域ごとにオブジェクトを分ける
    areaObjParts = []
    for i in range(len(trList)/4):
        areaObjParts.append(trList[i*4:(i+1)*4])
    
    # 各地域の気象予報データを取り出して配列に入れる
    areaParts = []
    for areaObj in areaObjParts:
        name = areaObj[0].div.contents[0]
        periodParts = parseEachPeriod(areaObj[1:])
        areaParts.append({"name": name,
                          "periodParts": periodParts})

    # パースしたデータを一つにまとめる
    parsedData = {"regionName": regionName,
                  "announceTime": announceTime,
                  "observatoryName": observatoryName,
                  "areaParts": areaParts,
                  "summary": summary}

    return parsedData

    
def parseEachPeriod (_areaObj):
    '''
    引数で受け取った地域のHTMLから今日(今夜)、明日、明後日ごとの
    予報データ(天気、降水確率、気温など)をパースして返す
    '''

    periodParts = []
    for periodObj in _areaObj:  # 今日、明日、明後日の予報データを順にパースする
        periodData = {}
        day_code = periodObj.find("th", "weather")
        # 日付を取り出す 例:今日26日、明日27日、明後日28日
        m = re.match(u"\n(今日|今夜|明日|明後日)(\d+)日", day_code.contents[0])
        periodData["day"] = m.group(2)
        # 天気コードを取り出す
        if day_code.img:
            m = re.match("img\/(\d+)\.png", day_code.img["src"])
            periodData["code"] = m.group(1)
        else: periodData["code"] = ""
        # 天気、風、波の予報を取り出す
        WEATHER_NAME_LIST = [u"晴れ", u"くもり", u"煙霧", u"砂じんあらし", u"地ふぶき",
                u"霧", u"霧雨", u"雨", u"みぞれ", u"雪", u"あられ", u"ひょう", u"雷"]
        infoObj = periodObj.find("td", "info")
        wind_weather_wave = unicode(infoObj.renderContents(), "utf-8")
        wind_weather, wave = wind_weather_wave.split("<br />")
        periodData["wave"] = wave if wave != "\n" else ""
        windWords = []
        weatherWords = []
        flag = False
        for word in wind_weather.split(" "):
            if flag:
                weatherWords.append(word)
            else:
                if word in WEATHER_NAME_LIST:
                    flag = True
                    weatherWords.append(word)
                else:
                    windWords.append(word)
        periodData["weather"] = " ".join(weatherWords)
        periodData["wind"] = " ".join(windWords)
        # 降水確率の予報を取り出す
        rainObj = periodObj.find("table", "rain")
        percentList = []
        for td in rainObj.findAll("td", align="right"):
            m = re.match(u"(\d+)%", td.contents[0])
            percentList.append(m.group(1) if m else "")
        periodData["rain"] = percentList[:]
        # 気温の予報(都市名、最高・最低気温)を取り出す
        tmpObj = periodObj.find("td", "city")
        cityName = tmpObj.contents[0] if tmpObj else ""
        lowest = ""
        minObj = periodObj.find("td", "min")
        if minObj and minObj.string:
            m = re.match(u"(\d+)度", minObj.string)
            lowest = m.group(1) if m else ""
        highest = ""
        maxObj = periodObj.find("td", "max")
        if maxObj and maxObj.string:
            m = re.match(u"(\d+)度", maxObj.string)
            highest = m.group(1) if m else ""
        periodData["temperature"] = {"cityName": cityName,
                                     "lowest": lowest,
                                     "highest": highest}
        periodParts.append(periodData)

    return periodParts


def printWeatherForecast (_weatherForecast):
    '''
    引数で受け取った天気予報データを見やすく整形して出力する
    '''

    info = _weatherForecast
    p(u"天気予報:" + info["regionName"])
    p(u"発表日時:" + info["announceTime"])
    p(u"観測所 :" + info["observatoryName"])
    print ""
    for area in info["areaParts"]:   # 各地域ごとに
        p(area["name"])
        for period, dayName in zip(area["periodParts"], [u"今日", u"明日", u"明後日"]):
            p("\t" + dayName + period["day"] + u"日")
            p("\t\t" + u"天気コード:" + period["code"])
            p("\t\t" + u"天気予報:" + period["weather"])
            p("\t\t" + u"風の予報:" + period["wind"])
            p("\t\t" + u"波の予報:" + period["wave"])
            p("\t\t" + u"降水確率:")
            for k, v in zip(["00-06", "06-12", "12-18", "18-24"], period["rain"]):
                p("\t\t\t" + k + ":" + v + "%")
            p("\t\t" + u"気温予報:" + period["temperature"]["cityName"])
            p("\t\t\t" + u"最低:" + period["temperature"]["lowest"] + u"度")
            p("\t\t\t" + u"最高:" + period["temperature"]["highest"] + u"度")
        print ""
    print "=" * 50
    p(info["summary"])


def main ():
    BASE_URL = "http://www.jma.go.jp/jp/yoho/"
    TARGET_NUM = 353  # [301-356]
    TARGET_FILE = str(TARGET_NUM) + ".html"

    # HTMLデータの取得
    userAgent = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)"
    request = urllib2.Request(BASE_URL + TARGET_FILE)
    request.add_header("User-Agent", userAgent)
    respons = urllib2.urlopen(request)
    htmlData = respons.read()
    htmlTimestamp = respons.info()["Last-Modified"]
    #print htmlTimestamp

    # HTMLデータから気象予報データを取り出す
    result = parseWeatherForecast(htmlData)

    # 気象予報データを整形して出力する
    printWeatherForecast(result)


if __name__ == "__main__": main()

このスクリプトでは、現在の沖縄本島地方の天気予報(http://www.jma.go.jp/jp/yoho/353.html)を取得しますが、main()内のTARGET_NUMの値を301から356の間で変化させることで他の地域の天気予報を取得することができます。実際に実行すると、以下のように沖縄本島地方の天気予報データが出力されはずです。公開されていない天気予報データの部分は、空文字列を出力します。

 % python parser.py
天気予報:沖縄本島地方
発表日時:2010/10/1 13:42
観測所 :沖縄気象台

本島中南部
	今日1日
		天気コード:210
		天気予報:くもり 夜 晴れ 所により 夕方 まで 雷
		風の予報:北東の風 後 東の風
		波の予報:波 1.5メートル
		降水確率:
			00-06:%
			06-12:%
			12-18:30%
			18-24:20%
		気温予報:那覇
			最低:度
			最高:30度
	明日2日
		天気コード:110
		天気予報:晴れ 昼過ぎ から くもり
		風の予報:東の風
		波の予報:波 1.5メートル
		降水確率:
			00-06:10%
			06-12:10%
			12-18:20%
			18-24:20%
		気温予報:那覇
			最低:25度
			最高:31度
	明後日3日
		天気コード:201
		天気予報:くもり 時々 晴れ
		風の予報:東の風 後 北西の風
		波の予報:波 1.5メートル
		降水確率:
		気温予報:
			最低:度
			最高:度

本島北部
	今日1日
		天気コード:210
		天気予報:くもり 夜 晴れ 所により 夕方 まで 雷
		風の予報:北東の風 後 東の風
		波の予報:波 1.5メートル
		降水確率:
			00-06:%
			06-12:%
			12-18:30%
			18-24:20%
		気温予報:名護
			最低:度
			最高:30度
	明日2日
		天気コード:112
		天気予報:晴れ 後 くもり 夕方 一時 雨
		風の予報:東の風
		波の予報:波 1.5メートル
		降水確率:
			00-06:10%
			06-12:10%
			12-18:50%
			18-24:20%
		気温予報:名護
			最低:25度
			最高:31度
	明後日3日
		天気コード:201
		天気予報:くもり 時々 晴れ
		風の予報:東の風 後 北西の風
		波の予報:波 1.5メートル
		降水確率:
		気温予報:
			最低:度
			最高:度

久米島
	今日1日
		天気コード:210
		天気予報:くもり 夜 晴れ 所により 夕方 まで 雷
		風の予報:北東の風 後 東の風
		波の予報:波 1.5メートル
		降水確率:
			00-06:%
			06-12:%
			12-18:30%
			18-24:20%
		気温予報:久米島
			最低:度
			最高:30度
	明日2日
		天気コード:100
		天気予報:晴れ 明け方 から 朝 くもり
		風の予報:東の風
		波の予報:波 1.5メートル
		降水確率:
			00-06:20%
			06-12:20%
			12-18:10%
			18-24:10%
		気温予報:久米島
			最低:25度
			最高:30度
	明後日3日
		天気コード:201
		天気予報:くもり 時々 晴れ
		風の予報:東の風 後 北西の風
		波の予報:波 1.5メートル
		降水確率:
		気温予報:
			最低:度
			最高:度

==================================================
天気概況
平成22年10月1日13時42分 沖縄気象台発表

<b>沖縄本島地方では、大気の状態が不安定となっているため、1日夕方ま
で発達した積乱雲の下での落雷や突風、急な強い雨に十分注意して下さい。
</b>
 沖縄地方は、高気圧に覆われておおむね晴れていますが、沖縄本島地方で
は、大気の状態が不安定なため、にわか雨の所があります。

 1日は、沖縄地方は、高気圧に覆われておおむね晴れますが、沖縄本島地
方では、大気の状態が不安定なため、所によってはにわか雨か雷雨となる見
込みです。

 2日は、沖縄地方は、高気圧に覆われておおむね晴れますが、所によって
はにわか雨があるでしょう。

 沖縄本島地方では、1日夕方まで発達した積乱雲の下での落雷や突風、急
な強い雨に注意して下さい。

 沖縄地方の沿岸の海域では、波がやや高いでしょう。

おわりに

このスクリプトでは晴れ、雨、くもりのような日常的な天気予報に対してはテストしましたが、台風や降雪のような天気予報に対してはまだテストしてないので、ちゃんと動作するかは未確認です。

あと、TARGET_NUMの値を変化させれば全国の天気予報データが取得できますが、気象庁のFAQには、

天気予報やレーダーの画像、地震情報などを巡回ソフトを使って自動的に取得したい。

防災気象情報のページは、通常のブラウザで閲覧することを前提に各種情報を掲載しております。
自動巡回ソフト等による、定期的、自動的な気象データの収集等は、サーバーに負荷がかかる等の理由から、原則としてご遠慮いただいております。
ご理解お願いします。

と書かれているので、クローラーなどを作る場合は自己責任でね☆(ゝω・)v キャピ

チラシ裏

実は、わざわざスクリプトを書かなくても気象データを取得する方法があります。財団法人気象業務支援センターという所が、気象庁が集めた気象データの配信を行っているので、ここに気象データの配信申請を出せば良いみたいです。有料ですが…。ちなみに、料金表はココに書いてあります。
最近はこういうXMLでの配信も行っているらしいですが、月に数万も払えないのでスクリプトを書きました(´・ω・`)ショボーン


あと、Pythonでの日本語の扱い方がよくわかんないです。ググって見つけた、事前にsitecustomize.pyというファイルを作成して

import sys
sys.setdefaultencoding('utf-8')

という記述をしておく、という方法が何か嫌でした。なので日本語の文字列は、unicode型に変換してからいじくって(正規表現でパターンマッチとか)、出力する直前でunicode.encode()メソッドを使うって感じにしました。どんな方法が一番良いんですかね(-_-;)