Парсер ЦИАН своими руками: пошаговый гайд по сбору объектов, цен и истории изменений
Содержание статьи
- Введение: что вы получите в результате
- Предварительная подготовка: инструменты и окружение
- Базовые понятия: что нужно понимать перед началом
- Шаг 1: определяем цель и структуру данных
- Шаг 2: изучаем структуру страницы выдачи
- Шаг 3: пишем первый парсер циан для страницы выдачи
- Шаг 4: собираем карточки объектов
- Шаг 5: подключаем мобильные прокси и делаем сбор устойчивым
- Шаг 6: сохраняем данные и строим историю цен
- Шаг 7: автоматизируем запуск и расширяем на другие площадки
- Проверка результата: чек-лист готового парсера
- Типичные ошибки и решения
- Дополнительные возможности для продвинутых
- Faq: частые вопросы по созданию парсера недвижимости
- Заключение
Введение: что вы получите в результате
Рынок недвижимости живет цифрами. Кто-то ищет квартиру дешевле рынка, кто-то оценивает конкурентов в новостройках, кто-то строит отчеты для инвесторов. Общее у них одно: нужны актуальные данные по объектам и ценам, а вручную собрать их невозможно. Именно здесь пригодится собственный парсер ЦИАН.
В этом руководстве вы с нуля соберете рабочий инструмент, который умеет три вещи. Первое: обходить страницы выдачи по заданному фильтру и собирать список объектов с ценой, адресом, площадью и ссылкой. Второе: заходить в карточку каждого объекта и вытаскивать подробности вроде этажа, года постройки и типа дома. Третье, самое ценное: сохранять данные в базу и день за днем накапливать историю цен, чтобы вы видели, какие объекты подешевели, какие сняты с продажи и как двигается рынок в конкретном районе.
Итоговый результат выглядит так: у вас есть папка с Python-скриптами, файл базы данных SQLite и таблица, которую можно открыть в Excel или Google Sheets. Запускаете скрипт утром, получаете свежий срез. Через неделю запусков у вас уже есть динамика.
Для кого этот гайд
- Для маркетологов и аналитиков агентств недвижимости, которым нужен мониторинг цен по районам без покупки дорогих отчетов.
- Для арбитражников и владельцев бизнеса, которые ищут ниши и хотят понимать спрос и предложение в цифрах.
- Для начинающих разработчиков, которые хотят освоить парсинг на живом и понятном примере.
- Для инвесторов и частных покупателей, желающих ловить объекты со снижением цены раньше других.
Что нужно знать заранее
Опыт программирования не обязателен. Мы разберем каждую строку кода и объясним, зачем она нужна. Достаточно уметь устанавливать программы, открывать командную строку и копировать текст. Если вы хотя бы раз открывали инструменты разработчика в браузере, будет совсем легко. Если нет, мы покажем, где они находятся.
Единственное требование: внимательность. Парсинг чувствителен к опечаткам в названиях классов и адресах. Одна лишняя буква, и скрипт вернет пустой список. Не пугайтесь: у каждого шага есть проверочная точка, где вы убедитесь, что все идет по плану.
Сколько времени потребуется
Подготовка окружения занимает около 30 минут. Первый рабочий парсер выдачи вы напишете за час. Карточки объектов, прокси и база данных потребуют еще полтора-два часа. Итого от трех до четырех часов чистого времени, если идти без спешки. История цен начнет накапливаться сама, начиная со второго запуска.
Предварительная подготовка: инструменты и окружение
Прежде чем писать код, соберем все необходимое. Пропускать этот раздел не стоит: половина проблем у новичков возникает из-за неправильно установленного Python или отсутствующих библиотек.
Системные требования
- Компьютер на Windows 10 или 11, macOS или Linux. Подойдет любой ноутбук последних восьми лет.
- Минимум 4 ГБ оперативной памяти. Для варианта с браузерной автоматизацией желательно 8 ГБ.
- Около 2 ГБ свободного места на диске под Python, библиотеки и базу данных.
- Стабильное интернет-соединение.
Что нужно установить
- Python 3.11 или новее. Скачайте установщик с официального сайта python.org. На Windows при установке обязательно поставьте галочку Add Python to PATH внизу первого экрана. Без этого команда python не будет работать в терминале. На macOS Python часто уже есть, но лучше поставить свежую версию.
- Редактор кода. Рекомендуем Visual Studio Code: бесплатный, подсвечивает синтаксис и показывает ошибки. Установите расширение Python из встроенного магазина расширений (иконка с четырьмя квадратами на левой панели).
- Браузер Chrome или Edge. Нам понадобятся инструменты разработчика для изучения структуры страниц.
- Библиотеки Python. Установим их через терминал чуть ниже.
- Доступ к мобильным прокси. Понадобится на пятом шаге. Вам нужны адрес сервера, порт, логин, пароль и ссылка для смены IP-адреса. Все это выдается в личном кабинете сервиса при покупке. Если у вас пока нет прокси, первые шаги можно выполнить и без них.
Создаем рабочую папку и виртуальное окружение
- Создайте на диске папку с именем cian_parser. Избегайте русских букв и пробелов в пути: они иногда ломают инструменты.
- Откройте терминал. На Windows нажмите Win+R, введите cmd и нажмите Enter. На macOS откройте Terminal через Spotlight.
- Перейдите в папку командой cd и путем к ней, например:
cd C:\projects\cian_parserна Windows илиcd ~/projects/cian_parserна macOS. - Создайте виртуальное окружение командой
python -m venv venv. Это изолированная копия Python, чтобы библиотеки проекта не конфликтовали с системными. - Активируйте окружение. На Windows:
venv\Scripts\activate. На macOS и Linux:source venv/bin/activate. В начале строки терминала появится надпись (venv). - Установите библиотеки одной командой:
pip install requests beautifulsoup4 lxml pandas openpyxl. Установка займет одну-две минуты.
Проверка: введите в терминале python -c "import requests, bs4, pandas; print('ok')". Если на экране появилось слово ok без ошибок, окружение готово. Если вы видите ModuleNotFoundError, окружение не активировано или установка прервалась. Активируйте venv заново и повторите pip install.
Резервные копии
В этом проекте главная ценность не код, а накопленная база с историей цен. Ее нельзя восстановить: прошлые цены больше нигде не появятся. Поэтому с первого дня договоритесь с собой: файл базы данных копируется в облако или на внешний диск минимум раз в неделю. Позже мы добавим автоматическое копирование в скрипт.
Базовые понятия: что нужно понимать перед началом
Разберем термины, которые будут встречаться дальше. Если вы уже знакомы с парсингом, пролистайте раздел, но обратите внимание на юридический блок.
Ключевые термины простым языком
- Парсинг (скрапинг): автоматическое получение страницы сайта программой и извлечение из нее нужных данных. То же самое, что вы делаете глазами, но делает скрипт и в тысячу раз быстрее.
- HTML: язык разметки, из которого состоит любая веб-страница. Цена квартиры на ЦИАН лежит внутри HTML-тега с определенными атрибутами, и наша задача найти этот тег.
- Селектор: адрес элемента внутри HTML. Например, span с атрибутом data-mark равным MainPrice. По селектору парсер понимает, откуда брать цену.
- HTTP-запрос: обращение к серверу сайта. Браузер делает его, когда вы открываете страницу. Библиотека requests делает то же самое из кода.
- Заголовки запроса (headers): служебная информация, которую браузер отправляет вместе с запросом: тип браузера, язык, форматы данных. Сервер по ним решает, что отдать.
- Прокси: промежуточный сервер, через который проходят ваши запросы. Мобильные прокси используют IP-адреса сотовых операторов и позволяют менять адрес по команде.
- Пагинация: разбиение выдачи на страницы. Чтобы собрать все объекты, парсер должен пройти страницы с первой до последней.
- SQLite: легкая база данных в одном файле. Не требует установки сервера, встроена в Python. Идеальна для истории цен.
Как устроена выдача на площадках недвижимости
ЦИАН, Домклик, Яндекс Недвижимость и другие площадки работают по схожему принципу. Есть страница поиска с фильтрами: город, тип сделки, количество комнат, диапазон цены. Каждый фильтр превращается в параметр в адресной строке. Например, параметр deal_type со значением sale означает продажу, а room1 равный 1 добавляет однокомнатные квартиры. Понимание этих параметров дает вам мощный инструмент: вместо кликов по сайту вы просто формируете нужный адрес.
Внутри выдачи каждый объект представлен карточкой: заголовок, цена, адрес, несколько фото, ссылка на подробную страницу. Подробная страница содержит полные характеристики и часто дублирует все данные в скрытом блоке JSON, который сайт использует для отрисовки интерфейса. Этот блок парсить гораздо удобнее, чем HTML.
Юридические и этические рамки
Внимание: собирайте только общедоступные сведения об объектах: цену, площадь, адрес, характеристики дома. Не собирайте и не храните телефоны, имена и другие персональные данные продавцов и агентов: это регулируется законом о персональных данных, и нарушение грозит реальной ответственностью. Изучите пользовательское соглашение площадки перед началом работы и используйте данные для собственной аналитики, а не для перепродажи или создания копии сайта. Соблюдайте разумную частоту запросов: ваш парсер не должен создавать нагрузку, мешающую работе сервиса.
Такой подход не только законен, но и практичен. Аккуратный парсер с паузами и ротацией адресов работает месяцами, а агрессивный получает временные ограничения уже через час.
Шаг 1: Определяем цель и структуру данных
Цель этапа: четко описать, что именно мы собираем и как это будет храниться. Без этого шага вы напишете парсер, который тянет все подряд, а потом неделю разбираетесь в свалке данных.
- Сформулируйте бизнес-вопрос. Примеры: какие однокомнатные квартиры в Санкт-Петербурге подешевели за месяц более чем на 5 процентов; сколько стоит квадратный метр в новостройках конкретного района; как быстро уходят объекты дешевле определенной суммы.
- Определите фильтр выдачи. Для примера в этом гайде возьмем: продажа, вторичка, одно- и двухкомнатные квартиры, Москва, цена до 15 миллионов рублей. Вы подставите свои параметры.
- Составьте список полей. Для каждого объекта нам нужны: уникальный идентификатор объявления, ссылка, заголовок, цена, адрес, площадь общая, этаж и этажность, тип дома, год постройки, дата первого обнаружения, дата последней проверки. Для истории цен: идентификатор объявления, дата, цена.
- Откройте редактор кода и создайте в папке проекта файл config.py. Запишите в него параметры, которые будем менять чаще всего:
BASE_URL = 'https://www.cian.ru/cat.php'
SEARCH_PARAMS = {'deal_type': 'sale', 'engine_version': 2, 'offer_type': 'flat', 'region': 1, 'room1': 1, 'room2': 1, 'maxprice': 15000000}
MAX_PAGES = 5
PAUSE_MIN = 4
PAUSE_MAX = 9
DB_PATH = 'realty.db'Обратите внимание: в примере кода переносы строк обозначены символами новой строки, в редакторе просто пишите каждую переменную с новой строки. Параметр region равный 1 соответствует Москве, 2 соответствует Санкт-Петербургу. Коды других регионов вы найдете, применив фильтр на сайте и посмотрев адресную строку.
Совет: начните с MAX_PAGES равным 2-3. На каждой странице выдачи около 28 объектов, для отладки этого достаточно. Полный сбор запустите, когда убедитесь, что все поля извлекаются корректно.
Проверка: у вас есть файл config.py, а в блокноте или в голове зафиксирован список из 12 полей и один конкретный бизнес-вопрос. Если вопрос звучит как хочу все данные по всей России, вернитесь и сузьте его: полный сбор по стране это сотни тысяч объектов и совершенно другая инфраструктура.
Возможные проблемы
Не получается понять, какой параметр отвечает за нужный фильтр. Решение: откройте сайт, выставьте фильтр вручную, скопируйте адрес из адресной строки и разберите его по символам амперсанда. Каждая пара ключ равно значение и есть параметр.
Шаг 2: Изучаем структуру страницы выдачи
Цель этапа: найти в HTML те элементы, из которых будем брать цену, заголовок, адрес и ссылку. Это самый исследовательский шаг, и именно здесь новички чаще всего теряются, поэтому идем очень медленно.
- Откройте браузер и перейдите на страницу выдачи ЦИАН с вашими фильтрами. Убедитесь, что видите список квартир.
- Наведите курсор на цену любой квартиры, нажмите правую кнопку мыши и выберите пункт Просмотреть код (в Edge он называется Проверить). Откроется панель инструментов разработчика, а в ней подсветится элемент с ценой.
- Посмотрите на подсвеченную строку. На момент написания гайда это тег span с атрибутом data-mark равным MainPrice. Запишите этот атрибут: он и станет селектором для цены.
- Поднимайтесь выше по дереву элементов, кликая на родительские теги, пока не найдете тег, который охватывает всю карточку объекта целиком. Обычно это article с атрибутом data-name равным CardComponent. Когда вы наводите на него курсор в панели, на странице подсвечивается вся карточка с фото и ценой.
- Внутри карточки найдите заголовок (span с data-mark равным OfferTitle), адрес (несколько ссылок a с data-name равным GeoLabel, из которых складывается адрес) и ссылку на объект (тег a с href, ведущим на адрес вида cian.ru/sale/flat/номер). Запишите все четыре селектора.
- Найдите блок пагинации внизу страницы. Пролистайте выдачу до конца, кликните правой кнопкой на номер второй страницы и посмотрите, как выглядит ее адрес. Вы увидите параметр p равный 2. Значит, для перехода по страницам достаточно менять этот параметр.
Внимание: названия атрибутов data-mark и data-name на площадках периодически меняются при обновлении дизайна. Не копируйте селекторы из этого текста слепо: обязательно сверьте их с реальной страницей в панели разработчика. Умение самостоятельно найти селектор важнее любого готового списка.
Проверяем, есть ли скрытый JSON
Многие площадки хранят данные выдачи в готовом виде внутри тега script. Это удобнее HTML: не нужно склеивать адрес из кусочков.
- В панели разработчика нажмите Ctrl+F (на macOS Cmd+F) и введите слово offers или initialState.
- Если поиск нашел тег script с большим объемом текста, похожего на словарь с фигурными скобками, значит данные есть в JSON. Запомните имя переменной в начале этого блока.
- Если ничего не нашлось, не страшно: HTML-подход из следующего шага работает в любом случае.
Совет: откройте вкладку Network (Сеть) в панели разработчика, обновите страницу и отфильтруйте запросы по типу Fetch/XHR. Иногда сайт подгружает выдачу отдельным запросом в формате JSON. Если вы видите такой запрос с полем offers, парсить его проще всего: вы получаете чистые данные без HTML.
Проверка: у вас записаны селекторы для карточки, цены, заголовка, адреса и ссылки, вы знаете имя параметра пагинации. Кликнув по каждому селектору в панели, вы видите подсветку нужного элемента на странице.
Возможные проблемы
Панель разработчика показывает HTML, но там нет цены. Причина: сайт отрисовывает часть данных скриптом после загрузки. Решение: на вкладке Network проверьте, приходит ли цена отдельным запросом, либо используйте браузерную автоматизацию из раздела для продвинутых.
Шаг 3: Пишем первый парсер ЦИАН для страницы выдачи
Цель этапа: получить скрипт, который скачивает страницу выдачи, извлекает список объектов и выводит их в консоль. После этого шага у вас будет рабочий каркас, на который мы будем наращивать функции.
- Создайте в папке проекта файл parser.py.
- Импортируйте библиотеки и настройки в начале файла:
import time
import random
import requests
from bs4 import BeautifulSoup
from config import BASE_URL, SEARCH_PARAMS, MAX_PAGES, PAUSE_MIN, PAUSE_MAX- Опишите заголовки запроса. Сервер должен видеть в них обычный браузер с русской локалью, иначе вы можете получить не ту версию страницы:
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36', 'Accept-Language': 'ru-RU,ru;q=0.9', 'Accept': 'text/html,application/xhtml+xml'}- Напишите функцию загрузки страницы. Она принимает номер страницы, добавляет его к параметрам и возвращает HTML. Обязательно проверяем код ответа: 200 значит успех, все остальное сигнал остановиться и разобраться:
def fetch_page(page, session):
params = dict(SEARCH_PARAMS)
params['p'] = page
resp = session.get(BASE_URL, params=params, headers=HEADERS, timeout=30)
if resp.status_code != 200:
print('Статус', resp.status_code, 'на странице', page)
return None
return resp.text- Напишите функцию разбора. Она находит все карточки и для каждой вытаскивает поля. Обратите внимание на конструкцию с if: если элемента нет, мы не падаем с ошибкой, а записываем None:
def parse_cards(html):
soup = BeautifulSoup(html, 'lxml')
cards = soup.select('article[data-name=CardComponent]')
result = []
for card in cards:
link_tag = card.select_one('a[href*=/sale/flat/]')
price_tag = card.select_one('span[data-mark=MainPrice]')
title_tag = card.select_one('span[data-mark=OfferTitle]')
geo_tags = card.select('a[data-name=GeoLabel]')
if not link_tag or not price_tag:
continue
url = link_tag.get('href')
offer_id = url.rstrip('/').split('/')[-1]
price_text = price_tag.get_text()
price = int(''.join(ch for ch in price_text if ch.isdigit()))
title = title_tag.get_text(strip=True) if title_tag else None
address = ', '.join(g.get_text(strip=True) for g in geo_tags)
result.append({'offer_id': offer_id, 'url': url, 'title': title, 'price': price, 'address': address})
return result- Соберите главный цикл. Он проходит страницы, делает случайную паузу между запросами и складывает результаты в общий список. Случайная пауза важна: равные интервалы выглядят неестественно и создают пиковую нагрузку:
def collect_listing():
session = requests.Session()
all_offers = []
for page in range(1, MAX_PAGES + 1):
html = fetch_page(page, session)
if html is None:
break
offers = parse_cards(html)
print('Страница', page, 'объектов:', len(offers))
if not offers:
break
all_offers.extend(offers)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return all_offers
if __name__ == '__main__':
data = collect_listing()
for item in data[:5]:
print(item)
print('Всего собрано:', len(data))- Сохраните файл и запустите в терминале командой
python parser.py. Убедитесь, что окружение venv активно.
Разберем ключевые места. Session сохраняет cookies между запросами, поэтому сайт видит последовательное поведение одного посетителя, а не десяток разрозненных обращений. Функция select_one возвращает первый подходящий элемент или None, поэтому мы всегда проверяем результат перед вызовом get_text. Идентификатор объявления мы берем из ссылки: это последний фрагмент адреса, число вроде 312456789. Именно оно станет ключом для истории цен.
Совет: на этапе отладки сохраняйте HTML первой страницы в файл командой open('page1.html', 'w', encoding='utf-8').write(html). Тогда вы сможете отлаживать функцию разбора на локальной копии, не отправляя лишние запросы на сайт.
П��оверка: в консоли вы видите строки Страница 1 объектов: 28, Страница 2 объектов: 28 и далее, а внизу пять словарей с реальными ценами и адресами. Цены должны быть целыми числами без пробелов и знака рубля. Если вместо чисел пусто, вернитесь к селекторам из второго шага.
Возможные проблемы
Скрипт печатает объектов: 0 на первой странице. Причины: изменился селектор карточки либо сервер отдал страницу-заглушку. Откройте сохраненный page1.html в браузере и посмотрите, что вы получили. Если это страница с просьбой подтвердить, что вы не робот, увеличьте паузы и переходите к пятому шагу с прокси. Если это нормальная выдача, сверьте селекторы.
Ошибка ValueError при преобразовании цены. Причина: в тексте цены нет цифр, например написано Цена по запросу. Решение: оберните преобразование в try и записывайте None для таких случаев.
Шаг 4: Собираем карточки объектов
Цель этапа: научить парсер заходить на страницу каждого объекта и извлекать подробные характеристики: площадь, этаж, тип дома, год постройки. Эти поля нужны для расчета цены за квадратный метр и сравнения похожих квартир.
- Откройте в браузере страницу любого объекта из выдачи. Нажмите Ctrl+U, чтобы посмотреть исходный код страницы.
- Нажмите Ctrl+F и введите слово totalArea. На момент написания гайда данные карточки лежат в теге script внутри объекта конфигурации фронтенда, в ключе с названием вида frontend-offer-card. Вы увидите поля totalArea, floorNumber, floorsCount, buildYear, materialType и другие.
- Если поиск по totalArea ничего не дал, ищите характеристики в HTML: обычно это блок с парами название и значение, например Общая площадь и 38,5 м². Запишите селектор этого блока.
- Добавьте в parser.py функцию извлечения JSON из карточки. Мы находим нужный script, вырезаем из него объект по фигурным скобкам и разбираем модулем json:
import json
import re
def parse_offer_page(html):
soup = BeautifulSoup(html, 'lxml')
details = {}
for script in soup.find_all('script'):
text = script.string or ''
if 'totalArea' in text and 'offerData' in text:
m = re.search(r'totalArea[^0-9]*([0-9.,]+)', text)
if m:
details['area'] = float(m.group(1).replace(',', '.'))
m = re.search(r'floorNumber[^0-9]*([0-9]+)', text)
if m:
details['floor'] = int(m.group(1))
m = re.search(r'floorsCount[^0-9]*([0-9]+)', text)
if m:
details['floors_total'] = int(m.group(1))
m = re.search(r'buildYear[^0-9]*([0-9]{4})', text)
if m:
details['build_year'] = int(m.group(1))
break
return details- Здесь мы намеренно используем регулярные выражения вместо полного разбора JSON. Причина простая: скрипт на странице содержит не только JSON, но и код, и выделить чистый объект бывает непросто. Регулярные выражения ищут ключ и первое число после него, что достаточно надежно для числовых полей.
- Добавьте функцию, которая берет список объектов из выдачи и обогащает каждый данными карточки. Паузы здесь еще важнее, потому что запросов становится в 28 раз больше:
def enrich_offers(offers, session):
for i, offer in enumerate(offers, 1):
try:
resp = session.get(offer['url'], headers=HEADERS, timeout=30)
if resp.status_code == 200:
offer.update(parse_offer_page(resp.text))
else:
print('Карточка', offer['offer_id'], 'статус', resp.status_code)
except requests.RequestException as e:
print('Ошибка сети на', offer['offer_id'], e)
if i % 10 == 0:
print('Обработано карточек:', i)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return offers- В блоке if __name__ после collect_listing добавьте вызов enrich_offers(data, requests.Session()) и запустите скрипт заново с MAX_PAGES равным 1, чтобы не ждать долго.
Сколько это займет: 28 карточек при средней паузе 6 секунд это около 3 минут. Полный сбор 5 страниц выдачи с карточками займет примерно 15 минут. Это нормально. Парсер недвижимости не должен быть быстрым, он должен быть стабильным.
Совет: не перепарсивайте карточки при каждом запуске. Характеристики квартиры не меняются: площадь и год постройки достаточно собрать один раз. Меняется только цена, а она есть в выдаче. На шестом шаге мы сделаем так, чтобы карточка запрашивалась только для новых объектов. Это сократит число запросов в десятки раз.
Проверка: в выводе словарей появились ключи area, floor, floors_total и build_year с правдоподобными значениями: площадь от 15 до 200, этаж не больше этажности, год от 1900 до 2026. Если у части объектов поля отсутствуют, это нормально: не все продавцы заполняют год постройки.
Возможные проблемы
Регулярное выражение находит площадь комнаты вместо общей. Причина: в JSON есть похожие ключи вроде livingArea или kitchenArea. Решение: уточните шаблон, добавив перед ключом кавычку или двоеточие, чтобы он не совпадал с частью другого слова.
Шаг 5: Подключаем мобильные прокси и делаем сбор устойчивым
Цель этапа: распределить запросы через мобильные прокси с ротацией IP, добавить повторные попытки и корректную обработку ответов. После этого шага парсер смо��ет работать регулярно и подолгу, не создавая избыточной нагрузки с одного адреса.
Зачем парсеру недвижимости нужны мобильные прокси
Любая крупная площадка ограничивает частоту запросов с одного IP-адреса. Это защита от перегрузки, и она срабатывает на любой автоматике. Домашний адрес после нескольких сотен запросов начинает получать ответы 429 или страницы с проверкой. Мобильные прокси решают задачу иначе: вы получаете IP из пула сотового оператора, а по команде или по таймеру адрес меняется. Ваши запросы распределяются между адресами, нагрузка на каждый из них остается низкой, и парсер работает ровно. Для регулярного мониторинга цен это принципиально: вам нужны не разовые данные, а ежедневные срезы месяцами.
Настройка
- Откройте личный кабинет вашего сервиса мобильных прокси и найдите купленный прокси. Скопируйте четыре значения: хост, порт, логин, пароль. Также скопируйте ссылку для смены IP: обычно она выглядит как адрес с ключом и по обращению к ней прокси получает новый адрес.
- Добавьте в config.py параметры прокси. Никогда не записывайте пароли в код, который выкладываете куда-либо: держите их в отдельном файле или переменных окружения:
PROXY_HOST = 'ваш_хост'
PROXY_PORT = 'ваш_порт'
PROXY_USER = 'ваш_логин'
PROXY_PASS = 'ваш_пароль'
ROTATE_URL = 'ссылка_для_смены_ip'
ROTATE_EVERY = 25- Добавьте в parser.py функцию создания сессии с прокси. Библиотека requests принимает словарь с адресами для http и https:
from config import PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS, ROTATE_URL, ROTATE_EVERY
def make_session():
session = requests.Session()
proxy_url = 'http://' + PROXY_USER + ':' + PROXY_PASS + '@' + PROXY_HOST + ':' + PROXY_PORT
session.proxies = {'http': proxy_url, 'https': proxy_url}
return session
def rotate_ip():
try:
r = requests.get(ROTATE_URL, timeout=20)
print('Смена IP:', r.status_code)
time.sleep(5)
except requests.RequestException as e:
print('Не удалось сменить IP:', e)- Проверьте, что прокси работает. Создайте временный файл check_proxy.py с кодом, который запрашивает сервис определения IP через сессию и печатает ответ:
from parser import make_session
s = make_session()
print(s.get('https://api.ipify.org', timeout=20).text)- Запустите его. Вы должны увидеть IP-адрес, отличный от вашего домашнего. Вызовите rotate_ip и запустите проверку еще раз: адрес должен измениться.
- Теперь добавьте функцию запроса с повторными попытками. Она обрабатывает три ситуации: успешный ответ, ответ 429 или 403 (нужно подождать и сменить адрес), сетевую ошибку (повторить):
def safe_get(session, url, params=None, retries=3):
for attempt in range(1, retries + 1):
try:
resp = session.get(url, params=params, headers=HEADERS, timeout=30)
if resp.status_code == 200:
return resp
if resp.status_code in (429, 403):
print('Статус', resp.status_code, 'попытка', attempt, 'меняем IP и ждем')
rotate_ip()
time.sleep(30 * attempt)
continue
print('Неожиданный статус', resp.status_code)
return None
except requests.RequestException as e:
print('Сетевая ошибка', e, 'попытка', attempt)
time.sleep(10 * attempt)
return None- Замените вызовы session.get в fetch_page и enrich_offers на safe_get. Добавьте в enrich_offers счетчик: каждые ROTATE_EVERY запросов вызывайте rotate_ip. Это плановая ротация, которая не дает адресу накопить слишком много обращений.
Внимание: если вы получили ответ 429 или страницу с проверкой, не пытайтесь пробить ее частыми повторами. Это только ухудшит ситуацию для текущего адреса. Правильная реакция: остановиться, увеличить паузы, сменить IP и продолжить в спокойном темпе. Парсер, уважающий лимиты сайта, живет дольше и собирает больше.
Совет: используйте один прокси-канал на один поток парсинга. Соблазн запустить десять потоков через один адрес велик, но это прямой путь к ограничениям. Если нужна скорость, покупайте несколько каналов и распределяйте по ним разные регионы или разные фильтры.
Проверка: check_proxy.py показывает адрес мобильного оператора, после ротации адрес меняется. Парсер проходит две страницы выдачи с карточками без единого статуса 429. В логе видны строки Смена IP: 200 каждые 25 карточек.
Возможные проблемы
Ошибка ProxyError или 407. Причина: неверный логин или пароль либо не тот порт. Решение: проверьте данные в личном кабинете, убедитесь, что используете порт для HTTP-прокси, а не SOCKS. Если у вас SOCKS5, установите библиотеку pysocks и используйте префикс socks5h вместо http в proxy_url.
После ротации адрес не меняется. Причина: оператор выдал тот же адрес или ротация еще не применилась. Решение: увеличьте паузу после rotate_ip до 10 секунд и проверьте, не ограничена ли частота смены IP в тарифе.
Шаг 6: Сохраняем данные и строим историю цен
Цель этапа: перевести парсер с печати в консоль на запись в базу данных SQLite так, чтобы каждый запуск добавлял новую точку в историю цен, а не перезаписывал старую. Это сердце всего проекта.
Проектируем таблицы
Нам нужны две таблицы. Первая, offers, хранит объект: одна строка на объявление с характеристиками и датами. Вторая, prices, хранит цену на дату: несколько строк на объявление. Разделение нужно, чтобы не дублировать площадь и адрес при каждой записи цены.
- Создайте файл storage.py и опишите создание таблиц:
import sqlite3
from datetime import date
from config import DB_PATH
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.execute('CREATE TABLE IF NOT EXISTS offers (offer_id TEXT PRIMARY KEY, url TEXT, title TEXT, address TEXT, area REAL, floor INTEGER, floors_total INTEGER, build_year INTEGER, first_seen TEXT, last_seen TEXT, is_active INTEGER DEFAULT 1)')
conn.execute('CREATE TABLE IF NOT EXISTS prices (offer_id TEXT, checked_on TEXT, price INTEGER, PRIMARY KEY (offer_id, checked_on))')
conn.commit()
return conn- Добавьте функцию, которая возвращает множество уже известных offer_id. Она нужна, чтобы запрашивать карточки только для новых объектов:
def known_ids(conn):
rows = conn.execute('SELECT offer_id FROM offers').fetchall()
return set(r[0] for r in rows)- Напишите функцию сохранения. Для нового объекта вставляем строку в offers. Для любого объекта обновляем last_seen и записываем цену за сегодня. Конструкция INSERT OR REPLACE в prices означает: если сегодня цену уже записывали, обновить, иначе добавить:
def save_offers(conn, offers):
today = date.today().isoformat()
for o in offers:
conn.execute('INSERT OR IGNORE INTO offers (offer_id, url, title, address, area, floor, floors_total, build_year, first_seen, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (o['offer_id'], o['url'], o.get('title'), o.get('address'), o.get('area'), o.get('floor'), o.get('floors_total'), o.get('build_year'), today, today))
conn.execute('UPDATE offers SET last_seen = ?, is_active = 1 WHERE offer_id = ?', (today, o['offer_id']))
if o.get('price'):
conn.execute('INSERT OR REPLACE INTO prices (offer_id, checked_on, price) VALUES (?, ?, ?)', (o['offer_id'], today, o['price']))
conn.commit()- Добавьте функцию, которая помечает снятые объекты. Если объявление не встречалось в выдаче больше трех дней, считаем его неактивным. Это дает вам данные о скорости продаж:
def mark_inactive(conn, days=3):
conn.execute('UPDATE offers SET is_active = 0 WHERE julianday(?) - julianday(last_seen) > ?', (date.today().isoformat(), days))
conn.commit()- Перепишите главный блок parser.py так, чтобы он собирал выдачу, определял новые объекты, обогащал только их и сохранял все:
from storage import get_conn, known_ids, save_offers, mark_inactive
if __name__ == '__main__':
conn = get_conn()
session = make_session()
listing = collect_listing(session)
old = known_ids(conn)
new_offers = [o for o in listing if o['offer_id'] not in old]
print('Новых объектов:', len(new_offers), 'из', len(listing))
enrich_offers(new_offers, session)
save_offers(conn, listing)
mark_inactive(conn)
print('Сохранено. Всего в базе:', conn.execute('SELECT COUNT(*) FROM offers').fetchone()[0])- Не забудьте изменить collect_listing так, чтобы она принимала session параметром, а не создавала свою. Запустите скрипт. В папке проекта появится файл realty.db.
Смотрим историю цен
Создайте файл report.py, который выгружает изменения цен в Excel. Запрос ниже находит объекты, у которых последняя цена отличается от первой:
import sqlite3
import pandas as pd
from config import DB_PATH
conn = sqlite3.connect(DB_PATH)
query = 'SELECT o.offer_id, o.address, o.area, o.url, MIN(p.checked_on) AS first_date, MAX(p.checked_on) AS last_date, (SELECT price FROM prices WHERE offer_id = o.offer_id ORDER BY checked_on ASC LIMIT 1) AS first_price, (SELECT price FROM prices WHERE offer_id = o.offer_id ORDER BY checked_on DESC LIMIT 1) AS last_price FROM offers o JOIN prices p ON p.offer_id = o.offer_id GROUP BY o.offer_id'
df = pd.read_sql(query, conn)
df['change_pct'] = ((df['last_price'] - df['first_price']) / df['first_price'] * 100).round(1)
df['price_per_m2'] = (df['last_price'] / df['area']).round(0)
df = df.sort_values('change_pct')
df.to_excel('report.xlsx', index=False)
print(df.head(10))После первого запуска колонка change_pct будет нулевой: истории еще нет. Со второго дня появятся первые изменения. Через две недели вы увидите картину: сколько объектов снизили цену, на сколько в среднем, какие районы двигаются быстрее.
Совет: добавьте в конец parser.py копирование базы: shutil.copy(DB_PATH, 'backup_' + date.today().isoformat() + '.db'). Одна строка кода защитит недели накопленных данных. Раз в месяц удаляйте старые копии, оставляя последние пять.
Проверка: файл realty.db существует, report.xlsx открывается в Excel, в нем есть колонки с адресом, площадью, ценами и ценой за квадратный метр. Запустите parser.py второй раз через несколько минут: строка Новых объектов должна показать 0 или небольшое число, а карточки не должны запрашиваться повторно. Это подтверждает, что дедупликация работает.
Возможные проблемы
Ошибка database is locked. Причина: база открыта в другой программе, например в просмотрщике SQLite, или два экземпляра скрипта работают одновременно. Решение: закройте лишние программы и не запускайте скрипт параллельно самому себе.
В report.xlsx все объекты показывают одну и ту же дату. Причина: скрипт запускался только один день. Это ожидаемо, просто подождите следующих запусков.
Шаг 7: Автоматизируем запуск и расширяем на другие площадки
Цель этапа: сделать так, чтобы парсер запускался сам каждое утро, и подготовить код к подключению других площадок недвижимости. История цен ценна только при регулярности, поэтому автоматизация обязательна.
Расписание запуска
- На Windows откройте Планировщик заданий через поиск в меню Пуск. Нажмите Создать простую задачу. Введите имя, например Парсер недвижимости.
- Выберите триггер Ежедневно, задайте время, например 07:30. Раннее утро удобно: нагрузка на сайты минимальна, а к началу рабочего дня у вас свежие данные.
- В действии выберите Запустить программу. В поле Программа укажите полный путь к python.exe внутри папки venv, например C:\projects\cian_parser\venv\Scripts\python.exe. В поле Аргументы введите parser.py. В поле Рабочая папка укажите папку проекта.
- Сохраните задачу и нажмите Выполнить в правой панели, чтобы проверить. В папке проекта должна обновиться база.
- На macOS и Linux используйте cron. Введите в терминале crontab -e и добавьте строку:
30 7 * * * cd /путь/к/cian_parser && ./venv/bin/python parser.py >> run.log 2>&1. Лог всех запусков будет копиться в run.log.
Подготовка к другим площадкам
Домклик, Яндекс Недвижимость, Метр квадратный и региональные порталы устроены похоже, но селекторы и параметры фильтров у них свои. Чтобы не переписывать парсер под каждую, выделите то, что отличается, в отдельные модули.
- Создайте папку sites внутри проекта. В ней создайте файл cian.py и перенесите туда функции fetch_page и parse_cards вместе с параметрами поиска. Оставьте в них одинаковый интерфейс: функция parse_cards принимает HTML и возвращает список словарей с одними и теми же ключами offer_id, url, title, price, address.
- Для новой площадки создайте файл, например domclick.py, и повторите исследование из второго шага: откройте выдачу, найдите карточку, цену, ссылку и параметр пагинации. Напишите свои версии fetch_page и parse_cards.
- В offer_id добавляйте префикс площадки, например cian_312456789 и domclick_98765. Иначе идентификаторы с разных сайтов могут совпасть и перемешать историю.
- В parser.py импортируйте модули из sites и запускайте сбор по каждому в цикле. Таблицы в базе общие: одна схема на все источники, а колонка source подскажет, откуда объект.
Важное замечание про Домклик и Яндекс Недвижимость: эти площадки активно используют внутренние API в формате JSON, которые видны на вкладке Network. Их выдача часто удобнее для парсинга, чем HTML, но структура ответов меняется чаще. Проверяйте селекторы и поля раз в месяц.
Совет: для одного и того же объекта, выставленного на нескольких площадках, цены могут различаться. Сравнение таких пар дает интересную аналитику и помогает находить продавцов, которые где-то снизили цену, но забыли обновить в другом месте. Сопоставлять объекты можно по адресу, площади и этажу.
Проверка: задача в планировщике выполнилась вручную без ошибок, в run.log или в истории планировщика видна успешная отметка. Структура папок содержит sites с как минимум одним модулем, парсер запускается из корня проекта и работает как прежде.
Возможные проблемы
Планировщик пишет, что задача выполнена, но база не обновилась. Причина: скрипт запустился из другой рабочей папки и создал новую пустую базу где-то еще. Решение: заполните поле Рабочая папка в задаче или используйте абсолютный путь к базе в config.py.
Проверка результата: чек-лист готового парсера
Пройдите по списку и отметьте каждый пункт. Если все выполнено, у вас полноценный парсер ЦИАН с историей цен.
- Скрипт parser.py запускается командой из активированного окружения без ошибок импорта.
- Выдача собирается с нескольких страниц, количество объектов на странице соответствует тому, что вы видите в браузере.
- Цены сохраняются целыми числами, адреса читаемы, ссылки открываются.
- Карточки запрашиваются только для новых объектов, в логе видна строка Новых объектов с уменьшающимся числом при повторных запусках.
- Запросы идут через мобильный прокси, check_proxy.py показывает адрес оператора, ротация меняет его.
- При статусе 429 парсер делает паузу и меняет IP, а не падает.
- Файл realty.db растет, таблица prices получает новые строки каждый день.
- report.xlsx формируется и открывается, через несколько дней в нем появляются ненулевые изменения цен.
- Автозапуск настроен, лог фиксирует каждый прогон.
- Резервная копия базы создается автоматически.
Как протестировать целиком
- Удалите или переименуйте realty.db, чтобы начать с чистого листа.
- Установите MAX_PAGES равным 2 и запустите parser.py. Засеките время: должно уйти около 6-8 минут с учетом карточек.
- Запустите report.py и убедитесь, что в отчете около 56 строк.
- Откройте realty.db любым просмотрщиком SQLite или выполните в Python запрос SELECT COUNT(*) FROM prices. Число должно совпасть с числом объектов.
- Запустите parser.py снова. Время выполнения должно сократиться до минуты, потому что карточки не запрашиваются. Число строк в prices не изменится, так как дата та же.
- Измените в базе цену одного объекта вручную на любое другое число, поменяйте дату записи на вчерашнюю и запустите парсер. В report.xlsx этот объект должен показать изменение цены. Так вы убедитесь, что логика истории работает, не дожидаясь реальных изменений.
Показатели успеха
Доля объектов с заполненной площадью выше 90 процентов. Доля запросов со статусом 200 выше 97 процентов. Ни одного необработанного исключения за прогон. Время полного прогона предсказуемо и не растет от запуска к запуску. Если показатели ниже, вернитесь к разделу с типичными ошибками.
Типичные ошибки и решения
Собрали проблемы, с которыми сталкиваются практически все, кто пишет парсер объявлений недвижимости впервые. Формат: проблема, причина, решение.
Парсер возвращает 0 объектов, хотя вчера работал
Причина: площадка обновила верстку и изменила атрибуты data-mark или data-name. Решение: откройте выдачу в браузере, повторите второй шаг и обновите селекторы. Заведите привычку хранить селекторы в одном месте в начале модуля, чтобы правки занимали минуту. Добавьте в парсер проверку: если на первой странице 0 объектов, отправлять себе уведомление в мессенджер.
Цены сохраняются с ошибкой в тысячу раз
Причина: у части объектов цена указана в тысячах или с пометкой за месяц, либо в текст попала цена за квадратный метр. Решение: убедитесь, что берете именно MainPrice, а не соседний элемент с ценой за метр. Добавьте проверку разумности: цена продажи квартиры в Москве меньше миллиона рублей почти наверняка ошибка разбора, логируйте такие случаи.
Статус 429 приходит уже на третьей странице
Причина: паузы слишком короткие или прокси еще не подключен, все запросы идут с одного домашнего IP. Решение: увеличьте PAUSE_MIN и PAUSE_MAX до 6 и 12, подключите мобильный прокси, включите плановую ротацию каждые 20-25 запросов. Проверьте, что вы не запустили несколько экземпляров скрипта одновременно.
Ошибка UnicodeEncodeError при печати в консоль Windows
Причина: стандартная консоль Windows не всегда корректно выводит кириллицу. Решение: выполните в терминале chcp 65001 перед запуском или добавьте в начало скрипта строку с sys.stdout.reconfigure(encoding='utf-8'). Также можно писать логи в файл вместо консоли.
Адрес собирается неполным или с дублями
Причина: адрес на карточке состоит из нескольких ссылок GeoLabel, часть из них дублирует город и округ. Решение: убирайте дубликаты, сохраняя порядок, либо берите адрес из карточки объекта, где он представлен единой строкой. Для аналитики по районам добавьте отдельное поле district, вырезая его из адреса по известному списку районов.
Парсер работает в ручном запуске, но не в планировщике
Причина: планировщик использует другой интерпретатор Python без установленных библиотек или другую рабочую папку. Решение: указывайте абсолютный путь к python.exe внутри venv и заполняйте поле рабочей папки. Перенаправляйте вывод в файл лога, чтобы видеть ошибки.
База весит гигабайты через месяц
Причина: вы сохраняете полный HTML страниц или все поля JSON в базу. Решение: храните только нужные поля. Если хотите сохранять исходные страницы для повторного разбора, складывайте их в сжатые файлы на диске, а не в SQLite. Раз в квартал выполняйте команду VACUUM для уплотнения базы.
Одинаковые объекты дублируются под разными идентификаторами
Причина: продавец снял объявление и выставил заново, получив новый номер. Решение: добавьте дополнительный ключ сопоставления из адреса, площади и этажа. Объекты с одинаковым ключом, но разными offer_id можно связать в отдельной таблице и считать историю цены по связке. Это уже продвинутая аналитика, но именно она показывает реальные снижения цены, скрытые за переопубликацией.
Дополнительные возможности для продвинутых
Базовый парсер готов. Если хотите большего, вот направления, которые дают максимальную отдачу.
Браузерная автоматизация через Playwright
Некоторые страницы подгружают данные скриптами уже после загрузки, и requests получает пустой каркас. В таких случаях используйте Playwright: он управляет настоящим браузером. Установите его командами pip install playwright и playwright install chromium. Прокси передается при запуске браузера в параметре proxy со словарем server, username, password. Дождитесь появления карточек через page.wait_for_selector и передайте page.content() в уже написанную функцию parse_cards. Учтите, что браузер потребляет в десять раз больше ресурсов, поэтому используйте его точечно, только для проблемных страниц.
Параллельный сбор по нескольким прокси-каналам
Если нужно собирать несколько регионов, купите отдельный мобильный прокси на каждый регион и запускайте отдельный процесс на канал. Не используйте многопоточность внутри одного канала: смысл распределения нагрузки теряется. Простой способ: параметр региона передается скрипту аргументом командной строки, а планировщик запускает несколько задач с разными аргументами и небольшим сдвигом по времени.
Уведомления о снижении цены
Добавьте в конец парсера сравнение сегодняшней цены с предыдущей для каждого объекта. Если снижение больше заданного порога, например 3 процента, формируйте сообщение с адресом, старой и новой ценой, ссылкой и отправляйте себе через бота в мессенджере. Это превращает парсер из инструмента аналитики в инструмент действия: вы узнаете о выгодных объектах в течение часа после изменения.
Аналитика и визуализация
С pandas вы можете группировать данные по районам и считать медианную цену квадратного метра, долю объектов со снижением, среднее время экспозиции (разница между first_seen и last_seen для неактивных объектов). Библиотека matplotlib построит график динамики за месяц в три строки кода. Загрузите отчет в Google Sheets, и коллеги без навыков программирования получат живую панель показателей.
Хранение в PostgreSQL
Когда объектов станет больше ста тысяч, SQLite начнет тормозить на аналитических запросах. Переезд на PostgreSQL прост: схема таблиц та же, меняется только строка подключения и библиотека (psycopg2 вместо sqlite3). Делайте это только при реальной необходимости: для одного города SQLite хватает на годы.
Мониторинг здоровья парсера
Записывайте в отдельную таблицу runs время старта, время окончания, число собранных объектов, число ошибок и число ротаций IP. Если объектов резко стало меньше или ошибок больше 5 процентов, отправляйте уведомление. Такой мониторинг позволяет заметить смену верстки в день ее появления, а не через две недели по пустому отчету.
FAQ: частые вопросы по созданию парсера недвижимости
Законно ли парсить ЦИАН и другие площадки?
Сбор общедоступной информации об объектах для личной аналитики в целом допустим, но условия каждой площадки описаны в ее пользовательском соглашении, и его нужно прочитать. Категорически нельзя собирать персональные данные продавцов, публиковать скопированную базу как свою и создавать нагрузку, мешающую работе сервиса. Если планируете коммерческое использование данных, проконсультируйтесь с юристом.
Почему именно мобильные прокси, а не серверные?
Адреса мобильных операторов являются общими для тысяч реальных абонентов и постоянно меняются. Площадки относятся к ним лояльнее, чем к адресам дата-центров, с которых реальные покупатели почти не заходят. Плюс возможность смены IP по ссылке дает управляемую ротацию без покупки сотен адресов.
Как часто запускать парсер для истории цен?
Раз в сутки оптимально. Цены на недвижимость меняются редко, чаще одного раза в день собирать нет смысла, а нагрузка растет. Если нужно ловить снижения оперативно, запускайте дважды в день утром и вечером, но только по узкому фильтру.
Сколько объектов можно собрать за день через один прокси?
При паузах 4-9 секунд и плановой ротации это примерно 500-700 запросов в час без проблем, или 8-12 тысяч в сутки при круглосуточной работе. Для мониторинга одного города обычно достаточно 2-3 тысяч запросов в день, потому что карточки запрашиваются только для новых объектов.
Что делать, если селекторы изменились и я не могу их найти?
Вернитесь ко второму шагу и идите от цены: правый клик на цене, Просмотреть код, поднимайтесь по дереву до карточки. Ищите атрибуты со словами data и осмысленными названиями: они стабильнее классов со случайными символами. Также проверьте вкладку Network: возможно, данные стали приходить отдельным JSON-запросом, и парсить их станет даже проще.
Можно ли обойтись без прокси для небольшого проекта?
Для разового сбора двух-трех страниц можно. Для ежедневного мониторинга с сотнями запросов домашний адрес быстро начнет получать ограничения, и данные станут неполными. История цен с пропусками теряет ценность, поэтому для регулярной работы прокси нужен.
Как парсить аренду, а не продажу?
Измените параметр deal_type на rent и добавьте тип аренды в параметрах поиска: долгосрочная или посуточная. Ссылки на объекты будут содержать rent вместо sale, поэтому обновите селектор ссылки в parse_cards. Остальная логика, включая историю цен, работает без изменений.
Нужно ли хранить фотографии объектов?
Для ценовой аналитики нет. Фотографии занимают много места и не нужны для расчетов. Если вы строите каталог для внутреннего использования, сохраняйте только ссылки на изображения, а не сами файлы.
Как понять, что объект продан, а не просто снят?
Площадки не сообщают причину снятия. Косвенный признак: объект исчез из выдачи и не появился снова в течение месяца. Объекты, которые пропадают и появляются через несколько дней с другой ценой, скорее переопубликованы. Связывайте их по адресу и площади, как описано в разделе ошибок.
Что делать, если данные нужны по десяти городам?
Заведите список регионов в config.py и запускайте сбор в цикле с отдельным прокси-каналом на каждые два-три города. Сдвигайте запуски по времени, чтобы не собирать все одновременно. База остается общей, поле региона добавьте в таблицу offers.
Заключение
Давайте посмотрим, что вы сделали. Вы подготовили окружение с Python и библиотеками, разобрались, как устроена выдача площадки недвижимости, и научились находить селекторы самостоятельно. Написали парсер ЦИАН, который собирает выдачу и карточки объектов. Подключили мобильные прокси с ротацией и повторными попытками, благодаря чему сбор стал устойчивым и предсказуемым. Спроектировали базу с историей цен, настроили отчеты и автозапуск по расписанию. Это полноценный рабочий инструмент, а не учебный пример.
Что делать дальше: дайте парсеру поработать две недели без изменений. За это время накопится история, а вы увидите слабые места: где падает доля заполненных полей, какие объекты дублируются, где отчет требует новых колонок. Только после этого добавляйте функции. Затем подключите вторую площадку, используя модульную структуру из седьмого шага: вы удивитесь, насколько быстрее это пойдет во второй раз.
Куда развиваться: уведомления о снижении цен превратят инструмент в источник сделок. Аналитика по районам и типам домов сделает вас экспертом рынка с цифрами на руках. Связывание переопубликованных объектов покажет реальные скидки, которых не видно на сайте. А аккуратное отношение к площадке, паузы, ротация адресов через мобильные прокси и сбор только нужных данных, позволит инструменту работать месяцами без сбоев.
Парсинг недвижимости это не про скорость, а про регулярность и качество данных. Вы заложили правильный фундамент. Удачи в сборе, и пусть ваша база растет с каждым утром.