見出し画像

Webから取得したデータを視覚化する(Using Databases with Python: Week 5)

引き続き、ミシガン大学がCoursera上で開講しているPython for Everybody Specializationの第4コース、Using Databases with Pythonを受講した記録です。前回のWeek 4までに、データベースに関する基礎知識とPythonプログラムでデータベースの構築およびデータの抽出を学びました。

Week 5では、これまでの手法を用いてGoogle Geocoding APIを用いたデータの取得とデータベース化、その後htmlによる視覚化(ビジュアライゼーション)を行っていきます。



1.プログラムの処理手順

プログラムは、あるデータソースから①データを取得する、②データを処理(クリーニング)する、③分析、視覚化するという段階を経て、私たちがプログラムでやりたいことを実現します。第3コースでWebからの情報を取得する、第4コースのこれまでの講義で保存されたデータをデータベース形式に構築、抽出するというステップを既に学習しています。

この講義では、Pythonプログラムを構築し、Geocoding APIを用いて取得したデータをGoogle Mapにて視覚化することを目標とします。

具体的には、where.dataというファイルを読み込み、Geocoding APIを用いてgoogleからデータを取得し、JSON形式のファイルを取得したうえでデータをSQLite形式のデータベースに格納し、そのデータベースを活用してwhere.jsというJavaScriptファイルを生成し、where.htmlファイルに読み込ませて視覚化を行います。それでは、一つずつ手順を見ていきましょう。


2.ファイルを読み込んでgeocoding APIを実行する

このプログラムでは、where.dataというテキストファイルを読み込み、書かれている住所をGeocoding APIを用いて座標等をGoogle MapsよりJSON形式で取得し、取得したデータをリレーショナル・データベースとしてgeodata.sqliteというファイルで保存します。例によって長いので、いくつか部分に分けて解説していきます。

import urllib.request, urllib.parse, urllib.error
import http
import sqlite3
import json
import time
import ssl
import sys

api_key = '***********************'
serviceurl = "https://maps.googleapis.com/maps/api/geocode/json?"

ここまでは導入部です。いくつか初めて出てくるライブラリが混ざっていますが、とりあえずはこれらのライブラリを使うということを頭に入れておきましょう。

マスクされているGoogle APIキーの取得方法は第3コースWeek 6の講義メモをご参照ください。

conn = sqlite3.connect('geodata.sqlite') #...[1]
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS Locations (address TEXT, geodata TEXT)''') #...[2]

# Ignore SSL certificate errors
ctx = ssl.create_default_context() #...[3]
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

fh = open("where.data") #...[4]
count = 0
for line in fh: #...[5]
   if count > 200 : #...[6]
       print('Retrieved 200 locations, restart to retrieve more')
       break

[1]で、geodata.sqliteというリレーショナル・データベースを開きます。ファイルが存在しない場合は、新しいファイルを作成します。そしていつものように、ファイルをハンドルする機能を持つcurをcursor()で定義し、[2]でテーブルが存在しない場合はLocationsテーブルを作成します。Columnは、addressおよびgeodataの2つを用意しています。
[3]以降はSSLという接続について色々と記述していますが、コメントアウトで解説してある通りSSL認証エラーを無視するとしています。この点は講義のポイントではないため、Dr. Chuckの解説でも多くは触れられていません。ひとまず読み飛ばしましょう。

[4]から、検索をしたいデータを読み込んでいきます。第2コースWeek 3で取り扱ったように、ファイルのハンドルfhを定義し、ファイルwhere.dataを読み込みます。その後は、[5]のforループにて1行ずつ処理していきます。
[6]は例外処理のようなもので、1回の処理件数が200を超えると自動的に処理を終えるように作られています。200件以上のデータを処理したい場合は、もう一度Pythonプログラムを実行します。

   address = line.strip() #...[7]
   print('')
   cur.execute("SELECT geodata FROM Locations WHERE address= ?",
       (memoryview(address.encode()), )) #...[8]

   try:
       data = cur.fetchone()[0] #...[9]
       print("Found in database ",address)
       continue
   except:
       pass #...[10]

続いて、[7]で1行のテキストデータの左右の空白を取り除きます。
[8]は、既に保存されているデータベースのLocationテーブルからaddressがファイルから読み込まれた[7]のアドレスに一致するかを確認しています。addressはファイルから読み込んで格納した変数で、現状はUnicodeで保存されています。データベースに格納されている文字形式はUTF-8になっていますので、address.encode()を行いUTF-8形式に変換してSQLを実行する必要があります。通常であれば、hoge = address.encode()というように新しい変数が必要となるのですが、ここではmemoryview()を用いて、新たな変数を用意することなくUTF-8に変換するような対応を行っています。使用するメモリ容量を削減するためですね。memoryviewに関する詳しい挙動は以下のサイトに解説があります。

[9]では、上記でSELECTしたgeodataがあればデータベースにあることをprintで伝え、ループの始めに戻ります。fetchoneが何もないということでNoneを返した場合はexcept文に飛んでいきますが、[10]のexcept文ではpassと書かれています。passという命令は文字通り「何もしないで次の文に進む」という意味です。try~except構文でexcept以下をインデントすることも可能ですが、見にくくなるためpassで処理し、try~except構文を抜けてこのままループを続行します。

   parms = dict() #...[11]
   parms["address"] = address
   if api_key is not False: parms['key'] = api_key
   url = serviceurl + urllib.parse.urlencode(parms) #...[12]
   print('Retrieving', url)
   uh = urllib.request.urlopen(url, context=ctx) #...[13]
   data = uh.read().decode() #...[14]
   print('Retrieved', len(data), 'characters', data[:20].replace('\n', ' '))

テキストデータから読み込んだアドレスがデータベースにないとき、Google Geocoding APIを読み込み、情報を取得します。
[11]ではディクショナリ形式のparmsを用意し、アドレスとキーの対を下の2行で準備します。
[12]はUTF-8用のURLデータを準備し、[13]で実際にURLを開くリクエストをします。ここで、context=ctxという文がありますが、[3]で説明したSSLを開くためのオプションと考えてください。エラーが発生しても無視する設定にしているという理解でOKです。
最後に、[14]でGeocoding APIから取得したJSONデータをdata変数に格納します。Pythonプログラムに適用できるよう、decode()していることに注意しましょう。

   count = count + 1 #...[15]
   try:
       js = json.loads(data) #...[16]
   except:
       print(data)  # We print in case unicode causes an error
       continue

   if 'status' not in js or (js['status'] != 'OK' and js['status'] != 'ZERO_RESULTS') : #...[17]
       print('==== Failure To Retrieve ====')
       print(data)
       break
   cur.execute('''INSERT INTO Locations (address, geodata)
           VALUES ( ?, ? )''', (memoryview(address.encode()), memoryview(data.encode()) ) ) #...[18]
   conn.commit() #...[19]
   if count % 10 == 0 : #...[20]
       print('Pausing for a bit...')
       time.sleep(5)

さてこのプログラムの最後の部分です。まずは[15]のカウンタですが、これは[20]で読み込みを一時停止してアイドリングの時間を作るためのカウンタです。
[16]で、読み込んだJSONデータを構造化した変数jsに格納します。データが正しく読み込めなかった場合はexcept文に飛び、読み込んだデータを表示してループの先頭に戻ります。
[17]では、JSONデータにおける['status']がOKであること、および['status']が検索結果なし(ZERO_RESULTS)を返していないことを確認しています。エラーがない場合は、[18]の処理に進みます。

[18]で、実際にデータベースに書き込んでいきます。先に定義したLocationテーブルのaddressとgeodataのColumnにデータを保存していきますが、encode()が必要になるので[8]と同様、メモリ容量削減のためmemoryviewを用いてencode()していきます。[19]で、データベースを更新します。
[20]では10カウントごとに5秒の停止時間を設けています。

ここまでで、geodata.sqliteというデータベースができたと思います。念のため、SQLiteで中身を確認してみましょう。

キャプチャ17

右のウィンドウで、geodataのそれぞれの要素にはJSONのデータがまるまる入っているのが分かります。表計算ソフトでは通常このような処理はできませんが、データベースソフトであればこのように複数行にわたるデータもそれぞれ格納できるのは有用ですね。
それでは、次の項で出来上がったデータベースを活用して視覚化する準備を行っていきます。


3.リレーショナル・データベースをもとに視覚化に使うファイルを作成する

前項までで、where.dataというテキストデータのキーワードをもとにGoogle Geocoding APIを用いて位置情報などを取得し、データベースを構築し、それぞれのレコードのgeodataにはJSON形式のデータが格納されていることが理解できました。

次は、このデータベースを使ってwhere.jsというファイルを作成します。where.jsは後に用いるwhere.htmlに使われるJavaScriptファイルですが、JavaScriptのプログラミングを行うのではなく、実際には以下のようにGoogle Mapで表示されるよう座標と住所のデータが記録されているものになります。

キャプチャ18

ここで、住所の項に記録されているのはUTF-8形式の文字であることに注意しましょう。よく見てみると、欧州の言語で使われる特殊文字やヘブライ語がありますね。

import sqlite3
import json
import codecs

conn = sqlite3.connect('geodata.sqlite') #...[1]
cur = conn.cursor()
cur.execute('SELECT * FROM Locations') #...[2]

fhand = codecs.open('where.js', 'w', "utf-8") #...[3]
fhand.write("myData = [\n") #...[4]

[1]で、前項で作成したgeodata.sqliteを読み込みます。そのうえで、[2]にてSQLを用いてLocationテーブルのすべてのColumn(*)をSELECT抽出します。
[3]では、UTF-8形式でwhere.jsというファイルを作成する準備を行います。これまでは読み取り専用モードでファイルを開いていましたが、今度は書き込みが必要になります。しかも、Pythonが取り扱う形式はUnicodeであるため、UTF-8にencodeする必要が生じます。これを可能にするために、codecsライブラリを用います。'w'は書き込み専用、"utf-8"はエンコード形式です。
[4]で最初の行である"myData=["と改行を書き込んでいきます。

count = 0
for row in cur : #...[5]
   data = str(row[1].decode()) #...[6]
   try: js = json.loads(str(data)) #...[7]
   except: continue
   if not('status' in js and js['status'] == 'OK') : continue #...[8]
   lat = js["results"][0]["geometry"]["location"]["lat"] #...[9]
   lng = js["results"][0]["geometry"]["location"]["lng"]
   if lat == 0 or lng == 0 : continue

[5]のfor文のループは、データベースを一件一件ループするという意味です。
[6]で、geodataの項だけをdataという変数に読み込んでいます。データベースにあるデータもUTF-8形式で保存されていますから、いったんdecode()が必要です。geodataのそれぞれのRowはJSON形式になっていますから、[7]でjs変数に構造化した形で読み込みます。エラーがあれば、continueでfor文の先頭に戻ります。
[8]で、'status'が正常にOKとなっていることを確認し、エラーがあればcontinueでfor文の先頭に戻ります。

[9]以下で緯度と経度をそれぞれ読み込みます。経度がlat、緯度がlng変数に取り込まれます。Geocoding APIの仕様についてはGoogle公式の解説ページをご参照ください。

   where = js['results'][0]['formatted_address'] #...[10]
   where = where.replace("'", "")
   try :
       print(where, lat, lng)
       count = count + 1
       if count > 1 : fhand.write(",\n")
       output = "["+str(lat)+","+str(lng)+", '"+where+"']" #...[11]
       fhand.write(output) #...[12]
   except:
       continue
fhand.write("\n];\n") #...[13]
cur.close()
fhand.close()
print(count, "records written to where.js")

次に、Google Mapが読み込む際に使われるformatted_addressを[10]でwhere変数に読み込みます。この際、言語によってはアポストロフィ'があるので、その後の処理に影響が出ないように取り除きます。
[11]で、実際に経度(lat)、緯度(lng)、住所(where)の一組のデータをJavaScriptで読み込めるように文字列として成型します。[12]で、これらのデータを実際に書き込みます。

すべてのgeodata.sqliteにあるgeodataレコードの読み込みが終了し、書き込みが終了したらwhere.jsの最初に書き込んだ"["を[13]で閉じ、データベースおよびwhere.jsを閉じます。


4.HTMLを開いて視覚化できているか確認する

最後に、用意されているwhere.htmlを開きます。

キャプチャ19

これで、where.jsに保存されている位置情報データがHTMLにより読み込まれ、Google Mapのピン留め機能によって視覚化されたことが確認できます。詳しい説明はこの講義では取り扱われませんが、HTMLに埋め込まれているJavaScriptは以下のとおりです。

   <script src="where.js"></script>
   <script>
     function initialize() {
       var myLatlng = new google.maps.LatLng(37.39361,-122.099263)
       var mapOptions = {
         zoom: 3,
         center: myLatlng,
         mapTypeId: google.maps.MapTypeId.ROADMAP
       }
       var map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
       i = 0;
       var markers = [];
       for ( pos in myData ) {
           i = i + 1;
           var row = myData[pos];
		    window.console && console.log(row);
           // if ( i < 3 ) { alert(row); }
           var newLatlng = new google.maps.LatLng(row[0], row[1]);
           var marker = new google.maps.Marker({
               position: newLatlng,
               map: map,
               title: row[2]
           });
           markers.push(marker);
<!-- New options for MarkerClusterer function to display markers -->
	    var options = {
			imagePath: 'http://rawgit.com/googlemaps/js-marker-clusterer/gh-pages/images/m'
			}	
       }
<!-- New var -->
	var markerCluster = new MarkerClusterer(map, markers, options);
     }
   </script>


以上で、第4コースの内容は終了です。ここまで受講してみた感想として、

・Dr. Chuckの説明は非常に親切でコンセプトをかいつまんで説明してくれた
・サンプルのコードを応用しながら課題をこなすのは非常に勉強になった
・重要でないコードの説明(memoryview()など)は省略される傾向にあるので、そこは自習で補う必要がある。
・やり遂げた!!
・しかしやはり英語がネック(超重要)

全体を通して、とても勉強になりました。試行錯誤をしながらの課題だったので、想定よりも時間がかかりました。これから受講を考えていらっしゃる方は、演習で標準学習時間よりも2割~3割くらい多めの時間がかかるものと考えていただければ良いかなと思います。それでは、最後の第5コースの記録もお楽しみに!からあげでした。

画像4


この記事が気に入ったらサポートをしてみませんか?