Apartments.com, один из крупнейших арендных маркетплейсов в сети, а страницы его объявлений содержат именно те структурированные данные, которые нужны для сравнительного анализа арендных цен, исследования рынка и генерации лидов в сфере недвижимости: название и адрес объекта, ежемесячная арендная плата, количество спален, ванных комнат, площадь и список удобств. Проблема в том, что эти страницы рендерятся на стороне клиента, а сайт жёстко защищается от автоматизированного трафика, поэтому обычный HTTP-запрос возвращает практически пустую оболочку вместо нужного объявления.
Это руководство показывает, как надёжно парсить Apartments.com на Python. Вы создадите небольшой готовый к запуску парсер, который получает отрендеренное объявление через Crawling API, разбирает нужные поля с помощью BeautifulSoup и выводит чистые структурированные данные. Всё руководство ограничивается публичными данными объявлений, а юридический раздел ближе к концу, не формальность, поэтому прочтите его перед запуском на реальных объёмах.
Что вы создадите
Скрипт на Python, который принимает публичный URL объявления Apartments.com, получает отрендеренный HTML через Crawling API и извлекает структурированную запись об объекте. В качестве примера используется одно объявление об аренде, из которого извлекаются следующие поля:
- Название и адрес наименование объекта и его почтовый адрес.
- Аренда ежемесячная арендная плата или диапазон, указанный в объявлении.
- Спальни количество спальных комнат.
- Ванные количество ванных комнат.
- Площадь площадь юнита в квадратных футах.
- Удобства список характеристик, например кондиционер, парковка или стиральная машина в юните.
Почему обычный запрос не работает на Apartments.com
Если запросить URL объявления Apartments.com с помощью простого HTTP-клиента, вы получаете ответ со статусом 200 и практически без данных объявления в теле. Два фактора работают против вас. Во-первых, Apartments.com рендерит большую часть содержимого объявления в браузере с помощью JavaScript, поэтому исходный HTML, это тонкая оболочка, заполняемая только после выполнения скриптов страницы. Во-вторых, сайт быстро помечает автоматизированный трафик: IP датацентров и паттерны запросов, не похожие на настоящий браузер, получают вызов или CAPTCHA ещё до того, как достигают отрендеренного содержимого.
Поэтому работающий парсер Apartments.com в одном запросе требует двух вещей: браузера, который реально рендерит страницу, и IP, который платформа воспринимает как реального посетителя. Можно собрать это самостоятельно с headless-браузером плюс пулом ротирующих резидентских прокси, но сборка и поддержание этой связки составляет большую часть работы. Crawling API объединяет и то, и другое в одном вызове: вы отправляете ему URL с JavaScript-токеном, он рендерит страницу за доверенным IP и возвращает готовый HTML для разбора.
Crawlbase предлагает два типа токенов. Обычный токен получает статичный HTML; JavaScript (JS) токен сначала рендерит страницу в реальном браузере. Apartments.com заполняет поля объявления на стороне клиента, поэтому здесь нужен JS-токен. При использовании обычного токена возвращается та же пустая оболочка, что и при обычном запросе, и из неё нечего извлекать.
Предварительные требования
Перед написанием кода нужно подготовить несколько вещей. Это не займёт много времени.
Базовые знания Python. Вы должны уметь писать и запускать скрипт на Python и устанавливать пакеты через pip. Если язык для вас нов, официальная документация Python и любой вводный курс доведут вас до уровня, который предполагает это руководство.
Python 3.8 или выше. Проверьте версию командой python --version. Если Python не установлен, скачайте его с python.org или через дистрибутив вроде Anaconda.
Аккаунт Crawlbase и JS-токен. Зарегистрируйтесь, откройте панель управления и скопируйте JavaScript (JS) токен со страницы документации аккаунта. Обращайтесь с токеном как с паролем: он аутентифицирует ваши запросы, поэтому не включайте его в систему контроля версий.
Настройка проекта
Создайте виртуальное окружение для изоляции зависимостей проекта, затем установите две библиотеки, которые нужны парсеру.
python --version python -m venv apartments_env source apartments_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте окружение командой apartments_env\Scripts\activate вместо строки с source. Две зависимости выполняют основную работу: crawlbase, официальный клиент Crawling API, а beautifulsoup4 разбирает возвращаемый HTML, позволяя извлекать отдельные поля по CSS-селектору. Если вы ещё не работали с этим парсером, руководство по BeautifulSoup, хорошее дополнение к этому материалу.
Шаг 1: Получить отрендеренное объявление
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с JS-токеном и запросите URL объявления. Проверка кода статуса перед разбором делает сбои заметными, а не молчаливыми.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_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__": page_url = "https://www.apartments.com/2630-n-hamlin-ave-chicago-il/kvl7tm9/" html = crawl(page_url) print(html[:500] if html else "No HTML returned")
Два параметра ожидания важны для цели с клиентским рендерингом. ajax_wait указывает API дождаться завершения загрузки асинхронного содержимого, а page_wait делает дополнительную паузу на фиксированное число миллисекунд после загрузки, чтобы элементы с задержкой рендеринга появились до захвата страницы. Пяти секунд достаточно для начала; увеличьте это значение, если поля объявления возвращаются пустыми. Запустите скрипт командой python scraper.py и вы должны увидеть реальную разметку объявления, а не пустую оболочку, возвращаемую обычным запросом. Это подтверждает работу рендеринга до написания первого селектора.
Apartments.com требует отрендеренной страницы за доверенным IP, и всё это в одном вызове. Crawling API принимает JS-токен, запускает страницу в реальном браузере, ротирует резидентские IP на стороне сервера и возвращает готовый HTML, поэтому вам не нужно запускать собственный headless-флот и пул прокси. Сначала укажите публичное объявление на бесплатном тарифе.
Шаг 2: Разобрать поля объявления с помощью BeautifulSoup
Имея отрендеренный HTML, загрузите его в BeautifulSoup и извлеките каждое поле по его селектору. Apartments.com располагает основные данные объявления в предсказуемой структуре, поэтому вы можете сопоставить название, аренду, спальни, ванные, площадь и удобства с отдельными селекторами. Оберните всё извлечение в вспомогательные функции, возвращающие None или пустой список при отсутствии элемента, чтобы отсутствие одного поля не прерывало выполнение.
from bs4 import BeautifulSoup def text_of(soup, selector): el = soup.select_one(selector) return el.get_text(strip=True) if el else None def detail_at(soup, index): rows = soup.select(".rentInfoDetail") return rows[index].get_text(strip=True) if index < len(rows) else None def scrape_listing(html): soup = BeautifulSoup(html, "html.parser") address = soup.select_one(".propertyAddress") location = ", ".join( s.get_text(strip=True) for s in address.select("span") ) if address else None amenities = [ s.get_text(strip=True) for s in soup.select("#amenitiesSection .specInfo span") ] return { "name": text_of(soup, "#propertyName"), "location": location, "rent": detail_at(soup, 0), "beds": detail_at(soup, 1), "baths": detail_at(soup, 2), "size": detail_at(soup, 3), "amenities": amenities, }
Вспомогательные функции text_of и detail_at делают одно и то же полезное действие в двух формах: запрашивают элемент и возвращают None при его отсутствии вместо выброса исключения. Адрес формируется объединением текста каждого элемента span внутри .propertyAddress, поскольку Apartments.com разбивает улицу, город и штат на отдельные элементы. Удобства возвращаются как список, потому что в объявлении может быть от нуля до десятков пунктов. Такая структура делает извлечение устойчивым к отсутствию поля в конкретном объявлении, что встречается часто: не каждый объект указывает площадь или полный список удобств.
Имена классов Apartments.com (строки rentInfoDetail, обёртка #amenitiesSection, span-элементы адреса) меняются без предупреждения. Воспринимайте приведённые выше селекторы как отправную точку, а не как контракт. Если поле возвращает None или пустой список, повторно проверьте живое объявление в инструментах разработчика браузера и обновите селектор. Периодическое обновление селекторов, норма для любого производственного парсера, а не признак поломки.
Шаг 3: Собрать всё вместе
Теперь свяжите получение данных и разбор в один готовый к запуску скрипт. Получите отрендеренный HTML, передайте его парсеру и выведите структурированную запись.
import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_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 text_of(soup, selector): el = soup.select_one(selector) return el.get_text(strip=True) if el else None def detail_at(soup, index): rows = soup.select(".rentInfoDetail") return rows[index].get_text(strip=True) if index < len(rows) else None def scrape_listing(html): soup = BeautifulSoup(html, "html.parser") address = soup.select_one(".propertyAddress") location = ", ".join( s.get_text(strip=True) for s in address.select("span") ) if address else None amenities = [ s.get_text(strip=True) for s in soup.select("#amenitiesSection .specInfo span") ] return { "name": text_of(soup, "#propertyName"), "location": location, "rent": detail_at(soup, 0), "beds": detail_at(soup, 1), "baths": detail_at(soup, 2), "size": detail_at(soup, 3), "amenities": amenities, } def main(): page_url = "https://www.apartments.com/2630-n-hamlin-ave-chicago-il/kvl7tm9/" html = crawl(page_url) if not html: return data = scrape_listing(html) print(json.dumps(data, indent=2)) if __name__ == "__main__": main()
Как выглядит результат
Запустите полный скрипт командой python scraper.py и получите чистую структурированную запись об объявлении, готовую к записи в JSON, CSV или базу данных.
{ "name": "2630 N Hamlin Ave", "location": "2630 N Hamlin Ave, Chicago, IL, 60647", "rent": "$2,350", "beds": "2 bd", "baths": "1 ba", "size": "1,000 sq ft", "amenities": ["Air Conditioning", "Dishwasher", "Basement", "Laundry Facilities"] }
Масштабирование по объявлениям и пагинации
Одно объявление, это демонстрация; реальная задача охватывает целый поиск. Apartments.com пагинирует результаты поиска, поэтому паттерн состоит из двух уровней: обходите каждую страницу результатов поиска для сбора URL объявлений, а затем получайте каждое объявление через ту же функцию, что вы уже написали. Поскольку каждое объявление имеет одну и ту же структуру, парсер работает со всеми без изменений.
import time def collect_listing_urls(search_html): soup = BeautifulSoup(search_html, "html.parser") cards = soup.select("article.placard a.property-link") return [a["href"] for a in cards if a.get("href")] def scrape_search(base_url, pages): listings = [] for page in range(1, pages + 1): search_html = crawl(f"{base_url}{page}/") if not search_html: continue for url in collect_listing_urls(search_html): html = crawl(url) if html: listings.append(scrape_listing(html)) time.sleep(2) return listings results = scrape_search("https://www.apartments.com/chicago-il/", pages=3) with open("listings.json", "w") as f: json.dump(results, f, indent=2)
Apartments.com добавляет номер страницы к пути поиска, поэтому перебор page обходит набор результатов. time.sleep(2) между получением объявлений намеренный: он задаёт темп, чтобы не нагружать сайт, что является наиболее эффективной привычкой для предотвращения блокировок. Настройте количество страниц и slug города под вашу цель.
Как оставаться незаблокированным
Даже при обработке рендеринга Apartments.com отслеживает трафик, характерный для парсеров. Несколько привычек поддерживают работоспособность сессии и применимы к любой хорошо защищённой коммерческой цели.
-
Регулируйте темп запросов. Быстрый обход страниц объявлений в плотном цикле, самый быстрый способ попасть под ограничения или получить CAPTCHA. Распределяйте запросы, как делает приведённый выше
sleep, и варьируйте цели вместо обхода одного пути на полной скорости. - Полагайтесь на ротацию. Пул резидентских IP распределяет запросы по множеству реальных пользовательских адресов, чтобы ни один не превысил лимит скорости. Crawling API управляет этим за вас; если вы строите свой стек, именно эту часть нужно реализовать правильно.
- Читайте коды статусов. Если сессия начинает возвращать вызовы или ошибки, это сигнал о том, что текущий темп или уровень IP недостаточен. Воспринимайте это как сигнал к снижению активности, а не как шум, который нужно игнорировать.
Более широкий план действий описан в как парсить сайты без блокировок и подробнее в как обходить CAPTCHA при парсинге. Если вы предпочитаете маршрутизировать собственный трафик через ротирующий пул вместо управляемого API, Smart AI Proxy (также называемый AI Proxy) предоставляет ту же ротацию резидентских IP в виде drop-in прокси-эндпоинта.
Законен ли парсинг Apartments.com?
Допустимость парсинга Apartments.com зависит от условий использования Apartments.com, вашей юрисдикции и того, что вы делаете с данными. Условия использования ограничивают автоматизированный доступ, поэтому парсинг может нарушать эти условия независимо от тщательности вашего инструментария. Ни один из приведённых здесь примеров кода этого не меняет; он просто обеспечивает работу технической части. Ознакомьтесь с условиями использования Apartments.com и его robots.txt и воспринимайте оба документа как границу того, что вы собираете.
Несколько принципов, которых стоит придерживаться. Собирайте только публичные данные объявлений: название объекта и адрес, арендную плату, количество спален и ванных, площадь и публично указанный список удобств, которые любой может видеть без аккаунта. Уважайте ожидаемые лимиты скорости Apartments.com и поддерживайте объём запросов достаточно низким, чтобы не перегружать серверы. Избегайте всего, что связано с идентифицируемыми людьми, включая контактные данные арендодателей, агентов или управляющих недвижимостью, указанных на странице. Если вы планируете коммерческое повторное использование или массовый сбор данных, получите разрешение или официальное соглашение, а не исходите из того, что молчание означает согласие.
Это руководство намеренно ограничено публичными страницами объявлений, поскольку именно это позволяет работе оставаться защищаемой. Оно не охватывает данные за логином, сохранённые поиски или данные аккаунтов, персональные или контактные данные людей, страницы, защищённые логином, а также любые попытки обойти аутентификацию. Только публичные данные объявлений. Если вашему проекту нужно больше, лицензионное соглашение или провайдер данных о недвижимости, правильный путь, а не более хитрый парсер.
Ключевые выводы
- Apartments.com рендерится на стороне клиента. Обычный запрос возвращает пустую оболочку, поэтому перед разбором необходимо отрендерить страницу.
-
Нужны одновременно рендеринг и доверенный IP. Crawling API с JS-токеном делает и то, и другое в одном вызове;
ajax_waitиpage_waitуправляют длительностью ожидания содержимого. - BeautifulSoup выполняет извлечение. Сопоставьте название, адрес, аренду, спальни, ванные, площадь и удобства с текущими селекторами и ожидайте их устаревания.
- Масштабируйтесь через пагинацию поиска, затем цикл по объявлениям. Собирайте URL с каждой страницы результатов, получайте каждое объявление тем же парсером и задавайте темп с коротким sleep.
- Оставайтесь в рамках публичных данных. Соблюдайте условия использования Apartments.com и robots.txt, собирайте только публичные поля объявлений и никогда не касайтесь аккаунтов, логинов или персональных контактных данных людей.
Часто задаваемые вопросы
Почему обычный запрос не возвращает данные с Apartments.com?
Потому что Apartments.com рендерит содержимое объявлений на стороне клиента с помощью JavaScript. Исходный HTML, это оболочка, заполняемая только после выполнения скриптов страницы в браузере, поэтому обычный HTTP-запрос возвращает статус 200 с пустыми полями аренды, спален, ванных и удобств. Чтобы получить реальные данные, нужно сначала отрендерить страницу, что и делает за вас JS-токен Crawling API.
Нужен ли для Apartments.com обычный токен или JS-токен?
JS-токен. Обычный токен получает статичный HTML, который на Apartments.com является той же пустой оболочкой, что возвращает обычный запрос. JS-токен рендерит страницу в реальном браузере перед возвратом HTML, поэтому поля объявления присутствуют при разборе BeautifulSoup.
Какие данные можно спарсить из объявления Apartments.com?
Публичные поля объявления: название объекта и почтовый адрес, ежемесячную арендную плату или диапазон, количество спален и ванных, площадь и список удобств. Ограничивайтесь данными, видимыми любому посетителю без аккаунта, и избегайте персональных контактных данных арендодателей, агентов или управляющих недвижимостью, выходящих за рамки публичных объявлений, которые рассматривает это руководство.
Мои селекторы возвращают None или пустой список. Что изменилось?
Скорее всего, разметка Apartments.com. Строки rentInfoDetail, обёртка #amenitiesSection и span-элементы адреса меняются без предупреждения, поэтому работавшие в прошлом месяце селекторы могут сломаться. Повторно проверьте живое объявление в инструментах разработчика браузера и обновите селекторы. Периодическое обновление, норма для любого производственного парсера.
Как обрабатывать пагинацию объявлений в городе?
Apartments.com добавляет номер страницы к пути поиска, поэтому вы последовательно обходите каждую страницу результатов, собираете ссылки на объявления из карточек на ней и получаете каждое объявление тем же парсером. Добавляйте короткий sleep между запросами и останавливайтесь, когда страница не возвращает новых карточек. Функция scrape_search выше показывает полный цикл.
Как избежать блокировок при парсинге Apartments.com?
Поддерживайте низкую скорость запросов на IP, задавайте темп с коротким sleep, варьируйте цели вместо обхода одного пути и маршрутизируйте через ротирующие резидентские IP, чтобы ни один адрес не превысил лимит скорости. Crawling API управляет ротацией и пулом доверенных IP за вас; если вы строите свой стек, именно в эту часть стоит вложиться. Следите за кодами статусов и снижайте активность при появлении вызовов.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
