デジタル推進課

KNIME・Excel Macro・Power Automateなど日々の業務で使用できる自動化ツールを中心に書き綴ります

Python - SUUMOで関東一人暮らしお得物件を見つけたい! ~ 不動産賃貸物件スクレイピング

f:id:makkynm:20210612080807p:plain

 

誰向きの記事か?

この記事はこんな方に向いています。

SUUMOから物件情報を抽出して

  • お得物件を自動で探したい方
  • Pythonのウェブスクレイピングを学びたい方
  • 物件データを解析したい方

 

はじめに

こんにちは、自動化大好きまっきーです。普段は自動化ツールKNIMEについて解説しています。諸事情あり、現在東京のお得物件を探し回っています。でもSUUMOやその他のサイトのUIって個人的にもう少し足りないんです。。

特に下記の機能ですね。是非実装して欲しいです。

  • 目的駅までの徒歩+電車の所要時間、運賃でフィルタリング(例: 東京駅まで徒歩+電車で60分以内の物件を洗い出す)
  • 地図上から家賃を色表示、フィルタリング
  • 周辺平均坪単価より安い
  • 東京都内だけでなく、関東圏内でフィルタリングを効かせたい

賃貸を探すときどうせならお得な物件に住みたいですよね。そしてそれを効率的に探したいです。不動産の物件データベースにアクセスできればいいんですが、不動産業者でもない私がアクセスできるわけがありません。

なので1週間くらい頑張りました。今回は第一歩となる、SUUMOのスクレイピングについて解説したいと思います。

 

 今回のテーマ ~SUUMOの物件データ~

f:id:makkynm:20210609151949p:plain

 今回のテーマ ~SUUMOの物件データ~

 

覚えてほしいこと

SUUMOスクレイピング個人利用目的のみOK。

 

やりたいこと

東京から比較的近い、関東の一人暮らしお得物件を見つける!

必須条件

条件:管理費・共益費込み 12万円以下 15分以内 15年以内 2階以上/室内洗濯機置場/バス・トイレ別 / 鉄筋系/鉄骨系/ブロック・その他/ 東京都

任意条件

  • 東京駅までの徒歩+電車の所要時間が60分以内
  • 物件が相場よりも安くお得
  • 川や海の近く

目的達成までのステップ

  1. SUUMOをスクレイピングして物件情報収集
  2. 前処理をして必要情報を取得
  3. 解析にかけてお得物件を洗い出す

 

今回はその第一ステップとして、SUUMOをスクレイピングして、東京都内の物件データを集めてきます。

 

ソースファイル

ソースファイルはこちらからダウンロードできます。

Inputファイルはありません。本来であればcsvファイルなどをInputファイルにすべきなんですが、面倒でやめました。

BaseURLsの部分を変更すれば使えると思います。Jupyter Notebookのファイルはデバッグで使った部分も残っているので少し汚いです。。

github.com

 一応、全コードを一番下につけておきました。

 

実行方法と出力フォーマット

実行までのステップ

Macでも、Windowsでも問題ありません。Pythonが実行できる環境を用意してください。ここら辺は一般的なPythonのお話なのでスキップします。モジュールも、特に変なものは使っておらず、ごくごく一般的なものです。

  1. Python3をインストールする
  2. 必要モジュールをインストールする(BeautifulSoup / requests / pandas / time / urljoin)
  3. ソースファイル上のBaseURLsを編集:SUUMOのURLのルール(詳細は後ほど)に基づき、重複のない大きな括りのURLを入力します。例えば、東京都の全物件一覧の1ページ目と神奈川県の全物件一覧の1ページ目とかですね。
  4. ソースファイルを実行する

 

実行時の動き

実行時、大きく2つの進捗状況が確認できます。

  1. District Status:BaseURLsに基づく総数と現在の状況。BaseURLsのリストに4つ入力すると、1/4から始まり、4/4で終わります。
  2. Room Detail Status:BaseURLsから検索されるページの総数と現在の状況。例えばBaseURLsが東京都全体で最大のページ数が640だとすると、1/640 ~ 640/640まで続きます。

現在5秒の待機処理がそれぞれのステップで入っています。サーバーに負荷のかからない十分に長い秒数を設定しましょう。この待機時間と合計URL数でおよその終了時間が予測できますね。

f:id:makkynm:20210612074418p:plain

実行時の動き

出力フォーマット

出力はBaseURLsごとにされます。結果は部屋ごとに、csvで出力されます。

f:id:makkynm:20210609152017p:plain

元物件データ

CSVファイルのデータはちょっとExcelで見ずらかったので、KNIMEを使って見せています。1行は部屋単位です。セルの結合とかはしていないので、複数部屋が空いている場合は、住所などの情報は重複した表示になります。

f:id:makkynm:20210609152035p:plain

出力CSVデータ

f:id:makkynm:20210609152049p:plain

出力CSVデータ続き

これらのデータは、もちろん前処理にかける必要があります。こちらの前処理については、KNIMEを使って次回解説してみようと思います。

 

  

スクレイピングコード解説

全体ワークフロー

下記のようにプログラムが実行されます。

f:id:makkynm:20210611220521p:plain

プログラムの構造

ラフですが、大まかなフローチャートは下記の通りです。

f:id:makkynm:20210611220957p:plain

全体ワークフロー フローチャート

 

各関数の役割

これらの関数は私が作ったものなので、全然無視していただいていいです。ただ意味が分かってた方が改修しやすいですよね。

  • PageNum_Easy:最大ページ数を取得して、全ページ数のURLを作成する関数です。BaseURLを引数として、全ページのURLのリストを返します。SUUMOのページ数の表示が「&page=」になっていることを前提にこの関数が使えます。
  • Parsedistrict:ページ内に表示されている建物の要素をリストで取得します。建物ごとにParseRoomDetailを呼び出します。返ってきた結果はRoomDetailsに格納します。
  • ParseRoomDetail:建物1つの要素を引数として持ち、建物の空き部屋情報を取得して返します。
  • Recursive_PageNum:使っていません。全ページのURLを再帰関数で取得するものです。時間がかかる上に、サーバーに負荷がかかるので使わなくていいと思います。
各モジュールの役割
#必要なライブラリをインポート
from bs4 import BeautifulSoup
import requests
import pandas as pd
import time
from urllib.parse import urljoin
  • BeautifulSoup:ウェブスクレイピングメインモジュール。htmlの構造解析用
  • requests:htmlを取得するため
  • pandascsvで出力するため
  • time:sleepを入れてサーバーへの負荷を減らすため
  • urljoin:相対URLを絶対URLに変換するため

 

検索元URLリスト -  BaseURLs

まずInputファイルに当たる部分です。本来Inputファイルで読むべきですが、今回は面倒だったので直接入力しました。

ここでは、検索元URLを指定します。SUUMOでは、検索条件がURLに現れてきます

BaseURLs = [
    #東京都
    "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=13",
    #神奈川県
    "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=14",
    #千葉県
    "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=12",
    #埼玉県
    "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=11"
]
SUUMOのURLの構造

SUUMOのURLの構造について詳しくみていきます。

例えば、

条件:12万円以下 15分以内 15年以内 2階以上/室内洗濯機置場/バス・トイレ別 管理費・共益費込み 鉄筋系/鉄骨系/ブロック・その他/ 東京都

の場合のURLは

https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=13

となります。

条件を一つ一つ変えると理解できるのですが、代表的なURLの意味は、

  • cb=0.0 : 0万円以上
  • ct=12.0 : 12万円以下
  • et=15 : 15分以内
  • cn=15:築15年以内
  • ta=13 :東京都(都道府県コード13)

 というような感じです。

このほか区や市単位になると、エリアコードが出てきます。例えば東京23区はsc=13101 ~ 13123で表されます。

 

今回は、都道府県ごとに結果を出したいので、ta=の部分のみを変えたURLを入れておきます。それぞれの都道府県コードは下記の通りです。

  • ta=11:埼玉県
  • ta=12:千葉県
  • ta=13:東京都
  • ta=14:神奈川県

 

メインプログラム - BaseURLsごとスクレイピング実行・出力

メインプログラムになります。BaseURLsごとに処理をしています。途中関数を2回呼び出して、最終的にそれらの結果をpandasを使用してcsvファイルで出力しています。

#Scraping Main
# RoomDetails = []  #BaseURLsのデータをまとまりで出したい場合
for iMcount, url in enumerate(BaseURLs):
    print("District Status: " + str(iMcount + 1) + "/" + str(len(BaseURLs)))
    All_PageFullURLs = []
    All_PageFullURLs = PageNum_Easy(url)
    RoomDetails = [] #データをBaseURLsごと出力
    RoomDetails = Parsedistrict(All_PageFullURLs, RoomDetails) #ページごとの物件情報取得 / Pageのループはこの中
    df = pd.DataFrame(RoomDetails, columns = HeaderNames)
    filename = "SUMMO_FullRoom_" + str(iMcount) + ".csv"
    df.to_csv(filename)
大枠ステータスの表示

まず、Printの部分でBaseURLsごとのステータスを出力しています。これにより、大枠の進捗状況が確認できます。今回で言うと、都道府県レベルでどれが終わったのかが分かりますね。

    print("District Status: " + str(iMcount + 1) + "/" + str(len(BaseURLs)))
全ページのURL取得 - PageNum_Easy関数の呼び出し

次に、PageNum_Easy関数を呼び出して、全ページのURLを取得しています。返り値は全URLのListになっています。

    All_PageFullURLs = PageNum_Easy(url)
スクレイピング結果の取得 - Parsedistrict関数の呼び出し

次に、取得した前URLをスクレイピング実行関数"Parsedistrict"に引き渡します。返り値は、スクレイピング結果になっています。

    RoomDetails = Parsedistrict(All_PageFullURLs, RoomDetails) #ページごとの物件情報取得
スクレイピング結果の出力 - pandas data frameにしてcsv出力

最後に取得した全データをListからPandasのDatafameに変換して、csv出力していきます。出力はBaseURLsごとに行われます。

    df = pd.DataFrame(RoomDetails, columns = HeaderNames)
    filename = "SUMMO_FullRoom_" + str(iMcount) + ".csv"
    df.to_csv(filename)

この時のHeaderNamesは下記のように、Main Programの前で取得しています。これは、どのページも部屋の情報が入っているテーブルの形は同じであるという前提で、BaseURLsの最初のページを代表値としてヘッダ情報を取得しています。

また、建物に紐づく情報などは、手入力で追加しています。

#Header Name
soup = BeautifulSoup(requests.get(BaseURLs[0]).content, "html.parser")
body = soup.find("body")
RoomtableHeadElem =  body.find("div",{'class':'cassetteitem'}).find("thead").find_all("th")
HeaderNames = [temp.get_text() for temp in RoomtableHeadElem]
HeaderNames.append("NewArrival")
HeaderNames.append("RoomDetailLink")
HeaderNames.extend(["マンション名", "住所", "最寄り駅", "築年数", "建物高さ","SearchURL"])
HeaderNames = [temp.replace("\xa0", str(i)) for i, temp in enumerate(HeaderNames)] #空白の場合行番号で置き換え

 

全ページURL取得関数 - PageNum_Easy関数

具体的に、各関数の中身について解説します。この関数は、SUUMOのページ数の表示が「&page=」になっていることを前提に作られています。

def PageNum_Easy(url):
    #データ取得
    result = requests.get(url)
    c = result.content
    #HTMLを元に、オブジェクトを作る
    soup = BeautifulSoup(c, "html.parser")
    #ページ数を取得
    body = soup.find("body")
    pages = body.find("div",{'class':'pagination pagination_set-nav'}) #Page数の部分のhtmlを抜き出す
    links = pages.select("a[href]") #link付きaタグを抜き出す
    #ページ選択で数値になっているものを引っ張ってくる("次へ"を除く / 1のみの場合は空リスト)
    nPages = [int(link.get_text()) for link in links if link.get_text().isdigit()]
    if len(nPages) == 0:
        PageFullURLs = [url]
    else:
        MaxPages = sorted(nPages)[-1] #Max page数
        PageFullURLs = []
        for ipg in range(MaxPages):
            base_URL = url + '&page=' + str(ipg+1)
            PageFullURLs.append(base_URL)
    return PageFullURLs
 Step1 - ページ数のHTML部抜き出し
  • BaseURLのHTMLを取得し、ページ数の部分を抜き出します。この時、リンクが有効のもののみを抜き出すので、1ページ目は抜き出されません。

    f:id:makkynm:20210612064923p:plain

    全ページURL取得

f:id:makkynm:20210612073718p:plain

全ページURL取得
Step2 - ページ数を数値に変換

「次へ」のリンクは邪魔なので、数値に変換できるもののみをリストに格納します。ここでのnPagesはページ数の数値のリストになります。

    #ページ選択で数値になっているものを引っ張ってくる("次へ"を除く / 1のみの場合は空リスト)
    nPages = [int(link.get_text()) for link in links if link.get_text().isdigit()]
Step3 - 最大ページ数を取得

リスト内の最大値を取得すれば、最大ページ数が得られます。ここで注意点として、1ページしかなかった場合空リストになるので、if文で条件分けしています。最大数が分かったら、あとはURLを「&page=」の規則で作るだけです。

    if len(nPages) == 0:
        PageFullURLs = [url]
    else:
        MaxPages = sorted(nPages)[-1] #Max page数
        PageFullURLs = []
        for ipg in range(MaxPages):
            base_URL = url + '&page=' + str(ipg+1)
            PageFullURLs.append(base_URL)

 

物件要素取得関数 - Parsedistrict関数

全ページURLリストを引数として、スクレイピング結果を返す関数です。建物ごとの実際のスクレイピングは次の関数ParseRoomDetailが担当しています。

ページごとにループを回しています。

def Parsedistrict(PageFullURLs, RoomDetails):
    for icount, url in enumerate(PageFullURLs):
        print("    Room Detail Status: " + str(icount + 1) + "/" + str(len(PageFullURLs)))
        try:
            #データ取得
            result = requests.get(url)
            c = result.content
            #HTMLを元に、オブジェクトを作る
            soup = BeautifulSoup(c, "html.parser")
            #物件リストの部分を切り出し
            summary = soup.find("div",{'id':'js-bukkenList'})
            #マンション名、住所、立地(最寄駅/徒歩~分)、築年数、建物高さが入っているcassetteitemを全て抜き出し - デフォルト設定で最大30件の物件表示
            cassetteitems = summary.find_all("div",{'class':'cassetteitem'})
            #マンションの数でループ
            for EstateElem in cassetteitems:
                RoomDetails.extend(ParseRoomDetail(EstateElem, url))
        except requests.exceptions.RequestException as e:
            print("エラー : ",e)
        time.sleep(5)
    print("total # of Rooms: " + str(len(RoomDetails)))
    return RoomDetails
Step1 - 建物ごとのHTML要素を取得

まずは建物の要素を取得します。cassetteitemsに、建物ごとの要素がリストで格納されています。SUUMOではデフォルトで最大30件の表示なので、このリストの大きさは30です。

            #データ取得
            result = requests.get(url)
            c = result.content
            #HTMLを元に、オブジェクトを作る
            soup = BeautifulSoup(c, "html.parser")
            #物件リストの部分を切り出し
            summary = soup.find("div",{'id':'js-bukkenList'})
            #マンション名、住所、立地(最寄駅/徒歩~分)、築年数、建物高さが入っているcassetteitemを全て抜き出し - デフォルト設定で最大30件の物件表示
            cassetteitems = summary.find_all("div",{'class':'cassetteitem'})
Step2 - スクレイピング結果を出力用リストに格納

次に、建物ごとでループを回してParseRoomDetail関数を呼び出します。ParseRoomDetail関数は、その建物の全部屋のスクレイピング結果をリストで返してくれるので、.extend()を使用して出力用リスト、RoomDetailsに格納していきます。

            #マンションの数でループ
            for EstateElem in cassetteitems:
                RoomDetails.extend(ParseRoomDetail(EstateElem, url))

 

建物・部屋情報スクレイピング関数 - ParseRoomDetail関数

建物ごとにスクレイピングを実行する関数です。建物のHTML要素を引数として、部屋ごとのスクレイピング結果をリストで返します。

def ParseRoomDetail(EstateElem, url):
    #マンション名取得
    EstateName = EstateElem.find("div",{'class':'cassetteitem_content-title'}).get_text()
    #住所取得
    EstateAddress = EstateElem.find("li",{'class':'cassetteitem_detail-col1'}).get_text()
    #最寄り駅取得
    EstateLocationElem = EstateElem.find("li",{'class':'cassetteitem_detail-col2'}).find_all("div",{'class':'cassetteitem_detail-text'})
    EstateLocations = [EstateLocation.get_text() for EstateLocation in EstateLocationElem] #リストで取得
    EstateLocation = ' --- '.join(EstateLocations)
    #築年数と建物高さを取得
    EstateCol3Elem = EstateElem.find("li",{'class':'cassetteitem_detail-col3'}).find_all("div")
    EstageAge = EstateCol3Elem[0].get_text()
    EstageHight = EstateCol3Elem[1].get_text()
    #Header Info をListに
    HeaderInfo = [EstateName, EstateAddress, EstateLocation, EstageAge, EstageHight, url]
    #階、賃料、管理費、敷/礼/保証/敷引,償却、間取り、専有面積が入っているtableを全て抜き出し
    RoomtableElem =  EstateElem.find("table",{'class':'cassetteitem_other'})
    RoomDetail = []
    #Contents
    for rooms in RoomtableElem.find_all("tbody"):
        Roomtable = [temp.get_text() for temp in rooms.findAll('td')]
        if "cassetteitem_other-checkbox--newarrival" in rooms.td['class']:
            Roomtable.append("New")
        else:
            Roomtable.append("Exsiting")
        Roomlinks = EstateElem.select("a[href]") #link付きaタグを抜き出す
        #"詳細を見る"の表示になっているリンクを引っ張ってくる
        RoomDetailURL = [link.get("href") for link in Roomlinks if link.get_text() == "詳細を見る"][0]
        RoomFullDetailURL = urljoin("https://suumo.jp/", RoomDetailURL) #相対パス -> 絶対パス
        Roomtable.append(RoomFullDetailURL)
        #Add Header Info
        Roomtable.extend(HeaderInfo)
        RoomDetail.append(Roomtable)
    return RoomDetail
Step1 - 建物単位の情報をスクレイピング

まず、建物単位の情報を抜き出してHeaderInfoにリストで格納します。マンション名や最寄り駅などですね。

マンション名

マンション名は{'class':'cassetteitem_content-title'}です。

 

f:id:makkynm:20210612072930p:plain

建物単位のスクレイピング - マンション名
    #マンション名取得
    EstateName = EstateElem.find("div",{'class':'cassetteitem_content-title'}).get_text()

 

住所

f:id:makkynm:20210612073204p:plain

建物単位のスクレイピング - 住所
    #住所取得
    EstateAddress = EstateElem.find("li",{'class':'cassetteitem_detail-col1'}).get_text()

 

最寄り駅

最寄り駅は3つで区切られているので、あとから処理しやすいように「---」を入れて1つの要素として格納します。

f:id:makkynm:20210612073245p:plain

建物単位のスクレイピング - 最寄り駅
    #最寄り駅取得
    EstateLocationElem = EstateElem.find("li",{'class':'cassetteitem_detail-col2'}).find_all("div",{'class':'cassetteitem_detail-text'})
    EstateLocations = [EstateLocation.get_text() for EstateLocation in EstateLocationElem] #リストで取得
    EstateLocation = ' --- '.join(EstateLocations)

 

築年数と建物高さ

築年数と建物高さは、確実に2つだったので、[0]と[1]でハードコードしています。

f:id:makkynm:20210612073352p:plain

建物単位のスクレイピング - 築年数と建物高さ
    #築年数と建物高さを取得
    EstateCol3Elem = EstateElem.find("li",{'class':'cassetteitem_detail-col3'}).find_all("div")
    EstageAge = EstateCol3Elem[0].get_text()
    EstageHight = EstateCol3Elem[1].get_text()

 

データの格納

最後に結果をリストに格納します。また、どの検索ページだったのかを記録するために最後に検索ページURLを格納しています。

    #Header Info をListに
    HeaderInfo = [EstateName, EstateAddress, EstateLocation, EstageAge, EstageHight, url]

 

Step2 - 部屋情報のTable要素を抜き出す

次に部屋単位の情報をスクレイピングしていきます。その前準備として、部屋情報の記載があるTableのHTML要素を抜き出します。

#階、賃料、管理費、敷/礼/保証/敷引,償却、間取り、専有面積が入っているtableを全て抜き出し RoomtableElem = EstateElem.find("table",{'class':'cassetteitem_other'})

f:id:makkynm:20210612073955p:plain

Step2 - 部屋情報のTable要素を抜き出す
Step3 - 部屋単位の情報をスクレイピング

部屋ごとの情報を抜き出します。1つの建物で複数空き部屋がある場合は、部屋の数だけループが回ることになります。

テーブルのテキスト情報は一括で抜き出してしまいます。

    RoomDetail = []
    #Contents
    for rooms in RoomtableElem.find_all("tbody"):
        Roomtable = [temp.get_text() for temp in rooms.findAll('td')]

 

Step3.1 - 新着かどうか

新着かどうかは、画像のクラスを見ると判断できます。

        if "cassetteitem_other-checkbox--newarrival" in rooms.td['class']:
            Roomtable.append("New")
        else:
            Roomtable.append("Exsiting")

f:id:makkynm:20210612074520p:plain

新着の場合

f:id:makkynm:20210612074540p:plain

既存物件の場合

 

Step3.2 - 部屋の詳細リンクを取得する

部屋の詳細リンクは下記のようにhrefを取ってくれば取得できます。このリンクは相対URLになっているので、あとから面倒にならないように絶対URLに変換してあげます。

        Roomlinks = EstateElem.select("a[href]") #link付きaタグを抜き出す
        #"詳細を見る"の表示になっているリンクを引っ張ってくる
        RoomDetailURL = [link.get("href") for link in Roomlinks if link.get_text() == "詳細を見る"][0]
        RoomFullDetailURL = urljoin("https://suumo.jp/", RoomDetailURL) #相対パス -> 絶対パス
        Roomtable.append(RoomFullDetailURL)

 

ソースコード(コピペ用)

最後に解説したソースコードを載せておきます。(GitHubにありますけどね) github.com

 

#必要なライブラリをインポート
from bs4 import BeautifulSoup
import requests
import pandas as pd
import time
from urllib.parse import urljoin

def PageNum_Easy(url):
    #データ取得
    result = requests.get(url)
    c = result.content
    #HTMLを元に、オブジェクトを作る
    soup = BeautifulSoup(c, "html.parser")
    #ページ数を取得
    body = soup.find("body")
    pages = body.find("div",{'class':'pagination pagination_set-nav'}) #Page数の部分のhtmlを抜き出す
    links = pages.select("a[href]") #link付きaタグを抜き出す
    #ページ選択で数値になっているものを引っ張ってくる("次へ"を除く / 1のみの場合は空リスト)
    nPages = [int(link.get_text()) for link in links if link.get_text().isdigit()]
    if len(nPages) == 0:
        PageFullURLs = [url]
    else:
        MaxPages = sorted(nPages)[-1] #Max page数
        PageFullURLs = []
        for ipg in range(MaxPages):
            base_URL = url + '&page=' + str(ipg+1)
            PageFullURLs.append(base_URL)
    return PageFullURLs

def ParseRoomDetail(EstateElem, url):
    #マンション名取得
    EstateName = EstateElem.find("div",{'class':'cassetteitem_content-title'}).get_text()
    #住所取得
    EstateAddress = EstateElem.find("li",{'class':'cassetteitem_detail-col1'}).get_text()
    #最寄り駅取得
    EstateLocationElem = EstateElem.find("li",{'class':'cassetteitem_detail-col2'}).find_all("div",{'class':'cassetteitem_detail-text'})
    EstateLocations = [EstateLocation.get_text() for EstateLocation in EstateLocationElem] #リストで取得
    EstateLocation = ' --- '.join(EstateLocations)
    #築年数と建物高さを取得
    EstateCol3Elem = EstateElem.find("li",{'class':'cassetteitem_detail-col3'}).find_all("div")
    EstageAge = EstateCol3Elem[0].get_text()
    EstageHight = EstateCol3Elem[1].get_text()
    #Header Info をListに
    HeaderInfo = [EstateName, EstateAddress, EstateLocation, EstageAge, EstageHight, url]
    #階、賃料、管理費、敷/礼/保証/敷引,償却、間取り、専有面積が入っているtableを全て抜き出し
    RoomtableElem =  EstateElem.find("table",{'class':'cassetteitem_other'})
    RoomDetail = []
    #Contents
    for rooms in RoomtableElem.find_all("tbody"):
        Roomtable = [temp.get_text() for temp in rooms.findAll('td')]
        if "cassetteitem_other-checkbox--newarrival" in rooms.td['class']:
            Roomtable.append("New")
        else:
            Roomtable.append("Exsiting")
        Roomlinks = EstateElem.select("a[href]") #link付きaタグを抜き出す
        #"詳細を見る"の表示になっているリンクを引っ張ってくる
        RoomDetailURL = [link.get("href") for link in Roomlinks if link.get_text() == "詳細を見る"][0]
        RoomFullDetailURL = urljoin("https://suumo.jp/", RoomDetailURL) #相対パス -> 絶対パス
        Roomtable.append(RoomFullDetailURL)
        #Add Header Info
        Roomtable.extend(HeaderInfo)
        RoomDetail.append(Roomtable)
    return RoomDetail

def Parsedistrict(PageFullURLs, RoomDetails):
    for icount, url in enumerate(PageFullURLs):
        print("    Room Detail Status: " + str(icount + 1) + "/" + str(len(PageFullURLs)))
        try:
            #データ取得
            result = requests.get(url)
            c = result.content
            #HTMLを元に、オブジェクトを作る
            soup = BeautifulSoup(c, "html.parser")
            #物件リストの部分を切り出し
            summary = soup.find("div",{'id':'js-bukkenList'})
            #マンション名、住所、立地(最寄駅/徒歩~分)、築年数、建物高さが入っているcassetteitemを全て抜き出し - デフォルト設定で最大30件の物件表示
            cassetteitems = summary.find_all("div",{'class':'cassetteitem'})
            #マンションの数でループ
            for EstateElem in cassetteitems:
                RoomDetails.extend(ParseRoomDetail(EstateElem, url))
        except requests.exceptions.RequestException as e:
            print("エラー : ",e)
        time.sleep(5)
    print("total # of Rooms: " + str(len(RoomDetails)))
    return RoomDetails

#検索1ページ目
#ta=が都道府県コード
#条件: 12万円以下 15分以内 15年以内 2階以上/室内洗濯機置場/バス・トイレ別 管理費・共益費込み 鉄筋系/鉄骨系/ブロック・その他
BaseURLs = [
    #東京都
    "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=13",
    #神奈川県
    "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=14",
    #千葉県
    "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=12",
    #埼玉県
    "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?url=%2Fchintai%2Fichiran%2FFR301FC001%2F&ar=030&bs=040&pc=30&smk=&po1=25&po2=99&co=1&kz=1&kz=2&kz=4&tc=0400101&tc=0400501&tc=0400301&shkr1=03&shkr2=03&shkr3=03&shkr4=03&cb=0.0&ct=12.0&et=15&mb=0&mt=9999999&cn=15&ta=11"
]

#Header Name
soup = BeautifulSoup(requests.get(BaseURLs[0]).content, "html.parser")
body = soup.find("body")
RoomtableHeadElem =  body.find("div",{'class':'cassetteitem'}).find("thead").find_all("th")
HeaderNames = [temp.get_text() for temp in RoomtableHeadElem]
HeaderNames.append("NewArrival")
HeaderNames.append("RoomDetailLink")
HeaderNames.extend(["マンション名", "住所", "最寄り駅", "築年数", "建物高さ","SearchURL"])
HeaderNames = [temp.replace("\xa0", str(i)) for i, temp in enumerate(HeaderNames)] #空白の場合行番号で置き換え

#Scraping Main
# RoomDetails = []  #BaseURLsのデータをまとまりで出したい場合
for iMcount, url in enumerate(BaseURLs):
    print("District Status: " + str(iMcount + 1) + "/" + str(len(BaseURLs)))
    All_PageFullURLs = []
#     All_PageFullURLs = Recursive_PageNum(All_PageFullURLs, url) #全てのページのURLを取得
    All_PageFullURLs = PageNum_Easy(url)
    RoomDetails = [] #データをBaseURLsごと出力
    RoomDetails = Parsedistrict(All_PageFullURLs, RoomDetails) #ページごとの物件情報取得 / Pageのループはこの中
    df = pd.DataFrame(RoomDetails, columns = HeaderNames)
    filename = "SUMMO_FullRoom_" + str(iMcount) + ".csv"
    df.to_csv(filename)

 

おわりに

お疲れ様でした。物件データって身近なビックデータなので解析しよう!!っていう気になりますよね。それが自分の住む場所を決めるかもしれないですし。ぜひご自身の物件探しや研究活動に使用いただければと思います。

普段私のブログではKNIMEというETLノーコードツールをメインに紹介しています。もし興味があればぜひ遊びにきてください。Twitterもやっているのでぜひフォローいただければと思います。ではまた!

twitter.com

degitalization.hatenablog.jp

 

参考リンク