ウェブスクレイピングによるデータ取得

特殊講義「インターネットを活用した経済データの分析」講義資料

2/16/2017

1 Rによるウェブスクレイピング

ウェブスクレイピングは,ウェブ上に存在する情報を収集 (scrape) する方法,特に後述のAPIを用いずに情報を収集する方法を指す. ここではまず,(1) 「たくさんのファイルをダウンロードして,読み込む」作業をRによって自動化する.

とはいえ,研究で用いたいデータが常に簡単にダウンロードできる形 (csvやzip) で提供されている訳ではない.たとえば,日々変動する株価や為替レートのようなデータをリアルタイムで取得したい場合もある.しかし,多くの場合,これらのデータは「文字の情報」としてはウェブ上に存在するものの,ダウンロードしやすい.csvや.zipのような形では提供されていない.そこで,(2) 為替レートと例に, ウェブ上に存在する文字列を収集する作業をRによって自動化するコードも例示する.

いずれの作業にしても,R (や他の言語によるコード) がなければ何百何千回と,画面に張り付きクリックし続けなければならない.そんなことをしていればミスも出るし,時間の浪費にしかならない.Rを用いれば,こんな人生の無駄遣いにしかならない作業から解放される.この作業のためだけに人を雇う余裕があるとしても,R (や他の言語) を使える人雇った方が「ポチポチ」な人を雇うよりも圧倒的に効率がよい.

2 セットアップ:パッケージの読み込みとディレクトリ設定

Rのデフォルト環境ではこうした作業を行なうことはできないので,必要なパッケージを読み込む.

library(lubridate)
library(pathological)
library(rvest)
library(stringr)
library(tidyverse)
library(XML)

また,ファイルをダウンロードした先が分からなくなると (あるいは,意図していない場所に保存されると) いけないので,ダウンロードしたファイルを保存するディレクトリを設定する. 下記のパスは教員 (伊藤) のMacにあわせてあるので,各自実行する際にはdir_pathに指定するディレクトリを自分のPC/Macにあわせて変更しなくてはならない.

dir_path <- "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/"
create_dirs(dir_path)
## /Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/ 
##                                                                          FALSE

1行目では,dir_pathに作業ディレクトリへのパスを格納している.

2行目のcreate_dirs(dir_path)では,dir_path のディレクトリを新たに作成している.「ディレクトリ (フォルダ) を作る」というと右クリックしたくなるかもしれないが,そんなことをしなくてもこのpathological::create_dirsで簡単に作成 (自動化) できる. pathological::create_dirs関数は,存在しないディレクトリが指定された場合はディレクトリを作成し,既に存在するディレクトリへのパスが引数に指定された場合はFALSEを返す (ここではFALSEが返ってきている).?pathologicalとRのコンソールに打てば,このパッケージが提供する他の便利な関数の解説が表示されるので,時間があるときに一読することを勧める.

3 ウェブ上のファイルの自動取得

ウェブ上で公開されてるデータセット (csvやzipファイル) を入手して,研究に利用したいとしよう.ウェブスクレイピングという発想がなければ,すべてのファイルへのリンクを一つずつクリックして,ダウンロードし,解凍し,ソフトウェアに読み込んでいくことになる.しかし,こんな作業は手間がかかるし,途中で電話でもかかってくれば,どこまでダウンロードしたかも忘れてしまう.さらに,研究している中でデータがアップデートされたときには,一から手間のかかる作業を繰り返す羽目になる.

3.1 rvestパッケージ

Rを使えば,こんなバカな作業は全て自動化できる.さらに,下の画像のHadley Wickham氏 (現在R開発チームの主要人物でもあり,一部R界隈では羽鳥先生Hadley神として崇められている) が作成したrvestパッケージを用いると,驚くほど簡単にこうした作業を自動化してしまえる.

ちなみに,R界隈にはHadley氏のパッケージを多様し,彼のRプログラミングの思想を崇拝する羽鳥教と呼ばれる一派が存在する. 入信するかどうかはともかくとして,羽鳥教で多用されるパッケージはRを用いる上で必須なので彼のGitHubページ を見ることをお勧めする.

なお,以下で多用するrvestパッケージの使い方については,英語だが次のウェブサイトが参考になる.

3.2 作業例:AidDataが提供するデータセットの取得

例として,途上国への国際援助のデータを提供するAidDataのウェブサイトを取り上げる.AidDataではいくつか種類のデータが提供されているが,ここでは援助が提供される国ではなく,具体的な「場所」の情報を持つ空間データ (spatial data/geocoded data) を取りあげよう.AidDataのウェブサイト にアクセスすると,いくつかのデータセットの説明と,zipファイルをダウンロードするためのハイパーリンクが貼られている.

3.2.1 URLの設定

AidDataが提供するデータセットを取得するため,まずAidDataのウェブサイトのurlをRに教える.

page <- read_html("http://aiddata.org/subnational-geospatial-research-datasets")

3.2.2 リンクの取得

どれぐらい簡単かを示すため,上記のウェブページに存在するzipファイルへの全ハイパーリンクを取得するためのコードを先に示す.下記のコードを実行するだけで,22個のデータセットをダウンロードするための準備が整ってしまう.

zip_url = page %>%
  html_nodes("a") %>%   ## find all links
  html_attr("href") %>% ## pull out url
  str_subset("\\.zip")  ## pull out zip links

3.2.3 確認

簡単すぎて心配になるので,本当に取得できているか確認してみる.

str(zip_url)
##  chr [1:22] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/ChineseOfficialFinanceEcologicalSensitive_GeocodedResearchRel"| __truncated__ ...

このようにzip_urlは文字列のベクトルなので,コンソールにzip_urlとだけ入力すればその中身を確認できる.

zip_url
##  [1] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/ChineseOfficialFinanceEcologicalSensitive_GeocodedResearchRelease_Level1_v1.0.1.zip"
##  [2] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/FTS_CentralAfricanRepublic_GeocodedResearchRelease_Level1_v1.0.zip"                 
##  [3] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/EM-DAT_PHL.zip"                                                                     
##  [4] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/NepalEarthquake_GeocodedResearchRelease_Level1_v1.0.zip"                            
##  [5] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/AfghanistanAIMS_GeocodedResearchRelease_Level1_v1.1.1.zip"                          
##  [6] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/SomaliaAIMS_GeocodedResearchRelease_Level1_v1.1.1.zip"                              
##  [7] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/ColombiaAIMS_GeocodedResearchRelease_Level1_v1.1.1.zip"                             
##  [8] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/BangladeshSelectDonors_GeocodedResearchRelease_Level1_v1.1.1.zip"                   
##  [9] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/IraqAIMS_GeocodedResearchRelease_Level1_v1.3.1.zip"                                 
## [10] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/NigeriaAIMS_GeocodedResearchRelease_Level1_v1.3.1.zip"                              
## [11] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/HondurasAIMS_GeocodedResearchRelease_Level1_v1.3.1.zip"                             
## [12] "https://github.com/KAPPS-/public_datasets/raw/master/geocoded/WorldBank_GeocodedResearchRelease_Level1_v1.4.1.zip"                                    
## [13] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/NepalAIMS_GeocodedResearchRelease_Level1_v1.4.1.zip"                                
## [14] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/UgandaAIMS_GeocodedResearchRelease_Level1_v1.4.1.zip"                               
## [15] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/SenegalAIMS_GeocodedResearchRelease_Level1_v1.5.1.zip"                              
## [16] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/DRC-AIMS_GeocodedResearchRelease_Level1_v1.3.1.zip"                                 
## [17] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/AllWorldBank_IBRDIDA.csv.zip"                                                       
## [18] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/aiddata_china_1_1_1.xlsx.zip"                                                       
## [19] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/TimorLesteAIMS_GeocodedResearchRelease_Level1_v1.4.1.zip"                           
## [20] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/Malawi_release_17april2012.xlsx.zip"                                                
## [21] "https://github.com/AidData-WM/public_datasets/raw/master/geocoded/AfDB_2009_2010_AllApprovedProjects.xlsx.zip"                                        
## [22] "https://github.com/AidData-WM/public_datasets/raw/master/community/aid_locations_during_civil_wars_south_of_sahara.zip"

22個のzipファイルへのリンクが取得できている.これと同じことを「ポチポチ」してしようと思うと,リンクにマウスオーバーして,右クリックして,「リンクを取得する」のような項目をクリックして,コピペして,という作業を22回繰り返さなくてはならない (人生の無駄遣い).Rのコードを書けば,1秒とかからない.

3.2.4 コードの説明

さて,このコードで行っている作業を解説しておく.

zip_url = page %>%
  html_nodes("a")
zip_url
## {xml_nodeset (120)}
##  [1] <a href="#main-content" class="element-invisible element-focusable" ...
##  [2] <a href="/" title="Visit the main page" rel="home">\n            <i ...
##  [3] <a href="http://facebook.com/aiddata" title="" class="link link-top ...
##  [4] <a href="http://twitter.com/aiddata" title="" class="link link-topl ...
##  [5] <a href="/blog/rss.xml" class="link link-toplevel link-rss">RSS</a>
##  [6] <a href="/user/login" title="" class="main-link">Log in</a>
##  [7] <a href="/search/site" class="search-link">\n  <span class="search- ...
##  [8] <a href="http://aiddata.org/our-story" title="" class="subnav-title ...
##  [9] <a href="http://aiddata.org/about-aiddatas-work" title="">Our Portf ...
## [10] <a href="http://aiddata.org/partnerships" title="">Our Partnerships ...
## [11] <a href="http://aiddata.org/in-the-news" title="">In the News</a>
## [12] <a href="http://aiddata.org/aiddata-staff" title="" class="subnav-t ...
## [13] <a href="http://aiddata.org/experts" title="">Our Experts</a>
## [14] <a href="http://aiddata.org/aiddatas-research-and-evaluation-unit"  ...
## [15] <a href="http://aiddata.org/affiliated-researchers" title="">Affili ...
## [16] <a href="http://aiddata.org/join-the-team" title="" class="subnav-t ...
## [17] <a href="http://aiddata.org/partner-with-us" title="">Partner with  ...
## [18] <a href="http://aiddata.org/aiddata-center-for-development-policy"  ...
## [19] <a href="http://aiddata.org/aiddata-summer-fellows" title="">AidDat ...
## [20] <a href="http://aiddata.org/aiddata-research-consortium" title="">A ...
## ...

までを実行すると,何やら大量の文字列が取得されている.rvest::html_nodesは,指定したウェブサイト (html) 内のタグを全て抽出する.length(zip_url)の結果から分かるように,このウェブサイトには121のタグが含まれている.

次に,取得した内容からリンク先のurlを抽出する.この作業を行うには,rvest::html_attr関数を用いればよい.

zip_url = page %>%
  html_nodes("a") %>%   ## find all links
  html_attr("href")     ## pull out url
zip_url %>% head(10)
##  [1] "#main-content"                         
##  [2] "/"                                     
##  [3] "http://facebook.com/aiddata"           
##  [4] "http://twitter.com/aiddata"            
##  [5] "/blog/rss.xml"                         
##  [6] "/user/login"                           
##  [7] "/search/site"                          
##  [8] "http://aiddata.org/our-story"          
##  [9] "http://aiddata.org/about-aiddatas-work"
## [10] "http://aiddata.org/partnerships"

しかし,この内容には最終的にほしいzipファイル以外へのリンクも含まれていることが分かる.zipファイルをダウンロードするには,zipファイルへのリンクのみを抽出しなくてはならない.

そこで,「zipファイルへのリンクには共通するが,zipファイル以外へのリンクにはない」特徴を考える.それぞれの文字列の最後を見れば分かる通り,zipファイルへのリンクは“.zip”で終わっている. したがって,「“.zip”で終わる」文字列を抽出すればよい.

この作業には,stringrパッケージのstr_detect()関数を用いればよい.stringr::str_detct()関数は,引数として与えられたオブジェクト (文字列) が特定の文字列を含むかどうか判断し,含まれていればTRUEを,含まれていなければFALSEを返す.

str_subset("\\.zip")"\\.zip"は「.zipで終わる」という意味の正規表現 (regular expression) である.正規表現は文字列を表現するための方法の一つで,文字列の中から特定の文字の並びやパターンを検索する上で多用される.ここで用いた「.zipで終わる」という検索は,その典型といえる.ここでは正規表現自体には立ち入らないが,stringrパッケージではRで簡単に正規表現を扱うための関数を多数用意されている.

zip_url = page %>%
  html_nodes("a") %>%   ## find all links
  html_attr("href") %>% ## pull out url
  str_subset("\\.zip")  ## pull out zip links

これで,上に示した通りzipファイルへの22のリンクが無事取得できた.

3.3 ファイルのダウンロード

さて,ここでの作業の目的はリンクを取得することではなく,リンク先のファイルを自動でダウンロードすることにある. 残る作業は,既に取得したリンクのベクトルを使って,ファイルを実際にダウンロードするだけとなった.

ここでは,多数のリンクに対して「ダウンロードする」という一定の作業を繰り返し行なう,という作業が必要になる. こうした作業には,大きく分けて2つの方法を用いることができる.1つは自作の関数とapplyファミリーと呼ばれる関数群を用いる方法,もう1つはfor文を用いる方法である.ここでは,意味がわかりやすいfor文を用いる.

まずは,保存先のディレクトリを作成・指定する.

# Automatically download zip files
tmp_dir = file.path(dir_path, "050-ScrapedData")
zip_dir  <- file.path(tmp_dir, "AidData_asof_1225_2016")
create_dirs(zip_dir)
## /Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData//050-ScrapedData/AidData_asof_1225_2016 
##                                                                                                                 FALSE

「入れ物」ができたので,ここにAidDataのデータセットを保存していく.ここでは次のコードで行なう.

# Pull out each url using a for loop
zip_loop = 1:length(zip_url)
for (i in zip_loop) {
    tmp_url = zip_url[i]
    zip_file = strsplit(tmp_url,split="/")[[1]][9]      ## zip file name
    zip_file = str_replace(zip_file, "\\?raw=true","")
    zip_file = file.path(zip_dir, zip_file)    ## zip file name as file path
    download.file(tmp_url, destfile = zip_file) ## download zip file
    unzip(zip_file, exdir=zip_dir)              ## unzip
    Sys.sleep(1)
}

このコードはやや煩雑だが,for文の中の1行目でまずi番目のurlを取得している.続く3行のコードはzipファイルを保存するときにつけるファイル名とパスを設定している.その上で,download.file()関数を用いて,i番目のurlで公開されているファイルをダウンロードしている.unzip()関数は,名前から分かる通り,引数として与えたパスにあるzipファイルを解凍してくれる.最後のSys.sleep(1)は「1秒間まつ (待ってから次のiの処理を実行する)」という命令を出している.この例のように,短い時間内に特定のウェブサイトに大量にアクセスすると,不正アクセスと勘違いされる可能性があるので,わざと実行を遅らせている (それでも「ポチポチ」に比べれば圧倒的に早い).

4 for文を用いたファイルの読み込みの自動化

さて,ここまでの作業によって,たくさんのファイルのダウンロードを自動化できた.これ自体はありがたく,時間の節約にもなるが,実際にこれらのデータを用いて解析を行なうにはまずダウンロードしたファイル群をRに読み込まなければならない.1つや2つのファイルなら,ファイルがあるディレクトリに行って,右クリックしてパスを取得して,1つずつRで読み込む,という作業もできるが,ファイル数が多くなるとこれは現実的ではない.また,ディレクトリ名やファイル名を変更してしまうと,逐一パスを手動で書き換えなくてはならず,手間がかかる.ダウンロードと同じように,ファイルのパス取得や読み込みも自動化できないだろうか?

Rの関数を用いれば,こうした煩雑な作業も簡単に自動化できる.ここでは,せっかくダウンロードしたのでAidDataのデータセットを例に,ファイルを自動で読み込む手順を示す.

4.1 ファイルパスの自動取得:list.files()関数の活用

まずは,ファイルのパスを取得する.ダウンロードした全てのファイルを読み込んでもよいが,一部は.csvファイル,一部は.xlsxファイルとやや扱いにくい.また,フォルダによっては複数のファイルにデータセットが分かれている.

ここでは,現在のところ政治学・経済学における紛争研究でもっとも頻繁に用いられているUCDP/AidData “Aid Locations during Civil Wars South of the Sahara dataset” を例として用いる. このデータセットは,1989-2008年の期間に内戦を経験したアフリカ諸国における援助の空間データを提供する.上のコードで取得したファイルの中では,“public release” というフォルダの中に保存されている.

さて,“public release”の中には複数の.xlsxファイルが保存されている.これらのパスを一括して取得するには,list.files()関数を用いる.

aid_dir = "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/050-ScrapedData/AidData_asof_1225_2016/Public Release"
aid_dir = "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release"
aid_files_path = list.files(aid_dir, recursive=FALSE, pattern="xlsx", full.names=TRUE)

2行目のコードは,list.files()関数を用いて“xlsx”という文字列を含むファイルのパスを取得している.recursive=FALSEは,「サブディレクトリの中に保存しているファイルは無視する」という指定を加えている.“public release”の中にはサブディレクトリはないが,もし存在する場合にはサブディレクトリ内のファイルへのパスもあわせて取得したい場合もあれば,無視したい場合もある.recursive=FALSEもしくはrecursive=TRUEとすることで,この2つを使い分けられる.

このコードによって,きちんと「欲しいファイルパス」が取得され,パスを格納したベクトルができているかを確認する.

aid_files_path
##  [1] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Angola_Conflict_20June2012.xlsx"       
##  [2] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Burundi_Conflict_20June2012.xlsx"      
##  [3] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/CAR_Conflict_20June2012.xlsx"          
##  [4] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Chad_Conflict_20June2012.xlsx"         
##  [5] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Comoros_Conflict_27June2012.xlsx"      
##  [6] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/CongoRep_Conflict_27June2012.xlsx"     
##  [7] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Djibouti_Conflict_27June2012.xlsx"     
##  [8] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/DRC_Conflict_27June2012.xlsx"          
##  [9] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Eritrea_Conflict_27June2012.xlsx"      
## [10] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Ethiopia_Conflict_27June2012.xlsx"     
## [11] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/GuineaBissau_Conflict_27June2012.xlsx" 
## [12] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/GuineaConacry_Conflict_27June2012.xlsx"
## [13] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/IvoryCoast_Conflict_27June2012.xlsx"   
## [14] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Lesotho_Conflict_27June2012.xlsx"      
## [15] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Liberia_Conflict_27June2012.xlsx"      
## [16] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Mozambique_Conflict_27June2012.xlsx"   
## [17] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Nigeria_Conflict_27June2012.xlsx"      
## [18] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Rwanda_Conflict_27June2012.xlsx"       
## [19] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/SierraLeone_Conflict_27June2012.xlsx"  
## [20] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Somalia_Conflict_27June2012.xlsx"      
## [21] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Sudan_Conflict_27June2012.xlsx"        
## [22] "/Users/Gaku/Dropbox/000-DropAny/000-GIS-related/110-MicroData/070-Aid/AidData/Public Release/Uganda_Conflict_27June2012.xlsx"

無事,22個のファイルへのパスが取得できた.

list.files()関数はRでのファイル読み込みに伴う「面倒さ」を解決してくれる非常に便利な関数なので,使いこなせるようになることが望ましい.日本語での解説記事もウェブに転がっているので,たとえば この記事 を参照し,色々と試してみてほしい.

4.2 エクセルファイルの読み込み

このデータセットはやや不親切で,データがエクセルファイルで提供されている.本来,データセットを提供する際には不要な情報を含むエクセルではなく.csvや.txtを用いることが望ましいが,世の中にはこうしたデータセットもある (絶対真似しないように).こうしたファイルをみると小一時間文句を言いたくなるが,ぐっとこらえてコードを考える.

Rでエクセルファイルを読み込む方法はいくつかあるが,ここでは羽鳥教の神器の1つであるreadxlパッケージを用いる. aid_files_pathに保存したファイルパスを1つ読み込むには,次のようにすればよい.

library(readxl) ## load package
tmp_dat = read_excel(aid_files_path[1]) ## 最初のファイルパスを読み込む
tmp_dat ## 読み込めたか確認
## # A tibble: 3,891 × 22
##    `project id` `event id`  rname recipient_id dname donor_id  year
##           <dbl>      <dbl>  <chr>        <dbl> <chr>    <dbl> <dbl>
## 1           598     598.01 ANGOLA            9  AFDB       60  1991
## 2           598     598.02 ANGOLA            9  AFDB       60  1991
## 3           598     598.03 ANGOLA            9  AFDB       60  1991
## 4           598     598.04 ANGOLA            9  AFDB       60  1991
## 5           598     598.05 ANGOLA            9  AFDB       60  1991
## 6           598     598.06 ANGOLA            9  AFDB       60  1991
## 7           598     598.07 ANGOLA            9  AFDB       60  1991
## 8           598     598.08 ANGOLA            9  AFDB       60  1991
## 9           598     598.09 ANGOLA            9  AFDB       60  1991
## 10          598     598.10 ANGOLA            9  AFDB       60  1991
## # ... with 3,881 more rows, and 15 more variables: loctext <chr>,
## #   numbloc <dbl>, lat <dbl>, long <dbl>, first <chr>, second <chr>,
## #   precision <dbl>, comtorig <dbl>, origtyp <chr>, usdcr <dbl>,
## #   usdco <dbl>, crspcode <dbl>, src <chr>, data_source_id <dbl>,
## #   oecd_id <dbl>

関数名から直感的に分かる通り,ここで用いたreadxl::read_excel()関数はエクセルファイルを読み込むために用いる関数である.他の類似関数とは異なり,この関数はヘッダー・列名の設定や各コラム (列) のデータタイプの設定を「自動で」やってくれる (ユーザが明示的に指定することもできる).また,複数のシートを含むエクセルファイルを読み込みたい場合には,sheet=1のようにシート番号を指定する引数を与える必要がある. なお,この関数に限らず,下記のように関数名のみをコンソールに入力してやれば,関数の中身を見て引数等を確認できる.

read_excel
## function (path, sheet = 1, col_names = TRUE, col_types = NULL, 
##     na = "", skip = 0) 
## {
##     path <- check_file(path)
##     ext <- tolower(tools::file_ext(path))
##     switch(excel_format(path), xls = read_xls(path, sheet, col_names, 
##         col_types, na, skip), xlsx = read_xlsx(path, sheet, col_names, 
##         col_types, na, skip))
## }
## <environment: namespace:readxl>

さて,上述のように,このデータセットは援助の規模や種類だけでなく「援助の展開地点」という空間情報を保持している.せっかくなので,上で読み込んだアンゴラのデータで地図を書いてみる.

library(cshapes)
library(sp)
cshp_2015 <- cshp(date=as.Date("2015-1-1"), useGW=TRUE) ## 1.1.2015時点の国境線データ
tmp_aid = as.data.frame(tmp_dat) %>% filter(!is.na(lat), !is.na(long))
coordinates(tmp_aid) = ~long + lat ## 空間データ (ポイント・データ) に変換
## 簡略な作図.綺麗にするには少し工夫が必要 (省略)
par(mar=rep(0,4), oma=rep(.1,4))
plot(tmp_aid, cex=.65, pch=8, col="red3")
plot(cshp_2015, add=TRUE)
box()

きちんと地図が描けたので,ファイルの中身については安心してよさそうだ.

4.3 tibbleオブジェクトについての補足

なお,ここでオブジェクトのクラスがtibbleとなっていることにも注意してほしい.これは,エクセルファイルを読み込んだからということではなく,Wickham氏による一連のパッケージでのデフォルトのオブジェクト形式で読み込まれているためである.特段data.frameオブジェクトとの区別を意識する必要はないが,上の出力からもわかる通りtibbleオブジェクトでは,データの大きさ (行数と列数)や各列のデータタイプが一目瞭然となるため,特にこだわりがなければtibbleクラスの積極的な利用を強く勧める.この点については,講義資料「Rによるデータの読み込みと書き出し」でまとめているので,参照することを勧める.

4.4 for文によるファイル読み込みの自動化

ひとまずアンゴラのデータセットを読み込んだのはいいが,このデータセットは20以上のエクセルファイルからなる.1つ1つ読み込むのは大変なので,読み込み作業も自動化できないだろうか.実は,上で用いたfor文によるダウンロードのコードを多少変更することで,簡単に読み込み作業も自動化できる.

アンゴラのデータを読み込んだ際,tmp_dat = read_excel(aid_files_path[1])と,aid_files_pathの1番目の要素 (パス) を読み込んでいることを思い出してほしい. 直感的にいえば,このコードをaid_files_pathのすべての要素について実行すれば,すべてのファイルを読み込むことができる.ただし,複数のファイルをfor文で読み込んでいく場合,読み込んだファイルを格納しておく入れ物を逐次用意するか,読み込んだファイルのデータを順次結合して一まとめにしていく作業が必要になる.次のコードでは,読み込んだ個別のファイルを逐次結合して,大きなtibbleオブジェクトにまとめる.

path_loop = 1:length(aid_files_path)
for (i in path_loop) {
    tmp_path = aid_files_path[i]    # pull out ith file path
    tmp_dat = read_excel(tmp_path, na=c("."), col_types=rep("text", 22))  # read ith file
    if (i == 1) {
        aid_all = tmp_dat
    } else {
        aid_all = bind_rows(aid_all, tmp_dat)   # combine
    }
}

このコードではまず,tmp_path = aid_files_path[i]で取得したi番目のパスにあるファイルを,tmp_dat = read_excel(tmp_path)によってtmp_datというオブジェクトに読み込んでいる. この2行のコードで,tmp_datはiが1のときにはaid_files_pathの1番目のパスにあるファイルを読み込み,iが2のときには2番目のパスにあるファイルを読み込む,といった具合に,逐次iに入る数字に対応するパスのファイルを読み込んでいる.なお,ファイルの数が22なので,当然iの最大値の22になる.なお,ここで読み込んでいる一連のデータには一部誤植が含まれているので,col_types=rep("text", 22)という「おまじない」をしている.このデータを用いた解析をする場合にはこれが問題になるが,ここではファイルの自動読み込みが重要なので気にしないことにしておく.

ここまでの作業で22のファイルを逐次読み込むことはできるが,毎回tmp_datの中身が書き換わってしまうのため,コードを回し終わるとtmp_datの中に22番目のファイルが読み込まれた状態になってしまう.我々が今ほしいのは「22個のファイルを (縦に) 結合した大きなデータ」なので,これを得るにはどうしたらよいだろうか.

ifで始まるブロックのコードが,読み込んだファイルを逐次結合していく作業を行なってくれる.if文は条件分岐を表現する手法の一つで,「ある条件が満たされている場合には作業1を,満たされていない場合には作業2を行なう」といった命令を記述できる.ここでは,「iが1の場合にはaid_allという新たなオブジェクトにtmp_datを代入し,iが2以上の場合にはaid_alltmp_datを結合する」という作業を行なっている.dplyr::bind_rows()関数は,引数に与えられたdata.frameオブジェクトやtibbleオブジェクトを「縦に」結合する.「横」に結合するにはdplyr::bind_cols()関数を用いればよい.この2つの関数はデフォルトのrbind()関数とcbind()関数に対応する.詳細は省略するが,dplyr::bindの方がデフォルトの関数比べて柔軟で使いやすい (ただし,行列オブジェクトには使えないので,rbind()cbind()も覚えておくこと).

この作業が意図した通りに実行されていれば,読み込んだ22個のファイルを縦に結合した,大きなデータがaid_allに格納されているはずなので,確認してみよう.

aid_all
## # A tibble: 69,721 × 23
##    `project id`         `event id`  rname recipient_id dname donor_id
##           <chr>              <chr>  <chr>        <chr> <chr>    <chr>
## 1           598             598.01 ANGOLA            9  AFDB       60
## 2           598             598.02 ANGOLA            9  AFDB       60
## 3           598             598.03 ANGOLA            9  AFDB       60
## 4           598             598.04 ANGOLA            9  AFDB       60
## 5           598 598.04999999999995 ANGOLA            9  AFDB       60
## 6           598 598.05999999999995 ANGOLA            9  AFDB       60
## 7           598 598.07000000000005 ANGOLA            9  AFDB       60
## 8           598 598.08000000000004 ANGOLA            9  AFDB       60
## 9           598             598.09 ANGOLA            9  AFDB       60
## 10          598              598.1 ANGOLA            9  AFDB       60
## # ... with 69,711 more rows, and 17 more variables: year <chr>,
## #   loctext <chr>, numbloc <chr>, lat <chr>, long <chr>, first <chr>,
## #   second <chr>, precision <chr>, comtorig <chr>, origtyp <chr>,
## #   usdcr <chr>, usdco <chr>, crspcode <chr>, src <chr>,
## #   data_source_id <chr>, oecd_id <chr>, Numbloc <chr>

どうやら,70,000行弱のデータが読み込まれている.22の国のデータが読み込まれているかも,きちんと確認しておく.このデータではrname変数が対象の国の名前を保持しているので,rname変数が保持するユニークな値 (“ANGOLA,” “SUDAN”等) の数を見れば,何カ国のデータが含まれているかを確認できる. あるベクトル (あるいはオブジェクト) のユニークな値 (の組み合わせ) を取得するには,Rデフォルトのunique()関数を用いる.

unique(aid_all$rname)
##  [1] "ANGOLA"                         "BURUNDI  "                     
##  [3] "CENTRAL AFRICAN REPUBLIC"       "CHAD"                          
##  [5] NA                               "COMOROS"                       
##  [7] "CONGO, REPUBLIC OF"             "DJIBOUTI"                      
##  [9] "AFRICA ("                       "GLOBAL"                        
## [11] "CONGO, DEMOCRACTIC REPBULIC OF" "ERITREA"                       
## [13] "ETHIOPIA"                       "GUINEA-BISSAU"                 
## [15] "GUINEA"                         "COTE D'IVOIRE"                 
## [17] "LESOTHO"                        "LIBERIA"                       
## [19] "MOZAMBIQUE"                     "NIGERIA"                       
## [21] "RWANDA"                         "SIERRA LEONE"                  
## [23] "SOMALIA"                        "SUDAN"                         
## [25] "UGANDA"

なぜか25のユニークな値があるが,たくさんのデータがきちんと読み込めている.なお,NAは欠測 (欠損) 値 (missing value) を示すので,無視してよい."AFRICA (""GLOBAL"はコードブック (説明書) にも記載がないので何かわからないが,2つ合わせても4行しかないので (とりあえず) 無視しておこう.

ちなみに,dplyr::distinct()関数もunique()関数と類似の働きをする.上でunique(aid_all$rname)と書いた部分をdplyrパッケージを活用して書くと,次のようになる.

aid_all %>% select(rname) %>% distinct
## # A tibble: 25 × 1
##                       rname
##                       <chr>
## 1                    ANGOLA
## 2                 BURUNDI  
## 3  CENTRAL AFRICAN REPUBLIC
## 4                      CHAD
## 5                      <NA>
## 6                   COMOROS
## 7        CONGO, REPUBLIC OF
## 8                  DJIBOUTI
## 9                  AFRICA (
## 10                   GLOBAL
## # ... with 15 more rows

5 文字情報の自動取得:リアルタイムの為替レート情報

さて,ここまでの例ではcsvやzipのようなダウンロードさえできれば,基本的にすぐに解析に利用できるデータセットを想定していた. しかしながら,現実に解析に用いたいデータは,こうした便利あるいは整理された形式でりようできるとは限らない. たとえば,リアルタイムの為替レートのように文字情報でしかデータがない場合はどうしたらよいだろうか.

必要な情報がウェブページで提供されているなら,ウェブスクレイピングによって簡単に情報を収集することができる. ここでは,Yahoo! Japan ファイナンス が提供する22の為替情報から,リアルタイムに為替レートを収集してデータセットをつくろう. もう少し具体的に言っておくと,Yahoo! Japan ファイナンスのウェブページをみると,22の為替についてbidとaskのレートが一覧で出ている. 以下のコードは,22のbidとaskの合計44の情報を削り取って保存する.

まずは,取得したデータを保存する先のディレクトリを作成する.target_dirは各自の環境にあわせて変更すること.

target_dir = "/Users/Gaku/Dropbox/040-ToyamaNIHU/090-ToyamaTeaching/010-Intro2econometrics/020-data/02020-currency"
create_dirs(target_dir)
## /Users/Gaku/Dropbox/040-ToyamaNIHU/090-ToyamaTeaching/010-Intro2econometrics/020-data/02020-currency 
##                                                                                                FALSE

これで「入れ物」(保存先のディレクトリ) ができたので,為替レートの情報を取得して保存する.44の為替レートの情報は,次のコードで簡単に取得・保存できる.

# PART 1
this_moment = as.character(now())   # record current date and time
yahoo_url = "http://info.finance.yahoo.co.jp/fx/list/" # url
master_html = read_html(yahoo_url)  # read webpage
# pull out links to get the labels
links = master_html %>%
    html_nodes("a") %>%             # find all links
    html_attr("href") %>%           # pull out hyperlinks
    str_subset("^.*fx/chart.*") %>% # contains "fx/chart"
    str_subset("^(?!.*(news|indicator)).+$") %>%    # drop "news" and "indicator"
    unique  # remove duplicates

# PART 2
# prepare object
exchange_dat = matrix(NA, nrow = length(links), ncol = 4)
exchange_dat = as.data.frame(exchange_dat)
names(exchange_dat) = c("exchange", "bid", "ask", "time")
# pull out info
for (i in 1:length(links)) {
    # pull out label
    exchange = str_to_upper(str_split(links[i], "/")[[1]][7])
    exchange_lab = str_c("#", exchange, "_chart_")

    # store values
    exchange_dat[i,1] = exchange    # label
    exchange_dat[i,2] = master_html %>%
        html_node(str_c(exchange_lab, "bid")) %>%
        html_text   # bid price
    exchange_dat[i,3] = master_html %>% 
        html_node(str_c(exchange_lab, "ask")) %>% 
        html_text   # ask price
    exchange_dat[i,4] = this_moment # current time
}
exchange_dat = exchange_dat %>% arrange(exchange)   # change order
this_moment = str_replace_all(this_moment, ":", "")
this_moment = str_replace_all(this_moment, " ", "_")
write_csv(exchange_dat, path = file.path(target_dir, str_c(this_moment, "_currency.csv")))  # save
exchange_dat        # check the output
##    exchange     bid     ask                time
## 1    AUDCHF 0.77352 0.77382 2017-02-15 17:15:37
## 2    AUDJPY  87.877  87.884 2017-02-15 17:15:37
## 3    AUDUSD 0.76719  0.7673 2017-02-15 17:15:37
## 4    CADCHF 0.77118 0.77148 2017-02-15 17:15:37
## 5    CADJPY  87.595  87.612 2017-02-15 17:15:37
## 6    CHFJPY 113.579 113.597 2017-02-15 17:15:37
## 7    CNHJPY  16.688  16.718 2017-02-15 17:15:37
## 8    EURAUD 1.37728 1.37744 2017-02-15 17:15:37
## 9    EURCHF 1.06545 1.06563 2017-02-15 17:15:37
## 10   EURGBP 0.84809 0.84823 2017-02-15 17:15:37
## 11   EURJPY  121.03 121.036 2017-02-15 17:15:37
## 12   EURUSD 1.05669 1.05674 2017-02-15 17:15:37
## 13   GBPCHF 1.25617 1.25645 2017-02-15 17:15:37
## 14   GBPJPY 142.696 142.707 2017-02-15 17:15:37
## 15   GBPUSD 1.24585 1.24596 2017-02-15 17:15:37
## 16   HKDJPY   14.74   14.77 2017-02-15 17:15:37
## 17   NZDJPY  82.156   82.17 2017-02-15 17:15:37
## 18   NZDUSD 0.71739 0.71756 2017-02-15 17:15:37
## 19   USDCHF 1.00827 1.00843 2017-02-15 17:15:37
## 20   USDHKD 7.76244 7.76274 2017-02-15 17:15:37
## 21   USDJPY 114.536 114.539 2017-02-15 17:15:37
## 22   ZARJPY   8.765   8.779 2017-02-15 17:15:37

たったこれだけのコードで,22の為替情報をリアルタイムに取得でき,取得したデータが2017-02-15_171537_currency.csvとして保存された.取得した情報を保持するexchange_datオブジェクトの中で,currency変数はUSDJPYのように通貨を,bid変数は売値を,ask変数は買値を,またtime変数は為替情報を取得した日時を示す.このコード全体を一定時間毎に繰り返してやれば,リアルタイムの為替レートを逐一収集し蓄積できる.現在の日時を取得する際に使用しているnow()関数はlubridateパッケージの関数で,now()を実行した時の日時を返す.lubridateパッケージの使い方については,このウェブページ (英語) が参考になる.

なお,このコードは大きく2つの部分に分かれる.

  1. 上記のYahoo!のウェブページにアクセスし,htmlを読み込む.また,22のレートそれぞれへのハイパーリンク (e.g., USDJPYのページへのリンク) を抽出するPART 1
  2. Yahoo!のウェブページの文字情報から為替レートの情報を抽出して保存するPART 2

コード一行一行の説明は冗長になり,また正規表現の知識が必要になるので省略する. 本講義の課題や卒論,自分の研究等でウェブスクレイピングを用いたい場合は個別に質問すること.

6 番外編:動画ファイルの自動取得 (悪用厳禁)

ここまでの例では,.zipファイルやエクセルファイル,文字情報など,一見して「データっぽい」ファイルを扱ってきた.しかし,ウェブスクレイピングの技法を応用できる対象は,こうしたファイルに限られる訳ではない.たとえば,pdfファイルや動画ファイルも同様の手順で取得することができる.

せっかくなので,ここではウェブスクレイピングに重宝されるXMLパッケージを用いてこうした作業の例も示しておく. さて,ここでは立山黒部アルペンルートのウェブサイトを事例に,動画ファイル取得の手順を考える. このウェブサイトでは,室堂・大観峰・弥陀ヶ原の3箇所に設置されたライブカメラで撮影した1日分の映像を,16秒ほどのタイムラプス動画として配信している. 以下では,この動画ファイルを自動でダウンロードするコードを考える.

this_day <- as.character(today()) ## date
## url
url_vec = c(
    "http://www.alpen-route.com/live_camera/midagahara.html",
    "http://www.alpen-route.com/live_camera/daikanbou.html",
    "http://www.alpen-route.com/live_camera/murodou.html"
    )
## file names
prefix = "/Users/Gaku/Dropbox/040-ToyamaNIHU/020-analysis/030-ScrapingMovie/"
mov_labz = c(
    str_c("1_midagahara_", this_day, ".mp4"),
    str_c("2_daikanbou_", this_day, ".mp4"),
    str_c("3_murodou_", this_day, ".mp4")
    )

取得したい動画ファイルは毎日更新されるので,「何月何日に取得した動画か」をファイル名に自動で含めてしまいたい. そこで,上のコードの1行目ではlubridate::today()関数を使って,今日の日付 (2017-02-15) を取得している. url_vecには,室堂・大観峰・弥陀ヶ原のタイムラプス動画が公開されているウェブページのurlを格納し,prefixには,動画ファイルの保存先ディレクトリへのパスを格納している. 最後に,mov_labzには,動画ファイルの名前を格納している.stringr::str_cは引数で与えられた複数の文字列を結合する関数で,たとえばstr_c("1_midagahara_", today, ".mp4")とすると“1_midagahara_2017-02-15.mp4”という出力が得られる.

以下のコードを実行すれば,動画ファイルを取得して保存することができる.

movie_loop = 1:length(url_vec)
for (i in movie_loop) {
    ## Extract info
    target.page = htmlParse(url_vec[i])   ## web page
    mov_part = getNodeSet(target.page, '//*[(@id = "mov")]')    ## extract movie part
    mov_part_str = sapply(mov_part, function(x)paste(capture.output(print(x))))[2,] ## extract movie url
    mov_part_str = str_replace(mov_part_str, "  <source src=\"", "")
    mov_part_str = str_replace(mov_part_str, "\"/>", "")

    ## Download movie file
    download.file(url=mov_part_str, destfile=str_c(prefix, mov_labz[i]), method="curl")
}