こんにちは
ゴリラ.vimを運営しているゴリラです。
以前Vimで電車乗り換えルート検索するプラグインを作りましたが、
今度はVimでコーディング中に天気を知りたくなるときがあるので天気情報プラグインを作りました。
何ができるの?
地域の天気情報(今日、明日)を表示する。
使い方
:Weather
コマンドを実行して天気情報を知りたい地域名もしくは市名を入力する。- 対象地域が見つかればウィンドウに表示されるので
j
ork
で選択してEnter
で確定できる。キャンセルしたいときはx
でウィンドウを閉じる。 - 日にちを選択して確定すると天気情報が表示される。
仕組み
本プラグインは以前作った乗換案内プラグインと同じ様にHTMLをパースして、分析しています。
HTTP通信とパースに関してはvital.vimのWeb.HTML
モジュールを使用しています。このモジュールは裏ではcurl
or wget
or python
が動いています。
プラグインの処理を大きく分けて3ステップがあります。
1. 地域一覧を検索して地域名とURLを取得する
2. 選択した地域のURLから天気情報を実際表示している画面のDOMを取得する
3. 日付を選択した後に、選択肢に応じてDOMをパースして画面を表示する
では、それぞれソースをベースに説明します。
地域一覧を取得
まずWeather
コマンドで呼ぶ関数を用意します。input()
関数を用意するとコマンドラインで入力した値がl:location
に代入されます。weather#get_city()
で地域一覧情報を取得します。
ポップアップウィンドウの選択インターフェイスはfilter
オプションに 標準提供されているpopup_filter_menu
を使うといい感じになります。
function! weather#weather() abort
letl:location = input("地域名:")redraw!
echo ""letl:url ="https://weather.yahoo.co.jp/weather/search/?p=".l:location
letl:city_list = weather#get_city(l:url)if len(l:city_list)==# 0
echo "地域が見つかりません"returnendifletl:list =[]for city inl:city_list
call add(l:list, city.name)endforcall popup_clear()call popup_menu(l:list,{ \"filter":"popup_filter_menu", \"callback":function("weather#choise_city",[l:city_list]), \"borderchars":['-','|','-','|','+','+','+','+'], \"title":"地域の選択", \})endfunction
続いて、地域情報一覧を取得します。
コメントに書いてあるとおりDOMを整形して、表示に使用する地域名とURLを返す関数を用意します。
" 地域毎名と天気情報URLを取得する" [" {'name': '埼玉県さいたま市大宮区', 'url': 'https://weather.yahoo.co.jp/weather/11/4310/11103.html'}," {'name': '埼玉県さいたま市浦和区', 'url': 'https://weather.yahoo.co.jp/weather/11/4310/11107.html'}," ..." ]function! weather#get_city(url) abort
" 例外が発生することがあるので、例外処理を行うtryletl:response =s:HTML.parseURL(a:url)catch"/.*/"
echo v:exception
return[]endtryletl:record =[]" DOM全体から`table` タグの要素を取得するfor item inl:response.findAll("table")if has_key(item.attr,"class")" `yjw_table3` というクラスがあればテーブル内のデータを取得するif item.attr.class ==# "yjw_table3"for td in item.findAll("td")if type(td)!=# type({})&& len(td.child)==# 0
continue
endiflet child = td.child[0]if type(child)!=# type({})
continue
endif" テーブル内のデータを1行ずつdictionaryを作って配列に追加するcall add(l:record,{"name": child.value(),"url":"https:". child.attr.href})endforendifendifbreakendforreturnl:record
endfunction
やっていることはシンプルですね。HTML
モジュールを使用するとDOMのオブジェクトを取得でき、findAll()
で要素を取得できます。value()
で要素内の文字データを取得できます。 child
は小要素になります。
このような感じで割と力技ですが、スクレイピングはこんな感じかなぁと思ったりします。
日付を選択して天気情報のDOMから情報を取得する
続いて、取得してデータをポップアップウィンドウで表示させる処理が必要なので、次のように書きました。
function! weather#choise_city(city_list, id, idx) abort
ifa:idx==# -1returnendifletl:city =a:city_list[a:idx-1]call popup_menu(["今日","明日"],{ \"filter":"popup_filter_menu", \"callback":function("s:choise_day",[l:city]), \'borderchars':['-','|','-','|','+','+','+','+'], \"title":"日付の選択" \})endfunction
callback
で指定する関数はポップアップウィンドウが閉じられたときに実行される関数です。名前のとおりですね。
s:choise_day
の処理は下になります。第1引数はweather#choise_city
で渡した [l:city]
になります。id
はポップアップウィンドウのIDで、idx
は選択したメニューの順番(1から)になります。
function!s:choise_day(city, id, idx) abort
ifa:idx==# -1returnendiftryletl:dom =s:HTML.parseURL(a:city.url)catch"/.*/"
echo v:exception
returnendtryletl:data =s:get_weather(l:dom,a:idx)if empty(l:data)
echo "データがありません"returnendifletl:title =l:data.title
letl:rows =l:data.rows
letl:table =s:T.new({ \"columns":[{},{},{},{},{},{},{},{},{}], \"header":l:rows[0], \})calll:table.rows(l:rows[1:])call popup_create(l:table.stringify(),{ \"title":l:title, \"moved":"any", \})endfunction
日付を選択した後に、選択肢に応じてDOMをパースして画面を表示する
日にちを選択すると s:shoise_day
が呼ばれ選択した地域の天気情報を s:get_weather
でDOMを整形したデータを取得します。
こちらも同様にテーブルのデータを取得して整形して返しています。
" DOMから今日の天気情報を取得" {" title: '今日の天気 - 6月22日(土)'," rows: [" [時刻,0時,3時,6時,9時,12時,15時,18時,21時]," [天気,曇り,曇り,雨,雨,曇り,晴れ,曇り曇り], " [気温(C),24,23,23,24,27,28,26,24]," [湿度(%),77,80,83,78,58,53,66,72]," [降水量(mm),0,0,6,0,3,0,0,0]" ]" }function!s:get_weather(dom, day_kind) abort
letl:day ="yjw_pinpoint_today"ifa:day_kind==# 2letl:day ="yjw_pinpoint_tomorrow"endifforl:item ina:dom.findAll("div")if has_key(l:item.attr,"id")ifl:item.attr.id ==# l:day
" titleletl:h3 = item.find("h3")if type(l:h3)!=# type({})&& len(l:h3.child)==# 0return{}endifletl:h3 =l:h3.child
letl:title = trim(l:h3[0],"\n").l:h3[1].value()" table dataletl:table = item.findAll("tr")letl:rows =[]forl:row inl:table
letl:columns =[]forl:column inl:row.findAll("td")call add(l:columns,l:column.value())endforcall add(l:rows,l:columns)endforreturn{"title":l:title,"rows":l:rows}endifendifendforendfunction
最後に
vital.vimはとっても便利です。テーブルのを作成する時もvital.vimを使用していますがこれもとっても便利です。
そしてポップアップウィンドウもとっても便利です。5/25に入ってからどんどん機能が完成していっていい感じになってきています。
興味ある方はぜひポップアップウィンドウも触ってみてください〜