Публичные листинги объектов недвижимости, одни из наиболее полезных данных в открытом вебе. Цена, количество спален и ванных, площадь и адрес присутствуют прямо на каждой странице результатов, а отслеживание изменения этих цифр во времени говорит о том, где рынок разогревается, где аренда падает и какие объекты оценены неправильно. Загвоздка в том, что одного снимка редко бывает достаточно. Данные о недвижимости ценны только тогда, когда они свежие, а значит, вам нужно собирать их снова и снова, по расписанию, не сидя у терминала и не контролируя каждый запуск.
Это руководство показывает, как автоматизировать извлечение данных о недвижимости с помощью JavaScript и Node.js. Вы создаёте запускаемый рабочий процесс, который получает публичные листинги через Crawling API, разбирает цену, количество спален, ванных, площадь, адрес и ссылку для каждого объекта, а затем автоматизирует всё тремя способами: плановый запуск по cron для регулярного сбора, асинхронный Crawler с вебхуком для больших объёмов и простое хранилище для результатов. Если вам нужен только разовый парсинг одного сайта, специализированные руководства по конкретным сайтам, ссылки на которые приведены ниже, подойдут лучше. Это руководство о том, как запускать задание повторно. Всё здесь ограничено публичными данными листингов, а раздел о законности ближе к концу, не формальность, поэтому прочитайте его, прежде чем применять это к реальным объёмам.
Что вы создадите
Node.js-рабочий процесс, который принимает публичный URL поиска объектов недвижимости, получает отрендеренный HTML через Crawling API, извлекает структурированную запись для каждого листинга на странице и запускается по расписанию. Для каждого объекта мы извлекаем следующие поля:
- Цена, указанная цена, как показана на карточке, например "$2,400/mo" или "$525,000".
- Спальни, количество спальных комнат.
- Ванные, количество ванных комнат.
- Площадь (кв. фут), площадь пола в квадратных футах.
- Адрес, почтовый адрес, указанный в листинге.
- Ссылка, URL страницы конкретного листинга.
Поверх парсера вы подключаете три уровня автоматизации: плановый запуск, асинхронный высоконагруженный путь и JSON-хранилище с временной меткой для каждого пакета, чтобы можно было сравнить один сбор с предыдущим.
Почему обычный запрос не работает на сайтах недвижимости
Если запросить URL поиска объектов обычным HTTP-клиентом, вы редко получите сетку листингов в ответ. Два фактора работают против вас. Во-первых, большинство современных порталов недвижимости рендерят результаты в браузере с помощью JavaScript, поэтому начальный HTML, почти пустая оболочка до выполнения скриптов страницы. Во-вторых, эти сайты агрессивно помечают автоматизированный трафик: IP-адреса датацентров и паттерны запросов, не похожие на действия настоящего браузера, блокируются CAPTCHA, ограничиваются по частоте или вообще блокируются ещё до отрендеренных листингов.
Таким образом, рабочий скрапер недвижимости нуждается в двух вещах в одном запросе: браузере, который действительно рендерит страницу, и IP-адресе, который платформа воспринимает как реального посетителя. Можно собрать это самостоятельно с помощью headless-браузера плюс пула ротируемых резидентных прокси, но соединение этих компонентов и поддержание их в рабочем состоянии, это основная работа, которая усложняется при запуске по расписанию и росте объёма. Crawling API объединяет оба компонента в один вызов: вы отправляете ему URL, он рендерит страницу за доверенным IP и возвращает готовый HTML для разбора с помощью cheerio.
Страницы листингов с интенсивным JavaScript требуют JS-токена, чтобы API запустил настоящий браузер перед возвратом HTML. Crawlbase даёт вам до 20 000 бесплатных запросов для начала, вы платите только за успешные запросы, а обычные запросы и JavaScript-запросы потребляют разное количество кредитов. Начните в бесплатном тарифе и убедитесь, что страница рендерится, прежде чем масштабировать.
Предварительные требования
Перед написанием кода необходимо подготовить несколько вещей. Это не займёт много времени.
Базовые знания JavaScript и Node.js. Вы должны уметь писать и запускать Node-скрипты и устанавливать пакеты с помощью npm. Достаточно уметь читать функции.
Node.js 16 или новее. Проверьте вашу версию с помощью node --version. Если она не установлена, установите её с сайта Node.js или через менеджер версий, например nvm.
Аккаунт и токен Crawlbase. Зарегистрируйтесь, откройте панель управления и скопируйте токен. Бесплатный тариф даёт вам до 20 000 запросов без карты. Обращайтесь с токеном как с паролем: он аутентифицирует ваши запросы, поэтому не включайте его в систему контроля версий.
Настройка проекта
Создайте папку проекта, инициализируйте её и установите библиотеки, необходимые рабочему процессу.
node --version mkdir real-estate-automation && cd real-estate-automation npm init -y npm install crawlbase cheerio node-cron express
Четыре зависимости выполняют основную работу: crawlbase, официальный Node-клиент для Crawling API и асинхронного Crawler, cheerio разбирает возвращённый HTML с помощью jQuery-подобного API, позволяя извлекать поля по CSS-селекторам, node-cron запускает парсинг по расписанию, а express принимает вебхук, который публикует асинхронный Crawler. Создайте файл с именем scraper.js в этой папке и добавьте код из шагов ниже.
Шаг 1: Получение отрендеренной страницы с листингами
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его своим токеном и запросите публичный URL поиска. Поскольку страница рендерится с помощью JavaScript, передайте { ajax_wait: true, page_wait: 3000 }, чтобы API дождался загрузки карточек листингов перед возвратом. Проверка кода статуса перед разбором позволяет явно выявлять ошибки, а не замалчивать их.
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const listingsURL = 'https://www.example-realty.com/homes-for-rent/ca/los-angeles'; api .get(listingsURL, { ajax_wait: true, page_wait: 3000 }) .then((response) => { if (response.statusCode === 200) { console.log(response.body.slice(0, 500)); } }) .catch((error) => console.error('API request error:', error));
Запустите скрипт командой node scraper.js, и вы должны увидеть реальную разметку листингов в начале тела, а не урезанную оболочку. Это подтверждает, что рендеринг работает ещё до написания единого селектора. Замените listingsURL на любой публичный поиск, который хотите отслеживать: по городу, по кварталу, по ценовому диапазону, URL, который сайт показывает при фильтрации поиска в браузере, и есть тот, что вы обходите.
Этот первый запрос только что вернул полностью отрендеренную страницу листингов без headless-браузера или прокси на вашей стороне. Crawling API запускает страницу в настоящем браузере, ротирует через резидентные IP на стороне сервера и обрабатывает CAPTCHA, которыми порталы недвижимости встречают скраперов, так что вы получаете готовый HTML из одного вызова, и тот же вызов выдерживает запуск по cron каждое утро. Направьте его на публичный поиск в бесплатном тарифе.
Шаг 2: Разбор каждого листинга с помощью cheerio
Имея отрендеренный HTML, загрузите его в cheerio и обойдите карточки листингов. Страница результатов располагает каждый объект в повторяющемся контейнере, поэтому вы выбираете каждую карточку, а затем читаете цену, количество спален, ванных, площадь, адрес и ссылку изнутри. Точные селекторы ниже взяты из типичной структуры карточки; вы скорректируете их под целевой сайт, осматривая одну карточку в инструментах разработчика браузера. Защитное чтение каждого поля предотвращает прерывание запуска из-за одного отсутствующего значения, а преобразование цены в число даёт то, что можно сортировать и сравнивать.
const cheerio = require('cheerio'); function parseListings(html) { const $ = cheerio.load(html); const properties = []; $('li[data-testid="listing-card"]').each((i, el) => { const card = $(el); const price = card.find('span.listing-card-price').text().trim(); const priceValue = parseFloat(price.replace(/[^0-9.]/g, '')); const beds = card.find('p:contains("Beds") strong').first().text().trim(); const baths = card.find('p:contains("Baths") strong').first().text().trim(); const sqft = card.find('p:contains("Sq Ft") strong').first().text().trim(); const address = card.find('a.listing-card-address').text().trim(); const href = card.find('a.listing-card-address').attr('href'); const link = href ? new URL(href, 'https://www.example-realty.com').href : ''; if (price && address) { properties.push({ price, priceValue, beds, baths, sqft, address, link }); } }); return properties; }
Несколько деталей обеспечивают точность. Каждая карточка находится внутри повторяющегося контейнера li, цена берётся из спана с ценой и также преобразуется в числовой priceValue для сортировки по возрастанию, а количество спален, ванных и площадь считываются из помеченных блоков с помощью селектора :contains(), устойчивого к небольшим переупорядочиваниям. Якорь адреса одновременно является ссылкой на листинг, поэтому один find даёт и то, и другое, а href разрешается в абсолютный URL, чтобы работать вне страницы. Только карточки с ценой и адресом добавляются в массив, что отсеивает рекламные плитки, которые сайты недвижимости вставляют в сетку результатов.
Имена классов карточек листингов и значения data-testid генерируются и меняются без предупреждения. Относитесь к приведённым выше селекторам как к начальному шаблону, а не контракту. Когда поле возвращается пустым, повторно осмотрите живую страницу в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов, норма для любого промышленного скрапера, а не признак поломки.
Шаг 3: Сборка парсинга и хранение результатов
Теперь соедините получение и разбор в одну функцию, возвращающую чистые записи, а затем записывайте каждый пакет на диск с временной меткой. Хранение каждого запуска в отдельном файле с временной меткой позволяет сравнивать один сбор с предыдущим и наблюдать изменение цен.
const fs = require('fs'); async function scrape(url) { const response = await api.get(url, { ajax_wait: true, page_wait: 3000 }); if (response.statusCode !== 200) { console.error(`Request failed: ${response.statusCode}`); return []; } return parseListings(response.body); } function save(properties) { properties.sort((a, b) => a.priceValue - b.priceValue); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const file = `listings_${timestamp}.json`; fs.writeFileSync(file, JSON.stringify(properties, null, 2)); console.log(`Saved ${properties.length} properties to ${file}`); return file; } async function runOnce() { const url = 'https://www.example-realty.com/homes-for-rent/ca/los-angeles'; const properties = await scrape(url); if (properties.length) save(properties); } module.exports = { scrape, save, runOnce, parseListings };
Вставьте функцию parseListings из шага 2 и настройку API из шага 1 в тот же файл, чтобы scrape мог их вызывать. Запустите node -e "require('./scraper').runOnce()", и вы получите отсортированный JSON-файл с временной меткой, содержащий все публичные листинги на странице. Это единица работы, которую уровни автоматизации ниже планируют и повторяют.
Автоматизация с помощью расписания
Разовый парсинг фиксирует один момент. Данные о недвижимости ценны только тогда, когда они актуальны, поэтому первый уровень автоматизации, регулярный запуск. С помощью node-cron вы держите процесс запущенным и вызываете runOnce по cron-выражению. Пример ниже запускается каждое утро в 7:00.
const cron = require('node-cron'); const { runOnce } = require('./scraper'); // Minute Hour DayOfMonth Month DayOfWeek cron.schedule('0 7 * * *', async () => { console.log(`Scheduled run at ${new Date().toISOString()}`); try { await runOnce(); } catch (error) { console.error('Scheduled run failed:', error.message); } }); console.log('Scheduler started. Waiting for the next run...');
Запустите его командой node schedule.js и оставьте работать на небольшом сервере или в контейнере. Каждое утро он парсит поиск и создаёт свежий файл с временной меткой, накапливая историю, по которой можно отслеживать изменения цен, новые листинги и объекты, исчезнувшие с рынка. Если вы предпочитаете не держать процесс запущенным, тот же вызов runOnce работает из записи системного cron или любого планировщика заданий; node-cron, просто внутрипроцессный вариант. Паттерн идентичен паттерну из руководства как автоматизировать парсинг Amazon, где расписание превращает разовый парсинг в пайплайн отслеживания.
Масштабирование с асинхронным Crawler и вебхуком
Плановый цикл хорош для нескольких поисков. Как только вы отслеживаете десятки городов или тысячи страниц листингов, ожидание каждого синхронного запроса по очереди становится медленным, а долгоживущий процесс, ненадёжным местом для хранения такого объёма работы. Асинхронный Crawler создан для этого: вы отправляете ему URL, Crawlbase получает и рендерит их на собственной инфраструктуре, а затем публикует каждую готовую страницу на вебхук, который вы хостите. Ваш код перестаёт ждать запросов и просто обрабатывает результаты по мере их поступления.
Сначала создайте небольшой эндпоинт, принимающий коллбэки. Crawler публикует отрендеренный HTML на него, поэтому разбор и хранение происходят прямо в обработчике.
const express = require('express'); const { parseListings, save } = require('./scraper'); const app = express(); app.use(express.text({ type: '*/*', limit: '10mb' })); app.post('/crawlbase-webhook', (req, res) => { const html = req.body; const properties = parseListings(html); if (properties.length) save(properties); res.sendStatus(200); }); app.listen(3000, () => console.log('Webhook listening on :3000'));
Затем отправьте URL поиска в Crawler, указав вебхук в качестве коллбэка. Crawler ставит каждый в очередь, рендерит и вызывает ваш эндпоинт с результатом, чтобы вы могли отправить большой пакет и дать ответам поступать потоком.
const { Crawler } = require('crawlbase'); const crawler = new Crawler({ token: 'YOUR_CRAWLBASE_TOKEN' }); const searches = [ 'https://www.example-realty.com/homes-for-rent/ca/los-angeles', 'https://www.example-realty.com/homes-for-rent/ca/san-diego', 'https://www.example-realty.com/homes-for-rent/ca/san-francisco', ]; for (const url of searches) { crawler.post( url, { callback: 'true', callback_url: 'https://your-server.com/crawlbase-webhook' }, { ajax_wait: true, page_wait: 3000 } ); }
Разделение, в этом и состоит суть. Crawler берёт на себя медленную, блокирующую часть, рендеринг и повторные попытки, на стороне Crawlbase, а ваш вебхук выполняет только быстрый шаг разбора и сохранения. Это разделение позволяет одному рабочему процессу перейти от трёх поисков к трём тысячам без удержания каждого запроса открытым. Вашему вебхуку нужен публичный URL во время разработки; инструмент туннелирования открывает доступ к localhost:3000, чтобы Crawler мог его достигнуть.
Как выглядит результат
Независимо от того, поступает ли запись из планового запуска или асинхронного вебхука, каждый пакет имеет одинаковую форму: один объект на листинг, отсортированный по возрастанию цены, с ценой, количеством спален, ванных, площадью, адресом и ссылкой.
[ { "price": "$2,400/mo", "priceValue": 2400, "beds": "2", "baths": "1", "sqft": "850", "address": "1234 Sunset Blvd, Los Angeles, CA 90026", "link": "https://www.example-realty.com/property/1234-sunset-blvd" }, { "price": "$3,150/mo", "priceValue": 3150, "beds": "3", "baths": "2", "sqft": "1,320", "address": "88 Maple Ave, Los Angeles, CA 90042", "link": "https://www.example-realty.com/property/88-maple-ave" } ]
Поскольку каждый запуск попадает в отдельный файл с временной меткой, сравнение двух пакетов, это разность множеств по полю link для новых и удалённых листингов, и соединение по link со сравнением priceValue для изменений цены. Эта разность, главная причина автоматизации: один парсинг говорит вам о рынке сегодня, плановая история говорит о том, куда он движется. Если вы хотите получать те же записи в таблице, старая версия этого рабочего процесса записывала напрямую в Excel с помощью ExcelJS, и добавить этот экспорт обратно, несколько строк поверх JSON-хранилища.
Сохранение незаблокированности при больших объёмах
Даже с обработанным рендерингом порталы недвижимости следят за трафиком, похожим на скраперы, а расписание, срабатывающее каждый день, делает паттерны легко заметными. Несколько привычек помогают поддерживать запуск в рабочем состоянии.
- Соблюдайте темп запросов. Распределяйте загрузки во времени, а не обрушивайтесь на страницы в плотном цикле. При парсинге многих поисков добавляйте задержку между ними или используйте асинхронный Crawler, который ставит работу в очередь и управляет темпом за вас.
- Используйте ротацию. Пул резидентных IP распределяет запросы по множеству адресов реальных пользователей, чтобы ни один из них не превысил лимит или CAPTCHA. Crawling API и асинхронный Crawler делают это за вас; если вы используете собственный стек, именно эта часть требует правильной реализации.
- Читайте коды статуса. Запуск, который начинает возвращать запросы верификации или не-200 ответы, сигнализирует, что текущая частота запросов или уровень IP больше не достаточны. Воспринимайте это как сигнал к снижению нагрузки, а не как шум для игнорирования.
Более детальный план действий см. в статье как парсить сайты без блокировок. Если вам нужно руководство по конкретному сайту, а не этот акцент на автоматизации, специализированные руководства по парсингу Zillow и парсингу Redfin охватывают конкретные структуры карточек и пагинацию этих порталов.
Законно ли парсить данные о недвижимости?
Разрешён ли парсинг сайта недвижимости, зависит от условий использования этого сайта, вашей юрисдикции и того, что вы делаете с данными. Большинство порталов ограничивают автоматический доступ в своих условиях, поэтому парсинг может нарушать эти условия независимо от того, насколько аккуратен ваш инструментарий. Ни один из приведённых здесь кодов это не изменяет; он просто делает техническую часть рабочей. Прочитайте Условия использования сайта и его файл robots.txt, и относитесь к обоим как к границе того, что вы собираете и как часто запрашиваете. Расписание делает соблюдение темпа важнее, а не менее важным.
Ограничивайте работу только публичными данными листингов: цена, количество спален, ванных, площадь, адрес и ссылка на листинг, которые может видеть любой на публичной странице результатов без аккаунта. Не собирайте персональные данные об агентах, владельцах или покупателях сверх того, что уже показывает публичный бизнес-листинг, и не создавайте профили людей на основе этих данных. GDPR и CCPA применяются в момент, когда персональные данные попадают в картину, а публичный адрес улицы, прикреплённый к имени человека, может под них подпадать. Поэтому опирайтесь на факты об объекте и избегайте данных о людях. Не распространяйте защищённые авторским правом медиафайлы портала, например фотографии листингов, как если бы они были вашими, и не трогайте ничего за логином.
Один момент, специфичный для этой отрасли: большая часть наиболее ценных данных о недвижимости поступает из MLS, а доступ к базам MLS почти всегда лицензируется, а не является свободным. Если вашему проекту нужны исчерпывающие, точные, redistributable данные листингов, правильный путь, лицензированный канал или официальный API, а не скрапер. Несколько крупных порталов ведут партнёрские программы или API для разработчиков именно по этой причине. Используйте их, когда вам нужен объём, гарантированная структура или коммерческие права. Это руководство намеренно ограничено публичными листингами на публичных страницах поиска, поскольку именно это делает работу защищённой.
Ключевые выводы
- Данные о недвижимости ценны только тогда, когда они свежие. Один парсинг, это снимок; автоматизация запуска по расписанию превращает его в историю, по которой можно отслеживать изменения цен и новые листинги.
- Рендерите за доверенным IP перед разбором. Порталы рендерят листинги на стороне клиента и жёстко блокируют, поэтому обычный запрос возвращает пустую оболочку или CAPTCHA; Crawling API рендерит страницу и ротирует резидентные IP в одном вызове.
- cheerio извлекает поля. Выбирайте каждую карточку листинга, затем читайте цену, количество спален, ванных, площадь, адрес и ссылку, преобразуя цену в число для сортировки и сравнения; ожидайте, что генерируемые имена классов будут меняться.
- Масштабируйте с асинхронным Crawler и вебхуком. Отправляйте URL в Crawler, дайте ему рендерить на стороне Crawlbase и пусть он публикует готовые страницы на ваш эндпоинт, чтобы рабочий процесс перешёл от трёх поисков к тысячам без удержания запросов открытыми.
- Ограничивайтесь публичными данными. Соблюдайте ToS и robots.txt каждого сайта, придерживайтесь публичных фактов об объекте и избегайте персональных данных; предпочитайте лицензированный канал MLS или официальный API для исчерпывающего или коммерческого использования.
Часто задаваемые вопросы
Как автоматизировать извлечение данных о недвижимости по расписанию?
Оберните парсинг в функцию и вызывайте её из планировщика. Простейший внутрипроцессный вариант, node-cron: задайте cron-выражение вроде 0 7 * * *, и оно будет вызывать вашу функцию runOnce каждое утро. Каждый запуск создаёт свежий файл с временной меткой, и вы накапливаете историю для сравнения. Если вы предпочитаете не держать процесс Node запущенным, та же функция работает из записи системного cron или любого планировщика заданий.
Когда использовать асинхронный Crawler вместо Crawling API?
Используйте синхронный Crawling API, когда вы парсите несколько поисков и хотите получить результат в том же вызове. Переключайтесь на асинхронный Crawler, когда вы отслеживаете десятки городов или тысячи страниц листингов: вы отправляете URL, Crawlbase рендерит их на собственной инфраструктуре и публикует каждую готовую страницу на вебхук, который вы хостите. Это разделение не позволяет вашему процессу ждать каждый медленный запрос по очереди.
Почему обычный запрос возвращает неполные данные с сайтов недвижимости?
Потому что большинство порталов рендерит свою сетку листингов на стороне клиента с помощью JavaScript и блокирует автоматизированный трафик CAPTCHA. Сырой HTTP-запрос с IP датацентра обычно возвращает пустую оболочку или страницу блокировки, а не карточки объектов. Чтобы получить полную страницу, нужно отрендерить её за доверенным IP, что и делает за вас Crawling API при передаче JavaScript-опций.
Какие поля можно извлечь из публичного листинга объекта?
Публичные факты на карточке результатов: цена, количество спален, количество ванных, площадь, адрес улицы и ссылка на полный листинг. Это руководство разбирает именно эти поля. Избегайте персональных данных об агентах, владельцах или покупателях, а также защищённых авторским правом медиафайлов, таких как фотографии листингов, которые несут юридические и лицензионные ограничения, не применимые к публичным фактам об объекте.
Мои селекторы возвращают пустые значения. Что изменилось?
Почти наверняка разметка сайта. Имена классов карточек листингов и значения data-testid генерируются и меняются без предупреждения, поэтому селекторы, которые работали в прошлом месяце, могут сломаться, особенно при запуске по расписанию без присмотра. Повторно осмотрите живую карточку в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов, норма для любого промышленного скрапера.
Что лучше: парсить или использовать канал MLS?
Для исчерпывающих, точных, redistributable данных листингов лицензированный канал MLS или официальный API портала является правильным инструментом, поскольку данные MLS почти всегда лицензируются, а не свободны для взятия. Парсинг публичных страниц поиска подходит для отслеживания публичных фактов листингов при умеренных объёмах, исследований и анализа ценовых движений в рамках публичных данных и условий каждого сайта. Выбирайте источник под использование: публичные факты и небольшой объём, в пользу скрапера, исчерпывающее или коммерческое использование, в пользу лицензированного канала.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
