SUUMOで東横線沿線の物件をスクレイピング

再来月に引っ越しを考えています。
SUUMOの物件情報のデータを取得し、簡単に周辺エリアの相場感を掴むことにしてみます。

# 環境
* Windows
* Python 3.12.0

# スクレイピング

はやたす先生の動画シリーズを参考に取得を行いました。

https://www.youtube.com/watch?v=fzpYaYnQ3Sc&list=PL4Y-mUWLK2t1LehwHVwAqxXTXw5xd-Yq8&index=1


## 対象を決める

SUUMOのウェブサイトから、引っ越し先で考えている東急東横線沿線で絞り込んだ検索結果の画面を用います。最寄り駅の候補はいくつかあるのですが、まずは全体感を見たいということでこの中の全ての物件情報を取り出してみます。

[SUUMO（東急東横線沿線）](https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=/chintai/ichiran/FR301FC001/&ar=030&bs=040&pc=50&smk=&po1=25&po2=99&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=9999999&et=9999999&mb=0&mt=9999999&cn=9999999&ra=014&rn=0220&ae=02201)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3664928/cb38d22b-0ca4-6355-f80c-d6c1daeb8c84.png)


:::note warn
下記の3点には十分に注意して進めましょう。
- アクセスするサイトの利用規約を確認する
- アクセスの回数、頻度は最小限に
- 収集したデータの取り扱いに注意する
:::

スクレイピング、クローリングは禁止と明示しているサイトもあるので、利用規約は十分確認するようにしてください。基本的には、自分のサイトに気ままに高頻度にアクセスされるのを好む人はいないので、そこは暗黙のルールとして配慮しながらやりましょう。。また取得したデータの二次利用にも十分気をつけてください。

## ライブラリのインポート

```Python
import requests
from bs4 import BeautifulSoup
from time import sleep
import pandas as pd
```
requestsはwebページへのアクセス、beautifusoupは取得したhtmlを解析するパーサーです。
大量のデータをスクレイピングする場合、サーバー側に負荷をかけないよう、time関数で処理ごとに時間を空けた方がよいです。
データ取得後の分析のためにpandasも入れておきます。

## urlにアクセス
```Python
# urlにアクセス
url = 'https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=/chintai/ichiran/FR301FC001/&ar=030&bs=040&pc=50&smk=&po1=25&po2=99&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=9999999&et=9999999&mb=0&mt=9999999&cn=9999999&ra=014&rn=0220&ae=02201&page=1'
r = requests.get(url)
# statusの確認：成功なら200、クライアント側エラーなら400番台、サーバー側エラーなら500番台
print(r.status_code)
# bs4で変数soupにhtml丸々入れておく
soup = BeautifulSoup(r.text, 'html.parser')
```
スクレイピングのコアな部分はこれだけです。あとは変数soupに入った情報を成型していく作業になります。ステータスコードの確認は飛ばしてもokです。

## ページ構造の確認
ここから先は、ディベロッパーモード（検証ツール）を使って実際のhtmlを見ながらやります。検証ツールの使い方は調べたらたくさん出てくると思うので省略します。
まずは、webページへのアクセスを1回で済ませるために、対象のページの中から欲しい情報が入っている大きなくくりを探します。今回、各物件/部屋が
```html:html
<div class_='cassetteitem'>
```

の中にそれぞれ入っていました。なので、.find_allを用いて中身をそれぞれまるごとくりぬきます。

```Python
# div > cassetteitemに物件情報が丸々入ってるので変数contentsに入れておく
contents = soup.find_all('div',class_='cassetteitem')
# 変数に何個入ったか確認
print(len(contents))

```

ここから先は.findメソッドでCSSセレクタを見ながら欲しい情報を少しずつ削り取っていく作業になります。

## 物件情報の取得

```Python
# １：物件情報の取得。まずは物件一つでデータを取得するため、1件目を変数に入れてトライアル
content = contents[0]
# cassetteitemの中の物件・建物情報を取得
detail = content.find('div',class_='cassetteitem-detail')
# cassetteitemの中の部屋情報を取得
table = content.find('table',class_='cassetteitem_other')

# 物件タイトルを変数titleに格納。.textで文字部分のみ抽出
title = detail.find('div',class_='cassetteitem_content-title').text
# 物件住所をaddressに格納。liの中から取得。.textで文字部分のみ抽出
address = detail.find('li',class_='cassetteitem_detail-col1').text
# 物件アクセスをaccessに格納。liの中から取得。.textで文字部分のみ抽出
access = detail.find('li',class_='cassetteitem_detail-col2').text
# 物件築年数をageに格納。liの中から取得。.textで文字部分のみ抽出
age = detail.find('li',class_='cassetteitem_detail-col3').text
```

## 部屋情報の取得
部屋情報はtdタグに格納されているので一気にぶっこめます。

```Python
# ２：部屋情報の取得。部屋情報はtrタグの中に入っている
tr_tags = table.find_all('tr',class_='js-cassette_link')
# 最初の一つを格納
tr_tag = tr_tags[0]
print(tr_tag)
# 一気にリストで取ってきて変数にぶっこむ
floor,price,first_fee,capacity = tr_tag.find_all('td')[2:6]
print(floor,price,first_fee,capacity)
# ぶっこんだあと細かく分ける
fee, management_fee = price.find_all('li')
deposit, gratuity = first_fee.find_all('li')
madori, menseki = capacity.find_all('li')
```
部屋ごとの家賃、敷礼、間取り、広さの情報も取得できました。
これらを辞書に入れておきます。

```Python
# テキストだけを一気に辞書にぶっこむ
d = {
    'title': title,
    'address': address,
    'access': access,
    'age': age,
    'floor': floor.text,
    'fee': fee.text,
    'management_fee': management_fee.text,
    'deposit': deposit.text,
    'gratuity': gratuity.text,
    'madori': madori.text,
    'menseki': menseki.text
}
```
.textメソッドで文字だけを抽出できます。

## for文で1ページ全体の情報を取得
contentのリスト一番目から必要な情報が取り出せたので、残りの49物件をfor文で処理します。
空のリストd_listを作っておいて、ここに情報を放り込むまでの処理を繰り返してもらいます。

```Python
# 空のリストを作成
d_list = []
# for文で物件1件ごとに取得→辞書に格納を繰り返し
for content in contents:
    # 物件情報と部屋情報を取得しておく
    detail = content.find('div',class_='cassetteitem-detail')
    table = content.find('table',class_='cassetteitem_other')
    # 物件情報から必要な情報を取得する
    title = detail.find('div',class_='cassetteitem_content-title').text
    address = detail.find('li',class_='cassetteitem_detail-col1').text
    access = detail.find('li',class_='cassetteitem_detail-col2').text
    age = detail.find('li',class_='cassetteitem_detail-col3').text

    # 部屋情報のブロックから、各部屋情報を取得する
    tr_tags = table.find_all('tr',class_='js-cassette_link')

    # 各部屋情報をforループで取得する
    for tr_tag in tr_tags:
        # 部屋情報の行から、欲しい情報を取得する
        floor,price,first_fee,capacity = tr_tag.find_all('td')[2:6]
        # さらに細かい情報を取得する
        fee, management_fee = price.find_all('li')
        deposit, gratuity = first_fee.find_all('li')
        madori, menseki = capacity.find_all('li')

        # 取得したすべての情報を辞書に格納する
        d = {
            'title': title,
            'address': address,
            'access': access,
            'age': age,
            'floor': floor.text,
            'fee': fee.text,
            'management_fee': management_fee.text,
            'deposit': deposit.text,
            'gratuity': gratuity.text,
            'madori': madori.text,
            'menseki': menseki.text
        }

        # 取得した辞書をd_listに格納する
        d_list.append(d)
```

これで1ページの物件情報がまるごと取得できました。
ここまででwebページへのアクセスは1回で済んでいます。

## for文で全ページの情報を取得
あとはページ送りをfor文で行えば完了です。最大ページ数は事前に調べておくか、取得するデータがゼロになったら処理を終了させるように組んでおけば大丈夫です。

:::note warn
webサイトに負荷をかけないよう、必ずアクセスごとの間隔を空けるようにしてください。
timeライブラリをインポートして、sleep()関数の処理も繰り返すようにしておくとよいです。
:::

こうして今回16,650件の部屋情報を取得できました。
同じ処理を何度もやらなくて済むよう、一旦csvとかに落としておくといいと思います。

# ゆるーくデータを眺める
可視化用のライブラリ
```Python
import matplotlib.pyplot as plt
import seaborn as sns
```

## 前処理
家賃が「14.4万円」と文字型になってしまっているので、データ型を小数に変換しておきます。
```Python
# データフレームを作成
df = pd.DataFrame(d_list)
# feeの列を14.4万円→14.4の小数に変換したい
# まず文字列型に変換
df['fee'] = df['fee'].astype(str)
# '万円'を空文字列に置換し、文字列から小数に変換
df['fee'] = df['fee'].str.replace('万円', '').astype(float)
```
他の列の値も同様に処理しておきます。
```Python
# 同様の処理を、management_fee, deposit, gratuity, mensekiにおいても実行する
# その前段として欠損値処理。'-'のセルを0に変換
df.replace('-', 0, inplace=True)

# それぞれの値を文字列型に変換
df['management_fee'] = df['management_fee'].astype(str)
df['deposit'] = df['deposit'].astype(str)
df['gratuity'] = df['gratuity'].astype(str)
df['menseki'] = df['menseki'].astype(str)
# それぞれの単位を空文字列に置換し、文字列から小数に変換
df['management_fee'] = df['management_fee'].str.replace('円', '').astype(float)
df['deposit'] = df['deposit'].str.replace('万円', '').astype(float)
df['gratuity'] = df['gratuity'].str.replace('万円', '').astype(float)
df['menseki'] = df['menseki'].str.replace('m2', '').astype(float)
# 確認
df[:5]
```

## 可視化
まずはヒストグラム
```Python
sns.histplot(df_toyoko,x='fee')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3664928/7d59b9e6-d8e5-39f8-e84a-567271492ea1.png)

feeがえぐ高い物件がいくつかあって、それに引っ張られてそうですね。
力技ですが、上限を絞ってみました。

```Python
# 外れ値を除外する
# 例えば、95パーセンタイルまでのデータを残す場合
quantile_value = df_toyoko['fee'].quantile(0.95)
filtered_data = df_toyoko[df_toyoko['fee'] < quantile_value]

# ヒストグラムをプロットする
plt.hist(filtered_data['fee'], bins=20)  # 適切なビンの数を指定することが重要です
plt.xlabel('Values')
plt.ylabel('Frequency')
plt.title('Histogram without outliers')
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3664928/330e58a7-5843-e44a-4905-9ad504762773.png)

10万円以下が多く出てきてますね。
間取り別で可視化してみます。


```Python
# カテゴリごとにヒストグラムをプロットする
categories = ['1LDK', 'ワンルーム', '1K', '1LDK', '2DK', '2LDK']
colors = ['blue', 'orange', 'green', 'red', 'purple', 'brown']
bar_width = 0.15

plt.figure(figsize=(10, 6))
for i, (category, color) in enumerate(zip(categories, colors)):
    df_feedist = df_feefiltered[df_feefiltered['madori'] == category]['fee']
    plt.hist(df_feedist, bins=20, color=color, alpha=0.5, label=category, align='mid', stacked=False, rwidth=bar_width, edgecolor='black', linewidth=1.5)
    bar_width += 0.03  # 横にずらす幅を微調整

plt.xlabel('Values')
plt.ylabel('Frequency')
plt.title('Histogram without outliers')
plt.legend(prop={'family': 'MS Gothic'})
plt.tight_layout()
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3664928/32d96ab1-76a7-95bf-da36-14cdf7fd77e7.png)

探している1LDK、2LDKあたりは15万円以内くらいが多いですね。
今回は探してないですが、意外と単身1Kは6-7万円くらいがボリュームなんですね。
私が住んでいた都内の東側と比べて安そう。

クロス集計ぐらいやってみます。

```Python

# feeを条件に分割
df['fee_category'] = pd.cut(df['fee'], bins=[0, 8, 10, 12, 14, 16, 18, float('inf')],
                                    labels=['under8', 'under10', 'under12', 'under14', 'under16', 'under18', 'over18'])

# クロス集計
cross_table = pd.crosstab(df['madori'], df['fee_category'])
cross_table

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3664928/99d3cc27-35e5-ae94-1cdd-879cd06855ec.png)

（表の一部です）
1LDKで見てみると、グラフではunder12が山でしたが、
数でみるとover18の合計と同じくらい。
東横沿線は中目黒や武蔵小杉、横浜など、タワマンエリアが多数あるため、
このあたりの分譲マンションがover18に多いと予想できます。

# 最後に
ここから、更にエリアを絞り込んだり家-駅間の距離で見てみたりなどしてみたのですが、
いいかげんこの記事を温めすぎているのでいったんここで上げます。
より面白い結果が分析できたらまたあげようかな。

ちなみにそうこうしているうちに引っ越しは終わりました。けっきょくこれくらいの分析なら、仲介会社に行ってその場で聞きまくるのが早いという。

# 参考

:::note
- 記事の作成
  - [Markdown記法 チートシート](https://qiita.com/Qiita/items/c686397e4a0f4f11683d)
  - [Qiita記事作成方法 初心者の備忘録](https://qiita.com/U-MA/items/996ae933ae94c5711883)
  - [Markdown記法 サンプル集](https://qiita.com/tbpgr/items/989c6badefff69377da7)

- スクレイピング
  - [居場所探しの為Pythonで物件データ分析 ~爬取したり可視化したり機械学習で予測モデルを作ったり~](https://qiita.com/phyblas/items/2b447c8f33afa6c8721c)
  - [スクレイピングで物件探し！その２](https://qiita.com/virusyun/items/8c0cfeb55dc55e4b0e1f)
  - [引っ越しすることになったので機械学習を使って全力で自分の住む家を決めようとした話](https://qiita.com/pao2/items/24728936ee6f6674ae37)

- 動画
  - [【Python × スクレイピング入門①-⑭】（はやたす先生）](https://www.youtube.com/watch?v=fzpYaYnQ3Sc&list=PL4Y-mUWLK2t1LehwHVwAqxXTXw5xd-Yq8&index=2)
  - [【Webスクレイピング超入門】2時間で基礎を完全マスター！PythonによるWebスクレイピング入門 連結版（いまにゅさん）](https://www.youtube.com/watch?v=VRFfAeW30qE)

:::