Pythonで駅周辺の飲食店データを食べログから取得して、彼の悲劇を救いたい。

突然だが先日、このようなツイートを拝見した。
<img width="584" alt="スクリーンショット 2019-03-21 20.47.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/380802/647a9394-0907-4316-17f4-07b7e7ad67c1.png">

おそらく彼はピッツァが大好きで、
その愛を抑えきれずに大金をはたいて、
わざわざピッツァ屋の近くに引っ越してきたのだろう。

しかし何の因果か、引っ越した瞬間にそのピッツァ屋が潰れてしまったというのだ。
彼にとってのエルサレムは失われてしまった。なんとも悲しい現実だ。

*----   どうかこの方には同じ過ちを犯してほしくない。     ----*

そう思って、私に何ができるかを考えた結果、
**"ピッツァ屋を取り巻く環境を予め把握できれば、閉店のリスクを予測し、悲劇を回避できるだろう"**
という結論に至った。

つまり、ピッツァ屋の周辺の賑わい度合い（=飲食店数）や同業態の競合数を把握できれば、
閉店のリスクがある環境かどうかを分析し、予め閉店のリスクを把握できるかもしれない。

そこで、「指定した駅周辺の飲食店情報」を食べログから取得するプログラムを作成してみた。

#具体的に何が取得できるの？
適当な"店舗のID"と"最寄り駅名で検索した際のURL"を渡せば、
最寄り駅周辺の全飲食店に関する、以下の情報が取得できる。

* 飲食店の最寄り駅名
* 飲食店の最寄り駅からの距離
* 食べログ上で登録されている、その飲食店の"ジャンルタグ"
<img width="400" alt="スクリーンショット 2019-03-19 21.48.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/380802/996e999b-3e80-bd5c-12ed-ad7137f08c53.png">
* 夜最高予算
* 夜最低予算
* 住所
* 交通手段
* 営業時間
* 定休日
* 予算
* 席数
* 個室
* 禁煙・喫煙
* 駐車場
* 空間・設備
* 飲み放題コース
* 利用シーン
* オープン日
* 電話番号
* 緯度（日本測地系）
* 経度（日本測地系）
         他

#データの準備（手作業）
**1.店舗の最寄り駅で検索した際のurlをコピーする。**
   1.1. 食べログで最寄り駅名で検索する。
<img width="244" alt="スクリーンショット 2019-03-19 21.59.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/380802/90779215-22bb-61e4-1c6e-b0c3b5bfdc1a.png">
1.2. 検索した際のurlをコピーする。
<img width="1249" alt="スクリーンショット 2019-03-19 22.13.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/380802/3109b3fb-079f-b797-2cbe-eaa2bc700e7f.png">
(参考)取得したい範囲を半径800mから変更したい場合は、サイト内下記の画面で距離を指定の上、生成されたurlをコピーする。
<img width="912" alt="スクリーンショット 2019-03-19 21.59.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/380802/5f7d2cc7-c369-ddde-9de6-2252d8b20cb6.png">

**2."ID", "url" の2項目を記したcsvを用意する。**
"url"は先程コピーしたもの。店舗"ID"は任意のものを記入。
<img width="321" alt="スクリーンショット 2019-03-19 22.53.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/380802/8e2beff9-ab44-0379-3fe2-179021c5cd2e.png">
(↑こんな感じ。)

複数の店舗を登録すれば、それぞれの最寄り駅周辺の飲食店データが取得できる。

作成したcsvファイルは適当な名前（一旦"店舗データ.csv"とする）で保存し、
スクリプトと同ディレクトリに配置する。

#実行環境
Python 3.7（Anaconda3系）
macOS Mojave 10.14.1

#処理の流れ
大まかな処理の流れとしては、
 1. 入力ファイルの1レコード目のURLを読み込む。
 2. 読み込んだURLにアクセスして、飲食店一覧ページに掲載されている飲食店情報を1店舗ずつ、一覧ページ＆詳細ページから取得する。
 3. 次のページに移動して、2.のプロセスを実行する
 4. 全ページ分の飲食店情報を取得したら、入力データの2レコード目のURLを読み込み、2、3を実行。
 5. 入力ファイルで記入した全てのレコードで、上記の処理を完了したら、データをエクスポートして終了。
 
と言った感じ。

まず、必要なライブラリをインポートする。

```python:scrape.py
from time import sleep
import requests
import re
import sys
import pandas as pd
from bs4 import BeautifulSoup
```
もし、スクレイピング用のライブラリ（BeautifulSoup4)が未インストールの場合
ターミナルからpipでインストールする。

```
$ pip install beautifulsoup4
```
入力ファイル名を、作成したcsvファイル名で設定するとともに、
任意の出力ファイル名を設定する。

```python:scrape.py
#設定---------------------------------
fname = u'店舗データ.csv' # 入力ファイル名
outfile = u'食べログ飲食店データ.csv' # 出力ファイル名
#---------------------------------
```

飲食店詳細ページ記載情報を取得する関数を定義する。

```python:scrape.py
### 関数1_飲食店詳細ページから競合店情報を取得
def syosai_scrape(syosai_url):
    html = requests.get(syosai_url)
    soup = BeautifulSoup(html.text, "html.parser")
    data=[]
    tables = soup.find_all("table",{"class":"c-table c-table--form rstinfo-table__table"})
    for table in tables:
        rows = table.find_all("tr")
        for row in rows:
            t_col1 = row.find("th").text.replace('\n',' ').replace('\t',' ').strip()
            t_col2 = row.find("td").text.replace('\n',' ').replace('\t',' ').strip()
            col1 = re.sub(r'\s+',u' ',t_col1) # 連続する空白を一つの空白に置換
            col2 = re.sub(r'\s+',u' ',t_col2) # 連続する空白を一つの空白に置換
            data.append([col1,col2])

    result = [""]*24
    for row in data:
        if u"店名" == row[0]:
            result[0] = row[1]
        if u"ジャンル" == row[0]:
            result[1] = row[1]
        if u"住所" == row[0]:
            result[2] = row[1].replace(' 大きな地図を見る 周辺のお店を探す','') # 不要なキーワードを削除
        if u"交通手段" == row[0]:
            result[3] = row[1]
        if u"営業時間" == row[0]:
            result[4] = row[1]
        if u"定休日" == row[0]:
            result[5] = row[1]
        if u"予算" == row[0]:
            result[6] = row[1]
        if u"予算（口コミ集計）" == row[0]:
            result[7] = row[1]
        if u"席数" == row[0]:
            result[8] = row[1]
        if u"個室" == row[0]:
            result[9] = row[1]
        if u"禁煙・喫煙" == row[0]:
            result[10] = row[1]
        if u"駐車場" == row[0]:
            result[11] = row[1]
        if u"空間・設備" == row[0]:
            result[12] = row[1]
        if u"飲み放題コース" == row[0]:
            result[13] = row[1]
        if u"コース" == row[0]:
            result[14] = row[1]
        if u"ドリンク" == row[0]:
            result[15] = row[1]
        if u"料理" == row[0]:
            result[16] = row[1]
        if u"利用シーン" == row[0]:
            result[17] = row[1]
        if u"サービス" == row[0]:
            result[18] = row[1]
        if u"オープン日" == row[0]:
            result[19] = row[1]
        if u"電話番号" == row[0]:
            result[20] = row[1]

    ll= soup.find("script",{"type" : "application/ld+json"}).string

    lat_st = ll.find("latitude")+10
    lat_ed = ll.find(",",lat_st)
    latitude = ll[lat_st:lat_ed] # 店舗緯度取得

    lon_st = ll.find("longitude")+11
    lon_ed = ll.find("}",lon_st)
    longitude = ll[lon_st:lon_ed] # 店舗経度取得

    result[21]=latitude
    result[22]=longitude

    result[23] = soup.find("span",{"class" : "linktree__parent-target-text"}).string # 最寄り駅取得

    return result
```

次に、飲食店情報を取得する関数を定義する。

```python:scrape.py
### 関数2_飲食店一覧ページから競合店情報を取得
def scrape(html, tcode):
    soup = BeautifulSoup(html, "html.parser") # 対象ページhtml全取得
    data=[]

    ##  飲食店名、詳細ページリンク先詳細情報取得
    info = soup.find_all("a",{"class":"list-rst__rst-name-target cpy-rst-name"}) # 対象タグ取得
    syosai_df = pd.DataFrame()
    shopname = []
    for name in info:
        hr = name.get("href") # 詳細ページリンク取得
        name = name.text # タグ内テキスト部抽出
        name = name.replace("\u3000","") # 不要文字列除去
        name = name.replace(",","") # 不要文字列除去
        shopname.append(name)
        scr = syosai_scrape(hr) # 詳細ページ情報をリストで取得
        s = pd.Series(scr, index=["店名", "ジャンル", "住所", "交通手段", "営業時間", 
                                  "定休日", "予算", "予算（口コミ集計）", "席数", "個室", 
                                  "禁煙・喫煙", "駐車場", "空間・設備", "飲み放題コース", 
                                  "コース", "ドリンク", "料理", "利用シーン", "サービス", 
                                  "オープン日", "電話番号", "緯度", "経度", "最寄駅"])
        syosai_df = syosai_df.append(s, ignore_index = True)
    syosai_df = syosai_df[2:22] # 必要列のみ抽出
        
    ## 飲食店詳細ページURL取得
    info = soup.find_all("a",{"class":"list-rst__rst-name-target cpy-rst-name"}) # 対象タグ取得
    
    ## 最寄り駅、駅からの距離、ジャンル取得
    info = soup.find_all("span",{"class":"list-rst__area-genre cpy-area-genre"})# 対象タグ取得
    station_name = []
    station_distance = []
    jenre1 = []
    jenre2 = []
    jenre3 = []
    for sj in info:
        sj = sj.text # タグ内テキスト部抽出
        station_name.append(sj[:sj.find('駅')+1]) # 駅名取得
        station_distance.append(sj[sj.find('駅')+1: sj.find('m')]) # 駅からの距離取得
        if sj.count('、') == 0: # ジャンルが1つの場合
            jenre1.append(sj[sj.find('/')+2:]) # ジャンル1取得
            jenre2.append('') # ジャンル2取得
            jenre3.append('') # ジャンル3取得
        else:
            jenre1.append(sj[sj.find('/')+2: sj.find('、')]) # ジャンル1取得
        if sj.count('、') == 1: # ジャンルが2つの場合
            jenre2.append(sj[sj.find('、')+1:]) # ジャンル2取得
            jenre3.append('') # ジャンル3取得
        elif sj.count('、') == 2: # ジャンルが3つ以上の場合
            jenre2.append(sj[sj.find('、')+1: sj.find('、',sj.find('、')+1)]) # ジャンル2取得
            jenre3.append(sj[sj.find('、',sj.find('、')+1)+1:]) # ジャンル3取得

    ## 夜予算取得
    info = soup.find_all("span",{"class":"c-rating__val list-rst__budget-val cpy-dinner-budget-val"}) # 対象タグ取得
    budget_min = []
    budget_max = []
    for bud in info:
        bud = bud.text #タグ内テキスト部抽出
        bud = bud.replace('￥', '').replace(',', '').replace('-', '') # 不要文字列除去
        budget_min.append(bud[:bud.find('～')]) # 最低予算取得
        budget_max.append(bud[bud.find('～')+1:]) # 最高予算取得

    ## 取得情報マージ
    ten_code = [tcode]*len(shopname) #店舗ID付与
    data = pd.DataFrame({'ID' : ten_code, '店名' : shopname, '最寄駅名' : station_name, '駅からの距離' : station_distance, 'ジャンル1' : jenre1, 'ジャンル2' : jenre2, 'ジャンル3' : jenre3, '夜最低予算' : budget_min, '夜最高予算' : budget_max})
    data = pd.concat([data, syosai_df], axis=1)
    return data
```
さいごに、全ページ分のURLを取得する関数を定義する。

```python:scrape.py
### 関数3_1駅に対する店舗一覧全ページのURL取得
def get_write_data(url, tcode):
    PageNum = 99999 # 取得する一覧ページ数
    sleep_time_list = 1 # 店舗一覧ページURLを取得する間隔（秒）
    sleep_time_detail = 1 # スクレイピングを実施する間隔（秒）
    dataset = pd.DataFrame() # スクレイピング結果格納用のデータフレーム
    url_list_all = [url] # 全商品一覧ページURL格納リスト
    for i in range(PageNum):
        try:
            html = requests.get(url) # 店舗一覧ページのHTML取得
        except ChunkedEncodingError:
            print(u"エラー：URL取得時にエラーが発生しました。60秒ほど時間をおいて再度実行してください。")

        ## 商品一覧ページのHTML取得可否確認
        if html.status_code == requests.codes.ok:
            soup = BeautifulSoup(html.text, 'html.parser') # Soupオブジェクト取得
            print(u"一覧ページのURL取得 成功: "+str(i)+"/"+str(PageNum))
        else:
            print(u"一覧ページのURL取得 失敗: "+str(i)+"/"+str(PageNum)+" error code:"+str(html.status_code))
            sys.exit()

        ## NextPageのURL取得
        nextpage = soup.find("a", {"class": "c-pagination__arrow c-pagination__arrow--next"})
        if nextpage:
            #url = urljoin(web_url,nextpage.get("href"))
            url = nextpage.get("href")
            url_list_all.append(url)
        else:
            break
        sleep(sleep_time_list)
    print(u"取得したURL数: "+str(len(url_list_all)))

    ## 商品一覧ページのHTML取得とスクレイピング
    for i in range(len(url_list_all)):
        html = requests.get(url_list_all[i]) # ページのHTML取得
        if html.status_code == requests.codes.ok:
            print(u"ページのHTML取得 成功: "+str(i)+"/"+str(len(url_list_all)))
            data = scrape(html.text, tcode) # スクレイピング実行
            dataset = pd.concat([dataset, data], ignore_index=True)
        else:
            print(u"ページのHTML取得 失敗: "+str(i)+"/"+str(len(prod_url))+" error code:"+str(html.status_code))
            sys.exit()
        sleep(sleep_time_detail) # 処理間隔
    return dataset
```
予め用意したcsvを読み込み、関数を実行、ファイルを出力する。

```python:scrape.py
###【主処理】ファイルの読み込み、スクレイピング、ファイル出力
print(u'インプットファイル読み込み：開始')

## 取得対象駅URLが記されたファイル読み込み
df = pd.read_csv(fname)
tenpo_code = df['ID'] # 読み込み対象フィールド名（店舗番号）
tabelog_url = df['url'] # 読み込み対象フィールド名（食べログURL）
code = tenpo_code.values.tolist() # 店舗ID格納リスト
stations = tabelog_url.values.tolist() # URL格納リスト
print(u'インプットファイル読み込み：終了')

## 関数実行
cnt = 0 #ループ回数カウンター
dataframe = pd.DataFrame() # スクレイピング結果格納用のデータフレーム
for i in range(len(stations)):
    cnt += 1
    print(cnt, u'レコード目クローリング、スクレイピング：開始---------------------------------')
    url = stations[i]
    tcode = code[i]
    df = get_write_data(url, tcode) #関数②実行
    dataframe = pd.concat([dataframe, df], ignore_index=True) #データフレームへ追加
    print(cnt, u'レコード目クローリング、スクレイピング：終了---------------------------------')

## データセット出力
print(u'全レコード分スクレイピング終了')
# ファイル出力
dataframe.to_csv(outfile, encoding="utf-8", index=False)
print(u'ファイル出力完了')
```
#取得したデータをどう活用するか？
駅周辺の飲食店の数や、競合店数（分析対象のお店のジャンル"イタリアン"と、同じジャンルをもつ飲食店数）、価格帯、ジャンルの傾向から、分析対象の店がどれほど熾烈な競争に揉まれているか、周辺は賑わっているかが基礎統計を取るだけでもわかるだろう。

その他にも、各変数を用いて、生存予測モデルの説明変数として組み込むでも良し。

また、飲食店の緯度経度と自店舗の緯度経度から「自店舗から50m以内に競合店はいくつあるか？」といった変数も作成できる。（※要日本測地系→世界測地系変換）

#さいごに
彼にこのTipsが届くように祈っている。

また、Pythonを触り始めてからまだ日が浅いので、稚拙なコードが散見されると思います。
質問・指摘等ありましたら、ぜひ気軽コメントください。

LOVE コウメ。