Remove standalone PF artifacts

This commit is contained in:
Grendgi
2026-06-04 15:31:10 +03:00
parent dd3edd7088
commit 2ff44091b5
7 changed files with 26 additions and 357 deletions

155
README.md
View File

@@ -1,143 +1,40 @@
# DLD Monitor
# monitoring-pf
Внутренний инструмент для агентства недвижимости в Дубае: мониторит цены объявлений конкурентов на **PropertyFinder.ae** и **Bayut.com** по DLD Permit Number, шлёт уведомления в Telegram при:
Сервис мониторинга объявлений PropertyFinder/Bayut по DLD Permit Number для
портала. Он хранит проекты, конкурирующие объявления и историю цен, а UI
публикуется через portal по `/monitoring-pf`.
- 📈📉 изменении цены конкурента,
- ❌ удалении объявления (404 / withdrawn),
- 🆕 появлении нового объявления с тем же permit (новый брокер выставил ту же квартиру).
## Назначение
## Архитектура
- отслеживать изменение цены конкурента;
- фиксировать удаление/withdrawn объявлений;
- находить новые объявления с тем же DLD Permit Number;
- уведомлять ответственных сотрудников через Telegram.
```
┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Web UI │ │ Scheduler │ │ Telegram Bot │
│ (FastAPI) │ │ (APScheduler) │ │ (polling) │
│ add project │ │ every N hours │ │ /start /check │
└──────┬───────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
└──────────────┬──────┴───────────────────────┘
┌────────────────┐
│ monitor │
│ service │ ← скрапит PF и Bayut, пишет в SQLite
└────────────────┘
▼ уведомления в TG конкретному employee
## Развёртывание
Сервис рассчитан на запуск внутри портала/k8s. Манифесты лежат в `k8s/`.
Перед применением заполните секреты в `k8s/secrets.yaml`.
```bash
kubectl apply -k k8s
```
## Локальный запуск (Windows)
### 1. Создать виртуальное окружение и поставить зависимости
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
```
### 2. Получить токен Telegram-бота
Уже есть? Отлично. Если нет:
1. Откройте Telegram, найдите [@BotFather](https://t.me/BotFather).
2. Отправьте `/newbot`, придумайте имя.
3. Скопируйте токен вида `123456:ABC-DEF…`.
### 3. Создать `.env`
```powershell
Copy-Item .env.example .env
notepad .env
```
В `.env` вставьте:
```
TG_BOT_TOKEN=ваш_токен_от_botfather
SCRAPE_INTERVAL_HOURS=4
ADMIN_CHAT_ID= # опционально — куда слать системные ошибки
```
### 4. Запустить три процесса (в **трёх** разных окнах PowerShell)
**Окно 1 — веб-интерфейс:**
```powershell
.\.venv\Scripts\Activate.ps1
python run_web.py
```
Откройте http://127.0.0.1:8000
**Окно 2 — Telegram-бот:**
```powershell
.\.venv\Scripts\Activate.ps1
python -m app.bot
```
**Окно 3 — фоновый сканер:**
```powershell
.\.venv\Scripts\Activate.ps1
python -m app.scheduler
```
## Первое использование
1. Откройте бота в Telegram и отправьте `/start` — он зарегистрирует ваш `chat_id`.
2. В веб-UI перейдите в **Сотрудники** — убедитесь, что вы там есть с ✓ TG.
3. Нажмите **+ Новый проект**, заполните:
- Название (например: «Marina Pinnacle 1502, 2BR»)
- DLD Permit Number (Trakheesi)
- Тип сделки (продажа/аренда)
- Владелец = вы
4. На странице проекта нажмите **Проверить сейчас** — система найдёт все объявления конкурентов с этим permit на PF и Bayut.
5. Дальше фоновый сканер сам будет проверять каждые `SCRAPE_INTERVAL_HOURS` часов и слать уведомления в Telegram.
## Команды бота
- `/start` — подключить себя как сотрудника (запоминает chat_id)
- `/list` — список ваших проектов
- `/check` — запустить проверку всех ваших проектов сейчас
- `/whoami` — показать свой chat_id
Standalone-скрипты локального Windows-запуска и compose-обвязка удалены, чтобы
проект не дублировал инфраструктуру портала.
## Структура
```
```text
app/
├── config.py настройки из .env
├── db.py SQLAlchemy engine + session
├── config.py настройки окружения
├── db.py SQLAlchemy engine/session
├── models.py Employee, Project, CompetitorListing, PriceHistory
├── web.py FastAPI роуты и UI
├── bot.py Telegram-бот
├── scheduler.py APScheduler фоновый сканер
├── scrapers/
│ ├── base.py httpx + парсинг __NEXT_DATA__
│ ├── propertyfinder.py
│ └── bayut.py
├── services/
│ ├── monitor.py детект изменений, основная бизнес-логика
│ └── notifier.py отправка в TG
└── templates/ Jinja2 (Bootstrap 5)
data/
└── monitor.db SQLite (создаётся автоматически)
├── scheduler.py фоновый сканер
├── scrapers/ PropertyFinder/Bayut парсеры
├── services/ бизнес-логика и уведомления
└── templates/ Jinja2 UI
k8s/ манифесты для портала
```
## Перенос на сервер
Когда придёт время — нужно:
1. Поставить Python 3.11+ на сервер (Linux).
2. Скопировать репозиторий, `pip install -r requirements.txt`.
3. Создать `.env`.
4. Поставить три процесса под `systemd`:
- `dld-monitor-web.service``python run_web.py`
- `dld-monitor-bot.service``python -m app.bot`
- `dld-monitor-scheduler.service``python -m app.scheduler`
5. Поставить nginx + TLS перед веб-портом (8000).
## Возможные проблемы скрапинга
PF/Bayut могут начать блокировать запросы при частых обращениях. Признаки:
- В логах сканера видны `Blocked by site (403/429)`.
- Поиск возвращает 0 объявлений, хотя они должны быть.
Что делать:
1. Уменьшите `SCRAPE_INTERVAL_HOURS` (реже = меньше риска).
2. Если не помогает — добавьте Playwright (headless-браузер). Заготовка: `pip install playwright && playwright install chromium`, затем заменить `fetch_html` на запуск через Playwright.
3. Опционально — прокси.

View File

@@ -1,90 +0,0 @@
# Журнал сессии (2026-05-25)
## Что построили
Внутренний мониторинг цен конкурентов на propertyfinder.ae и bayut.com для HOME LIGA REAL ESTATE.
**Стек:** FastAPI + Jinja2 (Bootstrap 5) + SQLAlchemy/SQLite + APScheduler + python-telegram-bot + httpx/BS4.
**Три процесса** (каждый — свой .bat-лаунчер):
- `run_web.bat` — веб-UI на http://127.0.0.1:8000
- `run_bot.bat` — Telegram-бот (polling)
- `run_scheduler.bat` — фоновый сканер каждые `SCRAPE_INTERVAL_HOURS` часов
## Эволюция архитектуры
### Первая попытка (отвергнута)
**Идея:** сотрудник вводит DLD Permit Number своего объекта → система автоматически ищет «то же permit» на PF и Bayut → находит объявления конкурентов.
**Почему не сработало:**
1. В Дубае **каждый брокер получает свой permit на свою публикацию**. Два брокера, рекламирующих одну квартиру = два разных permit. Permit не идентифицирует физический объект — он идентифицирует конкретное объявление конкретного брокера.
2. PF на странице объявления показывает permit **картинкой** через сервис верификации, не plain text (anti-scraping).
3. PF search `?q=<permit>` — free-text по названию/описанию, не структурированный фильтр.
### Финальная архитектура
**Manual URL list + опциональные подсказки:**
1. Сотрудник создаёт проект (название, тип сделки, владелец, опц. building/bedrooms/sqft).
2. На странице проекта **вручную вставляет URL** объявления конкурента → система делает single-page fetch, парсит `__NEXT_DATA__`, добавляет в трекинг.
3. Если указано здание — кнопка «🔍 Подобрать похожие» ищет на PF/Bayut по `building + bedrooms` и предлагает кандидатов с кнопкой «+ Отслеживать».
4. Каждые 4 часа фоновый сканер делает refetch каждого отслеживаемого URL → детектит:
- 📈📉 изменение цены
- ❌ удаление (URL отдаёт 404)
- ♻️ возвращение из удалённого статуса
5. Уведомления — в Telegram личкой владельцу проекта.
## Модель данных
- `Employee` — name, tg_chat_id (опц.), tg_username
- `Project` — title, deal_type, owner_id, our_price, building, bedrooms, size_sqft, our_url, dld_permit (все после `owner` — опционально)
- `CompetitorListing` — source (PF|Bayut), external_id, url, current_price, status (active|removed), agent_name, agency_name, first_seen, last_seen
- `PriceHistory` — listing_id, price, recorded_at
## Как подключиться сотруднику
1. В TG найти бота → отправить `/start`.
2. Отправить `/whoami` → бот пришлёт chat_id.
3. В вебе http://127.0.0.1:8000/employees → найти/создать запись → вставить chat_id → Сохранить.
## Известные ограничения
- **PF/Bayut могут блокировать** при частых запросах (видно как `Blocked by site (403/429)` в логах). Решение — увеличить интервал; если уже не помогает — добавить Playwright fallback.
- **Подсказки эвристические**: ищем по совпадению building name в title + bedrooms-фильтр. Могут попасть «другие квартиры в том же здании» — поэтому добавление в трекинг через ручное подтверждение.
- **Permit как plain text** на PF не отдаётся. Если когда-нибудь понадобится — нужен OCR на verification-image.
## Что в .env
```
TG_BOT_TOKEN=<токен от @BotFather>
SCRAPE_INTERVAL_HOURS=4
ADMIN_CHAT_ID= # опц.
```
## Структура проекта
```
DLD Permit Number/
├── run_web.bat, run_bot.bat, run_scheduler.bat
├── run_web.py
├── requirements.txt
├── .env (не в git — содержит токен)
├── data/monitor.db (создаётся автоматически)
├── app/
│ ├── config.py ← settings + резолвит относительные SQLite-пути в абсолютные
│ ├── db.py, models.py
│ ├── web.py ← FastAPI: CRUD проектов, /listings add/delete, /suggest
│ ├── bot.py ← /start, /whoami, /list, /check
│ ├── scheduler.py
│ ├── scrapers/{base,propertyfinder,bayut}.py
│ ├── services/{monitor,notifier}.py
│ └── templates/ ← projects_list, project_form, project_detail, suggest, employees, base
```
## Что протестировать в первую очередь
1. Удалить старый `data/monitor.db` (схема изменилась).
2. Запустить три .bat файла.
3. `/start` боту → `/whoami` → chat_id → вписать в Сотрудники.
4. Создать тестовый проект (Aykon City Tower B, 2BR).
5. Вставить URL реального объявления конкурента → проверить, что добавилось с ценой/брокером.
6. Жмякнуть «Подобрать похожие» → посмотреть кандидатов.
7. Жмякнуть «Проверить сейчас» → если цена не менялась, изменений не будет (это нормально); для теста алертов можно поменять цену в БД руками или подождать реального изменения.

View File

@@ -1,89 +0,0 @@
r"""Одноразовая диагностика: проверяет всю цепочку мониторинга.
Запуск: & "<...>\.venv\Scripts\python.exe" diagnose.py
Только чтение БД + проверка токена TG + живой re-fetch. Сообщения НЕ шлёт."""
from __future__ import annotations
import sys
import httpx
from app.config import settings
from app.db import SessionLocal
from app.models import CompetitorListing, Employee, PriceHistory, Project
from app.services.monitor import check_project
def line(c: str = "-") -> None:
print(c * 60)
def main() -> None:
db = SessionLocal()
try:
# --- 1. Содержимое БД ---
line("=")
print("1) ДАННЫЕ В БАЗЕ")
line("=")
projects = db.query(Project).all()
if not projects:
print(" ! В базе нет ни одного проекта.")
return
for p in projects:
owner = p.owner
chat = owner.tg_chat_id if owner else None
chat_str = chat if chat else "НЕ ЗАДАН (уведомления не дойдут!)"
print(f"\n Проект #{p.id}: {p.title}")
print(f" тип: {p.deal_type.value} | наша цена: {p.our_price}")
print(f" владелец: {owner.name if owner else ''} | tg_chat_id: {chat_str}")
print(f" последняя проверка: {p.last_checked_at or 'ещё не было'}")
listings = p.listings
print(f" отслеживаемых объявлений конкурентов: {len(listings)}")
for l in listings:
hist = db.query(PriceHistory).filter(PriceHistory.listing_id == l.id).count()
price = f"{l.current_price:,.0f} {l.currency}".replace(",", " ") if l.current_price else "БЕЗ ЦЕНЫ"
print(f" - [{l.source.value}] {l.status.value} | {price} | история: {hist} зап.")
print(f" {(l.title or 'без названия')[:70]}")
print(f" брокер: {l.agency_name or ''} | {l.url}")
# --- 2. Telegram токен ---
line("=")
print("2) TELEGRAM")
line("=")
if not settings.tg_bot_token:
print(" ! TG_BOT_TOKEN не задан в .env")
else:
try:
r = httpx.get(
f"https://api.telegram.org/bot{settings.tg_bot_token}/getMe",
timeout=15.0,
)
if r.status_code == 200 and r.json().get("ok"):
bot = r.json()["result"]
print(f" OK токен валиден: @{bot.get('username')} ({bot.get('first_name')})")
else:
print(f" ! getMe вернул {r.status_code}: {r.text}")
except Exception as e:
print(f" ! Ошибка связи с Telegram: {e}")
print(f" ADMIN_CHAT_ID: {settings.admin_chat_id or 'не задан'}")
# --- 3. Живой re-fetch (то, что делает сканер раз в 4 ч) ---
line("=")
print("3) ЖИВАЯ ПЕРЕПРОВЕРКА (re-fetch всех объявлений)")
line("=")
for p in projects:
print(f"\n Проект #{p.id}: {p.title} — проверяю {len(p.listings)} объявл...")
changes = check_project(db, p)
if changes:
print(f" обнаружено изменений: {len(changes)} (в реальном прогоне ушли бы в TG):")
for c in changes:
print(" " + c.replace("\n", " | ").replace("<b>", "").replace("</b>", ""))
else:
print(" изменений нет — все объявления успешно перезагружены, цены прежние.")
line("=")
print("Готово. Если у объявлений есть цены и статус active, а токен валиден — цепочка рабочая.")
finally:
db.close()
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,37 +0,0 @@
services:
web:
build: .
image: dld-permit-monitor:latest
container_name: dld-web
restart: unless-stopped
env_file: .env
environment:
WEB_HOST: 0.0.0.0
WEB_PORT: 8000
ports:
- "8080:8000"
volumes:
- ./data:/app/data
command: ["python", "run_web.py"]
bot:
image: dld-permit-monitor:latest
depends_on:
- web
container_name: dld-bot
restart: unless-stopped
env_file: .env
volumes:
- ./data:/app/data
command: ["python", "-m", "app.bot"]
scheduler:
image: dld-permit-monitor:latest
depends_on:
- web
container_name: dld-scheduler
restart: unless-stopped
env_file: .env
volumes:
- ./data:/app/data
command: ["python", "-m", "app.scheduler"]

View File

@@ -1,4 +0,0 @@
@echo off
cd /d "%~dp0"
".venv\Scripts\python.exe" -m app.bot
pause

View File

@@ -1,4 +0,0 @@
@echo off
cd /d "%~dp0"
".venv\Scripts\python.exe" -m app.scheduler
pause

View File

@@ -1,4 +0,0 @@
@echo off
cd /d "%~dp0"
".venv\Scripts\python.exe" run_web.py
pause