QGISを使って店舗位置を表示する

これまでの記事でAOKIと青山の店舗名と住所をwebスクレイピングCSVファイルに保存しました。

WEBスクレイピング - AOKI編 - - 道草を楽しむブログ

WEBスクレイピング - 青山編 - - 道草を楽しむブログ


今回はフリーのGISソフトであるQGISを使用して店舗位置を地図上に表示してみます。

【目次】


環境


ジオコーディング

QGISは地物位置のインプットデータとして住所を用いることができないため、これまで取得した住所情報を緯度経度といった座標値に変換するジオコーディングが必要になります。

ジオコーディングには、東京大学空間情報科学研究センターが提供する「CSVアドレスマッチングサービス」を利用します。

f:id:hhgingisland:20180522213422j:plain:w300

パラメータ設定で以下の内容を記載します。

  • 対象範囲:全国街区レベル(経緯度・世界測地系
  • 住所を含むカラム番号:2 (AOKIの場合)、3(青山の場合)
  • 入力ファイルの漢字コード:シフトJISCSVファイルを保存した時の文字コードによって変わります)

送信ボタンを押すと、入力ファイルに緯度経度情報等(fX、fY、iConf、iLvlの4列)が追加されたcsvファイルが返ってきます。

fXが経度、fYが緯度を表しています。
なお、下図の1行目にある項目名col0~col2の自分が分かる名称に変えておくと、QGISでラベル表示するときに分かりやすいです。

f:id:hhgingisland:20180522215751j:plain:w400


QGIS上に店舗位置を表示する

QGISを起動し、「デリミティッドテキストファイルからレイヤを作成」をクリックし、ジオコーディングしたCSVファイルを入力します。

パラメータ設定はデフォルトで大丈夫かと思いますが、エンコーディングとXYフィールドに経度と緯度の列項目名を正しく指定できているか確認しましょう。

f:id:hhgingisland:20180522220342j:plain:w400

「OK」を押して店舗位置の座標系をJGD2000に設定すると店舗位置が表示されます。 緑丸がAOKI、紫丸が青山の店舗位置です。

なお、OTFを有効にし座標系はWEBメルカトル(EPSG: 3857)にしています。


f:id:hhgingisland:20180526154548p:plain


ベースマップの追加

店舗位置だけでも日本の概形が分かりますが、あると便利なのでベースマップを一応入れておきます。

OpenLayers Plugin」という便利なプラグインhttps://plugins.qgis.org/plugins/openlayers_plugin/ )をインストールすれば、Google MapsOpenStreetMapなどの地図を表示することができます。私の場合はOpenStreetMapを入れました。

f:id:hhgingisland:20180526155344p:plain


道路レイヤの追加

国道沿いに店舗があるかどうか調べたいので道路情報を追加します。

国土数値情報 ダウンロードサービス」には、平成9年の古い道路データ(http://nlftp.mlit.go.jp/ksj/gmlold/meta/ksjshpgml-N01.html)しか見当たりませんでしたが、それを使うことにします(探し方が悪かったか??)。

QGISの「ベクタレイヤの追加」によりダウンロードしたシェープファイル(N01-07L-2K_Road.shp)をロードします。座標系はJGD2000 (EPSG: 4612)です。

このデータは道路種別コード属性(N01_001)により高速道路、一般国道主要地方道の3タイプの道路が格納されています。

f:id:hhgingisland:20180526155651p:plain:w400

レイヤプロパティのスタイルで、「分類された」を用いてN01_001の属性値によりシンボルの場合分けをします。

一般国道(水色の線)のみを表示させた場合は以下の図のようになりました。

f:id:hhgingisland:20180526160142p:plain
国土交通省国土政策局「国土数値情報(道路)」をもとに編集したものである。


高速道路に関しては新しいデータ(http://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N06-v1_2.html)があり、このデータはバイパスも含むようなのでこちらもダウンロードして使うことにします。

こちらも表示すると以下のようになります。ピンク線が高速道路です。

f:id:hhgingisland:20180526160425p:plain
国土交通省国土政策局「国土数値情報(道路)」をもとに編集したものである。


作成したマップを眺める

作成したマップを見てみると以下のことが言えそうです。

  1. 青山は全国展開していてるが、AOKIは出店エリアが絞られている。
  2. AOKIと青山は近いところにありそう。
  3. 両店舗は主要国道沿いに分布してそう。


f:id:hhgingisland:20180526162608p:plain:w400
AOKIレイヤ(緑)を上、青山レイヤ(紫)を下にして表示
f:id:hhgingisland:20180526163903p:plain:w400
AOKIレイヤ(緑)を下、青山レイヤ(紫)を上にして表示

上の2つの図はAOKIと青山レイヤの上下間を変えたものです。

紫点の青山は全国にまんべんなく分布しているのに対し、緑色のAOKIは中国地方や四国に店舗がほぼなく、町が大きそうなところに店舗を絞って出店していそうです。

また、上の2つ目の図を見ますと、紫色のAOKIレイヤが青山レイヤをほぼ覆いかくしていることから、AOKIと青山の店舗は両者近いところにありそうです。

f:id:hhgingisland:20180526160425p:plain

そして上図のように水色の一般国道上に各店舗がのっていることから、国道沿いに店舗が分布してそうだと推測できます。

以上、定性的にAOKIと青山の位置関係を考えてみました。


次の記事では、少し定量的に位置関係を調べてみたいと思います。

WEBスクレイピング - 青山編 -

前回の記事ではpython + Beautiful Soupを使ってAOKIの店舗住所をスクレイピングしてみました。

hhgingisland.hatenablog.com

今回は青山の店舗住所を抽出したいと思います。
作業環境は前回の記事と同様です。


青山の店舗検索ページ

f:id:hhgingisland:20180529001253p:plain


青山の店舗検索ページ(https://www.y-aoyama.jp/shop/?lc=header)にアクセスし調べてみたところ、上の画像のように左上の都道府県欄にどこかの都道府県を指定しないと店舗情報が出てきませんでした。

上の画像では北海道を指定しており、
URLはhttps://www.y-aoyama.jp/shop/?pref=1となっています。

?pref=以下の数値が各都道府県と対応づけられているので、この数値を変えることで、全国の店舗名と住所を抽出できそうです。


ソースHTMLを見てみる

f:id:hhgingisland:20180522205425j:plain


Google chromeの開発者ツールを使って見ていくと、

  • 店舗名は、<div class=” arrowTicket_body”> → <h2>の内部テキスト
  • 住所は、<div class=” arrowTicket_body”> → <p class=” arrowTicket_text arrowTicket_margin”>の内部テキスト

であることが分かります。


以下のコードのようにfind_all()get_text()を使って簡単に店舗名と住所(検索ページの最初に記載されている1店舗分ですが)を抽出することができました。

なお、get_text()で得られた住所情報は、郵便番号と住所が混じっていためsplit()を用いて分離しています。

#! python3
# -*- coding: utf-8 -*-

import requests
from bs4 import BeautifulSoup

url = r'https://www.y-aoyama.jp/shop/?pref=1'

res = requests.get(url)
res.raise_for_status()

soup = BeautifulSoup(res.content, 'html.parser')
shop_elems = soup.find_all('div', class_='arrowTicket_body')

# 店舗名
shop_name = shop_elems[0].h2.get_text()
# 住所
shop_address_elem = shop_elems[0].find_all('p', class_='arrowTicket_text arrowTicket_margin')
shop_address = shop_address_elem[0].get_text()

# 郵便番号と住所を分離し保存 
post_code = shop_address.split(None,1)[0]
address = shop_address.split(None,1)[1]


コード内のshop_elemsはページ内の店舗情報が記載されているdiv要素のリストです。

ページ内の店舗情報を抽出する場合には、リスト内でループを回すことで取得できます。


全店舗の位置情報を取得するためのコード

上述のコードで店舗名と店舗住所が抽出できたので、それを基にfor文を使用して全店舗の情報を抽出していきます。

ソースコードを下に貼りました。流れは以下の通りです。

  • 都道府県のページに移動するために、都道府県ごとの店舗名と住所を取得しリストに保存。
  • 既に閉店しているものを除外。
  • 住所に &nbsp が入っているのを除外(文字化け対策)。
  • 抽出した情報をCSVファイルに格納。


取得した住所情報には、既に閉店した店舗情報が入っており、それらは店舗名に「完全閉店」と書かれているので、リスト内包記とfilter関数を用いて閉店した店舗を除外しています。

また、住所に半角スペースの一種である&nbsp (0xA0)が含まれており、本環境では文字化けするのでreplace()を使用して除外しています。


以上で、AOKIと青山の店舗位置情報を得ることができたので、次回からQGISを用いて各店舗の位置関係を調べていきたいと思います。


#! python3
# -*- coding: utf-8 -*-

import csv
import requests
from bs4 import BeautifulSoup

# 店:青山(全国)
# 変数prefは「都道府県」ドロップリストの並び順になっている模様
url = r'https://www.y-aoyama.jp/shop/'

res = requests.get(url)
res.raise_for_status()

# 都道府県名とそのidを取得
soup = BeautifulSoup(res.content, 'html.parser')
pref = []
pref_id = []

pref_elem = soup.find_all('option')
for i in range(len(pref_elem)):
        pref.append(pref_elem[i].get_text())
        pref_id.append(pref_elem[i]['value'])

# 最初と最後に格納される'選択してください'を削除
pref = pref[1:-1]
pref_id = pref_id[1:-1]

# pref&pref_idを用いて各都道府県のページに移動し店舗情報の抽出
# 店舗名と店舗住所の抽出用パラメータの初期化
shop_list = []
shop_name = []
post_code = []
shop_address = []
shop_pref = []

# 都道府県ごとにループ
for i in range(len(pref)):
        res_temp = requests.get(url+'?pref='+pref_id[i])
        res_temp.raise_for_status()

        # 店舗名と店舗情報の抽出
        soup_temp = BeautifulSoup(res_temp.content, 'html.parser')
        shop_elems = soup_temp.find_all('div', class_='arrowTicket_body')
        for shop_elem in shop_elems:
                shop_name.append(shop_elem.h2.get_text())
                shop_address_elem = shop_elem.find_all('p', class_='arrowTicket_text arrowTicket_margin')
                post_code.append(shop_address_elem[0].get_text().split(None,1)[0])
                shop_address.append(shop_address_elem[0].get_text().split(None,1)[1])
                shop_pref.append(pref[i])

# 閉店している店舗があるためそれらを除外する shop_nameに'完全閉店'と書かれている。
close_shop_indexes = [i for i, x in enumerate(shop_name) if '完全閉店' in x]

for close_shop_index in close_shop_indexes:
        shop_name[close_shop_index] = ''
        shop_address[close_shop_index] = ''
        shop_pref[close_shop_index] = ''

shop_name = list(filter(lambda x:x != '', shop_name))
shop_address = list(filter(lambda x:x != '', shop_address)のみ)
shop_pref = list(filter(lambda x:x != '', shop_pref))

# &nbsp を除外(いくつかの店舗の住所に含まれており文字化けする)
shop_address = [x.replace('\xa0', '') for x in shop_address]


# csvファイルに書き出し
output_file = open('aoyama_all.csv', 'w', newline='')
output_writer = csv.writer(output_file)

for i in range(len(shop_name)):
        output_writer.writerow([shop_name[i], post_code[i], shop_address[i], shop_pref[i]])

output_file.close()

WEBスクレイピング - AOKI編 -

前回の記事では、AOKIと青山の店舗位置関係を調べたくなったので、pythonによるwebスクレイピングをして住所情報を抽出しよう、と意気込みました。

hhgingisland.hatenablog.com


今回Webスクレイピングをするにあたり使用したモジュールとバージョンを先に書いておきます。

  • Requests 2.14.2
    • Webページをダウンロード。
  • Beautiful Soup 4.6.0
    • HTMLをパース。

またPCのOSとwebブラウザおよびPythonのバーションは以下のものを使用しました。


私はwebスクレイピングをはじめて行うので全くスマートでないコードを書いていると思います。こういう風に書いた方が良い等ありましたら是非教えて頂きたいです。

それではまずAOKIのサイトのHTMLを見てみます。


AOKIの店舗検索ページ

AOKIのwebサイト(https://www.aoki-style.com/)にアクセスしページ右上に「店舗検索」ボタンがあるので押します。

ページ左上の「都道府県別で探す」でデフォルトのまま「北海道」として検索ボタンを押してみると下図のように店名と住所がセットで表示されます。

f:id:hhgingisland:20180522002011j:plain

このページのURLは、
https://www.aoki-style.com/shoplist/search_result?prefecture_id=1&p=1
です。

検索結果が1ページあたり10軒表示され、次の10軒の検索結果を表示する場合はURLの末尾のp=以下に数字を指定することで見ることができます。

また、URLのprefecture_id=1が北海道を示すため、idの値を変えることで全都道府県の検索結果を見ることができます。

全国の店舗情報をプログラミングで抽出するには、各都道府県prefecture_idと検索結果ページpの2つの変数のループを回すことで取得できそうです。


しかしこれはちょっとコード書くの面倒だなあと気が進まず、サイトを色々見ていたところ、以下のURLをセットすると全店舗の検索結果が出ました。

https://www.aoki-style.com/shoplist/search_result?


f:id:hhgingisland:20180522193323j:plain

都道府県ループが減らせ、ページ番号を増やしていくだけで全店舗情報を抽出できそうです。

このページを基準にして情報を抽出していきたいと思います。


ソースHTMLを見てみる

f:id:hhgingisland:20180522211253j:plain


Google chromeの開発者ツールを使って見ていくと上の画像のように、

  • 店舗名は、<div class=”shop-list-shop-name”> → <p> →
    <a>の内部テキスト
  • 住所は、店舗名が記載されたdiv要素の次のdiv要素にあり、
    その中の<p class=”shop-list-shop-address”>要素の内部テキスト

であることが分かります。


以下のようなコードを書くと店舗名と住所(検索ページのはじめに記載されている1店舗分ですが)を取得できました。

#! python3
# -*- coding: utf-8 -*-

import requests
import bs4

url = r'https://www.aoki-style.com/shoplist/search_result?&p=1'

res = requests.get(url)
res.raise_for_status()

soup = bs4.BeautifulSoup(res.content, 'html.parser')
shop_name_elem = soup.find_all('div', class_='shop-list-shop-name')

# 店舗名
name = shop_name_elem[0].a.get_text()
# 住所
shop_details = shop_name_elem[0].next_sibling.next_sibling
address =shop_details.find_all('p', 'shop-list-shop-address')[0].get_text()


shop_name_elemはページ内の全店舗名を含むdiv要素を格納しているリストで、要素は10個あります(1ページあたり10軒の店舗名が記載されているため)。

上記コードでは、リストの0番目の要素に対して、a.get_text()をすることで、店舗名を導出しています。

したがってfor文を回すことで、ページ内の店舗名を全て取得できるようになります。


店舗住所については、next_siblingを2回使用して取得を試みています。

住所は店舗名を含むdiv要素の次に書かれているdiv要素なので、next_siblingは1個だけ書けばよいのでは?と思うかもしれませんが、div要素の後に改行コード\nが入っているため、next_siblingをもう1個書き足さないと目的のdiv要素の情報を取得できません。

後は、find_all()get_text()を使用して住所を抽出しています。


全店舗の位置情報を取得するためのコード

店舗名と店舗住所が抽出できたので、これを基にfor文を使用して全店舗の情報を抽出していきます。以下にソースコードを貼りましたのでご参照下さい。

なお、コードでは後にQGISで分析する際に各店舗の都道府県名が分かると便利かと思い、抽出した住所から正規表現を用いて都道府県名を抽出しています。

そして店舗名、店舗住所、都道府県名の3つの要素を最終的にCSVファイルに保存しています。


次回は青山の店舗情報を取得していきたいと思います。

#! python3
# -*- coding: utf-8 -*-

import csv, re
import requests
import bs4


# 店:青木(全国)
url = r'https://www.aoki-style.com/shoplist/search_result?'

res = requests.get(url)
res.raise_for_status()

soup = bs4.BeautifulSoup(res.content, 'html.parser')

# 全店舗数の抽出
shop_num_result = soup.find_all('div', class_='result-text')
a = shop_num_result[0].text.split(' / ')
shop_num = a[1].split('件')

# 全店舗検索結果ページ数の導出(1ページあたり10軒表示される)
max_page_num = int(shop_num[0]) // 10 +1

# 都道府県を抽出(わざわざやる必要もないがサイトから都道府県名を抜き出す)
shop_pref = []
shop_pref_elem = soup.find_all('option')
for i in range(len(shop_pref_elem)):
    shop_pref.append(shop_pref_elem[i].get_text())


# 各店舗検索結果ページのurl&htmlドキュメントを取得
url_list = []
res_list = []
for i in range(max_page_num):
        url_list.append(url+'&p='+str(i+1))
        res_temp = requests.get(url_list[i])
        res_temp.raise_for_status()
        res_list.append(res_temp)

# 店舗名と店舗住所の抽出用パラメータの初期化
shop_list = []
shop_name = []
shop_address = []

# 店舗名と店舗住所の抽出
# ページ1~最後のページの一つ前までの処理
for i in range(max_page_num - 1):
        soup = bs4.BeautifulSoup(res_list[i].content, 'html.parser')
        shop_name_elem = soup.find_all('div', class_='shop-list-shop-name')
        for j in range(10):
                # 店舗名
                name = shop_name_elem[j].a.get_text()
                shop_name.append(name)
                # 店舗住所
                shop_details = shop_name_elem[j].next_sibling.next_sibling
                address =shop_details.find_all('p', 'shop-list-shop-address')[0].get_text()
                shop_address.append(address)

# 最後のページの処理
soup = bs4.BeautifulSoup(res_list[max_page_num-1].content, 'html.parser')
shop_name_elem = soup.find_all('div', class_='shop-list-shop-name')
lastpage_shop_num = int(shop_num[0]) % 10

for j in range(lastpage_shop_num):
        # 店舗名
        name = shop_name_elem[j].a.get_text()
        shop_name.append(name)
        # 店舗住所
        shop_details = shop_name_elem[j].next_sibling.next_sibling
        address =shop_details.find_all('p', 'shop-list-shop-address')[0].get_text()
        shop_address.append(address)

# 店舗住所の都道府県名の取得
pref = []
for i in range(len(shop_name)):
    for j in range(len(shop_pref)):
        if re.match(shop_pref[j], shop_address[i]):
            pref.append(re.match(shop_pref[j], shop_address[i]).group())
            break

# csvファイルに書き出し
output_file = open('aoki_all.csv', 'w', newline='')
output_writer = csv.writer(output_file)

for i in range(len(shop_name)):
        output_writer.writerow([shop_name[i], shop_address[i], pref[i]])

output_file.close()

「AOKI」の近くに「青山」があるか検証してみる

f:id:hhgingisland:20180520215148j:plain

 

都心から離れた国道沿いの風景というと大体同じだなと思っている人は多いと思います。

最近そのような国道沿いを車で走っていた時にふと思いました。

「国道沿いにAOKIとか洋服の青山がよくあるけど、AOKIがあるところの近くに青山ってあるよな・・・」

f:id:hhgingisland:20180526205343p:plain

Google マップの画像に追記

グーグルマップでちょっと調べてみると、上の写真のようにAOKI(写真左手前)の近くに青山(写真右奥)がありました。

他の店舗もそうなのかなと気になったので、

  • 仮説①「AOKI」の近くに「青山」がある
  • 仮説②「AOKI」と「青山」は国道沿いに多くある

を検証してみたいと思います。

検証方法

店舗間の位置関係を調べたいので丁度最近使っていたフリーのGIS(地理情報システム)ソフトであるQGISを用いて調べてみます(勉強にもなる!)。

大枠は以下の通り。

  1. 「AOKI」と「青山」のサイトから店舗住所を抽出する。
  2. 住所情報をQGISにローディングし店舗間位置を分析する。

1については、手動でエクセルにコピペしようかと思っていましたが、これまた最近pythonの勉強をしていたので、pythonでWebスクレイピングをして情報を取得してみることにします。

Pythonに関しては、Twitterでフォローしているとくさん@nori76 (id:nori76)が紹介していた「O'Reilly Japan - 退屈なことはPythonにやらせよう」を参考にしています。

Pythonによるテキストやエクセルファイルの操作方法とかのってて、業務効率化に役立っています。

 

ということで、次の記事からwebスクレイピングをして店舗情報を抽出していきたいと思います。