皆様おはようございます。以前、天気予報をスクレイピング しましたが、htmlのパースはソースが書き換えられると非常に厄介なのでどうしたものかと思っていたところ、livedoorがWeather Hacks(気象データ配信サービス) を提供しているので、こちらを利用してお天気Hackすることにしました。
お天気Webサービス仕様 によると、基本URL+地域のIDの形式で気象データをjson形式で取得できるとのこと。地域のID (パラメータ名:city) は全国の地点定義表(RSS) 内の「1次細分区(cityタグ)」のidがそれにあたるそうです。というわけで、順序としては
- 全国の地点定義表(RSS) のXMLから取得したい地方のidを抜き出す
- 抜き出したidを引数に与えてJSONデータを取得する
となります。
■ 1. 全国の地点定義表(RSS) のXMLから取得したい地方のidを抜き出す
WEBから取得するXMLデータはある程度綺麗な前提(ということになっている)ですが、xmllintで整形しなおします。ファイルに出力するのが嫌なので、標準出力を食わせることにします。
$ AREANAME=東京 ; xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" <city title="東京" id="130010" source="http://weather.livedoor.com/forecast/rss/area/130010.xml"/>
$ AREANAME=札幌 ; xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" <city title="札幌" id="016010" source="http://weather.livedoor.com/forecast/rss/area/016010.xml"/>
$ AREANAME=横浜 ; xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" <city title="横浜" id="140010" source="http://weather.livedoor.com/forecast/rss/area/140010.xml"/>
こんな感じでよろしくやってくれました。XMLデータから必要な情報はidだけなので、欲しい列で検索してもよいのですが、スペースを改行に置換して「id」で始まる行を抜き出すことにします。まずはスペースを改行に置換して改行以外の行を表示してみましょう。
$ AREANAME=東京 ; xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | egrep . <city title="東京" id="130010" source="http://weather.livedoor.com/forecast/rss/area/130010.xml"/>
欲しい列で検索しなかった理由ですが、もし「city title」がある日「city_title」のように変更されてしまうとそこで列が変わってしまうので、強制的に改行にしてしまっています。仮に「id="nnnnnn"」が「id = "nnnnnn"」になっていても xmllint がよろしく直してくれますが、キー名は人間が自由につけられるものなので、列で区切ろうとするとスペースの有無はどうしても考慮しなければならず、だったら行に直してしまったほうがよいという判断です。キー「id」と値の間をスペースで離してみたものを xmllint に食わせてスペースを改行に置換するとこうなります。
$ xmllint --format <(echo ' <city title="東京" id = "130010" source="http://weather.livedoor.com/forecast/rss/area/130010.xml"/>') | sed -e "s/[[:space:]]/\n/g" <?xml version="1.0"?> <city title="東京" id="130010" source="http://weather.livedoor.com/forecast/rss/area/130010.xml"/>
さて、東京のidを抜き出しましょう。
$ AREANAME=東京 ; xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | sed "-ne /^id/p" | sed -e "s/id=//;s/\"//g" 130010
これでステップ1はおしまいです。
■ 2. 抜き出したidを引数に与えてJSONデータを取得する
idを抜き出せたので、次にJSONのデータを抜き出します。そのまま標準出力に出してしまうと人間が読みにくいので、 jqでパースすることにします。jqはfedoraなら標準リポジトリ、CentOSならepelリポジトリにあるので、適宜インストールしておきましょう。
$ AREANAME=東京 ; AREAID=$(xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | sed "-ne /^id/p" | sed -e "s/id=//;s/\"//g") ; curl -s "http://weather.livedoor.com/forecast/webservice/json/v1?city=${AREAID}" | jq '.' { "description": { "publicTime": "2015-12-24T04:38:00+0900", "text": " 東海道沖には、前線を伴った低気圧があって、発達しながら東に進んでい\nます。\n\n【関東甲信地方】\n 関東甲信地方は、雨または曇りとなっています。\n\n 24日は、低気圧が東海上に抜け、次第に冬型の気圧配置となるため、日\n中は晴れ間もありますが、気圧の谷や湿った空気の影響で雲が広がりやすく\n、朝晩を中心に雨の降る所があるでしょう。伊豆諸島では朝まで雨で雷を伴\nう所もある見込みです。\n\n 25日は、はじめ気圧の谷の影響により、曇りで雨の降る所がありますが\n、その後は冬型の気圧配置となりおおむね晴れるでしょう。長野県北部や関\n東北部の山沿いでは雪や雨が降る見込みです。\n\n 関東近海では、24日から25日にかけて、うねりを伴い波が高いでしょ\nう。船舶は、高波に注意してください。\n\n【東京地方】\n 24日は、曇りで昼過ぎに一時晴れますが、所により弱い雨が降るでしょ\nう。\n 25日は、曇りで昼前から晴れますが、所により明け方まで弱い雨が降る\nでしょう。" }, "title": "東京都 東京 の天気", "copyright": { "image": { "height": 26, "title": "livedoor 天気情報", "url": "http://weather.livedoor.com/img/cmn/livedoor.gif", "link": "http://weather.livedoor.com/", "width": 118 }, "title": "(C) LINE Corporation", "link": "http://weather.livedoor.com/", "provider": [ { "name": "日本気象協会", "link": "http://tenki.jp/" } ] }, "publicTime": "2015-12-24T05:00:00+0900", "location": { "prefecture": "東京都", "area": "関東", "city": "東京" }, "forecasts": [ { "image": { "height": 31, "title": "曇り", "url": "http://weather.livedoor.com/img/icon/8.gif", "width": 50 }, "temperature": { "max": { "fahrenheit": "57.2", "celsius": "14" }, "min": null }, "date": "2015-12-24", "telop": "曇り", "dateLabel": "今日" }, { "image": { "height": 31, "title": "曇のち晴", "url": "http://weather.livedoor.com/img/icon/12.gif", "width": 50 }, "temperature": { "max": { "fahrenheit": "60.8", "celsius": "16" }, "min": { "fahrenheit": "46.4", "celsius": "8" } }, "date": "2015-12-25", "telop": "曇のち晴", "dateLabel": "明日" } ], "link": "http://weather.livedoor.com/area/forecast/130010", "pinpointLocations": [ { "name": "千代田区", "link": "http://weather.livedoor.com/area/forecast/1310100" }, { "name": "中央区", "link": "http://weather.livedoor.com/area/forecast/1310200" }, { "name": "港区", "link": "http://weather.livedoor.com/area/forecast/1310300" }, { "name": "新宿区", "link": "http://weather.livedoor.com/area/forecast/1310400" }, { "name": "文京区", "link": "http://weather.livedoor.com/area/forecast/1310500" }, { "name": "台東区", "link": "http://weather.livedoor.com/area/forecast/1310600" }, { "name": "墨田区", "link": "http://weather.livedoor.com/area/forecast/1310700" }, { "name": "江東区", "link": "http://weather.livedoor.com/area/forecast/1310800" }, { "name": "品川区", "link": "http://weather.livedoor.com/area/forecast/1310900" }, { "name": "目黒区", "link": "http://weather.livedoor.com/area/forecast/1311000" }, { "name": "大田区", "link": "http://weather.livedoor.com/area/forecast/1311100" }, { "name": "世田谷区", "link": "http://weather.livedoor.com/area/forecast/1311200" }, { "name": "渋谷区", "link": "http://weather.livedoor.com/area/forecast/1311300" }, { "name": "中野区", "link": "http://weather.livedoor.com/area/forecast/1311400" }, { "name": "杉並区", "link": "http://weather.livedoor.com/area/forecast/1311500" }, { "name": "豊島区", "link": "http://weather.livedoor.com/area/forecast/1311600" }, { "name": "北区", "link": "http://weather.livedoor.com/area/forecast/1311700" }, { "name": "荒川区", "link": "http://weather.livedoor.com/area/forecast/1311800" }, { "name": "板橋区", "link": "http://weather.livedoor.com/area/forecast/1311900" }, { "name": "練馬区", "link": "http://weather.livedoor.com/area/forecast/1312000" }, { "name": "足立区", "link": "http://weather.livedoor.com/area/forecast/1312100" }, { "name": "葛飾区", "link": "http://weather.livedoor.com/area/forecast/1312200" }, { "name": "江戸川区", "link": "http://weather.livedoor.com/area/forecast/1312300" }, { "name": "八王子市", "link": "http://weather.livedoor.com/area/forecast/1320100" }, { "name": "立川市", "link": "http://weather.livedoor.com/area/forecast/1320200" }, { "name": "武蔵野市", "link": "http://weather.livedoor.com/area/forecast/1320300" }, { "name": "三鷹市", "link": "http://weather.livedoor.com/area/forecast/1320400" }, { "name": "青梅市", "link": "http://weather.livedoor.com/area/forecast/1320500" }, { "name": "府中市", "link": "http://weather.livedoor.com/area/forecast/1320600" }, { "name": "昭島市", "link": "http://weather.livedoor.com/area/forecast/1320700" }, { "name": "調布市", "link": "http://weather.livedoor.com/area/forecast/1320800" }, { "name": "町田市", "link": "http://weather.livedoor.com/area/forecast/1320900" }, { "name": "小金井市", "link": "http://weather.livedoor.com/area/forecast/1321000" }, { "name": "小平市", "link": "http://weather.livedoor.com/area/forecast/1321100" }, { "name": "日野市", "link": "http://weather.livedoor.com/area/forecast/1321200" }, { "name": "東村山市", "link": "http://weather.livedoor.com/area/forecast/1321300" }, { "name": "国分寺市", "link": "http://weather.livedoor.com/area/forecast/1321400" }, { "name": "国立市", "link": "http://weather.livedoor.com/area/forecast/1321500" }, { "name": "福生市", "link": "http://weather.livedoor.com/area/forecast/1321800" }, { "name": "狛江市", "link": "http://weather.livedoor.com/area/forecast/1321900" }, { "name": "東大和市", "link": "http://weather.livedoor.com/area/forecast/1322000" }, { "name": "清瀬市", "link": "http://weather.livedoor.com/area/forecast/1322100" }, { "name": "東久留米市", "link": "http://weather.livedoor.com/area/forecast/1322200" }, { "name": "武蔵村山市", "link": "http://weather.livedoor.com/area/forecast/1322300" }, { "name": "多摩市", "link": "http://weather.livedoor.com/area/forecast/1322400" }, { "name": "稲城市", "link": "http://weather.livedoor.com/area/forecast/1322500" }, { "name": "羽村市", "link": "http://weather.livedoor.com/area/forecast/1322700" }, { "name": "あきる野市", "link": "http://weather.livedoor.com/area/forecast/1322800" }, { "name": "西東京市", "link": "http://weather.livedoor.com/area/forecast/1322900" }, { "name": "瑞穂町", "link": "http://weather.livedoor.com/area/forecast/1330300" }, { "name": "日の出町", "link": "http://weather.livedoor.com/area/forecast/1330500" }, { "name": "檜原村", "link": "http://weather.livedoor.com/area/forecast/1330700" }, { "name": "奥多摩町", "link": "http://weather.livedoor.com/area/forecast/1330800" } ] }
綺麗に整形されていますが、すべてのプロパティを出力してしまうと冗長なので、今回は府県天気予報の予報日毎の配列から天気アイコンを除いた内容をフィルタリングすることにします。このフィルタリングもjqでやってしまいましょう。
$ AREANAME=東京 ; AREAID=$(xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | sed "-ne /^id/p" | sed -e "s/id=//;s/\"//g") ; curl -s "http://weather.livedoor.com/forecast/webservice/json/v1?city=${AREAID}" | jq -r '.forecasts[] | .date,.dateLabel,.telop,.temperature.max.celsius,.min.celsius' 2015-12-24 今日 曇り 14 null 2015-12-25 明日 曇のち晴 16 null
jqコマンドに -r をつけることで、出力結果のダブルクォートを外してくれます。デフォルトでは今日の天気と明日の天気の両方が出力されるので、次に今日の天気だけを選択してみましょう。
$ AREANAME=東京 ; AREAID=$(xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | sed "-ne /^id/p" | sed -e "s/id=//;s/\"//g") ; curl -s "http://weather.livedoor.com/forecast/webservice/json/v1?city=${AREAID}" | jq -r '.forecasts[] | select (.dateLabel == "今日") | .date,.dateLabel,.telop,.temperature.max.celsius,.min.celsius' 2015-12-24 今日 曇り 14 null
上記のように、jqコマンドの中で「select (.dateLabel == "今日")」というフィルタを通してあげるだけです。残念ながらシェル変数やシェルのプロセス置換は取り込めないっぽい・・・あと、配列名も表示させたい・・・というわけでjqのフィルタはここでおしまいにして、bashのパイプで他のコマンドに渡してしまいましょう。手っ取り早くわかりやすいところで、 cat -n に渡して行番号をつけてしまいます。
$ AREANAME=東京 ; AREAID=$(xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | sed "-ne /^id/p" | sed -e "s/id=//;s/\"//g") ; curl -s "http://weather.livedoor.com/forecast/webservice/json/v1?city=${AREAID}" | jq -r '.forecasts[] | select (.dateLabel == "今日") | .date,.dateLabel,.telop,.temperature.max.celsius,.min.celsius' | cat -n 1 2015-12-24 2 今日 3 曇り 4 14 5 null
先頭のスペースが余計なので、awkでフィールド区切りしてしまいます。
$ AREANAME=東京 ; AREAID=$(xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | sed "-ne /^id/p" | sed -e "s/id=//;s/\"//g") ; curl -s "http://weather.livedoor.com/forecast/webservice/json/v1?city=${AREAID}" | jq -r '.forecasts[] | select (.dateLabel == "今日") | .date,.dateLabel,.telop,.temperature.max.celsius,.min.celsius' | cat -n | awk '{print $1,$2}' 1 2015-12-24 2 今日 3 曇り 4 14 5 null
あとは第一フィールドを置換すればよいのですが、例えば2行目を「今日」だけに置換しても何がなんだかわからんので、行末も置換する必要があるでしょう。というわけで一撃ワンライナーの完成形です。
$ AREANAME=東京 ; AREAID=$(xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | sed "-ne /^id/p" | sed -e "s/id=//;s/\"//g") ; curl -s "http://weather.livedoor.com/forecast/webservice/json/v1?city=${AREAID}" | jq -r '.forecasts[] | select (.dateLabel == "今日") | .date,.dateLabel,.telop,.temperature.max.celsius,.min.celsius' | cat -n | awk '{print $1,$2}' | sed -e "/^1[[:space:]]/s/1/\n予報日/;/^2/s/$/の天気 \(${AREANAME}\)/;/^2/s/^2[[:space:]]//;/^3/s/3 //;/^4/s/$/ 度/;/^4/s/4/最高気温/;/^5/s/5/最低気温/" 予報日 2015-12-24 今日の天気 (東京) 曇り 最高気温 14 度 最低気温 null
1行目だけ、sedの置換対象を「^1」ではなく「^1[[:space:]]」にしています。また、予報日も最初に改行を入れています。これは次に示すように、 .dateLabelのフィルタリングを行わずに出力した際に見やすくするためです。
$ AREANAME=東京 ; AREAID=$(xmllint --format <(curl -s http://weather.livedoor.com/forecast/rss/primary_area.xml) | egrep "(city title=\"${AREANAME}\")" | sed "-e s/[[:space:]]/\n/g" | sed "-ne /^id/p" | sed -e "s/id=//;s/\"//g") ; curl -s "http://weather.livedoor.com/forecast/webservice/json/v1?city=${AREAID}" | jq -r '.forecasts[] | .date,.dateLabel,.telop,.temperature.max.celsius,.min.celsius' | cat -n | awk '{print $1,$2}' | sed -e "/^1[[:space:]]/s/1/\n予報日/;/^2/s/$/の天気 \(${AREANAME}\)/;/^2/s/^2[[:space:]]//;/^3/s/3 //;/^4/s/$/ 度/;/^4/s/4/最高気温/;/^5/s/5/最低気温/;/^6/s/6/\n予報日/;/^7/s/$/の天気 \(${AREANAME}\)/;/^7/s/^7[[:space:]]//;/^8/s/8 //;/^9/s/$/ 度/;/^9/s/9/最高気温/;/^10/s/10/最低気温/" 予報日 2015-12-24 今日の天気 (東京) 曇り 最高気温 14 度 最低気温 null 予報日 2015-12-25 明日の天気 (東京) 曇のち晴 最高気温 16 度 最低気温 null
このように、今日と明日の天気予報がかなり見やすくなりました。あとはメールで飛ばすなりTwitterでつぶやくなり好きに使えますね。(∩´∀`)∩ワーイ
[amazonjs asin="4873111870" locale="JP" title="Spidering hacks―ウェブ情報ラクラク取得テクニック101選"]