概要
GISホームページ内の国土数値情報ダウンロードサービスにおいて、「1kmメッシュ別将来推計人口データ(H30国政局推計)」(以下、推計人口データ)が提供されている。 Pythonで推計人口データのWebページをスクレイピングして、ファイルを機械的にダウンロードしたい。
環境
アプローチの検討
推計人口データは以下のとおり都道府県別にファイルをダウンロードできるようになっている。
スクレイピングのアプローチを考えるため、ファイルをダウンロードする仕組みがどのようなHTMLソースで実現されているかをChromeでみてみる。
HTMLソースをみると、以下のような<tr>
タグの単位で1つの都道府県が構成されている(これは北海道の例)ので、この中の要素を調べることにする。
<tr> <td class="bgc1" id="prefecture01">北海道</td> <td class="txtCenter">世界測地系</td> <td class="txtCenter">平成30年</td> <td class="txtCenter">9.70MB</td> <td class="txtCenter">1km_mesh_suikei_2018_shape_01.zip</td> <td class="txtCenter"> <a class="waves-effect waves-light btn indigo btn_padding" id="menu-button" onclick="javascript:DownLd('9.70MB','1km_mesh_suikei_2018_shape_01.zip','/ksj/gml/data/m1kh30/m1kh30-18/1km_mesh_suikei_2018_shape_01.zip' ,this);"> <span id="1km_mesh_suikei_2018_shape_01.zip-open" style="display: block"> <i class="material-icons">file_download</i> </span> <span id="1km_mesh_suikei_2018_shape_01.zip-close" style="display: none"> <i class="material-icons">star</i> </span> </a> </td> </tr>
真ん中あたりにある<a>
タグのonclick
属性でJavaSciptの関数を呼んでいるところに注目すると、引数の1つにファイルのパスらしき文字列 /ksj/gml/data/m1kh30/m1kh30-18/1km_mesh_suikei_2018_shape_01.zip
が見える。
Webページのドメインhttps://nlftp.mlit.go.jp
をそのパスらしき文字列のあたまにつけ、https://nlftp.mlit.go.jp//ksj/gml/data/m1kh30/m1kh30-18/1km_mesh_suikei_2018_shape_01.zip
としたURLにアクセスすると、対象のファイルがダウンロードできる。つまり、国土数値情報ダウンロードサービスで提供されている推計人口データは、ファイルのパスをHTMLソースのなかから特定できれば、JavaScriptを実行しなくても目当てのファイルが取得できることになる。
PythonでWebページのスクレイピングを行う場合には、JavaScirptの実行が必要かどうかで使うライブラリが異なる。JavaScriptを実行して動的に取得するコンテンツを対象にスクレイピングを行う場合には、Pythonからブラウザを制御するライブラリとしてSelenium
を使う必要がある。一方、今回のようにJavaScriptの実行が不要な静的なWebページのスクレイピングでは、Selenium
を使う以外にrequests
+ Beautiful Soup
を使うことでも対応できる。ここでrequests
はWebページを取得するためのHTTPリクエストの実行、Beautiful Soup
はHTMLのタグを解析してPythonから個々の要素へアクセスしやすくするといった役割をそれぞれ担う。
ファイルダウンロードの実装
推計人口データのWebページをスクレイピングし、すべての都道府県分のファイルをダウンロードするプログラムをつくる。
使用するライブラリをインストールしておく。
> py -m pip install requests beautifulsoup4
はじめに、推計人口データのWebページのHTMLソースを取得する。
import requests req = requests.get('https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-mesh1000h30.html')
取得したHTMLソースの中身を解析して、Beautiful Soupオブジェクトにする。Beautiful Soupオブジェクトにすることで、タグ名を指定して要素を抽出(find_all()
メソッド)したり、抽出した要素の子要素(たとえば<tr>
の中の<td>
)にfor
文で1つずつアクセスしたりといったことができるようになる。
from bs4 import BeautifulSoup soup = BeautifulSoup(req.text, 'html.parser')
今回はファイルパスが書かれている箇所、つまり以下の<a>
タグのonclick
属性の値をすべての都道府県について取得したい。
<a class="waves-effect waves-light btn indigo btn_padding" id="menu-button" onclick="javascript:DownLd(<ファイルパスが書かれている>);"> ... </a>
そのためにHTMLソースからid
属性の値がmenu-button
である<a>
タグの要素を抽出し、その抽出結果からonclick
属性の値を取り出す。
anchors = soup.find_all('a', attrs = { 'id' : 'menu-button'}) for anchor in anchors: print(anchor['onclick'])
print()
の結果を見ると、以下のようにJavaScript関数の呼び出し部分が文字列として都道府県ごとに抽出できていることがわかる。
javascript:DownLd('9.70MB','1km_mesh_suikei_2018_shape_01.zip','/ksj/gml/data/m1kh30/m1kh30-18/1km_mesh_suikei_2018_shape_01.zip' ,this); javascript:DownLd('2.39MB','1km_mesh_suikei_2018_shape_02.zip','/ksj/gml/data/m1kh30/m1kh30-18/1km_mesh_suikei_2018_shape_02.zip' ,this); javascript:DownLd('4.11MB','1km_mesh_suikei_2018_shape_03.zip','/ksj/gml/data/m1kh30/m1kh30-18/1km_mesh_suikei_2018_shape_03.zip' ,this); ...
次に、都道府県ごとに得られたJavaScript関数の呼び出し文字列の中から、ファイルパスを抽出してファイルをダウンロードする処理を考える。
得られた文字列の中から、関数の3番目の引数にあたるファイルパスの箇所を抽出するために正規表現を用いる。
ファイルパスを抽出するためには、「スラッシュ/
ではじまり.zip
で終わる」文字列を最短マッチ(できるだけ短い範囲でマッチ)するパターンをつくればよさそうである。
Pythonで正規表現を扱う標準ライブラリre
を用いて、先ほど取得したJavaScript関数の呼び出し文字列の1件目を例にしてファイルパスが抽出できるか確認する。
import re pattern = re.compile(r'/.+?\.zip') match = pattern.search("javascript:DownLd('9.70MB','1km_mesh_suikei_2018_shape_01.zip','/ksj/gml/data/m1kh30/m1kh30-18/1km_mesh_suikei_2018_shape_01.zip' ,this);") print(match.group())
結果は以下のとおり、期待したファイルパスが抽出できた。
/ksj/gml/data/m1kh30/m1kh30-18/1km_mesh_suikei_2018_shape_01.zip
抽出したファイルパスにWebページのドメイン部分の文字列を結合してURLをつくり、ファイルをダウンロードする。ダウンロード後のファイル名はファイルパスから抽出する。
import os dl_file = 'https://nlftp.mlit.go.jp' + match.group() req_content = requests.get(dl_file).content file_name = os.path.basename(dl_file) with open(file_name ,mode='wb') as f: f.write(req_content)
処理をまとめたプログラム
ここまでの処理をまとめたプログラムは以下のとおり。追加でダウンロードファイルの保存先フォルダを設定するようにしている。なお、すべての都道府県のファイルをダウンロードする際にサーバに負荷をかけないよう、time.sleep()
で少なくとも1秒の間隔をあけて次のファイルをダウンロードするようにする。
import requests import re import os import time from bs4 import BeautifulSoup dl_dir = 'C:/PATH/TO/DOWNLOAD/' target_domain = 'https://nlftp.mlit.go.jp' target_page = target_domain + '/ksj/gml/datalist/KsjTmplt-mesh1000h30.html' req = requests.get(target_page) soup = BeautifulSoup(req.text, 'html.parser') anchors = soup.find_all('a', attrs = { 'id' : 'menu-button'}) for anchor in anchors: pattern = re.compile(r'/.+?\.zip') match = pattern.search(anchor['onclick']) file_url = target_domain + match.group() req_content = requests.get(file_url).content dl_file = dl_dir + os.path.basename(file_url) with open(dl_file ,mode='wb') as f: f.write(req_content) time.sleep(1)