はじめに
不動産業務や投資をもっと効率的・便利にするためにPythonを使った自動化やデータ取得のスキルを学んでいます。
今回はSUUMOの物件情報をPythonでスクレイピングし、CSV形式で出力するまでの過程をまとめました。
試行錯誤のやり取りも交えながら、不動産 × AI × Pythonがどのように現場で活かせるか、活かしていくにはどうしたらいいかを書いて行きたいと。
解決したかった課題
- 不動産情報を毎回手動で調べるのが手間(今回はSUMO)
- 気になる物件をコピペで集計する作業が非効率
- 定期的に更新される情報を、自動で取得・保存したい
使用技術・ライブラリ
- Python 3.11
- BeautifulSoup 4.12.3
- requests 2.31.0
- pandas 2.2.0
インストールはこちら:
pip install beautifulsoup4 requests pandas
実装の流れ
- SUUMOの物件一覧ページ(最大5ページ)を巡回
- 各物件の詳細ページへのリンクを取得
- 詳細ページから「価格」「所在地」「間取り」「面積」などを抽出
- pandasでDataFrame化し、CSVに出力
実際のコード(抜粋)
from bs4 import BeautifulSoup
import requests
import pandas as pd
results = []
for page in range(1, 6):
url = f"https://suumo.jp/jj/bukken/ichiran/JJ012FC001/?ar=050&bs=021&cn=9999999&cnb=0&ekTjCd=&ekTjNm=&et=20&hb=0&ht=9999999&kb=1&kki=102&kt=9999999&ohf=0&ra=050023&rn=3215&tb=0&tj=0&tt=9999999&po=0&pj=1&pc=100&page={page}"
response = requests.get(url)
soup = BeautifulSoup(response.text)
class_tags = soup.find_all("h2", class_="property_unit-title")
for class_tag in class_tags:
anchor_tag = class_tag.find("a")
if not anchor_tag:
continue
link = anchor_tag.get("href")
url_body = "https://suumo.jp" + link
response_body = requests.get(url_body)
soup_body = BeautifulSoup(response_body.text)
name_tag = soup_body.find("h1", class_=["fl", "w420", "mainIndexR"])
name = name_tag.text.strip() if name_tag else "物件名なし"
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "所在地" in th.get_text():
add_td = th.find_next_sibling("td")
address = add_td.get_text(strip=True)
break
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "価格" in th.get_text():
price_td = th.find_next_sibling("td")
price_text = price_td.text.strip().split("\n")[0].replace("\r", "")
break
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "間取り" in th.get_text():
layout_td = th.find_next_sibling("td")
break
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "建物面積" in th.get_text():
area_td = th.find_next_sibling("td")
break
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "土地面積" in th.get_text():
land_td = th.find_next_sibling("td")
break
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "完成時期(築年月)" in th.get_text():
age_td = th.find_next_sibling("td")
break
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "構造・工法" in th.get_text():
structure_td = th.find_next_sibling("td")
break
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "私道負担・道路" in th.get_text():
load = th.find_next_sibling("td")
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "交通"in th.get_text():
transport = th.find_next_sibling("td")
transport_text = transport.get_text(strip=True)
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "建ぺい率・容積率" in th.get_text():
BCR = th.find_next_sibling("td")
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "引渡可能時期" in th.get_text():
days = th.find_next_sibling("td")
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "用途地域" in th.get_text():
districts = th.find_next_sibling("td")
for th in soup_body.find_all("th", class_="w118 thSideLGray bdCell vat tal"):
if "その他制限事項" in th.get_text():
structure_td = th.find_next_sibling("td")
price_th_list = soup_body.find_all("th", class_="w120 thSideLGray bdCell vat tal")
for th in price_th_list:
if "その他概要・特記事項"in th.get_text():
Otherinformation_td = th.find_next("td")
Otherinformation_text = Otherinformation_td.get_text(strip=True)
results.append({
"物件名": name,
"所在地": address,
"価格": price_text,
"間取り": layout_td.get_text(strip=True),
"建物面積": area_td.get_text(strip=True),
"土地面積": land_td.get_text(strip=True),
"築年月": age_td.get_text(strip=True),
"構造・工法": structure_td.get_text(strip=True),
"私道負担・道路": load.get_text(strip=True),
"交通": transport.get_text(strip=True),
"建ぺい率・容積率": BCR.get_text(strip=True),
"引渡可能時期": days.get_text(strip=True),
"用途地域": districts.get_text(strip=True),
"その他概要・特記事項": Otherinformation_text
})
df = pd.DataFrame(results)
df.to_csv("suumo_result.csv", index=False, encoding="utf-8-sig")
実行結果(CSV出力)
出力されたCSVファイルはこのような形式になります:
物件名 | 所在地 | 価格 | 間取り | 建物面積 | 土地面積 | 築年月 |
---|---|---|---|---|---|---|
○○邸 | 愛知県名古屋市中川区 | 2,980万円 | 4LDK | 96.88㎡ | 120.35㎡ | 2020年1月 |
試行錯誤と学び
実装中はエラーやデータが取得できない・違うデータが出るなどのトラブルに何度も何度も直面しました。
つまずいた点とその中で学んだことを一部紹介します:
-
df = pd.DataFrame(sumo_data)
のように書いていたが、辞書1件しか保存されず1行しか出力されない
→ .append()でリスト化してからまとめてDataFrameに渡す方式に修正。
学び:スクレイピング結果はまずリストで貯める! -
全角スペース問題やコピペによる文字化け
コピーしたコードに全角スペースや改行混入がありどこがエラーになっているかわからずハマった。
学び:コードエラー時は空白や全角文字も疑え! -
<th>
と<td>
のペア構造を見落としていた
情報がすべて<th>
にラベル、<td>
に値という形式で並んでいるのに気づかず、個別にクラス名で探していた。
→.find_all("th")
でラベルを探し、.find_next_sibling("td")
で対応する値を取るという構造化が鍵でした。
学び:表形式の構造は"ペア"で取得せよ!
応用・発展アイデア
- 毎朝このスクリプトを実行し、最新物件を自動収集
- SlackやLINEに通知するように連携
- 検索条件を変えて地域や価格帯を比較分析
- Webアプリやダッシュボードにして社内活用
まとめ
不動産情報の取得・管理は、まだまだ手作業の領域が多く残っています。
PythonとAIの力を借りることで現場の効率化や意思決定のスピードを格段に上げることが可能です。
今回の取り組みは、「不動産 × AI × Python」の可能性を感じた第一歩でした。
自身のスキルを上げて今後もっとやれることを増やして通知機能や可視化までつなげていきたいと思います。