読者です 読者をやめる 読者になる 読者になる

すこしふしぎ.

VR/HI系院生による技術ブログ.まったりいきましょ.(友人ズとブログリレー中.さぼったら焼肉おごらなきゃいけない)

降水確率を取得しようとしたら案外手間取った話

python XML

こんにちは.1000chです. 気づけば2月に入り,だんだんと寒さが軽減されてきましたね. pythonでtwitterAPIをいじれるようになったので,天気予報botっぽいものを作ろうと思いました. 自分,気温とかは余り気にしないのですが,降水確率だけ気になったりします. なので,朝になったらフォロワーに今日の降水確率をtweetするbotを作ることを目指します.

とまぁ降水確率だけわかれば簡単じゃね?と思ったのですが,案外降水確率を得るところで苦労したので今回はそのお話です. (振り返ってみるとXMLパーサの使い方がメインになってましたwまあいいやw)

お天気APIしらべ

まずは降水確率を教えてくれそうなAPIを探します. 趣味利用なので,とりあえず無料のもので探します.

「天気 API」でぐぐると,さっそく天気予報のAPIをまとめてくださっている記事を見つけました. Hello API - 天気予報APIまとめ

このなかだとweather hacksrenki RSSYahoo!天気・災害RSSあたりが無料で利用できますね. しかしこれら,気温情報などは取得できるのですが,よくよく見ると降水確率はデータの中に入っていません. 天気予報で大事なのは降水確率だと思っていた人間からするとちょっとカルチャーショックです... Yahoo!APIとか登録してから気づいたよ^^

気を取り直して,今度は「降水確率 API」でぐぐります. そして見つけたのがこちらです. drk7.jp - 気象庁の天気予報情報をXMLで配信 なんでも気象庁のデータを独自で集めて公開していらっしゃるそうです. さっそくわが埼玉のデータを見ると,きちんと降水確率のデータが入っています!

f:id:ism1000ch:20140203224216p:plain

降水確率って"rainfall chance"っていうんですね.初めて知りました. ともあれ,このデータをうまいこと取得することを目指します.

データの取得 - urllib2でGET

さて,ここで「データをどのように取得すればよいのだろう?」という問題が発生しました. 自分が知っているAPI

GET statuses/home_timeline

みたいな,パラメータ込みでGETやらPOSTやらするとデータが返ってくる,というものです. APIリファレンスを見て初めて使い方を理解しておりました. しかし今回のdrk7.jpではサイト上でデータが公開されているだけです.

うむ.どうすればよいのだ.

とりあえず,埼玉県のデータを公開しているページに向かってGETを送り,どのようなデータが返ってくるのか見てみます.

In [1]: import urllib2 as u2

In [2]: url = "http://www.drk7.jp/weather/xml/11.xml"

In [3]: res = u2.urlopen(url)

In [4]: tx = res.read
res.read       res.readline   res.readlines

In [4]: tx = res.read()

In [5]: print tx
<?xml version="1.0" encoding="UTF-8"?>
<weatherforecast>
<title>weather forecast xml</title>
<link>http://www.drk7.jp/weather/xml/11.xml</link>
<description>気象庁の天気予報情報を XML で配信。11回 AM 6:00 ごろ更新。</description>
<pubDate>Mon, 3 Feb 2014 18:00:02 +0900</pubDate>

# 中略

    <info date="2014/02/03">
        <weather>晴れのちくもり</weather>
        <img>http://www.drk7.jp/MT/images/MTWeather/707.gif</img>
        <weather_detail>南東の風 のち 北西の風 やや強く 晴れ 夜 くもり</weather_detail>
        <wave>--</wave>
        <temperature unit="摂氏">
        <range centigrade="max">16</range>
        <range centigrade="min">4</range>
        </temperature>
        <rainfallchance unit="%">
        <period hour="00-06">0</period>
        <period hour="06-12">0</period>
        <period hour="12-18">10</period>
        <period hour="18-24">20</period>

はじめrequestsを使ったんですが,なぜか日本語部分が文字化けしてしまったため,今回はurllib2を利用しました.(エンコードのせいなのかなぁとは思うのですが,理由はよくわからんです.最近python使うとちょくちょくマルチバイト周りで困らせられ,その都度逃げていますw いずれキチンと理由を勉強しなくては.ぶっちゃけ今回ココが一番詰まったとか笑えない)

さて受信したデータを見てみると,txの中にxmlが文字列としてそのまま入っています. これならパースして降水確率のデータを取得できるかも.

データの取得 - XMLパース

pythonでのXMLのパースといえば標準ライブラリのElementTreeを使うのが一般的でしょうか. 前にもちょこっと触ったことあるんですが,これ個人的にちょっと使いづらいです.. つわけで復習がてら取得したtextをパースして降水確率の取得をやってみます.参考にするのはオフィシャルのここです.

In [34]: import xml.etree.ElementTree as ET

#文字列からツリーのルート取得
In [35]: tree = ET.fromstring(tx)

In [36]: tree
Out[36]: <Element 'weatherforecast' at 0x11040e5d0>

#ルートのタグ
In [37]: tree.tag
Out[37]: 'weatherforecast'

#子のタグ・属性の確認
In [38]: for child in tree:
   ....:     print child.tag,child.attrib
   ....:
title {}
link {}
description {}
pubDate {}
author {}
managingEditor {}
pref {'id': u'\u57fc\u7389\u770c'} #unicode文字化け?

# 子の確認はこんな感じでもできる
In [55]: tree.getchildren() #3.2で撤廃
Out[55]:
[<Element 'title' at 0x11040e610>,
 <Element 'link' at 0x11040e690>,
 <Element 'description' at 0x11040e6d0>,
 <Element 'pubDate' at 0x11040e710>,
 <Element 'author' at 0x11040e750>,
 <Element 'managingEditor' at 0x11040e790>,
 <Element 'pref' at 0x11040e7d0>]

In [57]: list(tree) #3.2からはこちらが推奨
Out[57]:
[<Element 'title' at 0x11040e610>,
 <Element 'link' at 0x11040e690>,
 <Element 'description' at 0x11040e6d0>,
 <Element 'pubDate' at 0x11040e710>,
 <Element 'author' at 0x11040e750>,
 <Element 'managingEditor' at 0x11040e790>,
 <Element 'pref' at 0x11040e7d0>]

## "rainfallchance"というtagを探そう!

# イテレータを使う

# イテレータ取得
In [93]: it = tree.iter()

In [94]: it
Out[94]: <generator object iter at 0x11045aa50>

# 進めてみる
In [95]: it.next()
Out[95]: <Element 'weatherforecast' at 0x11040e5d0>

In [96]: it.next()
Out[96]: <Element 'title' at 0x11040e610>

In [97]: it.next()
Out[97]: <Element 'link' at 0x11040e690>

In [98]: it.next()
Out[98]: <Element 'description' at 0x11040e6d0>

# イテレータ回してrainfallchanceを表示してみる
In [99]: for i in tree.iter():
                if i.tag == "info":
                    print i.attrib,
                if i.tag == "rainfallchance":
                    print i.tag
                if i.tag == "period":
                    print i.attrib,i.text
   ....:
{'date': '2014/02/03'} rainfallchance
{'hour': '00-06'} 0
{'hour': '06-12'} 0
{'hour': '12-18'} 10
{'hour': '18-24'} 20
{'date': '2014/02/04'} rainfallchance
{'hour': '00-06'} 20
{'hour': '06-12'} 30
{'hour': '12-18'} 60
{'hour': '18-24'} 30
{'date': '2014/02/05'} rainfallchance
{'hour': '00-06'} 20
{'hour': '06-12'} 20
{'hour': '12-18'} 20
{'hour': '18-24'} 20
# 以下略
 

...自分がelementEtree苦手なのはイテレータの使い方に慣れていないせいだということが判明しました.これもそのうち勉強しよう.

list(tree)で取得できるエレメントは子要素だけで孫要素が取れなかったので,かなり下のレイヤーにいるrainfallchanceタグを見つけるに祭してはtree.iter()イテレータ取得してループぶん回すことにしました.毎度タグを比較して,取得したい情報のときだけprintとしているのですが,正しい使い方かは甚だ疑問ですね..

リファレンスページ少し見てみると,XPathという表記方法でツリー中の検索ができると書いてありました. ルールは次のような感じ.

表記 意味
tag名 対応するタグを持つノード
. 現在ノード
.. 親ノード
a/b ノードaの子ノードb
* 現在ノードの子ノード.孫は含まない
// 現在ノードの子ノードすべて.孫も含む
[@attribute名] 対応する属性を持つノード
[@attribute名='value'] 対応する属性が指定した値であるノード
[tag名] 対応するタグを子に持つノード
[(int)position] 対応する位置にあるノード.負値で後方からのインデックス指定が可能

これをfind(), findall()の引数として利用できます. find()は対応するノードが見つかったらそれを一つ返し,findall()は対応するノードのリストを返してくれます.

使ってみるとこんな感じ.

In [100]: list(tree)
Out[100]:
[<Element 'title' at 0x11040e610>,
 <Element 'link' at 0x11040e690>,
 <Element 'description' at 0x11040e6d0>,
 <Element 'pubDate' at 0x11040e710>,
 <Element 'author' at 0x11040e750>,
 <Element 'managingEditor' at 0x11040e790>,
 <Element 'pref' at 0x11040e7d0>]

# tag名でfind
In [101]: tree.find("pref")
Out[101]: <Element 'pref' at 0x11040e7d0>

In [102]: tree.find("pref/area")
Out[102]: <Element 'area' at 0x11040e810>

# 子ノード(find)
In [103]: tree.find("*")
Out[103]: <Element 'title' at 0x11040e610>

# 子ノード(findall)
In [104]: tree.findall("*")
Out[104]:
[<Element 'title' at 0x11040e610>,
 <Element 'link' at 0x11040e690>,
 <Element 'description' at 0x11040e6d0>,
 <Element 'pubDate' at 0x11040e710>,
 <Element 'author' at 0x11040e750>,
 <Element 'managingEditor' at 0x11040e790>,
 <Element 'pref' at 0x11040e7d0>]

# 現在ノード
In [105]: tree.findall(".")
Out[105]: [<Element 'weatherforecast' at 0x11040e5d0>]

# 現在ノード以下すべての子要素
In [106]: tree.findall(".//")
Out[106]:
[<Element 'title' at 0x11040e610>,
 <Element 'link' at 0x11040e690>,
 <Element 'description' at 0x11040e6d0>,
 <Element 'pubDate' at 0x11040e710>,
 <Element 'author' at 0x11040e750>,
 <Element 'managingEditor' at 0x11040e790>,
 <Element 'pref' at 0x11040e7d0>,
 <Element 'area' at 0x11040e810>,
 <Element 'geo' at 0x11040e850>,
 <Element 'long' at 0x11040e890>,

# 中略

# 現在ノード以下,"rainfallchance"タグのノードリスト
In [107]: tree.findall(".//rainfallchance")
Out[107]:
[<Element 'rainfallchance' at 0x11040eb10>,
 <Element 'rainfallchance' at 0x11040ee50>,
 <Element 'rainfallchance' at 0x110412190>,
 <Element 'rainfallchance' at 0x110412490>,
 <Element 'rainfallchance' at 0x110412790>,
 <Element 'rainfallchance' at 0x110412a90>,
 <Element 'rainfallchance' at 0x110412d90>,
 <Element 'rainfallchance' at 0x110414210>,
 <Element 'rainfallchance' at 0x110414550>,
 <Element 'rainfallchance' at 0x110414850>,
 <Element 'rainfallchance' at 0x110414b50>,
 <Element 'rainfallchance' at 0x110414e50>,
 <Element 'rainfallchance' at 0x110419190>,
 <Element 'rainfallchance' at 0x110419490>,
 <Element 'rainfallchance' at 0x1104198d0>,
 <Element 'rainfallchance' at 0x110419c10>,
 <Element 'rainfallchance' at 0x110419f10>,
 <Element 'rainfallchance' at 0x11041c250>,
 <Element 'rainfallchance' at 0x11041c550>,
 <Element 'rainfallchance' at 0x11041c850>,
 <Element 'rainfallchance' at 0x11041cb50>]

# prefタグノード
In [108]: tree.findall(".//pref")
Out[108]: [<Element 'pref' at 0x11040e7d0>]

# prefタグノードのうち,id属性を持つもの(間違い)
In [110]: tree.findall(".//pref[id]")
Out[110]: []

# prefタグノードのうち,id属性を持つもの(正しい)
In [111]: tree.findall(".//pref[@id]")
Out[111]: [<Element 'pref' at 0x11040e7d0>]

# 全ノードのうち,id属性を持つもの(間違い)
In [112]: tree.findall(".//[@id]")
  File "<string>", line unknown
SyntaxError: invalid descendant

# 全ノードのうち,id属性を持つもの(正しい)
In [113]: tree.findall(".//*[@id]")
Out[113]:
[<Element 'pref' at 0x11040e7d0>,
 <Element 'area' at 0x11040e810>,
 <Element 'area' at 0x110412ed0>,
 <Element 'area' at 0x1104195d0>]

# infoタグ・date属性が2014/02/03のもの
# (北部・南部・秩父の3つらしい)
In [124]: tree.findall(".//info[@date='2014/02/03']")
Out[124]:
[<Element 'info' at 0x11040e910>,
 <Element 'info' at 0x110412fd0>,
 <Element 'info' at 0x1104196d0>]

# 上の全子要素
In [126]: tree.findall(".//info[@date='2014/02/03']//")
Out[126]:
[<Element 'weather' at 0x11040e950>,
 <Element 'img' at 0x11040e990>,
 <Element 'weather_detail' at 0x11040e9d0>,
 <Element 'wave' at 0x11040ea50>,
 <Element 'temperature' at 0x11040ea10>,
 <Element 'range' at 0x11040ea90>,
 <Element 'range' at 0x11040ead0>,
 <Element 'rainfallchance' at 0x11040eb10>,
 <Element 'period' at 0x11040eb50>,
 <Element 'period' at 0x11040eb90>,
 <Element 'period' at 0x11040ebd0>,
 <Element 'period' at 0x11040ec10>,
 <Element 'weather' at 0x110414050>,
# 略

# 上のうちrainchanceタグのもの
In [128]: tree.findall(".//info[@date='2014/02/03']//rainfallchance")
Out[128]:
[<Element 'rainfallchance' at 0x11040eb10>,
 <Element 'rainfallchance' at 0x110414210>,
 <Element 'rainfallchance' at 0x1104198d0>]

# areaタグのうち0番目/ infoタグでdateが"2014/02/03" / 降水確率情報取得
In [129]: tree.findall(".//area[0]/info[@date='2014/02/03']/rainfallchance/period")
Out[129]:
[<Element 'period' at 0x110419910>,
 <Element 'period' at 0x110419950>,
 <Element 'period' at 0x110419990>,
 <Element 'period' at 0x1104199d0>]

# 得られたデータをprint
In [130]: for i in tree.findall(".//area[0]/info[@date='2014/02/03']//period"):
    print i.attrib,i.text
   .....:
{'hour': '00-06'} 0
{'hour': '06-12'} 0
{'hour': '12-18'} 10
{'hour': '18-24'} 20

細かい機能確認から,目標であった降水確率の取得までやってみました. XPath利用ができると,「まずこのタグでつぎはこのタグのうち属性はこれで〜」みたいなことができるようになりますね.

イテレータ使ってがんばるよりかは,こちらのほうが楽に使えると思いますw

まとめ

長くなった.つかれた.