Realtor.com, один из крупнейших порталов недвижимости в США, и страницы его листингов содержат именно те структурированные данные, которые лежат в основе отслеживания цен, рыночных исследований и инвестиционного анализа: запрашиваемая цена, количество спален и ванных комнат, площадь, адрес и ссылка на каждый листинг. Эти публичные данные, сырьё для любого серьёзного анализа местного рынка жилья, однако страницы рендерятся на клиенте, а сайт жёстко защищается от автоматизированного трафика, поэтому обычный HTTP-запрос возвращает тонкую оболочку вместо нужных листингов.

В этом руководстве показано, как надёжно парсить Realtor.com с помощью Python. Вы создадите небольшой рабочий скрапер, который получает отрендеренную страницу поиска через Crawling API, считывает данные листингов, которые Realtor.com встраивает в скрытый скрипт __NEXT_DATA__, извлекает нужные поля, обрабатывает пагинацию и экспортирует чистые JSON и CSV. Всё руководство ограничено публичными данными листингов, а раздел о законности в конце не является шаблонным текстом, поэтому прочитайте его перед тем, как направить скрапер на реальные объёмы данных.

Что вы создадите

Скрипт на Python, который принимает публичный URL поиска Realtor.com для города и штата, получает отрендеренный HTML через Crawling API, парсит встроенный набор данных листингов и записывает одну структурированную запись на объект недвижимости. В качестве примера мы используем один город и извлекаем следующие поля:

  • Price запрашиваемая цена, указанная в листинге.
  • Beds количество спален.
  • Baths консолидированное количество ванных комнат.
  • Sqft внутренняя площадь дома.
  • Address улица, город, штат и почтовый индекс.
  • Link канонический URL листинга, восстановленный из его permalink.

Почему обычный запрос не работает на Realtor.com

Если запросить URL поиска Realtor.com с помощью простого HTTP-клиента, вы получите ответ со статусом 200 и почти без данных о листингах в видимой разметке. Против вас работают два фактора. Во-первых, Realtor.com, это приложение на Next.js, которое гидратирует листинги в браузере, поэтому данные находятся внутри JSON-блоба в скрытом теге <script id="__NEXT_DATA__">, а не в отрендеренных HTML-элементах, доступных для чтения напрямую. Во-вторых, сайт быстро определяет автоматизированный трафик: IP-адреса датацентров и паттерны запросов, не похожие на реальный браузер, получают проверку или CAPTCHA прежде, чем достигают полной страницы.

Таким образом, рабочему скраперу Realtor.com нужны две вещи в одном запросе: браузер, который реально рендерит страницу, и IP-адрес, который платформа воспринимает как реального посетителя. Можно собрать это самостоятельно с помощью headless-браузера и пула ротирующихся резидентских прокси, но сборка и поддержание их в рабочем состоянии составляет большую часть работы. Crawling API объединяет оба компонента в одном вызове: вы отправляете URL с JavaScript-токеном, API рендерит страницу за доверенным IP и возвращает готовый HTML с нетронутым содержимым __NEXT_DATA__ для парсинга.

Где находятся данные

Кликните правой кнопкой мыши на странице Realtor.com и выберите «Просмотр кода», затем найдите в HTML __NEXT_DATA__. Этот единственный скрытый скрипт содержит полный набор данных листингов, из которого рендерится страница, включая поля, которые видимый макет никогда не показывает. Считывать его значительно стабильнее, чем скрапить отдельные DOM-элементы, поскольку ключи JSON меняются реже, чем имена CSS-классов вокруг них.

Предварительные требования

Перед написанием кода необходимо подготовить несколько вещей. Ни одна из них не займёт много времени.

Базовые знания Python. Вы должны уметь писать и запускать Python-скрипты, устанавливать пакеты с помощью pip. Если язык вам незнаком, руководство по веб-скрапингу на Python охватывает основы, которые предполагает данный туториал.

Python 3.8 или выше. Проверьте версию командой python --version. Если он не установлен, скачайте с python.org или через дистрибутив, например Anaconda.

Аккаунт Crawlbase и JS-токен. Зарегистрируйтесь, откройте панель управления и скопируйте JavaScript (JS) токен. Вы получаете до 20 000 бесплатных запросов, карта не требуется. Относитесь к токену как к паролю: он аутентифицирует ваши запросы, поэтому держите его вне системы контроля версий.

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

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

bash
python --version

python -m venv realtor_env
source realtor_env/bin/activate

pip install crawlbase

В Windows активируйте окружение командой realtor_env\Scripts\activate вместо строки с source. Здесь нам нужна только одна сторонняя зависимость: crawlbase, официальный клиент для Crawling API. Поскольку данные листингов поступают в виде JSON внутри скрипта __NEXT_DATA__, встроенные модули Python json и re справляются с парсингом без отдельной HTML-библиотеки.

Шаг 1: Получение отрендеренной страницы поиска

Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с вашим JS-токеном и запросите URL поиска Realtor.com. Проверка кода статуса перед парсингом позволяет обнаруживать сбои явно, а не скрыто.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    search_url = "https://www.realtor.com/realestateandhomes-search/Los-Angeles_CA/pg-1"
    html = crawl(search_url)
    print(html[:500] if html else "No HTML returned")

Оба параметра ожидания важны для цели с клиентским рендерингом Next.js. ajax_wait говорит API подождать завершения загрузки асинхронного контента, а page_wait выдерживает фиксированное количество миллисекунд после загрузки, чтобы гидратированные данные были на месте до захвата страницы. Пять секунд, разумная отправная точка; увеличьте значение, если набор данных возвращается пустым. Запустите скрипт командой python realtor_scraper.py, вы должны увидеть реальную разметку страницы, содержащую скрипт __NEXT_DATA__, а не пустую оболочку, которую возвращает обычный запрос. Это подтверждает, что рендеринг работает ещё до написания первой строки парсинга.

Crawlbase Crawling API

Realtor.com требует отрендеренной Next.js-страницы за доверенным IP в одном вызове, прежде чем содержимое __NEXT_DATA__ вообще будет доступно для чтения. Crawling API принимает JS-токен, запускает страницу в реальном браузере, ротирует резидентские IP на стороне сервера и возвращает готовый HTML, так что вам не нужно самостоятельно запускать headless-флот и пул прокси. Попробуйте сначала на публичной странице поиска на бесплатном тарифе.

Шаг 2: Извлечение встроенного набора данных листингов

Имея в распоряжении отрендеренный HTML, извлеките JSON из скрипта __NEXT_DATA__ и перейдите к той его части, которая содержит результаты поиска. Realtor.com вкладывает результаты под props.pageProps с запасным путём под searchResults.home_search для страниц, использующих альтернативную форму. Оберните обращения к ключам так, чтобы отсутствующий ключ возвращал None вместо краша.

python
import re
import json

def extract_next_data(html):
    # The listing dataset lives in a hidden __NEXT_DATA__ script.
    match = re.search(
        r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
        html,
        re.DOTALL,
    )
    if not match:
        print("No hidden web data found.")
        return None
    return json.loads(match.group(1))

def get_results(data):
    # Prefer the pageProps path, fall back to the home_search shape.
    page_props = data.get("props", {}).get("pageProps", {})
    results = page_props.get("properties")
    if results:
        return results
    search = data.get("searchResults", {}).get("home_search", {})
    return search.get("results", [])

Функция extract_next_data использует одно регулярное выражение для извлечения содержимого скрипта __NEXT_DATA__ и его разбора как JSON, что избавляет от необходимости тянуть HTML-парсер только для чтения JSON-блоба. Вспомогательная функция get_results затем пробует массив properties под pageProps и при неудаче переключается на home_search.results, поскольку Realtor.com отдаёт обе формы в зависимости от того, как была достигнута страница. Каждое обращение использует dict.get со значением по умолчанию, так что страница со смещённой структурой возвращает пустой список, а не вызывает KeyError.

Шаг 3: Парсинг каждого объекта в плоскую запись

Каждый элемент массива результатов содержит блок description (спальни, ванные, площадь), блок location.address, list_price и permalink, из которого можно восстановить полный URL листинга. Сопоставьте их со словарём, чтобы результаты было удобно записывать в JSON или CSV.

python
def parse_property(item):
    description = item.get("description") or {}
    location = item.get("location") or {}
    address = location.get("address") or {}

    parts = [
        address.get("line"),
        address.get("city"),
        address.get("state_code"),
        address.get("postal_code"),
    ]
    full_address = ", ".join(p for p in parts if p)

    permalink = item.get("permalink")
    link = (
        f"https://www.realtor.com/realestateandhomes-detail/{permalink}"
        if permalink
        else None
    )

    return {
        "price": item.get("list_price"),
        "beds": description.get("beds"),
        "baths": description.get("baths_consolidated"),
        "sqft": description.get("sqft"),
        "address": full_address or None,
        "link": link,
    }

Защита or {} важна, поскольку Realtor.com устанавливает некоторые из этих вложенных объектов в null для листингов без данных, и вызов .get на None вызвал бы ошибку. Количество спален, ванных и площадь берутся прямо из блока description, где baths_consolidated, поле, которое Realtor.com использует для объединения полных и половинчатых ванных в одно число. Адрес строится путём объединения улицы, города, кода штата и почтового индекса с пропуском отсутствующих частей, а ссылка восстанавливается из permalink, назначаемого Realtor.com каждому объекту. Результат, одна плоская запись на листинг, подходящая для экспорта.

Ключи JSON тоже меняются

Структура __NEXT_DATA__ стабильнее CSS-селекторов, но не заморожена. Если price или sqft начинают возвращать None для каждой записи, выведите сырой JSON для одного объекта и перепроверьте имена ключей. Защитное чтение набора данных через .get означает, что переименование ключа приведёт к пустым полям, а не к краш, именно то, что нужно при автономном запуске.

Шаг 4: Сборка воедино с пагинацией

Одна страница, это демо; реальная задача охватывает полный набор результатов для города. Realtor.com пагинирует поиск с помощью чистого суффикса /pg-<PAGE>, поэтому вы строите URL для каждой страницы из города и штата, краулите её, извлекаете набор данных и парсите каждый объект. Небольшая пауза между страницами регулирует темп запуска.

python
import re
import json
import time
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

def find_properties(city, state, max_pages=1):
    listings = []
    for page in range(1, max_pages + 1):
        url = (
            "https://www.realtor.com/realestateandhomes-search/"
            f"{city}_{state.upper()}/pg-{page}"
        )
        html = crawl(url)
        if not html:
            continue
        data = extract_next_data(html)
        if not data:
            continue
        for item in get_results(data):
            listings.append(parse_property(item))
        print(f"Page {page}: {len(listings)} listings so far")
        time.sleep(2)
    return listings

def main():
    listings = find_properties("Los-Angeles", "CA", max_pages=3)
    print(json.dumps(listings, indent=2))

if __name__ == "__main__":
    main()

Функция find_properties воспроизводит подход оригинала: цикл обходит диапазон страниц, строит URL {city}_{state}/pg-{page} для каждой и добавляет каждый спарсенный объект в накопленный список. time.sleep(2) между страницами, намеренная мера, которая регулирует темп запуска, чтобы не перегружать сайт, это единственная наиболее эффективная привычка для избежания блокировки. Добавьте функции extract_next_data, get_results и parse_property из предыдущих шагов, и получите полный рабочий скрапер.

Как выглядит результат

Запустите полный скрипт командой python realtor_scraper.py и получите чистый список структурированных записей, по одной на листинг.

json
[
  {
    "price": 139000000,
    "beds": 12,
    "baths": "17",
    "sqft": null,
    "address": "1200 Bel Air Rd, Los Angeles, CA, 90077",
    "link": "https://www.realtor.com/realestateandhomes-detail/1200-Bel-Air-Rd_Los-Angeles_CA_90077_M17839-35941"
  }
]

Экспорт в JSON и CSV

Когда каждый листинг представлен плоским словарём, экспорт сводится к двум коротким функциям. JSON сохраняет полную вложенную форму; CSV преобразует данные в таблицу для работы с электронными таблицами, с одним столбцом на поле.

python
import csv
import json

def save_json(listings, path="realtor_listings.json"):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(listings, f, indent=2)

def save_csv(listings, path="realtor_listings.csv"):
    if not listings:
        return
    fields = ["price", "beds", "baths", "sqft", "address", "link"]
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fields)
        writer.writeheader()
        writer.writerows(listings)

Вызовите save_json(listings) и save_csv(listings) в конце функции main, и оба формата окажутся на диске. Явный список fields обеспечивает стабильный порядок столбцов CSV от запуска к запуску, что важно при дозаписи результатов в один файл или загрузке в инструмент, ожидающий фиксированный заголовок. Отсюда данные готовы для ноутбука, базы данных или модели ценообразования.

Как не попасть под блокировку при масштабировании

Даже при решённом вопросе рендеринга Realtor.com следит за трафиком, похожим на скрапинг, и его макет и защита меняются со временем. Несколько привычек помогут сохранить работоспособность более длительного запуска, и они применимы к любой серьёзной коммерческой цели.

  • Регулируйте частоту запросов. Долбить страницы в плотном цикле, самый быстрый способ получить ограничение скорости или CAPTCHA. Держите паузу (sleep) между страницами и избегайте краулинга одного города на полной скорости.
  • Используйте ротацию. Пул резидентских IP распределяет запросы по множеству адресов реальных пользователей, чтобы ни один не превысил лимит. Crawling API делает это за вас; если вы строите собственный стек, это та часть, которую нужно реализовать правильно.
  • Читайте коды статусов. Запуск, начавший возвращать проверки или ошибки, сигнализирует, что текущая скорость или IP-уровень больше недостаточны. Воспринимайте это как сигнал к снижению активности, а не как шум, который можно игнорировать.
  • Ожидайте изменений структуры. Realtor.com регулярно обновляет сайт, поэтому при появлении пустых полей перепроверяйте ключи __NEXT_DATA__, прежде чем считать скрапер сломанным.

Для общей методики см. руководства как парсить сайты, не попадая под блокировку и краулинг JavaScript-сайтов. При масштабировании на несколько городов формируйте пакеты URL поиска и пропускайте их через тот же цикл find_properties.

Законно ли парсить Realtor.com?

Допустимость парсинга Realtor.com зависит от условий использования Realtor.com, вашей юрисдикции и того, что вы делаете с данными. Условия ограничивают автоматизированный доступ, поэтому парсинг может нарушать их независимо от тщательности вашего инструментария. Ни один из кодов здесь не меняет этого; он лишь делает техническую часть рабочей. Ознакомьтесь с Условиями использования Realtor.com и его robots.txt, соблюдайте заявленные ограничения скорости и воспринимайте оба документа как границу того, что вы собираете.

Несколько линий, которых стоит придерживаться. Собирайте только публичные данные листингов: запрашиваемую цену, количество спален и ванных, площадь, адрес и ссылку на листинг, которые любой может увидеть без аккаунта. Значительная часть базовых данных Realtor.com поступает из лицензированных MLS-фидов и не может свободно использоваться повторно, поэтому запись об объекте может нести ограничения на использование даже при публичности страницы; воспринимайте поля из MLS как лицензированный контент, а не открытые данные. Избегайте всего, что связано с идентифицируемыми физическими лицами, включая имена и контактные данные агентов, брокеров или владельцев, отображаемые на странице, они являются персональными данными по нормам GDPR и CCPA. Если вы планируете коммерческое или массовое повторное использование данных, получите разрешение или лицензированный фид, а не предполагайте, что молчание равносильно согласию.

Данное руководство намеренно ограничено публичными страницами листингов, поскольку именно эта граница делает работу защищаемой. Оно не охватывает ничего, что скрыто за авторизацией, сохранённых поисков или данных аккаунта, персональных или контактных данных агентов и владельцев или любых попыток обойти аутентификацию. Только публичные данные листингов. Если вашему проекту нужно больше, Realtor.com и стоящие за ним MLS-системы предлагают официальные партнёрства и лицензированные фиды, правильный путь для производственных объёмов, а не более умный скрапер.

Итоги

Ключевые выводы

  • Realtor.com скрывает данные в __NEXT_DATA__. Листинги находятся в JSON-блобе в скрытом скрипте, поэтому нужно читать этот payload вместо парсинга DOM-элементов.
  • Нужны одновременно рендеринг и доверенный IP. Crawling API с JS-токеном рендерит Next.js-страницу и ротирует IP в одном вызове; ajax_wait и page_wait контролируют время ожидания.
  • Парсьте защитно. Извлекайте цену, спальни, ванные, площадь, адрес и ссылку через .get и защиту or {}, чтобы нулевое поле или переименование ключа приводили к пустым значениям, а не к крашу.
  • Пагинируйте через /pg-N и экспортируйте в обоих форматах. Обходите суффикс страницы, парсите каждый объект, затем записывайте JSON и CSV из одних и тех же плоских записей.
  • Оставайтесь на публичных данных. Соблюдайте ToS и robots.txt Realtor.com, воспринимайте поля из MLS как лицензированные и никогда не собирайте персональные данные агентов или владельцев.

Часто задаваемые вопросы

Почему обычный запрос не возвращает листинги от Realtor.com?

Потому что Realtor.com, это Next.js-приложение, которое гидратирует листинги в браузере. Данные находятся не в статичных HTML-элементах, возвращаемых сырым запросом, а внутри скрытого JSON-скрипта __NEXT_DATA__, который появляется только после рендеринга страницы. Чтобы получить их, нужно сначала отрендерить страницу, этим занимается JS-токен Crawling API, а затем считать JSON из этого скрипта.

Что такое скрипт __NEXT_DATA__ и зачем его парсить?

Это JSON-payload, который Next.js-сайты встраивают для гидратации страницы в браузере. На Realtor.com он содержит полный набор данных результатов поиска, включая цену, спальни, ванные, площадь, адрес и permalink для каждого листинга. Его считывание стабильнее, чем парсинг видимого HTML, поскольку ключи JSON меняются реже, чем имена CSS-классов вокруг них.

Нужен ли мне обычный токен или JS-токен для Realtor.com?

JS-токен. Обычный токен получает статичный HTML, который на Realtor.com не включает гидратированное содержимое __NEXT_DATA__. JS-токен сначала рендерит страницу в реальном браузере, так что встроенный набор данных присутствует при его извлечении и парсинге.

Как обрабатывать пагинацию по листингам города?

Realtor.com использует суффикс /pg-<PAGE> в URL поиска, поэтому вы строите {city}_{state}/pg-{page} для каждой страницы и циклически перебираете номер страницы. Функция find_properties выше делает именно это: краулит каждую страницу, извлекает набор данных, парсит каждый объект и делает паузу между страницами для соблюдения вежливой частоты запросов.

Какие поля можно извлечь из листинга Realtor.com?

Публичные поля листинга: запрашиваемая цена, количество спален и ванных, площадь, полный адрес и ссылка на листинг, восстановленная из permalink. Ограничивайтесь данными, видимыми любому посетителю без аккаунта, воспринимайте поля из MLS как лицензированный контент и избегайте персональных данных агентов, брокеров или владельцев, выходящих за рамки публичных листингов.

Можно ли отслеживать изменения цен и листингов со временем?

Да. Запускайте скрапер по расписанию, идентифицируйте каждый листинг по его permalink и сравнивайте поля цены и статуса между запусками для фиксации новых листингов, снижений цен и проданных объектов. Держите частоту запросов умеренной и сохраняйте только нужные публичные поля листинга. Для смежных целей в недвижимости см. руководства как парсить Zillow и как парсить Redfin.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга