【備忘録】Suumo賃貸物件の情報をスクレイピングしてみた。

<h2>はじめに</h2>
この記事では機械学習の際にSuumoが掲載している情報を用いてスクレイピング(BeautifulSoup)
してみた時の沼ってしまった点や知識をまとめる備忘録として書いております。
初投稿なのでわかりにくい解説等があるかもですが温かい目で見てやってください。。。

※スクレイピングには著作権侵害等の恐れがある為、規約などを確認して
使用するようにしましょう。

<h2>環境</h2>
<ul><li>macOS</li>
<li>Python 3.7.12</li>
<li>GoogleColaboratoryPro</li></ul>

<h2>学習内容</h2>
<ul><li>検索条件を絞ってデータ取得</li>
<li>詳細ページのURLを取得して各ページからデータ取得</li>
<li>スクレイピングを行う際に沼った点</li></ul>


<h2>１.検索条件を絞ってデータ取得</h2>
それでは早速本題に入っていこうと思います。
まずはSuumoの検索ページで条件を指定して必要な情報を確認、、、
![suumo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2241784/aa8b7d61-efc6-9e40-8c1b-8883760df879.png)

こんな感じですね。
今回は神戸市で条件を絞っています。~~(地元大好き民です)~~
スクレイピングでは取得したいページのHTMLを見ながら情報を引っ張り出していこう
という話なのでデベロッパーツールを見ていきます（Macの方はF12を押すと開けます。）
![suumo_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2241784/b4006d14-3640-d551-71db-1fda4392e18a.png)
開くとこんな感じ。
ここの"div"やら"class"やらを使っていこうという感じですね。
では次にコードを書いてスクレイピングしていこうと思います。

<h3>参照記事</h3>
・【コード解説】PythonでSUUMOの賃貸物件情報をスクレイピングする
　　https://myfrankblog.com/scraping-suumo-website/

<h3>今回使う主なライブラリ</h3>
<ul><li>retry</li>
<li>requests</li>
<li>BeautifulSoup</li></ul>
※もしインポートできない場合、”!pip install ライブラリ”を先に実行すると
できるようになるはずです。

下に全コード記載していき、後で解説していきたいと思います。

```python:
from retry import retry
import requests
from bs4 import BeautifulSoup
import pandas as pd 
import pickle
#{}の中にページ数が入る
base_url = 'https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=060&bs=040&ta=28&sc=28110&cb=0.0&ct=9999999&mb=0&mt=9999999&md=04&et=9999999&cn=0&shkr1=03&shkr2=03&shkr3=03&shkr4=03&sngz=&po1=25&pc=50&page={}'

@retry(tries=3, delay=10, backoff=2)
def get_html(url):
  r = requests.get(url)
  soup = BeautifulSoup(r.content, 'html.parser')
  return soup

all_data = []
max_page = 1

for page in range(1, max_page+1):
  url = base_url.format(page)
  soup = get_html(url)
  #物件ごとの情報抽出
  items = soup.find_all("div", attrs={"class": "cassetteitem"})
  for item in items:
    items_data = {}
    items_data['名称'] = item.find("div", attrs={"class": "cassetteitem_content-title"}).text.strip()
    items_data["カテゴリー"] = item.find("div", {"class": "cassetteitem_content-label"}).text.strip()
    items_data["アドレス"] = item.find("li", {"class": "cassetteitem_detail-col1"}).text.strip()
    items_data["築年数"] = item.find("li", {"class": "cassetteitem_detail-col3"}).findAll("div")[0].text.strip()
    items_data["構造"] = item.find("li", {"class": "cassetteitem_detail-col3"}).findAll("div")[1].text.strip()
    for access in item.find_all("li", {"class": "cassetteitem_detail-col2"}):
      access_list = []
      access_list.append(access.text.replace('\n', ' '))
      items_data['アクセス'] = access_list[0]
    #部屋ごとの情報抽出
    tbodys = item.find("table", {"class": "cassetteitem_other"}).findAll("tbody")
    for tbody in tbodys:
      #copy()しないとデータが更新されて同じ値が入ってしまうため注意！
      tbody_data = items_data.copy()

      tbody_data["階数"] = tbody.find_all("td")[2].text.strip()

      tbody_data["家賃"] = tbody.find_all("td")[3].find_all("li")[0].text.strip()
      tbody_data["管理費"] = tbody.find_all("td")[3].find_all("li")[1].text.strip()

      tbody_data["敷金"] = tbody.find_all("td")[4].find_all("li")[0].text.strip()
      tbody_data["礼金"] = tbody.find_all("td")[4].find_all("li")[1].text.strip()

      tbody_data["間取り"] = tbody.find_all("td")[5].find_all("li")[0].text.strip()
      tbody_data["面積"] = tbody.find_all("td")[5].find_all("li")[1].text.strip()

      for url in tbody.find_all("td", attrs={"class": "ui-text--midium ui-text--bold"}):
        url_list = []
        url_list.append("https://suumo.jp" + url.find("a").get("href"))
        #詳細情報と結合する際に使う為追加する。
        tbody_data['URL'] = url_list[0]
      
      all_data.append(tbody_data)
      
df = pd.DataFrame(all_data)
df.head(30)
```
![suumo_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2241784/18ea3874-85d6-6b01-f9a1-7581e42c8c46.png)

無事、出力できていますね。
これでスクレイピング→df作成完了となります。
では順番に解説していきます。

<h4>retry</h4>
プログラムがエラーした際の処理を書く際に使います。
関数get_htmlで用いているのですが失敗したら10秒後にもう一度を
合計３回試すといった処理が行われます。

<h4>requests</h4>
”requests.get(url)”で指定したURLからHTMLを持ってくる
処理が行われています。

<h4>BeautifulSoup</h4>
HTMLの解析するために使います。
"div"や"class"を指定すると中の情報を抽出する処理が行われます。
**find_all**: 指定した型とclass名と一致する中の情報を全て取得できます。
※同じclass名の中から２番目以降を取得する場合は"find_all"を使う。
**find**: 指定した型とclass名と一致する中の最初の情報が取得できます。

これらを使えれば後はfor文の構成と取得したい情報のclass名を探せば
簡単にスクレイピングができるようになります！
（補足：　WEBサイトでclass名を知りたい場合、必要な情報を選択した状態で
右クリックの”検証”で調べることができます。）

<h2>詳細ページのURLを取得して各ページからデータ取得</h2>
次に詳細ページの情報抽出ですが
WEBサイトの"詳細を見る"を検証で確認。
↓
URLをリストに格納
↓
情報抽出
↓
検索ページの情報と結合
という流れでスクレイピングを行っていきます。

```python:
#URL取得して詳細データ取得
url = base_url.format(1)
soup = get_html(url)
detail_urls_datas = []
#URLを抽出
for url in soup.find_all("td", attrs={"class": "ui-text--midium ui-text--bold"}):
  detail_urls_datas.append("https://suumo.jp" + url.find("a").get("href"))

detail_data = []
for detail_urls_data in detail_urls_datas:
  soup = get_html(detail_urls_data)
  items_data = {}
  items_data['向き'] = soup.find_all("tr")[6].find_all('td', attrs={"class": "property_view_table-body"})[0].text.strip()
  items_data["建物種別"] = soup.find_all("tr")[6].find_all('td', attrs={"class": "property_view_table-body"})[1].text.strip()
  items_data["URL"] = detail_urls_data
  detail_data.append(items_data)

detail_df = pd.DataFrame(detail_data)

all_df = pd.merge(df, detail_df, on='URL')
all_df.drop('URL', axis=1, inplace=True)
all_df.head()
```
![suumo_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2241784/152bd013-5b54-f5db-6d87-5c870ead920b.png)

前のデータと結合できていますね。

<h2>スクレイピングを行う際に沼った点</h2>
私がスクレイピングの際に沼ってしまった点について書いていこうと思います。

**"find_all"と”find”の扱いについて**
データ抽出の際に下記のエラーが起きました。

```python:
AttributeError: 'NoneType' object has no attribute 'findAll'
```
今見ても恐ろしいです。。。
２回同じエラーが起きたのですが
１回目はfor文で回して情報を取ろうとした時、このエラー文が出力され原因を突き止めると
class名が間違えており修正するとすぐ解消しました。
ところが２回目はclass名があっているのにも関わらずエラーが起きるのです。
そこでprint文で確かめてみようと表記させると情報が一部取得できていなかった為、
'NoneType'となっていました。
しかし、もう一度試してみると取得できており違う部分が取得できないという現象が起き
約３時間格闘しても分からず分からずPCを再起動させると直ったのです。つまりはバグ。
というしょうもないことに時間を費やしてしまいました。
皆さんも気をつけましょう。~~（たぶん自分だけ）~~