suumoの賃貸相場のデータをまとめるプログラムを組んでみた(Pythonでスクレイピングするコード)

この記事でわかること

suumoで掲載されている賃貸データを根こそぎ収集できるコードが分かる

私がやったこと

pythonをパソコンにインストール

必要なpythonライブラリをインポート

ChatGPTの推論モードを使って、やりたいことをひたすら指示。なぜならプログラミング未経験だから。

コード

import os
import sys
import subprocess
import requests
from bs4 import BeautifulSoup
import pandas as pd
import logging
from retry import retry
from datetime import datetime, timedelta
import concurrent.futures
import re
import urllib.parse
import threading
import tkinter as tk
from tkinter import simpledialog, messagebox

# -------------------------------------
# ログ出力の基本設定
# -------------------------------------
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# -------------------------------------
# HTTPリクエスト用の共通ヘッダ
# -------------------------------------
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/91.0.4472.124 Safari/537.36"
    )
}

# -------------------------------------
# 進捗状況管理用のグローバル変数
# -------------------------------------
progress_data = {
    "pages_completed": 0,
    "total_pages": 0,
    "start_time": None,
    "finished": False
}

# -------------------------------------
# 補助関数:ローカルファイルをOS標準のアプリで開く
# -------------------------------------
def open_file(filepath):
    """
    渡されたファイルパスのファイルをOS標準の方法で開く。
    Windows, macOS, Linuxに対応。
    """
    try:
        if sys.platform.startswith("win"):
            os.startfile(filepath)
        elif sys.platform.startswith("darwin"):
            subprocess.call(["open", filepath])
        else:
            subprocess.call(["xdg-open", filepath])
    except Exception as e:
        logging.error(f"ファイルオープン時のエラー: {e}", exc_info=True)

# -------------------------------------
# 補助関数:タイムデルタを HH:MM:SS 形式に変換
# -------------------------------------
def format_timedelta(td):
    total_seconds = int(td.total_seconds())
    hours = total_seconds // 3600
    minutes = (total_seconds % 3600) // 60
    seconds = total_seconds % 60
    return f"{hours:02}:{minutes:02}:{seconds:02}"

# -------------------------------------
# HTML取得(リトライ付き)
# -------------------------------------
@retry(tries=1, delay=1, backoff=2)
def get_html(url):
    """
    指定URLへHTTPリクエストを行い、HTMLをBeautifulSoupで返す。
    失敗時はリトライを実施。
    """
    try:
        response = requests.get(url, headers=HEADERS, timeout=10)
        response.raise_for_status()
        return BeautifulSoup(response.content, "html.parser")
    except requests.exceptions.RequestException as e:
        logging.error(f"リクエストエラー: {e}", exc_info=True)
        return None

# -------------------------------------
# 最大ページ数の取得
# -------------------------------------
def get_max_pages(url):
    """
    SUUMOの検索結果ページから最大ページ数を抽出する。
    ページネーションがなければ1を返す。
    """
    soup = get_html(url)
    if not soup:
        return 1

    pagination = soup.find("div", class_="pagination")
    if not pagination:
        logging.info("ページネーションが見つからないため、1ページのみ処理")
        return 1

    try:
        pages = [int(link.get_text()) for link in pagination.find_all("a") if link.get_text().isdigit()]
        max_page = max(pages) if pages else 1
        logging.info(f"取得した最大ページ数: {max_page}")
        return max_page
    except Exception as e:
        logging.error(f"ページ数抽出エラー: {e}", exc_info=True)
        return 1

# -------------------------------------
# HTML要素からテキスト抽出(存在しない場合はデフォルト値)
# -------------------------------------
def extract_text_or_default(element, default="データなし"):
    return element.get_text(strip=True) if element else default

# -------------------------------------
# 物件基礎情報の抽出
# -------------------------------------
def extract_property_data_base(item):
    """
    建物情報の基礎部分を抽出する(名称、住所、築年数、階建、構造、カテゴリ、詳細URL)。
    GoogleMapリンクも生成。
    """
    age, floor = extract_age_and_floor(item)
    base_data = {
        "名称": extract_building_name(item),
        "アドレス": extract_address(item),
        "築年数": age,
        "階建": floor,
        "構造": extract_structure(item),
        "カテゴリ": extract_category(item),
        "詳細URL": extract_detail_link(item),
    }
    base_data["GoogleMapリンク"] = generate_google_map_link(base_data["アドレス"])
    return base_data

def extract_age_and_floor(item):
    """
    築年数と階数情報を抽出。新築の場合は築年数0とする。
    """
    details = item.find("li", class_="cassetteitem_detail-col3")
    age = None
    floor = "データなし"

    if details:
        divs = details.find_all("div")
        if divs:
            age_text = extract_text_or_default(divs[0])
            if "新築" in age_text:
                age = 0
            else:
                match = re.search(r'(\d+)', age_text)
                if match:
                    age = int(match.group(1))
        if len(divs) > 1:
            floor = extract_text_or_default(divs[1])
    return age, floor

def extract_building_name(item):
    """ 建物名の抽出 """
    return extract_text_or_default(item.find("div", class_="cassetteitem_content-title"))

def extract_address(item):
    """ 住所の抽出 """
    return extract_text_or_default(item.find("li", class_="cassetteitem_detail-col1"))

def extract_structure(item):
    """ 構造情報の抽出(例:RCなど) """
    details = item.find("li", class_="cassetteitem_detail-col3")
    if details:
        divs = details.find_all("div")
        if len(divs) > 1:
            return extract_text_or_default(divs[1])
    return "データなし"

def extract_category(item):
    """ 物件カテゴリの抽出(マンション、一戸建てなど) """
    category = item.find("div", class_="cassetteitem_content-label")
    return extract_text_or_default(category)

def extract_detail_link(item):
    """ 詳細ページへのリンク抽出(完全なURLに整形) """
    link = item.find("a", class_="js-cassette_link_href")
    if link and link.get("href"):
        return "https://suumo.jp" + link.get("href")
    return "データなし"

# -------------------------------------
# 交通アクセス情報の抽出
# -------------------------------------
def parse_minimum_access(access_container):
    """
    交通アクセス情報を解析し、徒歩、バス、車の情報から最も優先度の高い情報を返す。
    """
    access_items = access_container.find_all("div", class_="cassetteitem_detail-text")
    if not access_items:
        return None

    walking_segments = []
    bus_segments = []
    car_segments = []

    for item in access_items:
        text = item.get_text(" ", strip=True)
        segments = re.findall(r'([^ ]+)\s+(歩|バス|車)(\d+)分', text)
        for seg in segments:
            time_val = int(seg[2])
            station_name = seg[0]
            mode = seg[1]
            if mode == "歩":
                walking_segments.append((time_val, station_name, "徒歩"))
            elif mode == "バス":
                bus_segments.append((time_val, station_name, "バス"))
            elif mode == "車":
                car_segments.append((time_val, station_name, "車"))

    station_walking = [seg for seg in walking_segments if seg[1].endswith("駅") and "(バス停)" not in seg[1]]
    if station_walking:
        chosen = min(station_walking, key=lambda x: x[0])
        return {"駅": chosen[1], "移動手段": chosen[2], "所要時間(分)": chosen[0]}
    if bus_segments:
        chosen = min(bus_segments, key=lambda x: x[0])
        return {"駅": chosen[1], "移動手段": chosen[2], "所要時間(分)": chosen[0]}
    if car_segments:
        chosen = min(car_segments, key=lambda x: x[0])
        return {"駅": chosen[1], "移動手段": chosen[2], "所要時間(分)": chosen[0]}
    if walking_segments:
        chosen = min(walking_segments, key=lambda x: x[0])
        return {"駅": chosen[1], "移動手段": chosen[2], "所要時間(分)": chosen[0]}
    return None

# -------------------------------------
# 部屋情報の抽出
# -------------------------------------
def extract_room_data(tbody, base_data):
    """
    部屋情報(家賃、管理費、間取り、面積など)を抽出し、基礎データに統合する。
    """
    try:
        data = base_data.copy()
        td_elements = tbody.find_all("td")
        if len(td_elements) < 6:
            return data

        # 階数
        data["階数"] = extract_text_or_default(td_elements[2])

        # 家賃/管理費
        rent_li = td_elements[3].find_all("li")
        rent = 0
        mgmt = 0
        if rent_li and len(rent_li) > 0:
            rent_text = extract_text_or_default(rent_li[0]).replace("万円", "").replace(",", "")
            try:
                rent = float(rent_text) if rent_text else 0
            except Exception:
                rent = 0
        if rent_li and len(rent_li) > 1:
            mgmt_text = extract_text_or_default(rent_li[1]).replace("円", "").replace(",", "")
            try:
                mgmt = float(mgmt_text) / 10000 if mgmt_text and mgmt_text.isdigit() else 0
            except Exception:
                mgmt = 0

        # 敷金/礼金
        deposit_li = td_elements[4].find_all("li")
        deposit = 0
        key_money = 0
        if deposit_li and len(deposit_li) > 0:
            deposit_text = extract_text_or_default(deposit_li[0]).replace("万円", "").replace(",", "")
            try:
                deposit = float(deposit_text) if deposit_text and deposit_text != "-" else 0
            except Exception:
                deposit = 0
        if deposit_li and len(deposit_li) > 1:
            key_money_text = extract_text_or_default(deposit_li[1]).replace("万円", "").replace(",", "")
            try:
                key_money = float(key_money_text) if key_money_text and key_money_text != "-" else 0
            except Exception:
                key_money = 0

        # 間取り/専有面積
        layout_li = td_elements[5].find_all("li")
        layout = extract_text_or_default(layout_li[0]) if layout_li and len(layout_li) > 0 else "データなし"
        area = 0
        if layout_li and len(layout_li) > 1:
            area_text = extract_text_or_default(layout_li[1])
            area_text = re.sub(r"[^\d\.]", "", area_text)
            if area_text.endswith("2"):
                area_text = area_text[:-1]
            try:
                area = float(area_text) if area_text else 0
            except Exception:
                area = 0

        data.update({
            "家賃(万円)": rent,
            "管理費(万円)": mgmt,
            "敷金(万円)": deposit,
            "礼金(万円)": key_money,
            "間取り": layout,
            "面積(㎡)": area
        })
        data["家賃/面積(円/㎡)"] = int(rent * 10000 / area) if area > 0 else 0

        # 交通アクセス情報の抽出
        parent_item = tbody.find_parent("div", class_="cassetteitem")
        access_li = parent_item.find("li", class_="cassetteitem_detail-col2")
        if access_li:
            access_info = parse_minimum_access(access_li)
            if access_info:
                data["駅"] = access_info["駅"]
                data["移動手段"] = access_info["移動手段"]
                data["所要時間(分)"] = access_info["所要時間(分)"]
            else:
                data["駅"] = "データなし"
                data["移動手段"] = "データなし"
                data["所要時間(分)"] = "データなし"
        else:
            data["駅"] = "データなし"
            data["移動手段"] = "データなし"
            data["所要時間(分)"] = "データなし"

        return data
    except Exception as e:
        logging.error(f"部屋データ抽出エラー: {e}", exc_info=True)
        return base_data

# -------------------------------------
# 詳細ページ情報の抽出(現在は機能停止中)
# -------------------------------------
def extract_detail_page_info(detail_url):
    """
    詳細ページの情報抽出は一時停止中のため、空の辞書を返す。
    """
    return {}

# -------------------------------------
# Googleマップ検索用リンクの生成
# -------------------------------------
def generate_google_map_link(address):
    if address == "データなし":
        return "データなし"
    query = urllib.parse.quote(address)
    return f"https://www.google.com/maps/search/?api=1&query={query}"

# -------------------------------------
# 重複行の削除
# -------------------------------------
def remove_duplicates(df):
    df_ikkodate = df[df["カテゴリ"] == "一戸建て"]
    df_others = df[df["カテゴリ"] != "一戸建て"]

    df_ikkodate = df_ikkodate.drop_duplicates(subset=["名称", "アドレス"])
    df_others = df_others.drop_duplicates(subset=["名称", "アドレス", "間取り", "面積(㎡)", "家賃(万円)"])

    return pd.concat([df_ikkodate, df_others], ignore_index=True)

# -------------------------------------
# 指定ページの処理
# -------------------------------------
def process_page(url, page, fetch_detail):
    """
    指定ページのSUUMO物件情報をスクレイピングし、基礎情報および部屋情報を返す。
    fetch_detail が True の場合は詳細ページの情報も取得するが、現在は停止中。
    """
    page_url = f"{url}&page={page}"
    logging.info(f"ページ {page} を処理中: {page_url}")

    soup = get_html(page_url)
    if not soup:
        logging.warning(f"ページ {page} の取得に失敗")
        return []

    page_data = []
    items = soup.find_all("div", class_="cassetteitem")
    properties = []

    for item in items:
        base_data = extract_property_data_base(item)
        properties.append((base_data, item))

    if fetch_detail:
        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
            future_to_prop = {}
            for base_data, _ in properties:
                if base_data["詳細URL"] != "データなし":
                    future = executor.submit(extract_detail_page_info, base_data["詳細URL"])
                    future_to_prop[future] = base_data
            for future in concurrent.futures.as_completed(future_to_prop):
                base_data = future_to_prop[future]
                try:
                    detail_info = future.result()
                    base_data.update(detail_info)
                except Exception as e:
                    logging.error(f"詳細ページ処理エラー: {e}", exc_info=True)

    for base_data, item in properties:
        table = item.find("table", class_="cassetteitem_other")
        if table:
            tbodys = table.find_all("tbody")
            for tbody in tbodys:
                page_data.append(extract_room_data(tbody, base_data))
        else:
            page_data.append(base_data)
    return page_data

# -------------------------------------
# 検索条件の取得
# -------------------------------------
def get_search_conditions(url):
    soup = get_html(url)
    if not soup:
        return "データなし"
    condition_element = soup.find("p", id="js-conditionbox-txtInfoAll")
    return extract_text_or_default(condition_element, "データなし")

# -------------------------------------
# メインのスクレイピング処理
# -------------------------------------
def scrape_suumo(url, fetch_detail=True):
    """
    SUUMOの検索結果全ページをスクレイピングする。
    検索条件とスクレイピング実行時刻を各データに追加し、進捗状況をグローバル変数 progress_data に更新する。
    """
    all_data = []
    max_pages = get_max_pages(url)
    progress_data["total_pages"] = max_pages
    progress_data["start_time"] = datetime.now()
    logging.info(f"全 {max_pages} ページの処理を開始")

    search_conditions = get_search_conditions(url)
    scrape_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
        future_to_page = {executor.submit(process_page, url, page, fetch_detail): page
                          for page in range(1, max_pages + 1)}
        for future in concurrent.futures.as_completed(future_to_page):
            page = future_to_page[future]
            try:
                page_data = future.result()
                logging.info(f"ページ {page} 完了、{len(page_data)} 件取得")
                for entry in page_data:
                    entry["検索条件"] = search_conditions
                    entry["スクレイピング日時"] = scrape_time
                all_data.extend(page_data)
            except Exception as e:
                logging.error(f"ページ {page} 処理中エラー: {e}", exc_info=True)
            progress_data["pages_completed"] += 1

    return all_data

# -------------------------------------
# CSV出力(重複削除含む)およびファイル名の返却
# -------------------------------------
def save_to_csv(data):
    """
    取得データをDataFrame化し、指定のCSVファイルに保存する。
    保存後、生成したCSVファイルのパスを返す。
    """
    if not data:
        logging.warning("取得データが空のため、CSVに保存できません")
        return None

    timestamp = datetime.now().strftime("%Y-%m%d-%H%M")
    filename = f"suumo_chintai_{timestamp}.csv"
    df = pd.DataFrame(data)

    columns_to_drop = [
        "エネルギー消費性能", "断熱性能", "目安光熱費", "取引態様",
        "取り扱い店舗", "物件コード", "SUUMO物件コード", "総戸数",
        "次回更新日", "入居", "取り扱い店舗物件コード"
    ]
    df = df.drop(columns=[col for col in columns_to_drop if col in df.columns], errors='ignore')
    df = remove_duplicates(df)
    df.to_csv(filename, index=False, encoding="utf-8-sig")
    logging.info(f"CSV保存完了: {filename}")
    return filename

# -------------------------------------
# Tkinter を用いた進捗表示更新
# -------------------------------------
def update_progress():
    pages_completed = progress_data.get("pages_completed", 0)
    total_pages = progress_data.get("total_pages", 0)
    start_time = progress_data.get("start_time")
    elapsed = datetime.now() - start_time if start_time else timedelta(0)
    avg_time = elapsed / pages_completed if pages_completed else timedelta(0)
    estimated_total = avg_time * total_pages if pages_completed else timedelta(0)
    remaining = total_pages - pages_completed
    progress_label.config(text=(
        f"進捗: {pages_completed}/{total_pages} ページ完了  (残り: {remaining} ページ)\n"
        f"経過時間: {format_timedelta(elapsed)}  /  予想所要時間: {format_timedelta(estimated_total)}"
    ))
    if not progress_data.get("finished", False):
        root.after(1000, update_progress)
    else:
        progress_label.config(text=progress_label.cget("text") + "\n完了!")

# -------------------------------------
# 別スレッドでスクレイピングを開始
# -------------------------------------
def start_scraping(url, fetch_detail):
    scraped_data = scrape_suumo(url, fetch_detail)
    csv_file = save_to_csv(scraped_data)
    progress_data["finished"] = True
    # CSVファイルが存在すれば自動で開く
    if csv_file:
        open_file(csv_file)

# --------------------------
# メイン処理(GUI)
# --------------------------
if __name__ == "__main__":
    # 初回はTkinterウィンドウ非表示でURL入力を促す
    root = tk.Tk()
    root.withdraw()

    url = simpledialog.askstring("SUUMO URL入力", "スクレイピングするSUUMOのURLを入力してください:")
    if not url:
        messagebox.showerror("エラー", "URLが入力されていません。")
        exit(1)

    # 詳細情報取得は一時停止中のため、False固定
    fetch_detail = False

    # 進捗表示用ウィンドウを表示
    root.deiconify()
    root.title("スクレイピング進捗")
    progress_label = tk.Label(root, text="進捗情報を取得中...", font=("Arial", 12))
    progress_label.pack(padx=20, pady=20)

    # スクレイピング処理を別スレッドで開始
    scraping_thread = threading.Thread(target=start_scraping, args=(url, fetch_detail))
    scraping_thread.start()

    # 進捗表示の更新開始
    root.after(1000, update_progress)
    root.mainloop()

コードの解説

私はプログラミング素人なので、ChatGPTに開設をお願いしてみた。以下、回答

以下は、このコードが何をしているのか、どのように動作するのかを初めてプログラミングに触れる方向けに、各部分ごとに解説したものです。


1. 概要

このコードは、不動産情報サイト「SUUMO」の物件情報をスクレイピングして、CSVファイルに保存するプログラムです。ユーザーにはGUI(グラフィカルユーザーインターフェース)を通じてURLの入力と進捗状況の確認ができ、処理完了後は自動的に生成されたCSVファイルが開かれます。処理の進行状況は、各ページの処理が終わるごとに更新されます。


2. 使用しているライブラリとその役割

  • requests
    Webページの情報を取得するためのHTTPリクエストを送るライブラリです。
  • BeautifulSoup (bs4)
    取得したHTMLを解析して、必要なデータを抽出するためのライブラリです。
  • pandas
    スクレイピングで取得したデータを表形式(DataFrame)に変換し、CSVファイルとして保存するために使います。
  • logging
    プログラムの実行状況やエラーの記録をコンソールに出力します。
  • retry
    HTTPリクエストが失敗した場合に、再試行(リトライ)するためのデコレーターを提供します。
  • datetime
    時刻の記録や経過時間の計算に使います。
  • concurrent.futures
    複数のページを同時に処理するための並列処理(スレッドプール)に利用します。
  • re
    正規表現を用いて、文字列中から必要な情報を抽出するために使用します。
  • urllib.parse
    URLのエンコードなど、URL操作に必要な処理を行います。
  • tkinter
    ユーザーに入力を求めたり、進捗状況を表示するGUIを構築するために使います。
  • ossyssubprocess
    CSVファイルを自動で開くために、OSに依存した方法でファイルを開く処理を実現します。

3. プログラムの大まかな流れ

  1. ユーザー入力とGUI表示
    • Tkinterを利用して、スクレイピングするSUUMOのURLを入力するダイアログを表示します。
    • 詳細情報の取得は一時停止中(fetch_detailFalseに固定)にしてあります。
    • 入力後、進捗状況を表示するウィンドウが表示され、処理中の進捗が更新されます。
  2. スクレイピング処理の開始
    • 入力されたURLから、対象となる全ページ数を取得し、各ページのデータを並列処理で取得します。
    • 各ページごとに、物件の基礎情報や部屋情報を抽出します。
    • 並列処理(ThreadPoolExecutor)を利用して、処理時間を短縮しています。
  3. CSVファイルへの保存と自動オープン
    • 取得したデータはpandasのDataFrameにまとめ、重複行の削除も行った上でCSVファイルに保存します。
    • 処理完了後、OS標準の方法で自動的にCSVファイルが開かれます。
  4. 進捗表示
    • グローバル変数 progress_data により、完了したページ数や開始時刻、全ページ数などが管理され、1秒ごとにTkinterのラベルが更新されます。
    • 経過時間と推定所要時間も計算し、表示しています。

4. 各部分の詳細な解説

4.1 ログ出力とヘッダの設定

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
  • logging を設定して、プログラムの実行過程やエラーを出力します。デバッグの際に便利です。

また、HEADERS 変数でWebリクエストの際のユーザーエージェントを設定しており、これにより実際のブラウザからのアクセスと似たリクエストが送信されます。


4.2 進捗状況管理用のグローバル変数

progress_data = {
    "pages_completed": 0,
    "total_pages": 0,
    "start_time": None,
    "finished": False
}
  • この変数は、スクレイピング処理の進行状況を記録するために使います。たとえば、全体で何ページ中何ページを処理したか、開始時刻や処理完了かどうかなどが格納されます。

4.3 補助関数

4.3.1 ファイルを開く関数 (open_file)
def open_file(filepath):
    ...
  • 処理が完了した後に、生成されたCSVファイルをOSの既定のアプリケーションで自動的に開くための関数です。OSごとにコマンドが異なるため、sys.platform を使って判別しています。
4.3.2 タイムデルタの整形 (format_timedelta)
def format_timedelta(td):
    ...
  • 経過時間や予想所要時間を「HH:MM:SS」の形式に変換するための関数です。進捗表示に利用されます。

4.4 Webページの取得と解析

4.4.1 get_html 関数
@retry(tries=1, delay=1, backoff=2)
def get_html(url):
    ...
  • 指定したURLからHTMLを取得し、BeautifulSoupオブジェクトに変換します。リクエストに失敗した場合は、リトライ(再試行)を行います。
4.4.2 最大ページ数の取得 (get_max_pages)
def get_max_pages(url):
    ...
  • SUUMOのページにあるページネーション(複数ページのリンク)の情報から、全ページ数を抽出します。これにより、スクレイピングするページ数がわかります。

4.5 物件情報の抽出

  • extract_text_or_default
    HTML要素からテキストを取得し、存在しない場合は「データなし」を返す関数です。
  • extract_property_data_base やその関連関数(extract_age_and_floorextract_building_nameextract_address など)
    これらは、物件の名称、住所、築年数、階数、構造、カテゴリ、詳細ページURLなどの基本情報をHTMLから抽出します。
  • parse_minimum_access
    交通アクセス情報(徒歩、バス、車など)から最も適切な情報を選び出す処理です。
  • extract_room_data
    部屋の詳細情報(家賃、管理費、敷金、礼金、間取り、面積など)を抽出し、既存の基礎情報に追加します。

4.6 複数ページのスクレイピング処理

4.6.1 process_page 関数
def process_page(url, page, fetch_detail):
    ...
  • 指定された1ページ分の情報を取得し、物件ごとに基礎情報と部屋情報を抽出します。
  • 詳細情報の取得は fetch_detailTrue の場合に行われますが、現在は機能停止中(False固定)です。
4.6.2 scrape_suumo 関数
def scrape_suumo(url, fetch_detail=True):
    ...
  • SUUMOの全ページにわたるスクレイピング処理を担当します。
  • concurrent.futures.ThreadPoolExecutor を使い、複数のページを並列で処理することで効率を向上させています。
  • 各ページ処理が完了するたびに、グローバル変数 progress_data が更新され、進捗状況が反映されます。

4.7 CSV出力と自動オープン

4.7.1 save_to_csv 関数
def save_to_csv(data):
    ...
  • 取得したデータをpandasのDataFrameに変換し、不要な列を削除(重複も除去)した上で、CSVファイルとして保存します。
  • ファイル名には現在の日時を含めることで、一意の名前にしています。
4.7.2 CSV自動オープン
  • スクレイピング処理完了後、open_file(csv_file) を呼び出して、生成されたCSVファイルを自動で開きます。

4.8 GUIでの進捗表示

4.8.1 update_progress 関数
def update_progress():
    ...
  • Tkinter のラベルウィジェットに、現在の進捗状況(完了ページ数/全ページ数、経過時間、予想所要時間)を更新して表示します。
  • root.after(1000, update_progress) を使って1秒ごとに更新しています。
4.8.2 別スレッドでのスクレイピング処理
def start_scraping(url, fetch_detail):
    ...
  • スクレイピング処理は別スレッドで実行されるため、GUIがブロックされずに進捗表示が可能になります。
  • この関数内で scrape_suumo を実行し、その後にCSVファイルを開く処理を行っています。

4.9 メイン処理

if __name__ == "__main__":
    ...
  • プログラムのエントリーポイントです。
  • 最初はTkinterを使ってURL入力のダイアログを表示し、その後進捗表示用ウィンドウを立ち上げます。
  • スクレイピング処理は別スレッドで開始され、進捗表示が更新され続けます。
  • 全体の処理が完了すると、CSVファイルが自動で開かれ、ウィンドウ内に「完了!」と表示されます。

5. まとめ

このコードは、Webスクレイピングの基本的な流れ(データ取得、解析、保存)に加えて、GUIでのユーザー入力や進捗表示、自動で結果を開く機能を実装しています。各関数がひとつの役割に集中しているため、初めてコードを読む人でも全体の流れや各部分の役割を理解しやすくなっています。コメントも充実しているので、どの部分がどのような処理をしているのか、学びながらプログラムの構造を把握できるでしょう。

以上が、コード全体の解説になります。これを参考に、各部分の処理や流れを確認しながら、プログラミングの学習に役立ててください。

やってみた感想

気合と根性でChatGPTと会話して、かなり時間はかかったが一定程度利用価値のあるコードが完成した。これをいちから学習してコーディングする時間と比べると、相当に時間が短縮していることは素人でもよく理解できる。

正確に指示できるように、機械が理解しやすい自然言語でプロンプトを打つのがコツだ。普段から意味不明な日本語を使っていると、ChatGPTのアウトプットも意味不明なものになる。

今後

不動産賃貸業における物件選定時のGPIの見込みをつける際に役立てるように、このコードを活用していきたい。

賃貸物件を探している方にとっても一定程度役に立つだろう。

コメント

タイトルとURLをコピーしました