OLX → Telegram Бот: Как автоматизировать поиск квартир в Ташкенте и отсеять маклеров

OLX → Telegram Бот: Как  автоматизировать поиск квартир в Ташкенте и отсеять маклеров

Задача

В Узбекистане снять квартиру через OLX — это лотерея. 80% объявлений размещают маклеры, которые берут комиссию 50–100% от месячной аренды. Каждое объявление — это либо посредник, либо перекуп, либо просто бот, который дублирует чужие фотографии.

Мне нужна была квартира в Ташкенте: Алмазарский район, 2–3 комнаты, цена $400–500 в месяц. Без посредников. И я решил написать бота, который сам бы парсил OLX и отсеивал маклеров.


Как мы строили систему

Бот работает на Python с библиотекой python-telegram-bot. Парсинг OLX через curl_cffi — обычный requests блокируется CloudFront.

Архитектура

text┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Telegram Bot │────▶│ OLX Parser │────▶│ SQLite DB │
│ (user-facing) │ │ (curl_cffi) │ │ (ads, users) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
JobQueue (час) HTML/API парсинг
broadcast_new_ads (дата, телефон, автор)

Стек

  • Python 3.12 + python-telegram-bot[job-queue]
  • curl_cffi — обход CloudFront
  • BeautifulSoup4 — парсинг HTML
  • SQLite — хранение объявлений и подписчиков
  • Systemd — автозапуск как службы
  • APScheduler — ежечасная рассылка

Проблема №1: OLX использует CloudFront

Обычный requests выдаёт 403. Классический requests не мог пройти защиту CloudFront. Мы использовали curl_cffi, который эмулирует TLS-отпечаток браузера.

Решение:

pythonfrom curl_cffi import requests

HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}

resp = requests.get(url, headers=HEADERS, impersonate="chrome", timeout=30)

impersonate="chrome" — ключевой параметр, который обходит CloudFront.


Проблема №2: API OLX не принимает строковые ID

Бот парсил URL ...-ID4qKuF.html, но API /api/v1/offers/4qKuF/ выдавал 404. Оказалось, что API принимает только числовые ID, а на странице они скрыты в HTML.

Решение: Загружаем страницу объявления и ищем числовой ID в data-id или в JSON-LD ("sku"):

pythondef extract_numeric_id(html):
# Ищем в JSON-LD
m = re.search(r'"sku"\s*:\s*"(\d+)"', html)
if m:
return m.group(1)
# Ищем data-id
m = re.search(r'data-id=["\'](\d+)["\']', html)
return m.group(1) if m else None

Теперь бот находит числовой ID и делает запрос к API.


Проблема №3: Телефон скрыт за кнопкой «Показать»

На OLX номер телефона отображается только после клика. Бот не умеет эмулировать клики, но данные всё равно есть в JSON на странице или в API.

Решение: Парсим телефон из JSON __PRERENDERED_STATE__ или из API после получения числового ID:

pythondef fetch_phone(ad_url, numeric_id):
# 1. Через API
data = requests.get(f"https://www.olx.uz/api/v1/offers/{numeric_id}/").json()
for p in data.get("params", []):
if p.get("key") == "phone":
return p.get("value", {}).get("label")
# 2. Через HTML (запасной вариант)
html = requests.get(ad_url).text
m = re.search(r'"label":\s*"([\d\s+]+)"', html)
return m.group(1) if m else None


Проблема №4: Как определить маклера?

На OLX нет тега «маклер». Но есть индикаторы:

  1. Количество объявлений автора > 3 → почти всегда маклер.
  2. Один номер телефона повторяется в 3+ объявлениях → 100% маклер.

Решение: Два прохода:

  1. Собираем все телефоны из найденных объявлений.
  2. Помечаем маклеров, где телефон встречается чаще 3 раз.

pythonphone_to_ads = {}
for ad in ads:
phone = fetch_phone(ad.url, ad.id)
phone_to_ads.setdefault(phone, []).append(ad.id)

for ad in ads:
if len(phone_to_ads.get(ad.phone, [])) > 3:
ad.is_realtor = True # маклер


Проблема №5: Даты публикации в разном формате

На OLX даты могут быть:

  • «Опубликовано сегодня в 16:11»
  • «Опубликовано 27 июля 2026 г.»
  • «Вчера в 10:30»

Решение: Регулярки с преобразованием в ISO-формат:

pythondef parse_date(text):
patterns = [
(r'Опубликовано\s+сегодня\s+в\s+(\d{1,2}):(\d{2})', 'today'),
(r'Опубликовано\s+(\d{1,2})\s+(\w+)\s+(\d{4})\s+г\.', 'full'),
]
for pattern, ptype in patterns:
m = re.search(pattern, text)
if m:
# преобразуем в ISO
return datetime(...).isoformat()


Проблема №6: Бот зависал при установке команд

При запуске asyncio.run(set_commands(app)) создавался отдельный event loop, который конфликтовал с основным. Бот падал с RuntimeError: Event loop is closed.

Решение: Использовали post_init callback:

pythonasync def post_init(app):
await app.bot.set_my_commands(commands)

app.post_init = post_init

Теперь команды устанавливаются корректно, без конфликтов.


Проблема №7: Автоматическая рассылка без дублей

Каждый час бот проверяет новые объявления и отправляет подписчикам. Чтобы не дублировать, используем seen.json.

Решение: Сохраняем ID отправленных объявлений и фильтруем:

pythonseen = json.load(open('seen.json'))
new_ads = [ad for ad in raw_ads if ad.id not in seen]
# отправляем
seen.update([ad.id for ad in new_ads])
json.dump(list(seen), open('seen.json', 'w'))


Функционал бота

Бот умеет:

text/start — Главное меню с датой последнего сканирования
/search — Поиск новых объявлений за 2 дня с сортировкой по дате
/recent — Показать последние найденные из БД
/subscribe — Подписаться на ежечасную рассылку
/unsubscribe — Отписаться
/status — Проверить подписку

Пример сообщения:

text🏠 OLX Поиск квартир

Я ищу объявления о долгосрочной аренде квартир в Ташкенте
и определяю, маклер это или владелец.

📌 Команды:
/search — найти новые объявления за последние 2 дня
/recent — показать последние найденные объявления
/subscribe — подписаться на ежечасную рассылку
/unsubscribe — отписаться от рассылки
/status — ваш статус подписки

🕒 Последнее сканирование: 2026-07-30T10:09:38.317900

Вывод объявления:

textОбъявление
💰 500 у.е.
🔗 https://www.olx.uz/d/obyavlenie/...
👤 Иван Петров
📦 Объявлений автора: 12
🔴 МАКЛЕР (более 3 объявлений)
📞 998901234567
📅 30.07.2026 16:11


Автозапуск как systemd-служба

Создаём /etc/systemd/system/olx-bot.service:

ini[Unit]
Description=OLX Telegram Bot
After=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=/bots/olxsearch
ExecStart=/bots/olxsearch/venv/bin/python /bots/olxsearch/bot.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Управление:

bashsudo systemctl enable --now olx-bot
sudo systemctl status olx-bot
journalctl -u olx-bot -f


Итоговый результат

  • ✅ Бот парсит OLX каждые 2 секунды (с задержками) и не банится.
  • ✅ Маклеры помечаются красным.
  • ✅ Телефоны и даты извлекаются даже со скрытых полей.
  • ✅ Ежечасная рассылка подписчикам.
  • ✅ Работает в любой момент по команде /search.
  • ✅ Квартиры сортируются по свежести.

За неделю бот отсеял 28 маклеров и показал только 40 объявлений от реальных владельцев.

Примеры команд

Поиск квартиры:

text/search

🔍 Ищу новые объявления... Это может занять некоторое время.

📊 Результаты поиска
Всего: 100
🔴 Маклеры: 28
🟢 Владельцы: 72
⚪ Не определено: 0

Выберите, что показать:
[🔴 Только маклеры] [🟢 Только владельцы] [📋 Все объявления]

Подписка на рассылку:

text/subscribe
✅ Вы подписались на ежечасную рассылку новых объявлений.

Ежечасный отчёт:

text🔄 Ежечасная проверка объявлений
📅 Время: 30.07.2026 10:09
📦 Найдено новых объявлений: 1
👥 Подписчиков: 1