山傘のプログラミング勉強日記

プログラミングに関する日記とどうでもよい雑記からなるブログです。

Pythonクローリング&スクレイピング[増補改訂版] ―データ収集・解析のための実践開発ガイドー その16

第5章 クローリング・スクレイピングの実践とデータの活用

取得したデータ活用を思いついていませんが、テキストを進めていきたいと思います。

ジオコーディング

YOLP(地図):Yahoo!ジオコーダAPI - Yahoo!デベロッパーネットワーク

住所から位置情報を取得するために Yahoo! のジオコードを使います。API を登録して利用することができます。

curl -s 'https://map.yahooapis.jp/geocode/V1/geoCoder?appid=********-&output=json&query=東京都台東区上野公園7番7号' | jq .

f:id:yamakasa3:20190911162644p:plain

美術館の位置情報

前に美術館の住所のデータを扱ったので、住所に基づいて Yahoo! ジオコードを利用して緯度経度を取得します。

import os
import dbm
import json
import logging
from typing import Iterator, Tuple, Union
import requests
from SPARQLWrapper import SPARQLWrapper
import sys
from pymongo import MongoClient
client = MongoClient('localhost', 27017)

YAHOO_GEOCODE_API_URL = 'https://map.yahooapis.jp/geocode/V1/geoCoder'
YAHOO_APPLICATION_ID = os.environ['YAHOO_APPLICATION_ID']

# キャッシュを保存するファイル (MongoDB)
db = client.scraping
geocode_collection = db.geocode_collection


def main():
    features = [] # 美術館のリスト

    for museum in get_museums():
        label = museum.get('label', museum['s'])
        address = museum['address']

        if 'lon_degree' in museum:
            lon, lat = sexagesimal_to_decimal(museum)
        else:
            lon, lat = geocode(address)
        
        print(label, address, lon, lat)

        if lon is None:
            continue

        features.append({
            'type': 'Feature',
            'geometory': {'type': 'Point', 'coordinates': [lon, lat]},
            'properties': {'label': label, 'address': address}
        })
    
    feature_collection = {
        'type': 'FeatureCollection',
        'feature': features,
    }
    with open('museums.geojson', 'w') as f:
        json.dump(feature_collection, f)

def get_museums() -> Iterator[dict]:
    logging.info('Executing SPARQL query...')
    # SPARQLのインスタンス
    sparql = SPARQLWrapper('http://ja.dbpedia.org/sparql')
    sparql.setQuery(r"""
    SELECT * WHERE {
        ?s rdf:type dbpedia-owl:Museum ;
        prop-ja:所在地 ?address .
        OPTIONAL { ?s rdfs:label ?label . }
        OPTIONAL {
            ?s prop-ja:経度度 ?lon_degree ;
            prop-ja:経度分 ?lon_minute ;
            prop-ja:経度秒 ?lon_second ;
            prop-ja:緯度度 ?lat_degree ;
            prop-ja:緯度分 ?lat_minute ;
            prop-ja:緯度秒 ?lat_second .
        }
        FILTER REGEX(?address, "^\\p{Han}{2,3}[都道府県]")
    } ORDER BY ?s
    """)
    sparql.setReturnFormat('json')
    response = sparql.query().convert()

    logging.info(f"Got {len(response['results']['bindings'])} results")

    for result in response['results']['bindings']:
        yield {name: binding['value'] for name, binding in result.items()}


def sexagesimal_to_decimal(museum: dict) -> Tuple[float, float]:
    """
    60進数の緯度・経度を10進数に変換する
    """
    lon = float(museum['lon_degree']) + float(museum['lon_minute']) / 60 + float(museum['lon_second']) / 3600
    lat = float(museum['lat_degree']) + float(museum['lat_minute']) / 60 + float(museum['lat_second']) / 3600

    return (lon, lat)


def geocode(address: str) -> Union[Tuple[float, float], Tuple[None, None]]:
    """
    引数で指定した住所をYahoo!ジオコーダで緯度と経度のペアを返す
    """
    print(address)
    if not geocode_collection.find_one({'address': address}):
        logging.info(f'Geocoding {address}...')
        req = requests.get(YAHOO_GEOCODE_API_URL, params={
            'appid': YAHOO_APPLICATION_ID,
            'output': 'json',
            'query': address,
        })
        response = json.loads(req.content.decode('utf-8'))
        if 'Feature' not in response:
            geocode_collection.insert_one({'address': address, 'lon': 'None', 'lat': 'None'})
            return (None, None)
        coordinates = response['Feature'][0]['Geometry']['Coordinates'].split(',')
        
        geocode_collection.insert_one({'address': address, 'lon': coordinates[0], 'lat': coordinates[1]})

    s = geocode_collection.find_one({'address': address})
    if s['lon'] == 'None' or s['lat'] == 'None':
        return (None, None)
  
    return (float(s['lon']), float(s['lat']))


if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    main()

f:id:yamakasa3:20190912222602p:plain

テキストでは、dbm を使ってデータを保存していたんですが、僕の環境では使うことができなかったので、MongoDB を使いました。