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

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

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

Python-скрипт, который берёт публичный URL поиска Trulia (например, объекты на продажу в Лос-Анджелесе, Калифорния), получает отрисованный HTML через Crawling API и извлекает структурированную запись для каждого листинга на странице. Из каждой карточки объекта мы получаем следующие поля:

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

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

Если запросить URL поиска Trulia обычным HTTP-клиентом, вы получите ответ со статусом 200 и почти без данных о листингах в теле. Против вас работают два фактора. Во-первых, Trulia рендерит большую часть контента результатов в браузере с помощью JavaScript, поэтому исходный HTML, это тонкая оболочка, которая заполняется только после того, как скрипты страницы выполнятся. Во-вторых, сайт быстро выявляет автоматизированный трафик: IP-адреса датацентров и паттерны запросов, не похожие на настоящий браузер, проходят проверку, ограничиваются по скорости или получают CAPTCHA ещё до того, как будут достигнуты отрисованные листинги.

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

Why the JS token

Crawlbase предлагает два типа токенов. Обычный токен получает статический HTML; JavaScript (JS) токен сначала рендерит страницу в настоящем браузере. Trulia заполняет карточки листингов на стороне клиента, поэтому здесь нужен JS-токен. Использование обычного токена возвращает ту же пустую оболочку, что и обычный запрос, и из неё нечего парсить.

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

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

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

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

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

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

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

bash
python --version

python -m venv trulia_env
source trulia_env/bin/activate

pip install crawlbase beautifulsoup4 pandas

В Windows активируйте окружение командой trulia_env\Scripts\activate вместо строки с source. Три зависимости выполняют всю работу: crawlbase, официальный клиент Crawling API, beautifulsoup4 парсит возвращаемый HTML, позволяя извлекать поля по CSS-селектору, а pandas обрабатывает экспорт в CSV в конце. Если вы не работали с парсером раньше, руководство по BeautifulSoup станет хорошим дополнением к этому туториалу.

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

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

python
from crawlbase import CrawlingAPI

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

options = {"ajax_wait": "true", "page_wait": 8000}

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

if __name__ == "__main__":
    search_url = "https://www.trulia.com/CA/Los_Angeles/"
    html = crawl(search_url)
    print(html[:500] if html else "No HTML returned")

Два параметра ожидания важны для такой цели, как эта, с рендерингом на стороне клиента. ajax_wait указывает API дождаться завершения загрузки асинхронного контента, а page_wait выдерживает фиксированное количество миллисекунд после загрузки, чтобы поздно рендеримые карточки появились до захвата страницы. Восемь секунд, разумный старт для Trulia; увеличьте значение, если листинги возвращаются пустыми. Crawling API возвращает заголовок cb_status (legacy pc_status), отражающий результат сканирования, поэтому проверяйте его, а не сырой HTTP-код. Запустите скрипт командой python trulia_scraper.py, и вы должны увидеть реальную разметку листингов, а не пустую оболочку обычного запроса. Это подтверждает работоспособность рендеринга ещё до написания единого селектора.

Crawlbase Crawling API

Trulia требует отрисованной страницы за доверенным IP-адресом в одном вызове, и параметры ajax_wait плюс page_wait, которые вы только что установили, позволяют переждать клиентскую загрузку. Crawling API принимает JS-токен, запускает страницу в настоящем браузере, на стороне сервера ротирует резидентские IP-адреса и отдаёт готовый HTML, избавляя вас от необходимости самостоятельно управлять парком headless-браузеров и пулом прокси. Направьте его на публичную страницу поиска на бесплатном уровне.

Шаг 2: Сбор карточек листингов

Перед извлечением отдельных полей необходимо получить набор карточек объектов на странице. На Trulia каждый листинг находится в элементе li, и все эти элементы li расположены внутри ul с атрибутом data-testid="search-result-list-container". Выбор прямых потомков этого контейнера даёт по одному узлу на объект.

python
from bs4 import BeautifulSoup

def get_listings(html):
    soup = BeautifulSoup(html, "html.parser")
    return soup.select('ul[data-testid="search-result-list-container"] > li')

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

Шаг 3: Парсинг полей из каждой карточки

Имея карточку, извлеките каждое поле по атрибуту data-testid. Trulia последовательно использует эти атрибуты в листингах, что делает их более стабильными для таргетирования, чем визуальные имена классов. Оберните каждый поиск так, чтобы при отсутствии элемента возвращался None, а не выбрасывалось исключение, поскольку не каждый листинг содержит все поля (например, листинг только земельного участка может не иметь количества спален или ванных).

python
def text_at(listing, selector):
    el = listing.select_one(selector)
    return el.get_text(strip=True) if el else None

def parse_listing(listing):
    link_el = listing.select_one('a[data-testid="property-card-link"]')
    link = "https://www.trulia.com" + link_el["href"] if link_el else None

    return {
        "price": text_at(listing, 'div[data-testid="property-price"]'),
        "address": text_at(listing, 'div[data-testid="property-address"]'),
        "beds": text_at(listing, 'div[data-testid="property-beds"]'),
        "baths": text_at(listing, 'div[data-testid="property-baths"]'),
        "size": text_at(listing, 'div[data-testid="property-floorSpace"]'),
        "link": link,
    }

Вспомогательная функция text_at выполняет однотипную работу: запрашивает элемент и возвращает его очищенный текст или None при отсутствии элемента, чтобы одно отсутствующее поле никогда не прерывало запуск. Цена хранится в property-price, адрес в property-address, спальни и ванные в property-beds и property-baths, площадь в property-floorSpace. Ссылка на страницу деталей находится в теге a с data-testid="property-card-link", и поскольку этот href относительный, вы добавляете к нему исходный домен Trulia, чтобы получить абсолютный URL.

Selectors drift

Значения data-testid Trulia стабильны сегодня, но не гарантированы. Когда поле возвращается как None во всех карточках, заново осмотрите живой листинг в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов, норма для любого производственного скрапера, а не признак поломки.

Шаг 4: Сборка полного скрипта

Теперь объедините получение данных, сбор карточек и парсинг полей в один рабочий скрипт. Получите отрисованный HTML, пройдитесь по карточкам, распарсите каждую в запись и выведите результаты в виде JSON.

python
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})
options = {"ajax_wait": "true", "page_wait": 8000}

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

def get_listings(html):
    soup = BeautifulSoup(html, "html.parser")
    return soup.select('ul[data-testid="search-result-list-container"] > li')

def text_at(listing, selector):
    el = listing.select_one(selector)
    return el.get_text(strip=True) if el else None

def parse_listing(listing):
    link_el = listing.select_one('a[data-testid="property-card-link"]')
    link = "https://www.trulia.com" + link_el["href"] if link_el else None
    return {
        "price": text_at(listing, 'div[data-testid="property-price"]'),
        "address": text_at(listing, 'div[data-testid="property-address"]'),
        "beds": text_at(listing, 'div[data-testid="property-beds"]'),
        "baths": text_at(listing, 'div[data-testid="property-baths"]'),
        "size": text_at(listing, 'div[data-testid="property-floorSpace"]'),
        "link": link,
    }

def main():
    search_url = "https://www.trulia.com/CA/Los_Angeles/"
    html = crawl(search_url)
    if not html:
        return

    listings = get_listings(html)
    results = [parse_listing(li) for li in listings]
    print(json.dumps(results, indent=2))

if __name__ == "__main__":
    main()

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

Запустите полный скрипт командой python trulia_scraper.py и получите чистый список структурированных записей, по одной на каждый листинг на странице, готовый для записи в JSON, CSV или базу данных.

json
[
  {
    "price": "$4,750,000",
    "address": "9240 W National Blvd, Los Angeles, CA 90034",
    "beds": "9bd",
    "baths": "9ba",
    "size": "6,045 sqft",
    "link": "https://www.trulia.com/p/ca/los-angeles/..."
  },
  {
    "price": "$1,499,999",
    "address": "245 Windward Ave, Venice, CA 90291",
    "beds": "4bd",
    "baths": "3ba",
    "size": "1,332 sqft",
    "link": "https://www.trulia.com/p/ca/venice/..."
  }
]

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

Обработка пагинации и экспорт данных

Одна страница, это демонстрация; реальная задача охватывает целый город. Trulia разбивает результаты поиска на страницы с помощью схемы на основе пути: к URL поиска добавляется последовательный сегмент страницы, поэтому первая страница, это /1_p/, вторая, /2_p/ и так далее. Перебор этого числа обходит набор результатов, а те же функции crawl и парсинга переиспользуются на каждой странице.

python
import json
import time
import pandas as pd

def scrape_pages(base_url, num_pages):
    results = []
    for page in range(1, num_pages + 1):
        page_url = f"{base_url}/{page}_p/"
        html = crawl(page_url)
        if not html:
            print(f"Skipping page {page}: no HTML.")
            continue
        listings = get_listings(html)
        if not listings:
            break
        results.extend(parse_listing(li) for li in listings)
        time.sleep(2)
    return results

def export(results):
    with open("trulia_listings.json", "w") as f:
        json.dump(results, f, indent=2)
    pd.DataFrame(results).to_csv("trulia_listings.csv", index=False)
    print(f"Saved {len(results)} listings to JSON and CSV.")

if __name__ == "__main__":
    base = "https://www.trulia.com/CA/Los_Angeles"
    data = scrape_pages(base, num_pages=3)
    export(data)

time.sleep(2) между страницами, намеренное решение: оно задаёт темп запуска, чтобы вы не перегружали сайт, и это единственная наиболее эффективная привычка для поддержания работоспособности. Цикл также завершается досрочно, если страница не возвращает карточек, поэтому вы никогда не запрашиваете страницы за последней страницей результатов. Функция export записывает и trulia_listings.json, и trulia_listings.csv; pandas превращает список словарей в плоскую таблицу, где каждое поле становится столбцом. Настройте количество страниц и слаг города в base под ваш целевой рынок.

Как оставаться незаблокированным

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

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

Более широкий план действий см. в руководстве по обходу блокировок при скрапинге. Если ваши целевые сайты активно используют JavaScript, руководство по сканированию JavaScript-сайтов подробнее рассматривает сторону рендеринга.

Законно ли парсить Trulia?

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

Несколько правил, которых стоит придерживаться. Собирайте только публичные данные листинга объектов: цену, адрес, спальни, ванные, площадь и ссылку на листинг, которые любой может видеть без аккаунта. Избегайте всего, что связано с идентифицируемыми людьми, включая контактные данные агентов, брокеров или владельцев, указанные в карточке, это выходит за рамки публичных листингов, рассматриваемых в этом руководстве. Одна деталь, специфичная для недвижимости, заслуживает отдельного упоминания: большая часть базовых данных об объектах на таких сайтах, как Trulia, поступает из фидов Multiple Listing Service (MLS), которые обычно лицензированы и имеют собственные ограничения на использование. Массовое переиздание этих данных может нарушать эти лицензии, даже если страница сама по себе публична.

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

Итоги

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

  • Trulia рендерится на стороне клиента. Обычный запрос возвращает пустую оболочку, поэтому необходимо рендерить страницу перед парсингом.
  • Рендеринг и доверенный IP-адрес нужны вместе. Crawling API с JS-токеном делает оба в одном вызове; ajax_wait и page_wait управляют временем ожидания загрузки карточек.
  • Таргетируйте стабильные атрибуты. Значения data-testid Trulia (property-price, property-address, property-beds, property-baths, property-floorSpace) управляют извлечением полей, каждая карточка ограничена одним li.
  • Пагинируйте по пути и экспортируйте в оба формата. Trulia использует сегменты страниц /N_p/; перебирайте их, парсите каждую карточку и записывайте результат в JSON и CSV с помощью pandas.
  • Работайте только с публичными данными. Соблюдайте условия использования и robots.txt Trulia, собирайте только публичные поля листингов, помните, что данные MLS часто лицензированы, и никогда не касайтесь аккаунтов, логинов и персональных контактных данных отдельных лиц.

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

Почему обычный запрос не возвращает данные с Trulia?

Потому что Trulia рендерит результаты поиска на стороне клиента с помощью JavaScript. Исходный HTML, это оболочка, которая заполняется только после выполнения скриптов страницы в браузере, поэтому сырой HTTP-запрос возвращает статус 200 с пустыми полями цены, спален, ванных и адреса. Чтобы получить реальные данные, необходимо сначала рендерить страницу, что и делает JS-токен Crawling API.

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

JS-токен. Обычный токен получает статический HTML, который на Trulia является той же пустой оболочкой, что и при обычном запросе. JS-токен рендерит страницу в настоящем браузере перед возвратом HTML, поэтому карточки листингов присутствуют при парсинге BeautifulSoup.

Какие данные можно парсить из листинга Trulia?

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

Как работает пагинация на Trulia?

Trulia использует схему на основе пути, добавляя последовательный сегмент страницы к URL поиска: /1_p/ для первой страницы, /2_p/ для второй и так далее. Функция scrape_pages выше перебирает это число, получает каждую страницу через Crawling API, парсит карточки и останавливается, когда страница не возвращает листингов.

Мои селекторы возвращают None для каждой карточки. Что изменилось?

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

Как парсить другие сайты недвижимости таким же способом?

Тот же паттерн переносится: рендерите страницу, собирайте карточки листингов и сопоставляйте каждое публичное поле с селектором. Специфика различается для каждого сайта, поэтому обратитесь к сопутствующим руководствам по парсингу Zillow и парсингу Realtor.com, а также к ориентированному на аренду руководству по Apartments.com, которые переиспользуют эту же структуру получения и парсинга.

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

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

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

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