準備

今回利用するパッケージを読み込む。インストールが必要な場合は、先にインストールすること。

library("plyr")
library("readr")
library("XML")
library("stringr")

HTML

基本的にすべてのウェブページ(現在表示中のこのページを含む)はHTMLを使って書かれている。HTMLとはHyper Text Markup Languageの略である。

例として、このページのソースコードを見てみよう。 多くのウェブブラウザが、現在閲覧中のページのソースコードを表示する機能を備えている。 Firefoxでは、“Cmd”(あるいは“Ctrl”)と “U” を同時に押すとソースが表示される。 Safariでは、 “Cmd” と “Option” と “V” を同時に押す。 Google Chromeの場合、 Macでは “Cmd” と “Option” と “U” を同時に、Windowsでは “Ctrl” と “U” を同時に押す。

ソースコードを見ればわかるように、通常の静的なウェブページは、HTML(とCSS)によって書かれたテキストファイルによって作られている。ウェブブラウザがHTMLの文法を理解し、綺麗に解釈した(見た目を整えた)ウェブページを私たちに見せてくれる。

Rだけでなく、HTMLも学ぶのは、

  1. 研究のためにオンラインでデータを収集する必要が生じる可能性が高い(そのためにHTMLの構造を理解する必要がある)
  2. 研究者になったら、自分の個人ウェブサイトをもつことが期待されている

といった理由による。

幸い、HTMLはコンピュータ言語の中では比較的平易なものであり、オンラインの学習教材(無料のものも含む)が大量に存在する。例えば、ココ でHTMLの基本を学ぶことができる。

正規表現 (Regular Expressions)

ウェブ上に存在するデータのほとんどはテキスト(文字)である。 また、その大部分はデータセットとして利用可能な状態に構造化されていない。 構造化されていない文字の集合そのものをデータとして利用する場合もあるが、たいていの場合、特定のパタンに合致する文字情報だけを取り出して利用したい。 よって、文字列の中から特定のパタンに当てはまるものを見つけてそれを取り出す必要がある。そのために使われるのが、正規表現 (regular expressions) である。

ここでは、基本的な正規表現を紹介する。

練習のために、2つの文字列 (character string) オブジェクトを作る。

eg_Acton <- 'Power tends to corrupt. Absolute power corrupts absolutely.'
eg_Wilde <- 'A little sincerity is a dangerous thing, and a great deal of it is absolutely fatal.'

文字列のマッチング

文字列情報の処理のために、stringr パッケージを利用する。 まず、stringr::sting_exract() 関数を用いて特定の文字列を取り出してみよう。

str_extract(eg_Acton, pattern = 'corrupt')
## [1] "corrupt"

eg_Actonオブジェクトには指定した文字列である ‘corrupt’ に一致する部分があるので、関数は指定した文字列そのものを返す。 このオブジェクトには ‘corrupt’ という文字列が2回登場するが、この関数は最初の ‘corrupt’ を発見した時点でそれを返し、2つ目の’corrupt’ には到達しない。

オブジェクトの中に指定した文字列が存在しない場合、NA が返される。

str_extract(eg_Acton, pattern = 'Acton')
## [1] NA

str_extract() は指定した文字列を1度しか返さないが、stringr::str_extract_all() は一致したものをすべてを返す。

str_extract_all(eg_Acton, pattern = 'corrupt')
## [[1]]
## [1] "corrupt" "corrupt"

この例が示す通り、 str_extract_all() はリスト (list) を返す。 リストの長さは文字列が一致した回数ではなく、パタンを探す対象として渡したオブジェクトの数である。 上の例では、1つのオブジェクトで2回マッチしているので、返されたリストの長さは1であり、リストの第一要素の長さは2である。

list_corrupt <- str_extract_all(eg_Acton, pattern = 'corrupt')
length(list_corrupt)        # length of the list
## [1] 1
length(list_corrupt[[1]])   # length of the first element of the list
## [1] 2

次の例では、リストの長さが2になる。

(list_absolute <- str_extract_all(c(eg_Acton, eg_Wilde), pattern = 'absolute'))
## [[1]]
## [1] "absolute"
## 
## [[2]]
## [1] "absolute"
length(list_absolute)       # length of the list
## [1] 2
length(list_absolute[[1]])  # length of the first element of the list
## [1] 1

これらの関数は、いずれも大文字と小文字を区別する (case-sensitive)。大文字、小文字の別を無視したいときはignore.case()を併せて使う。

str_extract_all(c(eg_Acton, eg_Wilde), pattern = ignore.case('absolute'))
## [[1]]
## [1] "Absolute" "absolute"
## 
## [[2]]
## [1] "absolute"

オブジェクトの「先頭で」特定の文字列に一致するものを見つけたいときは、“^” (the caret symbol) を次のように使う。

str_extract(eg_Acton, pattern = '^corrupt')
## [1] NA
str_extract(eg_Acton, pattern = '^Power')
## [1] "Power"

同様に、文字列の「末尾で」一致を探すときは、“$” を使う。

str_extract_all(c(eg_Acton, eg_Wilde), pattern = '.$')
## [[1]]
## [1] "."
## 
## [[2]]
## [1] "."

“|” 記号を使うと、指定したパタンのうちの「どれか」に合致する部分を抜き出せる(“|”記号は通常 “or” を表す。ここでも同様である)。

str_extract_all(eg_Acton, pattern = 'corrupt|absolute')
## [[1]]
## [1] "corrupt"  "corrupt"  "absolute"

正規表現を使うときは、スペース(空白)の扱いに注意が必要である。 例えば、文字列を指定するときに文字列と記号の間にスペースを含めると、結果が変わってしまう。

str_extract_all(eg_Acton, pattern = 'corrupt | absolute')
## [[1]]
## [1] " absolute"

この例では、関数が “corrupt ”(アルファベットの後に半角スペースがある)または “ absolute”(アルファベットの前に半角スペースがある)のいずれかに一致する部分を抜き出している。eg_Acton オブジェクトを見ると、どの “corrupt” も直後に半角スペースを持たない。一方で、“absolute” の直前には半角スペースがある(absolutelyという単語の直前)。したがって、後者だけが抜き出される。

一般的なパタン一致

ここまでは、特定の文字列に一致する文字列を見つけてきた。 しかし、正規表現を使うと、より一般的なパタンに一致する文字列を見つけることができる。

例えば、“.” (dot) は正規表現ではあらゆる文字 (character) と一致する。

str_extract_all(eg_Wilde, pattern = 'i.')
## [[1]]
## [1] "it" "in" "it" "is" "in" "it" "is"

ドット自体を取り出したいときは、バックスラッシュ と一緒にして “\.” とする。

str_extract_all(eg_Wilde, pattern = '\\.')
## [[1]]
## [1] "."

ここで、バックスラッシュ (“\”) はRで定義された文字列の意味から私たちを逃してくれるので、 エスケープ文字 (escape character) と呼ばれる。R(やその他の多くのプログラミング言語における正規表現)において “.” は「あらゆる文字」を意味するが、“\.” はその意味から私たちを解放し、ドットそのものとの一致を探してくれる。 ただし、バックスラッシュ自体が正規表現のバックスラッシュであることをRに伝えるために、バックスラッシュを二重にする必要がある。

“it” または “in”を見つけるには次のようにする。

str_extract_all(eg_Wilde, pattern = 'i[tn]')
## [[1]]
## [1] "it" "in" "it" "in" "it"

つまり、[] で囲まれ文字のうちいずれかが入るパタンで一致を探す。

パタンではなく、単語としての “it” と “is” のみを抜き出す(例えば、littleに含まれる “it” は除く)には、次のようにする。

str_extract_all(eg_Wilde, pattern = '\\bi[ts]\\b')
## [[1]]
## [1] "is" "it" "is"

“\b”は、そこが単語の境界 (boundary) であることを示す。

これらの例からわかるように、正規表現を使えば様々な条件にあった文字列を探し出すことができる。 以下に、よく使う正規表現のパタンを示す。

  • [:digit:] – 数字: 0 to 9
  • [:lower:] – 小文字: a to z
  • [:upper:] – 大文字: A to Z
  • [:alpha:] – アルファベット: a to z と A to Z
  • [:alnum:] – アルファベットと数字: a to z, A to Z, 0 to 9
  • [:punct:] – パンクチュエーション文字:! " # $ % & ’ ( ) * + , - . /
  • [:blank:] – 空白文字:スペースとタブ
  • [:space:] – スペース文字: スペース、タブ、改行とその他のスペース文字
  • [:print:] – 印刷可能な文字: [:alnum:], [:punct:], [:space:]

また、特別な意味を持つ記号もある(これらを使うときには “\\w” のようにバックスラッシュを1つ増やす必要がある)。

  • \w – 文字
  • \W – 文字以外
  • \s – スペース文字
  • \S – スペース文字以外
  • \d – 数字
  • \D – 数字以外
  • \b – 単語の境界
  • \B – 単語の境界以外
  • \< – 単語の先頭
  • \> – 単語の末尾

さらに、それぞれの正規表現に一致する回数も指定することができる。 例えば、5文字の単語を探すには、次のようにする。

str_extract_all(eg_Wilde, pattern = '\\b[:alpha:]{5}\\b')
## [[1]]
## [1] "thing" "great" "fatal"

数を指定するときは、以下の限量詞 (quantifier) を登場回数を指定したい表現の直後につける。

  • ?: 直前の表現はオプショナル(あってもなくてもよい)で、最大で1回一致
  • *: 直前の表現と0回以上一致
  • +: 直前の表現と1回以上一致
  • {n}: 直前の表現とちょうどn回一致
  • {n,}: 直前の表現とn回以上一致
  • {n,m}: 直前の表現とn回以上m回以下の一致

限量詞をつけない場合はちょうど1回一致である。

使用例をいくつか示す。

y_names <- c('Yamada', 'Yamaji', 'Yamamoto', 'Yamashita',
             'Yamai', 'Yanai', 'Yanagi', 'Yoshida')
unlist(str_extract_all(y_names, pattern = 'Ya.a.+'))
## [1] "Yamada"    "Yamaji"    "Yamamoto"  "Yamashita" "Yamai"     "Yanai"    
## [7] "Yanagi"
unlist(str_extract_all(y_names, pattern = 'Ya.a.?i'))
## [1] "Yamaji" "Yamai"  "Yanai"  "Yanagi"
unlist(str_extract_all(y_names, pattern = 'Y.+da'))
## [1] "Yamada"  "Yoshida"
unlist(str_extract_all(y_names, pattern = 'Yama.+t.?'))
## [1] "Yamamoto"  "Yamashita"
unlist(str_extract_all(y_names, pattern = 'Ya[:alpha:]{4}$'))
## [1] "Yamada" "Yamaji" "Yanagi"
unlist(str_extract_all(y_names, pattern = '^[:alpha:]{6,}$'))
## [1] "Yamada"    "Yamaji"    "Yamamoto"  "Yamashita" "Yanagi"    "Yoshida"
unlist(str_extract_all(y_names, pattern = '^[:alpha:]{6,8}$'))
## [1] "Yamada"   "Yamaji"   "Yamamoto" "Yanagi"   "Yoshida"
unlist(str_extract_all(y_names, pattern = '\\w+m\\w+'))
## [1] "Yamada"    "Yamaji"    "Yamamoto"  "Yamashita" "Yamai"

Rにおよる正規表現についてより詳しくは、Regular Expresions as used in R を参照されたい。 また、正規表現そのものについては、Jeffrey E. F. Friedl. 2006. Mastering Regular Expressions, 3rd ed. (O’Reilly) [株式会社ロングテール, 長尾高広 訳. 2008.『詳細 正規表現 第3版』オライリー・ジャパン] が詳しい。



オンラインでデータを集める

データセットのダウンロード

研究においてウェブで手に入るデータを使うことはよくある。 例として、世界中で起きたイベントデータを提供するGDELT プロジェクトのウェブサイト を訪れてみよう。 このページでは、たくさんのイベントファイルが提供されている。 データを入手するためには、それぞれのファイルへのハイパーリンクをクリックして各ファイルをダウンロードすればよい。 必要なファイルの数が少ないなら、この方法で問題ない。しかし、必要なファイルの数が多いとき、1つずつクリックするのは効率がよいとは言えない(私のような怠惰な人間はそんな面倒なことはしたくない)。

R(あるいは他のプログラム)を使えば、ハイパーリンクを1つずつクリックする手間を省くことができる。 Rに複数のファイルをダウンロードするよう命令すればよいのである。

例として、2015年のイベントデータをダウンロードしてみよう。 (注:ファイル容量が大きいので、ダウンロード自体に時間がかかる。すべてのファイルをダウンロードしようとするには相当の時間が必要ようなので、一部だけ指定すること。 自分で試すときは、2015を2012に変えることを勧める。)

まず、データセットへのハイパーリンクを表示するウェブページを特定する。

url <- 'http://data.gdeltproject.org/events/index.html'

次に、XML::getHTMLLinks() でページ内に存在するすべてのハイパーリンクを見つける。 ここで例として使っているGDELT のウェブページは、ハイパーリンクが一目でわかるように表示されている。 しかし、他のウェブサイトではハイパーリンクがわかりにくいこともある。 getHTMLLinks() を使えば、一見わかりにくいハイパーリンクでも簡単に見つけられる。

links <- getHTMLLinks(url)
links[1:5]    # check the content (the first 5 links only)
## [1] "md5sums"                            
## [2] "filesizes"                          
## [3] "GDELT.MASTERREDUCEDV2.1979-2013.zip"
## [4] "20160531.export.CSV.zip"            
## [5] "20160530.export.CSV.zip"

getHTMLLinks(url) は指定したページ内に存在するすべてのリンクを返す。したがって、その中にはデータセット以外へのリンクもある。 そこで、見つけたリンクの中から必要なものだけを選ぶ必要がある。 私たちが欲しいのは2015年のデータであるが、2015年のイベントデータのファイルが共有するパタンは何だろうか?ウェブページから、パタンを読み取ろう。

ウェブページ(あるいはそのソースコード)から、2015年のデータファイルはファイル名が “2015” で始まることがわかる。したがって、“2015” から始まるリンクだけを抜き出せばよい。 stringr::str_detct() は、渡された名前が持つオブジェクトが特定の文字列を含むかどうか判断し、TRUE またはFALSEを返す。私たちが見つけたいのは “2015” から始まる文字列なので、正規表現を使って “^2015” に一致するかどうかを判定すればよい(“^” は文字列の先頭であることを示す。したがって、“^2015” は “201510” には一致するが、 “102015” には一致しない)。

filenames_2015 <- links[str_detect(links, '^2015')]  # pick out the 2014 files
filenames_2015[1:10]   # check the content (the first 10 elements only)
##  [1] "20151231.export.CSV.zip" "20151230.export.CSV.zip"
##  [3] "20151229.export.CSV.zip" "20151228.export.CSV.zip"
##  [5] "20151227.export.CSV.zip" "20151226.export.CSV.zip"
##  [7] "20151225.export.CSV.zip" "20151224.export.CSV.zip"
##  [9] "20151223.export.CSV.zip" "20151222.export.CSV.zip"
length(filenames_2015)
## [1] 365

これで、2015年のイベントデータへの 365 個のリンクを見つけることができた。 これだけの回数のクリックをしたいだろうか?

データが存在する場所がわかったところで、データファイルをダウンロードしてみよう。ウェブ上のファイルは、download.file() でダウンロードできる。ダウンロードプロセスをわかりやすくするため、まず関数を作ろう。この関数は、ダウンロードしたファイルをfolder で指定したフォルダ(ディレクトリ)に保存する。

download_file <- function(file, base_url, folder) {
    ## Args: file = name of the target file
    ##       base_url = path to the dir where the file exists
    ##       folder = folder to save the file
    
    # create a folder in the current wd if it doesn't exist
    dir.create(folder, showWarnings = FALSE)
    file_url <- str_c(base_url, file)    # URL of the file
    outfile <- str_c(folder, '/', file)  # path to the file on computer
    if (!file.exists(outfile)) { # download only if the file doesn't exist
        download.file(file_url, outfile)
        Sys.sleep(1)    # pause one second after downloading a file
    }
}

この関数は、fileで指定された1つのファイルのみダウンロードする。 関数内でSys.sleep()を使うことによって、意図的にダウンロードの速度を落としている(関数が終了する前にRが1秒間止まる)。 これがなければダウンロードが早くできる。早いことは良いことのように思われるかもしれないが、この場合はそうでもない。 R(あるいは他のプログラムでも)が特定のウェブページに短時間の間に何度もアクセスすると、サーバがRからの要求に対応できず、ダウンしてしまうかもしれない。また、ウェブサイトの管理者は、私たちがサイトを攻撃していると勘違いするかもしれない。いずれにの場合も、データの提供者に迷惑をかけることになる。データを利用させてもらうのだから、相手に迷惑をかけてはいけない。 したがって、本来は人間が手動で行うことが想定されている作業をプログラミングによって自動化するにあたり、意図的に速度を落とし、サーバに過剰な負荷がかからないように工夫しなければならない。そこで、Rの動きを意図的に遅くするのである。

私たちがダウンロードしたいファイルは複数あるので、上で作ったdownload_file() 関数を一度に複数のファイルに適用したい。そのためにまず、ファイル名のリストを作る。

file_list <- as.list(filenames_2015)
file_list[1:3]  # check the content
## [[1]]
## [1] "20151231.export.CSV.zip"
## 
## [[2]]
## [1] "20151230.export.CSV.zip"
## 
## [[3]]
## [1] "20151229.export.CSV.zip"

これで準備は整った。lapply() を使い、download_file() 関数を file_list に適用しよう。

lapply(file_list, FUN = download_file,
       base_url = 'http://data.gdeltproject.org/events/',
       folder = 'GDELT_2015_zip')

指定したフォルダ内にダウンロードされたファイルを確かめてみよう(“./” は現在のフォルダを表す)。

saved <- list.files('./GDELT_2015_zip')
saved[1:5]
## [1] "20150101.export.CSV.zip" "20150102.export.CSV.zip"
## [3] "20150103.export.CSV.zip" "20150104.export.CSV.zip"
## [5] "20150105.export.CSV.zip"
length(saved)
## [1] 365

指定したフォルダ内に1個のzipファイルがあり、ファイルのダウンロードに成功したことがわかる。

ジップファイルの展開も、Rでできる。

dir.create('GDELT_2015_csv')  # create folder to save csv files
zip_list <- as.list(str_c('GDELT_2015_zip/', saved))  # list of the zip files 
# unzip the files and save them in exdir
lapply(zip_list, FUN = unzip, exdir = 'GDELT_2015_csv')

これで、zipファイルを展開してできたCSVファイルがGDELT_2014_csvという名前のフォルダに保存された。

saved <- list.files('GDELT_2015_csv')
saved[1:5]
## [1] "20150101.export.CSV" "20150102.export.CSV" "20150103.export.CSV"
## [4] "20150104.export.CSV" "20150105.export.CSV"
length(saved)
## [1] 365

CSVファイルは、readr::read_csv()関数で読み込むことができる。

ウェブスクレイピング

インターネット上には様々なデータが存在するが、いつもダウンロード可能なファイルとして存在するわけではない。 むしろ、多くの情報はページ上に文字情報として表示される。 よって、研究に必要なデータを集めるに、ウェブページ上の文字情報を掻き集める (scrape) ことが求められる。

必要な情報が単一のウェブページ上にあるなら、ページをコピーし、それをテキストファイルなどにペーストすることによってデータセットを作ることができる。あるいは、Outwit Hub (無料版)を使うことも考えられる。 しかし、多くの場合、必要な情報は複数のページに散らばっている。 また、ウェブページ上のすべての文字情報ではなく、一部の情報だけが必要になる場合がほとんどである。そんなときは、スクレピングを行うに限る。

Mitchell (2015) は、「ウェブスクレイピングは、APIを使うことなくデータを集める方法である」と述べている。これは暗に、API (Application Progrramming Interface) が使えるときは、APIを使った方がよいということを示唆している。例えば、e-Stat はAPIを提供しているので、ここからデータを入手する際にはスクレイピングはせず、API を利用すべきである(次回の授業内容)。しかし、すべてのウェブサイトがAPIを提供しているわけではないので、そういうときはスクレピングを使う。

現在、スクレイピングを行う最も簡単な方法は(おそらく) PythonBeautiful Soup または Scrapy を用いる方法である。 詳しくは、Ryan Mitchell. 2015. Web Scraping with Python. (O’Reilly) を参照。

以下、ごく簡単な例を使ってウェブスクレイピングを説明する。

例:庶民院議員の連絡先情報を収集する

例題として、英国庶民院 (House of Commons) 議員に関するデータを集めてみよう。 私たちが集めたい情報は、各議員の (1) 氏名、(2) 所属政党、(3) 選挙区、 (4) 電話番号であるとする。 英国議会は、公式サイトで各議員に関する情報を提供しているので、それを利用する。 英国議会のサイト www.parliament.uk を訪問してみよう。 このページで議員の氏名をクリックすると、その議員の情報を見ることができる。 地道に全員の氏名を順番にクリックすれば必要な情報は手に入りそうだが、かなり時間がかかるだろう。スクレイピン用のプログムが書けないなら、時間がかかってもやるしかない(研究とはそういうものである)。しかし、スクレイピングができるならそんな面倒なことはしなくてよい。スクレイピングを覚える最大の理由は、楽をするためである(プログラミング言語を覚えるのは、将来楽をするための投資である)。 私は楽をしたいので、スクレイピングで情報を集めることにする。

まず、現在開いているページのURLをスクレイピングのベースに指定する。

url <- 'http://www.parliament.uk/mps-lords-and-offices/mps/'

このページ上に各議員のページへのハイパーリンクがあるはずなので、getHTMLLinks()を使ってリンクを見つける。

links <- getHTMLLinks(url)
links[1:10]
##  [1] "/"                                                                        
##  [2] "/site-information/accessibility/"                                         
##  [3] "/site-information/privacy/"                                               
##  [4] "https://subscriptions.parliament.uk/accounts/UKPARLIAMENT/subscriber/new?"
##  [5] "/site-information/rss-feeds/"                                             
##  [6] "/site-information/contact-us/"                                            
##  [7] "/"                                                                        
##  [8] "/business/"                                                               
##  [9] "http://www.parliament.uk/business/commons/"                               
## [10] "http://www.parliament.uk/business/lords/"

これらのリンクの中には、議員の情報以外のページへのリンク(すなわち、私たちにとって不要なリンク)も含まれている。 そこで、私たちが求めているページ、つまり、庶民院議員個人の情報が提供されているURL を特定する必要がある。 議員のページをいくつかクリックしてみると、各議員のページのURLには、“biographies/commons” という文字列が含まれていることがわかる。 よって、先ほど見つけたリンクの中から、この文字列を含むものを抜き出せばよい。 stringr::str_detect() を使えば、特定の文字列を含むかどうか判定することができるので、これを利用する。

commons_MPs <- links[str_detect(links, 'biographies/commons/')]
commons_MPs[1:5]
## [1] "http://www.parliament.uk/biographies/commons/ms-diane-abbott/172"         
## [2] "http://www.parliament.uk/biographies/commons/debbie-abrahams/4212"        
## [3] "http://www.parliament.uk/biographies/commons/nigel-adams/4057"            
## [4] "http://www.parliament.uk/biographies/commons/adam-afriyie/1586"           
## [5] "http://www.parliament.uk/biographies/commons/ms-tasmina-ahmed-sheikh/4427"

議員が何人いるか(より正確には、議員のページがいくつあるか)確認してみよう。

# the number of MPs in the House of Commons
(n <- length(commons_MPs))
## [1] 649

上で書いたコードと公式ウェブサイトの情報の両者が正しいとすれば、649人の庶民院議員がいるということになる。 スクレイピングのおかげで、649回のクリックと各ページの中にある情報のコピペという退屈な作業を避けることができる。

準備が整ったので、スクレイピングしてみよう。 ウェブスクレイピングは、以下のステップで行う。

  1. ページを開く
  2. HTMLの文法を解析する (parse the HTML)
  3. HTMLタグを利用し、特定の内容を見つける
  4. 見つけた内容を抜き出す
  5. 次のページを開き、最後のページまで上のステップを繰り返す

ウェブページ(あるいはあらゆるテキストファイル)を開くには、read.lines() を使う。 この関数は、ファイルを1行ずつ読む。 次に、XML::htmlParse() 関数で、HTMLの構造を解析する。 そうすることで、HTMLの構造(タグなど)を利用して、ページ内の特定の内容にアクセスすることができるようになる。

ページ内のどこに必要な情報があるかを知るためには、ページのソースコード (HTML) を開いて読む必要がある。 HTMLの構造を理解し、必要な部分を効率的に指定するために、HTML の知識が不可欠である。HTMLは簡単なので、この機会に覚えてしまおう。

構造解析されたオブジェクトから特定の内容を抜き出すには、XML::xmlValue() を使う。その後、より細かい指定を行うためにstr_extract() と正規表現を使う。最後に stringr::str_sub() で余分なものを削除する。

次の関数は、1つのページから必要な情報をスクレイプするためのものである。

scrape_MP <- function(url) {
    ## Argument: url = URL of the target page
    ## Return: res = data frame containing 4 variables
    
    mp_source <- readLines(url, encoding = 'UTF-8')  # read the page
    mp_parsed <- htmlParse(mp_source, encoding = 'UTF-8') # parse the HTML
    
    # MP's name
    mp_name <- mp_parsed['//title']  # extract the "title" tag
    mp_name <- mp_name[[1]]          # extract the first element
    mp_name <- xmlValue(mp_name)     # extract the content (value)
    mp_name <- str_extract(mp_name, '[:upper:].+MP')  # extract the string of name
    mp_name <- str_sub(mp_name,      # trimming
                       start = 1, end = str_length(mp_name) - 3)    
    # MP's party
    # extract the "div" tag whose id is "commons-party"
    mp_party <- mp_parsed["//div[@id='commons-party']"]
    mp_party <- mp_party[[1]]        # extract the first element
    mp_party <- xmlValue(mp_party)   # extract the content
    mp_party <- str_extract(mp_party, '[:upper:].+')  # extract the string
    
    # MP's constituency
    # extract the "div" tag whose id is "commons-constituency"
    mp_const <- mp_parsed["//div[@id='commons-constituency']"]
    mp_const <- mp_const[[1]]
    mp_const <- xmlValue(mp_const)
    mp_const <- str_extract(mp_const, '[:upper:].+')    
    
    # MP's Phone number
    # extract the "p" tag whose data-generic-id is "telephone"
    mp_tel <- mp_parsed["//p[@data-generic-id='telephone']"]
    if (length(mp_tel) == 0) {  # if tel is not available
        mp_tel <- NA
    } else {
        mp_tel <- mp_tel[[1]]       # extract the list element
        mp_tel <- xmlValue(mp_tel)  # extract the content
        # find the string containing telephone number
        mp_tel <- str_extract(mp_tel, 'Tel:\\s+.+')
        # extract the phone number - format: 012 3456 7890
        mp_tel <- str_extract(mp_tel, '\\s\\d{3}\\s\\d{4}\\s\\d{4}')
        mp_tel <- str_sub(mp_tel, start = 2)  # trimming
    }
    # save in the data frame
    res <- data.frame(name = mp_name, 
                      party = mp_party, 
                      constituency = mp_const,
                      phone = mp_tel)
    Sys.sleep(1)   # pause a second 
    return(res)
}

1つのページをスクレイプするたびに1秒停止する行儀のよい関数である。

この関数をそれぞれの議員のページに適用するため、plyr::ldply() を使う。この関数は、.fun に渡された関数をリスト (list) に適用し(だから ldply)、 データフレーム (data frame) を返す(だから ldply)。

lst_MPs <- as.list(commons_MPs)
MP_contacts <- ldply(lst_MPs, .fun = scrape_MP)

できあがったデータフレームは、readr::write_csv() を用いてCSVファイルに保存しておこう。

dir.create('data', showWarnings = FALSE)
write_csv(MP_contacts, path = 'data/MP-contacts.csv')
# alternatively, we can use write.csv()
# write.csv(MP_contacts, file = 'data/MP-contacts.csv', row.names = FALSE)

こうして手に入れたCSVファイルがこれ である。



授業の内容に戻る