Pythonのスレイピングで国土数値情報の将来推計人口データをダウンロードする

概要

GISホームページ内の国土数値情報ダウンロードサービスにおいて、「1kmメッシュ別将来推計人口データ(H30国政局推計)」(以下、推計人口データ)が提供されている。 Pythonで推計人口データのWebページをスクレイピングして、ファイルを機械的にダウンロードしたい。

環境

  • Windows 11 Home: 21H2
  • Python: 3.9.0
    • requests: 2.26.0
    • beautifulsoup4: 4.10.0

アプローチの検討

推計人口データは以下のとおり都道府県別にファイルをダウンロードできるようになっている。

出典:https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-mesh1000h30.html

スクレイピングのアプローチを考えるため、ファイルをダウンロードする仕組みがどのような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)