Проект автоматизированного сбора данных о недвижимости на Python: Часть 1 – Очистка веб-страниц

В этой серии проектов, состоящей из четырех частей, мы создадим автоматизированный конвейер обработки данных о недвижимости, который будет анализировать списки недвижимости, очищать данные, визуализировать тенденции и отслеживать исторические цены с течением времени.

*** Почему именно Этот проект?

Этот проект на Python действительно заслуживает внимания, поскольку он позволяет вам продемонстрировать основные навыки автоматизации в реальных условиях с помощью Python. К концу этой серии уроков у вас будет инструмент, который может:

* Автоматически очищайте данные о недвижимости в реальном времени.

* Анализируйте и очищайте информацию о свойствах.

* Визуализируйте тенденции в данных с помощью интерактивной панели мониторинга.

***... Отслеживайте исторические изменения цен для долгосрочного анализа.

Разбивка проекта на части

Это довольно масштабный проект, поэтому имеет смысл разделить его на четыре отдельные части:

1. Очистка веб-страниц - Извлечение данных о недвижимости с помощью Selenium.

2️⃣ Data Cleaning & Analysis – Processing and analyzing property listings.

3. Панель мониторинга с Streamlit - Интерактивная визуализация тенденций.

4... Автоматизация и отслеживание исторических данных - Автоматическое управление скребком и отслеживание долгосрочных тенденций.

Часть 1: Веб-обработка данных о недвижимости с помощью Selenium

В первой части нашего проекта на Python мы создадим списки объектов недвижимости из Redfin с помощью Selenium. Мы собираемся извлечь ряд важных деталей, включая цены, адреса, кровати/ ванны, изображения и географические координаты. Затем мы сохраним эту информацию в структурированном наборе данных для анализа (который мы рассмотрим в части 2).

"Еще несколько предпосылок

Не волнуйтесь, если вы не являетесь экспертом в Python, но прежде чем погрузиться в код, убедитесь, что у вас есть необходимые навыки, которые сделают это путешествие более плавным и приятным.

* Базовые знания языка Python

Вы должны быть знакомы с:

  • Функции и циклы Python.
  • Работа с Pandas для обработки данных.
  • Основные понятия веб-скрейпинга (структура HTML, селекторы CSS).

Необходимые библиотеки

Мы будем использовать следующие пакеты Python:

Library Purpose
selenium Automates web browsing to extract real estate data
webdriver_manager Manages browser drivers for Selenium
pandas Stores and processes scraped data
json Parses structured listing details
random & time Introduces delays to avoid detection

° €”â1 Установка зависимостей

Прежде чем мы запустим наш редактор и приступим к написанию кода, убедитесь, что у вас есть все необходимые библиотеки, выполнив эту команду:

pip install selenium webdriver-manager pandas

1 Настройте свой проект

Прежде чем мы начнем кодировать, давайте настроим проект:

1. Убедитесь, что на вашем компьютере установлен Python. Если нет, загрузите его с официального сайта Python.2. Откройте свой любимый редактор кода или IDE.3. Создайте новый файл Python, например, scraper.py.

Отлично, а теперь давайте с головой погрузимся в наш редактор Python, чтобы начать сборку.

Шаг 1: Настройка Selenium и Firefox WebDriver

Для обработки веб-скрейпа в этом проекте на Python мы будем использовать Selenium.

Это мощный инструмент для автоматизации взаимодействия с браузером, который позволяет нам извлекать динамический контент из объявлений о недвижимости точно так же, как это сделал бы обычный пользователь.

from selenium import webdriver
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.firefox_profile import FirefoxProfile # Optional
from webdriver_manager.firefox import GeckoDriverManager

1 Почему именно селен?

Многие современные веб-сайты используют JavaScript для динамической загрузки данных. Простые скрейперы, такие как requests и BeautifulSoup, извлекают только статический контент, пропуская ключевые элементы. Selenium решает эту проблему, имитируя действия браузера и полностью отображая страницы.

Для запуска Selenium нам нужен WebDriver - инструмент, который служит связующим звеном между Python и веб-браузером. К счастью для нас, webdriver_manager автоматически устанавливает необходимые драйверы браузера и управляет ими за нас.

Шаг 2: Реализация настроек скрытности

Многие веб-сайты пытаются обнаружить и заблокировать автоматические скрипты. Мы можем снизить этот риск, выполнив:

  • Рандомизируем наш пользовательский агент с помощью Python random (чтобы наш скрипт отображался в разных браузерах).
  • Запуск браузера в автономном режиме (без открытия видимого окна).
  • Отключение автоматических флажков, которые веб-сайты проверяют с помощью антисоциальных ботов.
  • Использование профиля пользователя для нашего безголового браузера (необязательно)
import random
import json
import time

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/119.0",
]

# OPTIONAL: Update with your actual Firefox profile path
profile_path = "/home/your_name/.mozilla/firefox/abcdefgh.your-profile"

options = Options()
options.profile = FirefoxProfile(profile_path) # OPTIONAL
options.add_argument("--headless")  # Run in background (no GUI)
options.set_preference("dom.webdriver.enabled", False)
options.set_preference("useAutomationExtension", False)
options.set_preference("general.useragent.override", random.choice(USER_AGENTS))

service = Service(GeckoDriverManager().install())
driver = webdriver.Firefox(service=service, options=options)

* Этичный веб-скрейпинг и лучшие практики

Единственное, о чем я должен рассказать, это то, что многие веб-сайты активно обнаруживают и блокируют автоматические скрипты. Чтобы избежать идентификации как бота и обеспечить соблюдение этических норм, следуйте этим рекомендациям:

  • Уважайте доступ к файлу веб-сайта robots.txt “Некоторые сайты явно запрещают очистку.
  • Не перегружайте сервер - вводите случайные задержки между запросами (1-10 секунд), чтобы имитировать поведение человека.
  • Избегайте чрезмерных запросов - обрабатывайте только те данные, которые вам нужны.
  • Меняйте местами пользовательские агенты и IP-адреса... Это помогает предотвратить обнаружение и блокировку IP-адресов.
  • Никогда не нарушайте условия предоставления услуг веб-сайта. Всегда следите за тем, чтобы ваш вариант использования был юридически и этически обоснован.

Я старался придерживаться всех этих правил в этом проекте, и я бы рекомендовал вам делать то же самое в любых будущих проектах по очистке веб-страниц!

Шаг 3: Проверка списков недвижимости

1 Определите целевой URL-адрес

Шутки ради, мы собираемся выкупить недвижимость на Голливудских холмах у Redfin:

Для этого нам нужно получить URL-адрес страницы результатов поиска, а затем использовать его в нашем WebDriver.

Мы также добавим элементарную проверку, чтобы увидеть, указывает ли заголовок страницы на то, что доступ к странице был заблокирован. Это не должно быть проблемой, если мы придерживаемся этических норм, но я добавил это здесь для полноты картины.

base_url = "https://www.redfin.com/neighborhood/547223/CA/Los-Angeles/Hollywood-Hills"
driver.get(base_url)
time.sleep(random.uniform(5, 8))  # Random delay

# Check if we are being blocked
print(f"Page title: {driver.title}")
if "Access" in driver.title or "blocked" in driver.page_source.lower():
    print("🚨 WARNING: Redfin has blocked the request!")
    driver.quit()
    exit()

° €”Â1 Извлечение списков недвижимости

Для сбора данных о недвижимости мы:

  1. Найдите контейнер объявлений и объявления внутри этого контейнера, используя блок try-except для выхода, если мы не сможем найти какие-либо данные из списка.
  2. Извлеките данные о цене, кроватях, ваннах, квадратных метрах, ссылках на изображения и геолокации с помощью ряда блоков с возможностью исключения и резервных значений для пустых значений.
  3. Извлеките адрес и ссылки с помощью ряда блоков try-except с инструкциями continue, чтобы пропустить итерацию, если эти критические значения отсутствуют
  4. Найдите и нажмите на кнопку "Далее", чтобы перемещаться по страницам.

Обратите внимание, что мы используем цикл Python while, который прерывается только после того, как нам не удается найти кнопку "Следующая страница", поскольку это указывает на то, что мы находимся на последней странице результатов.

* Важно: селекторы CSS, которые я использовал ниже, работали на момент создания этого проекта в январе 2025 года. Но структура страниц веб-сайта может постоянно меняться, поэтому обязательно перепроверьте исходный код страницы, чтобы убедиться, что эти классы по-прежнему применимы.

scraped_data = []
page_number = 1

while True:
    print(f"📄 Scraping page {page_number}...")

    # Locate the main listings container
    try:
        container = driver.find_element("css selector", "div.HomeCardsContainer")
        listings = container.find_elements("css selector", "div.HomeCardContainer")
    except:
        print("🚨 Failed to locate the property list container. Exiting...")
        break

    print(f"✅ Found {len(listings)} listings on page {page_number}")

    for listing in listings:
        # Extract price
        try:
            price = listing.find_element("css selector", "span.bp-Homecard__Price--value").text.strip()
        except:
            price = "N/A"

        # Extract address
        try:
            address = listing.find_element("css selector", "div.bp-Homecard__Address").text.strip()
        except:
            print("⚠️ Skipping a listing due to missing address data")
            continue  # Skip listings with missing elements

        # Extract beds, baths, and sqft
        try:
            beds = listing.find_element("css selector", "span.bp-Homecard__Stats--beds").text.strip()
        except:
            beds = "N/A"

        try:
            baths = listing.find_element("css selector", "span.bp-Homecard__Stats--baths").text.strip()
        except:
            baths = "N/A"

        try:
            sqft = listing.find_element("css selector", "span.bp-Homecard__LockedStat--value").text.strip()
        except:
            sqft = "N/A"

        # Extract listing link
        try:
            link = listing.find_element("css selector", "a.link-and-anchor").get_attribute("href")
            link = f"https://www.redfin.com{link}" if link.startswith("/") else link
        except:
            print("⚠️ Skipping a listing due to missing link data")
            continue  # Skip listings with missing elements

        # Extract image URL
        try:
            image_element = listing.find_element("css selector", "img.bp-Homecard__Photo--image")
            image_url = image_element.get_attribute("src")
        except:
            image_url = "N/A"
        
        try:
            # Extract Geo-Coordinates (Latitude & Longitude)
            json_script = listing.find_element("css selector", "script[type='application/ld+json']").get_attribute("innerHTML")
            json_data = json.loads(json_script)

            latitude = json_data[0]["geo"]["latitude"]
            longitude = json_data[0]["geo"]["longitude"]
        except:
            latitude = "N/A"
            longitude = "N/A"

        # Store the data
        scraped_data.append({
            "Price": price,
            "Address": address,
            "Beds": beds,
            "Baths": baths,
            "SqFt": sqft,
            "Link": link,
            "Image URL": image_url, 
            "Latitude": latitude,
            "Longitude": longitude
        })

    # 🔹 Pagination: Check if a "Next Page" button exists
    try:
        next_button = driver.find_element("css selector", "button.PageArrow__direction--next")
        next_button.click()
        page_number += 1
        time.sleep(random.uniform(5, 10))  # Random delay before next request
    except:
        print("✅ No more pages. Scraping complete.")
        break  # Exit loop when no next page exists

Шаг 4: Сохранение данных

После завершения цикла и сбора исходных данных мы преобразуем их в формат данных Pandas. Затем мы можем сохранить эти извлеченные данные в CSV-файле для дальнейшего анализа в части 2 нашего проекта. Я добавляю это в каталог данных в моем основном каталоге проекта, чтобы упорядочить его.

Также важно, чтобы мы закрыли наш WebDriver после того, как закончим очистку наших данных.

import pandas as pd
df = pd.DataFrame(scraped_data)
df.to_csv("data/redfin_hollywood_hills.csv", index=False)
print(f"{len(df)} listings saved!")

driver.quit()

“Полный исходный код программы"

from selenium import webdriver
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.firefox_profile import FirefoxProfile
from webdriver_manager.firefox import GeckoDriverManager
import pandas as pd
import time
import random
import json

# 🔹 Define a list of User-Agents to rotate
USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/119.0",
]

# 🔹 Update with your actual Firefox profile path
profile_path = "/home/your-name/.mozilla/firefox/abcdefgh.your-profile"

# 🔹 Setup Firefox options for stealth mode
options = Options()
options.profile = FirefoxProfile(profile_path)
options.add_argument("--headless")  # Run in background (no GUI)
options.add_argument("--disable-gpu")
options.set_preference("dom.webdriver.enabled", False)
options.set_preference("useAutomationExtension", False)

# Randomly select a User-Agent
user_agent = random.choice(USER_AGENTS)
options.set_preference("general.useragent.override", user_agent)

# 🔹 Use WebDriver Manager to install Geckodriver
service = Service(GeckoDriverManager().install())
driver = webdriver.Firefox(service=service, options=options)

# 🔹 Redfin search URL for Hollywood Hills, Los Angeles
base_url = "https://www.redfin.com/neighborhood/547223/CA/Los-Angeles/Hollywood-Hills"

# 🔹 Initialize data storage
scraped_data = []

# Start scraping
driver.get(base_url)
time.sleep(random.uniform(5, 8))  # Random delay

# Check if we are being blocked
print(f"Page title: {driver.title}")
if "Access" in driver.title or "blocked" in driver.page_source.lower():
    print("🚨 WARNING: Redfin has blocked the request!")
    driver.quit()
    exit()

page_number = 1
while True:
    print(f"📄 Scraping page {page_number}...")

    # Locate the main listings container
    try:
        container = driver.find_element("css selector", "div.HomeCardsContainer")
        listings = container.find_elements("css selector", "div.HomeCardContainer")
    except:
        print("🚨 Failed to locate the property list container. Exiting...")
        break

    print(f"✅ Found {len(listings)} listings on page {page_number}")

    for listing in listings:
        # Extract price
        try:
            price = listing.find_element("css selector", "span.bp-Homecard__Price--value").text.strip()
        except:
            price = "N/A"

        # Extract address
        try:
            address = listing.find_element("css selector", "div.bp-Homecard__Address").text.strip()
        except:
            print("⚠️ Skipping a listing due to missing address data")
            continue  # Skip listings with missing elements

        # Extract beds, baths, and sqft
        try:
            beds = listing.find_element("css selector", "span.bp-Homecard__Stats--beds").text.strip()
        except:
            beds = "N/A"

        try:
            baths = listing.find_element("css selector", "span.bp-Homecard__Stats--baths").text.strip()
        except:
            baths = "N/A"

        try:
            sqft = listing.find_element("css selector", "span.bp-Homecard__LockedStat--value").text.strip()
        except:
            sqft = "N/A"

        # Extract listing link
        try:
            link = listing.find_element("css selector", "a.link-and-anchor").get_attribute("href")
            link = f"https://www.redfin.com{link}" if link.startswith("/") else link
        except:
            print("⚠️ Skipping a listing due to missing link data")
            continue  # Skip listings with missing elements

        # Extract image URL
        try:
            image_element = listing.find_element("css selector", "img.bp-Homecard__Photo--image")
            image_url = image_element.get_attribute("src")
        except:
            image_url = "N/A"
        
        try:
            # Extract Geo-Coordinates (Latitude & Longitude)
            json_script = listing.find_element("css selector", "script[type='application/ld+json']").get_attribute("innerHTML")
            json_data = json.loads(json_script)

            latitude = json_data[0]["geo"]["latitude"]
            longitude = json_data[0]["geo"]["longitude"]
        except:
            latitude = "N/A"
            longitude = "N/A"

        # Store the data
        scraped_data.append({
            "Price": price,
            "Address": address,
            "Beds": beds,
            "Baths": baths,
            "SqFt": sqft,
            "Link": link,
            "Image URL": image_url, 
            "Latitude": latitude,
            "Longitude": longitude
        })

    # 🔹 Pagination: Check if a "Next Page" button exists
    try:
        next_button = driver.find_element("css selector", "button.PageArrow__direction--next")
        next_button.click()
        page_number += 1
        time.sleep(random.uniform(5, 10))  # Random delay before next request
    except:
        print("✅ No more pages. Scraping complete.")
        break  # Exit loop when no next page exists

# Convert to DataFrame and save as CSV
df = pd.DataFrame(scraped_data)
df.to_csv("data/redfin_hollywood_hills.csv", index=False)

print(f"🎯 Scraping complete! {len(df)} listings saved to data/redfin_hollywood_hills.csv")

# Close the browser
driver.quit()

° €”® Далее: Очистка и анализ данных

В части 2 мы очистим и проанализируем наши собранные данные о недвижимости, чтобы обеспечить их структурированность для визуализации и дальнейшей обработки.

— Следите за обновлениями, чтобы не пропустить следующий урок!