rvest::html_table()的なものを自作する(お気持ち表明編)
細かい話はさておき、まずはこのページを見てください。
これは国土数値情報ダウンロードサービスのGISデータのデータの説明が書かれたページなんですが、ここから「属性情報」「地物情報」というのを抜き出そうとしてここ数日格闘しています。 スクレイピングのガチ勢のみなさまは、このテーブルをどう料理されるでしょうか。
つらい点
何がつらいのか、挙げていきましょう。
セルが結合されている
セルが結合されています。この程度であればまだかわいいもので、下の方にいくとこんな複雑なセル結合になっていたりします。
1つのテーブルに複数のテーブルが入っている
これは構造の話なので、ちょっとコードの実行結果で見てみましょう。
read_html()
で上のページのデータを取ってきて、テーブルだけを抜き出します。
なお、このページは古き良きテーブルレイアウトなので、html_nodes(css = "table")
だけだと一番外枠のテーブルにまでヒットしてしまいます。"table table"
が正解です。
library(rvest) library(dplyr, warn.conflicts = FALSE) library(purrr, warn.conflicts = FALSE) html <- read_html("http://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-P03.html") tables <- html %>% html_nodes(css = "table table")
この結果がどうなっているか見てみましょう。
tables #> {xml_nodeset (9)} #> [1] <table width="710" border="0" cellspacing="0" cellpadding="0" height="35"><tr>\n<td align="right"><img src="../../img/bar_up_secand.g ... #> [2] <table border="0" cellspacing="0" cellpadding="3" width="710" height="30"><tr><td> \r\n <p><a href="http://www.mlit.go.jp/kokud ... #> [3] <table align="left" cellspacing="0" cellpadding="3" width="350" border="0"><tr>\n<td width="10"> </td>\n<th bgcolor="#6666bb" align=" ... #> [4] <table style="border-bottom-width:0" border="1" cellspacing="0" cellpadding="3" width="730" align="center"><tr>\n<td bgcolor="##99ccf ... #> [5] <table style="border-top-width:0" border="1" cellspacing="0" cellpadding="3" width="730" align="center">\n<tr>\n<td bgcolor="#bbddff" ... #> [6] <table style="border-top-width:0" border="1" cellspacing="0" cellpadding="3" width="730" align="center">\n<tr bgcolor="#bbddff">\n<td ... #> [7] <table style="border-top-width:0" border="1" cellspacing="0" cellpadding="3" width="730" align="center">\n<tr bgcolor="#bbddff">\n<td ... #> [8] <table border="1" cellpadding="1" cellspacing="2" bgcolor="#CCCCFF" width="95%">\n<tr>\n<td colspan="8" bgcolor="#bbddff" class="head ... #> [9] <table border="0" cellpadding="1" cellspacing="2" width="650"><tr>\n<td valign="top" align="left"><input type="reset" value=" リセット ...
なんだ、けっこうあるじゃん、と思うかもしれませんが、ここで、「属性情報」「地物情報」を含んだテーブルだけに絞り込んでみます。
tables %>% keep(~ stringr::str_detect(html_text(.), "属性情報|地物情報")) #> {xml_nodeset (1)} #> [1] <table style="border-top-width:0" border="1" cellspacing="0" cellpadding="3" width="730" align="center">\n<tr bgcolor="#bbddff">\n<td ...
なんと、テーブル1つしかありません。人間の目には複数のテーブルがあるように見えますが、実は太字のヘッダ(に見えるもの)で区切られているだけで全部ひとつのテーブルなのです。。
つまり、ここからいい感じに値を取り出すには、先ほどのセル結合を見て空気を読んでテーブルを分割する必要があります。
ヘッダが<th>
タグじゃない
テーブルを無事に分割できたとして、ヘッダはどうやって見つければいいのでしょう。このテーブルには<th>
タグはありません。
html_nodes(tables, "th") #> {xml_nodeset (0)}
いい感じに背景色が付いている部分を見つけるしかありません。いい感じに背景色が付いている部分、というのは2種類あって(単純化のため<b>
タグとかは省いています)、
1) <tr>
タグにbgcolor
属性がついている
<tr bgcolor="#bbddff"> <td rowspan="3">属性情報</td> <td>属性名</td> <td>説明</td> <td>属性の型</td> </tr>
2) <tr>
タグにはついてないけど全<td>
タグにbgcolor
属性がついている
<tr> <td rowspan="3" bgcolor="#bbddff">属性情報</td> <td bgcolor="#bbddff">属性名</td> <td bgcolor="#bbddff">説明</td> <td bgcolor="#bbddff">属性の型</td> </tr>
という感じです。
colspan
の指定が間違っている
これは別のデータなんですけど、colspan
の設定が間違っていることもあります。
ヘッダ部分の<tr>
タグはこのようになっています。
<tr bgcolor="#bbddff"> <td rowspan="2" width="20%"><b>地物情報</b></td> <td width="18%"><b>地物名</b></td> <td colspan="4"><b>説明</b></td> </tr>
3つめの<td>
タグのcolspan
が4なので、図にすると以下のようになっているはずです。
1 2 6 +--+--+-----------+ | | | | +--+--+-----------+
しかし、その下の<tr>
タグを見ると…
<tr> <td>ニュータウン</td> <td colspan="2">都市の過密化への対策として郊外に新たに建設された新しい市街地.</td> </tr>
ということで、rowspan
で上から垂れ下がってきている左端の<td>
を差し引いても長さが合いません。
1 2 6 +--+--+-----------+ | | | | + +--+-----+-----+ | | | | +--+--+-----+ 4?
rvest::html_table()
ではだめな理由
rvest::html_table(fill = TRUE)
は、上のようにごちゃごちゃしたテーブルからでも、そこそこいい感じにデータを取り出してくれますが、ややこしい処理をしようと思うと自作する必要があります。
テキスト以外の情報も抜き出したい
当たり前ですが、テキストしか抜き出してくれません。しかし、今回は上に書いたようにbgcolor
属性とかを見ながらヘッダかどうかを判定したいので、その辺は自分でやる必要があります。
横方向にfillしてほしくない
fill = TRUE
を指定するとrvest::html_table()
は結合されているセルを同じ値で埋めてくれます。しかし、これはたぶん次回もうちょっと詳しく書きますが、縦方向(rowspan)は埋めてほしいですが、横方向(colspan)はそのままにしてほしいです。rvest::html_table()
はそんなに柔軟な指定はできないので、このあたりは自分でやる必要があります。
空白はいい感じに埋めてほしい
上にも貼ったこの入り組んだテーブルですが、これはこの空白部分を上の「エネルギー認定」で埋めたいです。これは、あとでtidyr::fill()
でやってもできそうなものですが、上に書いた横方向にfillしないという処理にするならこの段階でやる必要があります。
次回に続く
という感じで、なぜ自作したいかを書いていたら長くなってしまったので、実装は次回に…