【スクレイピング】SUUMOのサイトから物件情報を取得する~スクレイピング実装~

## はじめに
※このプログラムを作成した目的はスクレイピングのスキル獲得のためです。また、suumoの利用規約によって商用利用が禁止されているので絶対に悪用しないでください。


コード全体の記述量が多いため二部構成となっております。本記事はスクレイピング機能の実装についてのまとめですので、使用するライブラリとTkinterの実装については以下の記事をご参照ください。

https://qiita.com/kaich0prog17/private/e80d5c672206939a0a34

なお、環境やライブラリのインストールについては前回の記事に記載しているため、本記事では省略させていただきます。

## スクレイピング部分の実装
### UIに入力した情報を取得する
前回のTkinter実装と同様に、for文による繰り返し処理を実現できなかったため物件の条件受け取り部分がとても長くなっています。ご容赦ください。
まず、def でtkinterのイベントに連動して物件情報を取得する関数を定義します。
TkinterでURLを貼り付ける工程がありましたが、それを
```
suumo_url = url_entry.get()
driver.get(suumo_url)
```
で受け取って開くようにします。
```
def reload_suumo_search(event):
    ##tkinterと連動
    #URL取得
    suumo_url = url_entry.get()
    print(suumo_url)
    driver.get(suumo_url)
    time.sleep(5)

    #ページ数
    count_page_number = int(page_entry.get())
```
ここから、URLを開いた後の表示順序や条件入力を自動化していきます。
スーモでは新着順、おすすめ順などで物件の表示を並べ替える機能があります。UIの「並べ替え」項目で用意したラジオボタンを0~6の数字に置き替えましたが、ここでどのボタンを選んだのかを受け取る仕組みになっています。
間取りも基本的に同様の仕組みですが、こちらは複数選択が可能なチェックボタンで作成しているのでチェックの有無は"True"、"False"で判定します。
最後に、ページ上で「検索」をクリックする処理を行い、指定した条件と表示順を適用させます。

```
    #並べ替え
    if(radio_value.get() == 0):
        sort_change = driver.find_element_by_css_selector('#js-sortbox-sortPulldownSingle')
        sort_change.click()
        time.sleep(2)
        sort_recommend = driver.find_element_by_css_selector('#opt1_25')
        sort_recommend.click()
        time.sleep(2)

    if(radio_value.get() == 1):
        sort_change = driver.find_element_by_css_selector('#js-sortbox-sortPulldownSingle')
        sort_change.click()
        time.sleep(2)
        sort_lowest = driver.find_element_by_css_selector('#opt1_12')
        sort_lowest.click()
        time.sleep(2)

    if(radio_value.get() == 2):
        sort_change = driver.find_element_by_css_selector('#js-sortbox-sortPulldownSingle')
        sort_change.click()
        time.sleep(2)
        sort_highest = driver.find_element_by_css_selector('#opt1_15')
        sort_highest.click()
        time.sleep(2)

    if(radio_value.get() == 3):
        sort_change = driver.find_element_by_css_selector('#js-sortbox-sortPulldownSingle')
        sort_change.click()
        time.sleep(2)
        sort_newest = driver.find_element_by_css_selector('#opt1_09')
        sort_newest.click()
        time.sleep(2)

    if(radio_value.get() == 4):
        sort_change = driver.find_element_by_css_selector('#js-sortbox-sortPulldownSingle')
        sort_change.click()
        time.sleep(2)
        sort_year = driver.find_element_by_css_selector('#opt1_04')
        sort_year.click()
        time.sleep(2)

    if(radio_value.get() == 5):
        sort_change = driver.find_element_by_css_selector('#js-sortbox-sortPulldownSingle')
        sort_change.click()
        time.sleep(2)
        sort_occupied = driver.find_element_by_css_selector('#opt1_16')
        sort_occupied.click()
        time.sleep(2)

    if(radio_value.get() == 6):
        sort_change = driver.find_element_by_css_selector('#js-sortbox-sortPulldownSingle')
        sort_change.click()
        time.sleep(2)
        sort_address = driver.find_element_by_css_selector('#opt1_17')
        sort_address.click()
        time.sleep(2)

    #間取り
    if(boolean_0.get() == True):
        check_1R = driver.find_element_by_css_selector('#md0')
        check_1R.click()
    if(boolean_1.get() == True):
        check_1K = driver.find_element_by_css_selector('#md1')
        check_1K.click()
    if(boolean_2.get() == True):
        check_1DK = driver.find_element_by_css_selector('#md2')
        check_1DK.click()
    if(boolean_3.get() == True):
        check_1LDK = driver.find_element_by_css_selector('#md3')
        check_1LDK.click()
    if(boolean_4.get() == True):
        check_2K = driver.find_element_by_css_selector('#md4')
        check_2K.click()
    if(boolean_5.get() == True):
        check_2DK = driver.find_element_by_css_selector('#md5')
        check_2DK.click()
    if(boolean_6.get() == True):
        check_2LDK = driver.find_element_by_css_selector('#md6')
        check_2LDK.click()
    if(boolean_7.get() == True):
        check_3K = driver.find_element_by_css_selector('#md7')
        check_3K.click()
    if(boolean_8.get() == True):
        check_3DK = driver.find_element_by_css_selector('#md8')
        check_3DK.click()
    if(boolean_9.get() == True):
        check_3LDK = driver.find_element_by_css_selector('#md9')
        check_3LDK.click()
    if(boolean_10.get() == True):
        check_4K = driver.find_element_by_css_selector('#md10')
        check_4K.click()
    if(boolean_11.get() == True):
        check_4DK = driver.find_element_by_css_selector('#md11')
        check_4DK.click()
    if(boolean_12.get() == True):
        check_4LDK = driver.find_element_by_css_selector('#md12')
        check_4LDK.click()
    if(boolean_13.get() == True):
        check_over5K = driver.find_element_by_css_selector('#md13')
        check_over5K.click()
```

さて、次はUIで入力した数値を受け取る部分です。これらにはそれぞれに下限と上限を入力する項目を設けていますが、物件を探す際に必ずしも両方を指定するとは限りません。よって、未入力をプログラムが検出したとき、下限ならば"0"、上限の場合には大きな数字(例えば"9999999")を代入するif文を作りました。


```

    #築年数
    min_property_year = min_year_box.get()
    if(min_property_year == '新築'):
        min_property_year = 0
    elif min_property_year == '':
        min_property_year = 0
    else:
        min_property_year = int(min_property_year)
    max_property_year = int(max_year_box.get())
    if max_property_year == '':
        max_property_year = 9999

    #家賃+管理費
    min_total_price = int(min_price_box.get())
    if min_total_price == '':
        min_total_price = 0
    max_total_price = int(max_price_box.get())
    if max_total_price == '':
        max_total_price = 99999999

    #敷金
    min_deposit_price = int(min_deposit_box.get())
    if min_deposit_price == '':
        min_deposit_price = 0
    max_deposit_price = int(max_deposit_box.get())
    if max_deposit_price == '':
        max_deposit_price = 99999999
    #礼金
    min_key_money = int(min_key_box.get())
    if min_key_money == '':
        min_key_money = 0
    max_key_money = int(max_key_box.get())
    if max_key_money == '':
        max_key_money = 99999999

    #'検索'をクリックして画面を更新
    searchbutton = driver.find_element_by_css_selector('#js-conditionbox > div.conditionbox-body > div > dl:nth-child(3) > dd > div > div > ul > li:nth-child(2) > a')
    searchbutton.click()
    get_url = driver.current_url
    driver.get(get_url)
    driver.set_window_size(940,1100)
    time.sleep(5)
```
##
### 検索条件確定後の動作
ページ更新後に表示された物件の中から、条件と一致するものを取得するコードを実装していきます。なお、ここからは各ページに表示される物件数が30件であることを前提として解説したいと思います。その理由は、情報(要素)の取得をcssセレクタの数字部分の可変によって行っているためです。classタグなど他の取得方法も今後検討したいと思います。

以下のコードでは
```
for properties in driver.find_elements_by_class_name('cassetteitem'):
```
で物件を一つずつ切り分け、それぞれの情報を取得するものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2597913/b6c079ac-0a10-8a1a-aad1-bdafa0522a05.png)

画像のような形、つまり1つの家屋を「物件」の最小単位と考えます。表示されている部屋の一覧のうち、一番下のものを-1個目としてあらかじめ取得しておき上から順番に取得を繰り返し、最後の部屋情報(＝-1個目とした部屋)と一致すると次の物件の取得に移行します。
先ほど「要素の大部分をcss_selectorで抽出している」と述べましたが、サイトのHTMLの規則的な構造を利用して問題なく取得できるようにしています。for文の繰り返し処理で次の物件へ移行する際、CSSセレクタの数字部分に変更を加えることで実現しています。

```
    #number指定
    property_first_number = 2
    property_second_number = 1
    room_detail_number = 2
    next_page_number = 3 #次ページ部分のCSSの数字！

    for i in range(count_page_number):
        ##物件ごとに取得## 物件数は各ページ30件を想定
        for properties in driver.find_elements_by_class_name('cassetteitem'):
            for room_details in properties.find_elements_by_class_name('js-cassette_link'):

                ##外観
                pre_exterior = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child('+ str(property_first_number) +') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-detail > div.cassetteitem-detail-object > div > div > img')
                exterior = pre_exterior.get_attribute('src')

                ##物件名
                property_name = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child('+ str(property_first_number) +') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-detail > div.cassetteitem-detail-body > div > div.cassetteitem_content-title').text
                time.sleep(1)

                ##住所
                property_address = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child('+ str(property_first_number) +') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-detail > div.cassetteitem-detail-body > div > div.cassetteitem_content-body > ul > li.cassetteitem_detail-col1').text
                time.sleep(1)

                ##最寄り駅
                for nearests in driver.find_elements_by_css_selector('#js-bukkenList > ul:nth-child('+ str(property_first_number) +')'): #property_nearest_numberは1ページ30まで！
                    property_nearests = nearests.find_element_by_css_selector(' li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-detail > div.cassetteitem-detail-body > div > div.cassetteitem_content-body > ul > li.cassetteitem_detail-col2')
                    property_nearest1 = property_nearests.find_elements_by_class_name('cassetteitem_detail-text')[0].text
                    property_nearest2 = property_nearests.find_elements_by_class_name('cassetteitem_detail-text')[1].text
                    property_nearest3 = property_nearests.find_elements_by_class_name('cassetteitem_detail-text')[2].text
                    time.sleep(1)
                    break

                ##築年数と何階建か
                property_years = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child('+ str(property_first_number) +') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-detail > div.cassetteitem-detail-body > div > div.cassetteitem_content-body > ul > li.cassetteitem_detail-col3')
                property_year = property_years.find_element_by_css_selector(' div:nth-child(1)').text
                property_floor = property_years.find_element_by_css_selector(' div:nth-child(2)').text
                    #築年数と条件の判定
                if(property_year == '新築'):
                    property_year = '0年'


                ##間取りの写真
                room_photo = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td:nth-child(2) > div > img')
                room_photo_url = room_photo.get_attribute('src')

                ##空き部屋の階
                room_f = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr')
                room_floor = room_f.find_element_by_css_selector('td:nth-child(3)').text

                ##賃料+管理費
                room_price = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td:nth-child(4) > ul > li:nth-child(1) > span > span').text
                room_manage_price = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td:nth-child(4) > ul > li:nth-child(2) > span').text
                if(room_manage_price == '-'):
                    room_manage_price = '0円'
                else:
                    room_manage_price = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td:nth-child(4) > ul > li:nth-child(2) > span').text

                ##敷金・礼金
                deposit_price = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td:nth-child(5) > ul > li:nth-child(1) > span').text
                key_money = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td:nth-child(5) > ul > li:nth-child(2) > span').text

                ##間取り・面積
                floor_plan = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td:nth-child(6) > ul > li:nth-child(1) > span').text
                occupied_area = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td:nth-child(6) > ul > li:nth-child(2) > span').text

                ##リンク
                pre_detail_url = driver.find_element_by_css_selector('#js-bukkenList > ul:nth-child(' + str(property_first_number) + ') > li:nth-child(' + str(property_second_number) + ') > div > div.cassetteitem-item > table > tbody:nth-child(' + str(room_detail_number) + ') > tr > td.ui-text--midium.ui-text--bold > a')
                detail_url = pre_detail_url.get_attribute('href')

                time.sleep(2)
                ###取得する情報はここより上に記述
                room_detail_number += 1


                ##条件
                replace_property_year = int(re.findall(r"\d+", property_year)[0])
                print('築年数:',replace_property_year)
                if(min_property_year <= replace_property_year <= max_property_year):
                    property_year = property_year

                    replace_room_price = float(room_price.replace('万円',''))
                    replace_manage_price = int(re.findall(r"\d+", room_manage_price)[0])
                    total_price = int((replace_room_price*10000) + replace_manage_price)
                    print('家賃:',replace_room_price*10000)
                    print('管理費:',replace_manage_price)
                    print('家賃+管理費: ',total_price)
                    if(min_total_price <= total_price <= max_total_price):
                        #条件と照合
                        if(deposit_price == '-'):
                            deposit_price = '0円'
                            replace_deposit_price = int(deposit_price.replace('円','')) #敷金
                        else:
                            replace_deposit_price = float(deposit_price.replace('万円',''))*10000
                        print('敷金:',replace_deposit_price)
                        if(min_deposit_price <= replace_deposit_price <= max_deposit_price):
                            if(key_money == '-'):
                                key_money = '0円'
                                replace_key_money = int(key_money.replace('円','')) #礼金
                            else:
                                replace_key_money = float(key_money.replace('万円',''))*10000
                            print('礼金:',replace_key_money)
                            if(min_key_money <= replace_key_money <= max_key_money):

                                #出力前の調整
                                if(property_year == '0年'):
                                    property_year = '新築'

                                ###Discordに出力###
                                discord = Discord(url=Discord webbookのURL)
                                #間取りの写真
                                discord.post(content = exterior + '\n' + room_photo_url + '\n')
                                time.sleep(4)
                                #物件名・住所+最寄り駅+築年数・何階建か+物件のフロア+階・家賃その他+間取り・専有面積
                                discord.post(content = '物件名:' + property_name + '\n' + '住所:' + property_address + '\n' + '最寄り駅:' + '\n' + '・' + property_nearest1 + '\n' + '・' +  property_nearest2 + '\n' + '・' + property_nearest3 + '\n' + '築年数:' + property_year + ' / ' + '階:' +property_floor + '\n' + 'この物件のフロア:' + room_floor  + '\n' + '賃料:' + room_price + ' / ' + '管理費:' + room_manage_price + '\n' + '敷金:'+ deposit_price + ' / ' + '礼金:'+ key_money + '\n' + '\n' + '間取り:' + floor_plan +'/' + '/' + '専有面積:' + occupied_area)
                                time.sleep(1.5)
                                #詳細リンク
                                discord.post(content = '詳細情報(URL):'+ '\n' + detail_url)
                                time.sleep(5)

                            else:
                                print('礼金:条件に一致しませんでした')
                        else:
                            print('敷金:条件に一致しませんでした')
                    else:
                        print('家賃等:条件に一致しませんでした')
                else:
                    print('築年数:条件に一致しませんでした')
                

                #物件1つごとの-1個目の情報を取得、一致すると次の物件へ移行
                last_room_floor = properties.find_elements_by_class_name('js-cassette_link')[-1]
                if(room_f == last_room_floor):
                    print('物件の空き件数を取得し終えたためbreak')

```

繰り返しの発生に合わせて
```
property_second_number
property_first_number
```
の2つの変数の値を変更すると、find_element_by_css_selectorで指定する要素が次の物件で抽出する箇所に対応する仕組みとなっているのです。

```


                    #次の物件情報へ移行する際にproperty_second_numberを5まで+1更新、5になるとproperty_first_numberを+2させて次のブロックへ
                    property_second_number += 1 #1件ごとに+1
                    room_detail_number = 2 #property_second_numberが6になるまで
                    if(property_second_number == 6):
                        discord = Discord(url="")
                        discord.post(content = '次のブロックの情報を取得します')
                        property_second_number = 1
                        property_first_number += 2 #5件ごとに+2
                        time.sleep(1)
                        if(property_first_number == 14):
                            print('1ページの物件ブロックを超過しました') #ページ遷移
                            next_page = driver.find_element_by_css_selector('#js-leftColumnForm > div.pagination_set > div.pagination.pagination_set-nav > ol >li:nth-child('+ str(next_page_number) +') > a')
                            next_page_number += 2
                            next_page.click()
                            if(next_page) is None:
                                print('前ページの取得が終了しました')
                                break
                        time.sleep(5)
                        break
```

## おわりに
以上がスーモで物件情報を自動で取得するコードになります。
今回がQiitaへの初投稿なのですが、コードの量が多かったこともありうまく解説できているか不安です…もしご不明な点がございましたらぜひコメントにてお尋ねください。ご覧いただきありがとうございました！