高知の「あしたの分も買うちょくきね。～飲食券先買い応援プロジェクト～」の参加店舗を勝手にgoogle map上のマーカーでわかるようにした

**高知に行きたくても行けないので勝手にやりました**
https://script.google.com/macros/s/AKfycbzRJCvOuWC5uMFiqhl5soiLHnWbkGE5P-GAkxFmVT08u_3E2hRi/exec

あしたの分も買うちょくきね。～飲食券先買い応援プロジェクト～「参加店舗」
https://www.kochinews.co.jp/norikoeyo/asukau_eatery/
この店舗一覧をgoogle map上のマーカーでわかるようにしました

## 構成

- フロントはgoogle app script
- フロントからAPI Gateway+LambdaのAPIを呼び店舗情報を取得
- 店舗データはDynamoDBに保存

という完全サーバレスです

## まずは店舗一覧のデータ作成

https://www.kochinews.co.jp/norikoeyo/asukau_eatery/
このサイトをスクレイピングするlambdaを書きます（python）
座標取得するため、googleのAPIキーも事前に取得しておきました

```python

import requests
import re
import googlemaps
import time
import boto3

def lambda_handler(event, context):
    dynamo_db = boto3.resource('dynamodb')
    table = dynamo_db.Table('kouchi_shop')

    gmaps = googlemaps.Client(key=API_KEY)

    response = requests.get('https://www.kochinews.co.jp/norikoeyo/asukau_eatery/')
    response.encoding = response.apparent_encoding

    text = response.text.replace('\n', '')
    regex = r'<b>(\d+)(.*?)<\/b>\s*<br>(.*?)<br>'
    result = re.findall(regex, text)

    if result:
        for data in result:
            if not data[0]:
                continue
            number = data[0]
            shop_name = data[1].strip()
            href_result = re.findall(r'<a href=\"(.*?)\"', data[2])
            shop_address = re.sub(r'<a.*<\/a>', '', data[2]).strip()
            url = href_result[0] if href_result else ''
            # 座標取得
            geo_result = gmaps.geocode(shop_address)

            item = {
                'id': int(number),
                'name': shop_name,
                'address': shop_address,
                'url': url,
                'lat': str(geo_result[0]["geometry"]["location"]["lat"]),
                'lng': str(geo_result[0]["geometry"]["location"]["lng"])
            }
            print(item)
            table.put_item(Item=item)
            # 念のため連続でリクエストしないようにsleepを入れておく
            time.sleep(0.5)

    return {
        'statusCode': 200
    }
```
適当に一回実行してDynamoDBに店舗データを保存させます

beautifulsoupとかは使ってません（DOMの構造上あんまり意味なかったので）
ライブラリのインストールについてはこちらを参考にさせていただきました
https://tech.bita.jp/article/38

## 店舗一覧を取得するAPIを作る

これもLambdaで書いて、トリガーをAPI Gatewayにします

```python

import json
import boto3
from decimal import Decimal

def lambda_handler(event, context):
    dynamo_db = boto3.resource('dynamodb')
    table = dynamo_db.Table('kouchi_shop')
    
    return {
        'statusCode': 200,
        'body': json.dumps(table.scan(), default=decimal_default_proc)
    }

def decimal_default_proc(obj):
    if isinstance(obj, Decimal):
        return str(obj)
    raise TypeError
```

特に絞り込みとかしないので`scan()`で全件取得してるだけです
`body`は連想配列のままだとAPIリクエストしたときに`{"message":"Internal Server Error"}`が出るので必ず`json.dumps`が必要でした（これでちょっと嵌った）

あと、jsonに小数点があるとjsonエンコードでエラーになるので`decimal_default_proc`で文字列にキャストしてます（letとlngが小数点）
https://qiita.com/ekzemplaro/items/5fa8900212252ab554a3
こちらを参考にさせていただきました

## フロント作成

google app scriptでhtmlを作成し、店舗一覧取得APIからマーカーを設置します

```javascript
<script>
  var marker = [];
  var info_window = [];
  
  function init_map () {
    // 初期表示の中心
    var map = new google.maps.Map(document.getElementById('map'), {
      center: {
           lat: 33.561067,
           lng: 133.531465
      },
      zoom: 15
    });
    
    get_shop(map)
  }
  
  function marker_event(i) {
    marker[i].addListener('click', function() {
      info_window[i].open(map, marker[i]);
    });
  }

  function set_marker(items, map) {
    for (var i = 0; i < items.length; i++) {        
      // マーカーの設置
      marker[i] = new google.maps.Marker({
        position: {
          lat: parseFloat(items[i]['lat']),
          lng: parseFloat(items[i]['lng'])
        },
        map: map
      });
          
      // マーカーをクリックした際のポップアップ
      var text = '[' + String(items[i]['id']) + '] ' + items[i]['name'] + '<br>' + items[i]['address'];
      if (items[i]['url']) {
        text += '<br><a href="' + items[i]['url'] + '" target="_blank">' + items[i]['url'] + '</a>';
      }
      info_window[i] = new google.maps.InfoWindow({
        content: text
      });
      marker_event(i);
    }
  }
  
  function get_shop(map) {
    var req = new XMLHttpRequest();
    req.onreadystatechange = function() {
      if (req.readyState == 4 && req.status === 200) {
        const obj = JSON.parse(req.responseText);
        set_marker(obj['Items'], map)
      }
    }
    req.open('GET', '店舗一覧取得API');
    req.send();
  }
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=API_KEY&callback=init_map"></script>
```

フロント作成については以下を参考にさせていただきました
https://tonari-it.com/gas-button-event-javascript/
https://www.tam-tam.co.jp/tipsnote/javascript/post7755.html
https://webdesignday.jp/inspiration/technique/css/4213/

## 最後に

ここ最近毎年高知に行っていて、好きな土地の一つでもあります
早く高知に行けるようになって欲しいです

また、「あしたの分も買うちょくきね。～飲食券先買い応援プロジェクト～「参加店舗」」と私はまったくの無関係です
問題があるようでしたら即刻削除致します