スクレイピングで理想の物件探し！物件から目的地まで徒歩何分か自動取得するの巻


## はじめに

はじめまして、来年度引っ越しを控えているものです。
「電車が苦手なので通勤先から徒歩圏内がいいな～」
そう思いながら、物件探しをしていたのですが、、、
**指定先から物件までの距離を表示する機能が無い！**
SUUMOに地図から探す機能はあるのですが、物件の詳細をリストで見れなくて使いづらい。。
コードが恐ろしく汚いですが、お許しを><
ここをこうした方がいい！などご指摘お待ちしております！

※Google Maps Platform を使用するように変更しました。Googleは外部からのスクレイピングは利用規約で禁止しています。私のように迷惑をかけないようお願いいたします。利用規約
また、サーバーに負荷をかける行為のため、利用規約で禁止していないサイトでも節度を持ってやるよう注意しましょう。
本記事では、1分の間隔を取ることとします。

## 本日のゴール地点

指定した住所から物件まで徒歩x分以内の物件の家賃（管理費共益費込み）、徒歩何分か、物件名、家賃、階数、間取り、専有面積、その物件の賃貸サイト、googlemapのURLを取得すること。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993875/65b98e31-7842-7c99-b5a8-d4d1b7f68723.png)

コードの説明などはあまり無いのでご了承ください。
いずれまとめます。

## 前提

### 環境

Windows 10 バージョン 20H2
Python 3.7.4
jupyter notebook

### 必要なライブラリ

```python
pip install beautifulsoup4
pip install selenium
pip install openpyxl
pip install xlwt
```

### Chrome driver のダウンロード

chromeでスクレイピングするために必要になります。
「Chrome driver ダウンロード」で検索すると出てきます。
自身のchrome のバージョンと合っている driver をダウンロードする必要があります。

### Google Maps Platform

Google Maps Platform のDirections API を用いることで移動時間を計算します。
APIキー取得の方法はこちらの記事に詳しく書いてあります。
https://qiita.com/kngsym2018/items/15f19a88ea37c1cd3646

1か月に40000回までの呼び出しが無料なので、今回の目的は十分果たせそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993875/7ba4a41a-6422-e72f-f4db-99c68ca7f912.png)

### 利用するサイト

今回は賃貸サイトはSUUMOを利用します。
今後、別のサイトも含めてスクレイピングをする予定です。
移動時間は、google map を用いて出します。

### 想定

勤務地：東京スカイツリー
住みたい場所：墨田区
勤務時間：15分以内
1K
賃料：8万円以下

の物件を探すという想定で、コードを書いていきます。

## コード

### モジュールのimportと目的設定

まず、必要になるモジュールを import します。

```python
import time
import re
import pandas as pd
import urllib.request, json
import urllib.parse

from bs4 import BeautifulSoup
from selenium import webdriver
```

次に、目的地と、徒歩何分まで許容するかを定義します。

```python
# 目的地 東京スカイツリー
DESTINATION = '東京都墨田区押上１丁目１−２'
# 徒歩何分まで許容するか
DURATION = 15
```

### SUUMOサイトのスクレイピング

続いてSUUMOのサイトにアクセスします。

```python
# SUUMO スクレイピング
suumo_br = webdriver.Chrome('C:\\Users\\hohgehoge\\chromedriver') # Windowsの方は chromedriver までのパスを通す
# suumo_br = webdriver.Chrome() # Macの方
suumo_br.implicitly_wait(3)
# suumoの物件検索結果のURL
url_suumo = "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=030&bs=040&ta=13&sc=13107&cb=0.0&ct=8.0&co=1&et=9999999&md=02&cn=9999999&mb=0&mt=9999999&shkr1=03&shkr2=03&shkr3=03&shkr4=03&fw2="
suumo_br.get(url_suumo)
time.sleep(5)
print('SUUMO にアクセスしました')
```

変数 url_suumoには気に入った条件で絞り込みをした後のURLを入力してください。
本記事では、[東京都墨田区の１K賃料8万以下の物件](https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=030&bs=040&ta=13&sc=13107&cb=0.0&ct=8.0&co=1&et=9999999&md=02&cn=9999999&mb=0&mt=9999999&shkr1=03&shkr2=03&shkr3=03&shkr4=03&fw2=)で絞り込んでみます。

これで、suumoのサイトが開けました。
次に、このサイトのhtmlを解析し、物件の住所リストを取得します。

```python
soup = BeautifulSoup(suumo_br.page_source, 'html.parser')
# 物件の住所のリスト
addresses = [c.get_text() for c in soup.find_all('li', class_='cassetteitem_detail-col1')]
print(addresses)
```

出力結果

```result
['東京都墨田区両国２', '東京都墨田区緑４', '東京都墨田区東向島２', '東京都墨田区東向島２', '東京都墨田区八広５', '東京都墨田区緑４', '東京都墨田区千歳３', '東京都墨田区京島１', '東京都墨田区東向島６', '東京都墨田区東向島６', '東京都墨田区立花４', '東京都墨田区東向島５', '東京都墨田区八広６', '東京都墨田区立川４', '東京都墨田区両国２', '東京都墨田区八広２', '東京都墨田区墨田２', '東京都墨田区立花４', '東京都墨田区立花４', '東京都墨田区立花１', '東京都墨田区立花１', '東京都墨田区向島５', '東京都墨田区菊川１', '東京都墨田区東向島６', '東京都墨田区八広５', '東京都墨田区東向島１', '東京都墨田区東向島１', '東京都墨田区文花２', '東京都墨田区向島５', '東京都墨田区八広５']
```

続いて物件の名称を取得します。
なお、SUUMOの場合、物件の名称の所に駅名とか入ってたりする場合がありますが、今回はスルーです。

```python
# 物件数の確認
properties = soup.find_all('table', class_='cassetteitem_other')

# 建物名を取得
buildings = [c.get_text() for c in soup.find_all('div', class_='cassetteitem_content-title')]
print(buildings)
```

出力結果

```result

['エクスクルーシブアイディ両国', 'ソアラプラザ錦糸町', 'トウキョウミトストリート', '東武伊勢崎線 曳舟駅 11階建 築16年', 'ドルチェの森', 'カツパレス', '樋口ハイツ', '京成押上線 京成曳舟駅 5階建 築30年', 'グレースフルプレイス', '京成押上線 八広駅 2階建 築8年', 'リリックコート平井橋', 'クレイノボヌールII', 'プロスペリティスカイツリー', 'ライク菊川イースト', 'ＪＲ総武線 両国駅 7階建 築12年', 'リガーレ墨田レヴァンテ', '東武伊勢崎線 鐘ヶ淵駅 3階建 新築', '東武亀戸線 東あずま駅 3階建 築13年', 'Rilassante立花', 'ストール ハウス', '東武亀戸線 小村井駅 4階建 築2年', 'リヴシティ向島', 'ボナール', 'モラージュ・ナイン', 'ビーカーサ曳舟', 'ベルフォート', '東武伊勢崎線 曳舟駅 3階建 築3年', 'エル・ヴィエント・アース墨田吾妻', '東武伊勢崎線 曳舟駅 3階建 築6年', '京成押上線 八広駅 3階建 築15年']
```

1つの物件で複数の賃貸を取り扱っている場合と、1つの物件で単一の賃貸を取り扱っている場合でxpathが異なるので、それを対処するために、1つの物件で取り扱っている賃貸数を取得します。

```python
# サイト内の物件の数をカウントする
properties_num_list = []
for prop in properties:
    
    prop = str(prop)
    properties_num_list.append(prop.count('<tbody>'))
print(properties_num_list)
# [1, 12, 8, 8, 1, 2, 3, 1, 3, 3, 1, 1, 5, 4, 1, 4, 1, 1, 1, 1, 1, 5, 1, 1, 1, 2, 2, 3, 2, 1]
```

### Direction API で移動時間を取得

Direction API の設定

```python
endpoint = 'https://maps.googleapis.com/maps/api/directions/json?'
api_key = 'hogehoge' # 取得したキーを入れる
google_map_url = 'https://www.google.co.jp/maps/dir/'
# Google maps api のwalking, driving, bicycling, transit, の中から移動手段を選択できます。
mode = 'walking'
```

```python
# 時間を分に変換する関数
def time_conversion(travel_time):
    minute = re.search('\d*分', travel_time)
    hour = re.search('\d*時間', travel_time)
    day = re.search('\d*日', travel_time)
    if minute:
        minute = int(re.search('\d*', minute.group()).group())
    else: minute = 0
    if hour:
        hour = int(re.search('\d*', hour.group()).group()) * 60
    else: hour = 0
    if day:
        day = int(re.search('\d*', day.group()).group()) * 60 * 24
    else: day = 0

    convert_travel_time = minute + hour + day
    return convert_travel_time
```

先ほど取得した住所リストから、スカイツリーまでの移動時間を取得します。ブラウザが自動で操作され、ジャンジャン移動時間を取得しています。

```python
travel_times = []
map_url = []

for i, address in enumerate(addresses):
    nav_request = 'language=ja&origin={}&destination={}&mode={}&key={}'.format(address,DESTINATION, mode, api_key)
    nav_request = urllib.parse.quote_plus(nav_request, safe='=&')
    request = endpoint + nav_request
    response = urllib.request.urlopen(request).read()
    directions = json.loads(response)
    travel_time = directions['routes'][0]['legs'][0]['duration']['text']
    convert_travel_time = time_conversion(travel_time)
    travel_times.append(convert_travel_time)

    # google map の url を保存
    map_url.append(google_map_url + address + '/' + DESTINATION)
    print('徒歩時間:', travel_time)
```

### BeautifulSoupで必要な物件情報を取得

必要な物件情報を得るためにpathを設定します。

```python
# 物件のurl
# 一つの建物に対して複数の賃貸がある場合
path_1 = '//*[@id="js-bukkenList"]/ul['
path_2 = ']/li['
path_3 = ']/div/div[2]/table/tbody['
path_4 = ']/tr/td[9]/a'
# 1つの賃貸に対して単一の物件しかない場合
path_mono_1 = '//*[@id="js-bukkenList"]/ul['
path_mono_2 = ']/li['
path_mono_3 = ']/div/div[2]/table/tbody/tr/td[9]/a'

# 階数
# 一つの建物に対して複数の賃貸がある場合
path_floor = ']/tr/td[3]'
# 1つの賃貸に対して単一の物件しかない場合
path_mono_floor = ']/div/div[2]/table/tbody[1]/tr/td[3]'

# 賃料
# 一つの建物に対して複数の賃貸がある場合
path_rent = ']/tr/td[4]/ul/li[1]/span/span'
# 1つの賃貸に対して単一の物件しかない場合
path_mono_rent = ']/div/div[2]/table/tbody/tr/td[4]/ul/li[1]/span/span'

# 管理費
# 一つの建物に対して複数の賃貸がある場合
path_fee = ']/tr/td[4]/ul/li[2]/span'
# 1つの賃貸に対して単一の物件しかない場合
path_mono_fee = ']/div/div[2]/table/tbody[1]/tr/td[4]/ul/li[2]/span'

# 間取り
# 一つの建物に対して複数の賃貸がある場合
path_plan = ']/tr/td[6]/ul/li[1]/span'
# 1つの賃貸に対して単一の物件しかない場合
path_mono_plan = ']/div/div[2]/table/tbody[1]/tr/td[6]/ul/li[1]/span'

# 専有面積
# 一つの建物に対して複数の賃貸がある場合
path_area = ']/tr/td[6]/ul/li[2]/span'
# 1つの賃貸に対して単一の物件しかない場合
path_mono_area = ']/div/div[2]/table/tbody[1]/tr/td[6]/ul/li[2]/span'
```

賃料と管理費を合算する関数を書きます。

```python
# 賃料と管理費を合算する関数
def calc_rent(rent, fee):
    str_rent = rent.replace('万円', '')
    float_rent = float(str_rent) * 10000
    str_fee = fee.replace('円', '')
    float_fee = float(str_fee)
    return float_rent + float_fee
```

続いて、pathをもとに欲しい情報を取得してデータフレームにします。
（コード汚くてごめんなさい＞＜）

```python
df = pd.DataFrame(columns=['建物名', '通勤時間', '家賃', '階数', '間取り', '専有面積', 'map', 'url'])
i, j  = 1, 1
for prop_info in zip(travel_times, properties_num_list, buildings, map_url):
    if prop_info[0] > DURATION:
        # 許容徒歩時間より長い場合、continue
        print('Out of Duration')
        j += 1
        if j % 6 == 0:
            i += 1
            j = 1
        continue
    if prop_info[1] == 1:
        # url
        path = path_mono_1 + str(i) + path_mono_2 + str(j) + path_mono_3
        prop_url = suumo_br.find_element_by_xpath(path).get_attribute('href')
        # 階数
        path =  path_mono_1 + str(i) + path_mono_2 + str(j) + path_mono_floor
        prop_floor = suumo_br.find_element_by_xpath(path).text
        # 家賃
        path = path_mono_1 + str(i) + path_mono_2 + str(j) + path_mono_rent
        temp_rent = suumo_br.find_element_by_xpath(path).text
        # 管理費
        path = path_mono_1 + str(i) + path_mono_2 + str(j) + path_mono_fee
        temp_fee = suumo_br.find_element_by_xpath(path).text
        # 間取り
        path = path_mono_1 + str(i) + path_mono_2 + str(j) + path_mono_plan
        prop_plan = suumo_br.find_element_by_xpath(path).text
        # 専有面積
        path = path_mono_1 + str(i) + path_mono_2 + str(j) + path_mono_area
        prop_area = suumo_br.find_element_by_xpath(path).text
        prop_rent = calc_rent(temp_rent, temp_fee)
        print(prop_url)
        df = df.append({'建物名': prop_info[2], '通勤時間': prop_info[0], '家賃': prop_rent, '階数': prop_floor, '間取り': prop_plan, '専有面積': prop_area, 'map': prop_info[3], 'url': prop_url}, ignore_index=True)
    else:
        for k in range(1, prop_info[1] + 1):
            path = path_1 + str(i) + path_2 + str(j) + path_3 + str(k) + path_4
            prop_url = suumo_br.find_element_by_xpath(path).get_attribute('href')
            # 階数
            path =  path_1 + str(i) + path_2 + str(j) + path_3 + str(k) + path_floor
            prop_floor = suumo_br.find_element_by_xpath(path).text
            # 家賃
            path = path_1 + str(i) + path_2 + str(j) + path_3 + str(k) + path_rent
            temp_rent = suumo_br.find_element_by_xpath(path).text
            # 管理費
            path = path_1 + str(i) + path_2 + str(j) + path_3 + str(k) + path_fee
            temp_fee = suumo_br.find_element_by_xpath(path).text
            # 間取り
            path = path_1 + str(i) + path_2 + str(j) + path_3 + str(k) + path_plan
            prop_plan = suumo_br.find_element_by_xpath(path).text
            # 専有面積
            path = path_1 + str(i) + path_2 + str(j) + path_3 + str(k) + path_area
            prop_area = suumo_br.find_element_by_xpath(path).text
            prop_rent = calc_rent(temp_rent, temp_fee)
            print(prop_url)
            df = df.append({'建物名': prop_info[2], '通勤時間': prop_info[0], '家賃': prop_rent, '階数': prop_floor, '間取り': prop_plan, '専有面積': prop_area, 'map': prop_info[3], 'url': prop_url}, ignore_index=True)
    j += 1
    if j % 6 == 0:
        i += 1
        j = 1
```

中身を確認してみます。

```python
df.head()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993875/7b623257-63d6-9117-0e66-6006ac0c47e3.png)

### excelファイルへ

最後にエクセルファイルにして終了です。

```python
df.to_excel('sample.xlsx', encoding='utf_8_sig', index=False)
```

## 今後の課題

現状1ページしか読み込んでいないので、全ページを読み込むように改良します。
また、別の賃貸サイトのスクレイピングも行います。
他にも役に立ちそうな機能や物件情報があれば教えて頂けると幸いです。