Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Cross-Platform CI (Linux, macOS, Windows)

on:
push:
branches: [ "all-fixes" ]
pull_request:
workflow_dispatch:

jobs:
test:
name: ${{ matrix.os }} - Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.12"]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt pytest

- name: Run pytest
run: |
pytest -v
5 changes: 5 additions & 0 deletions AvitoParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def set_up():
one_file_for_link.value = config.one_file_for_link
parse_views.value = config.parse_views
parse_description.value = getattr(config, "parse_description", False)
parse_params.value = getattr(config, "parse_params", False)
save_xlsx.value = config.save_xlsx
use_webdriver.value = config.use_webdriver
use_bypass_api.value = config.use_bypass_api
Expand Down Expand Up @@ -113,6 +114,7 @@ def save_config():
"one_file_for_link": one_file_for_link.value,
"parse_views": parse_views.value,
"parse_description": parse_description.value,
"parse_params": parse_params.value,
"save_xlsx": save_xlsx.value,
"use_webdriver": use_webdriver.value,
"use_bypass_api": use_bypass_api.value,
Expand Down Expand Up @@ -560,6 +562,8 @@ def toggle(e):
tooltip=PARSE_VIEWS_HELP)
parse_description = ft.Checkbox(label="Парсить описание полностью", value=False,
tooltip="Загружает полное описание из карточки вместо краткого анонса")
parse_params = ft.Checkbox(label="Парсить характеристики", value=False,
tooltip=PARSE_PARAMS_HELP)
parse_phone = ft.Checkbox(label="Парсить телефоны", value=False, on_change=check_api_key_exist,
tooltip=PARSE_PHONE_HELP)

Expand Down Expand Up @@ -712,6 +716,7 @@ def toggle(e):
ft.Row([one_time_start, one_file_for_link]),
ft.Row([parse_views,
parse_description,
parse_params,
#parse_phone,
save_xlsx]),
]
Expand Down
1 change: 1 addition & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ one_time_start = false
one_file_for_link = false
parse_views = false
parse_description = false
parse_params = false
save_xlsx = true
use_webdriver = false
use_bypass_api = false
Expand Down
1 change: 1 addition & 0 deletions dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class AvitoConfig:
one_file_for_link: bool = False
parse_views: bool = False
parse_description: bool = False
parse_params: bool = False
save_xlsx: bool = True
use_webdriver: bool = True
use_bypass_api: bool = False
Expand Down
2 changes: 2 additions & 0 deletions lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
PARSE_VIEWS_HELP = "Дополнительно будет собираться количество просмотров: всего и за сегодня. Т.к. это требует " \
"открытия отдельной страницы - то время работы сильно увеличится. Не запускайте без прокси и если " \
"нужно получать информацию о новых объявлениях как можно быстрее"
PARSE_PARAMS_HELP = "Дополнительно будут собираться характеристики объявления («О помещении», параметры товара). " \
"Забираются из карточки объявления"
PARSE_PHONE_HELP = ("Будут дополнительно собираться временные номера телефонов. Для это нужен api ключ сайта spfa.pro, "
"его нужно ввести в разделе <Прокси и обход блокировок>. spfa берет плату за эту услугу")
NEED_TO_INSERT_API_KEY = ("Для парсинга телефонов нужен api ключ сайта spfa.pro, "
Expand Down
5 changes: 4 additions & 1 deletion load_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import tomllib
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
from dataclasses import fields
from pathlib import Path

Expand Down
1 change: 1 addition & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class Item(BaseModel):
total_views: int | None = None
today_views: int | None = None
phone: str | None = None
params: Optional[Dict[str, str]] = None


class ItemsResponse(BaseModel):
Expand Down
8 changes: 7 additions & 1 deletion parser/export/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class ExcelStorage(ResultStorage):
"Поднято",
"Просмотры (всего)",
"Просмотры (сегодня)",
"Телефон"
"Телефон",
"Параметры"
]

def __init__(self, file_path: Path):
Expand Down Expand Up @@ -142,6 +143,11 @@ def save(self, ads: list[Item]) -> None:
ad.total_views if ad.total_views is not None else "",
ad.today_views if ad.today_views is not None else "",
self.excel_safe(ad.phone or ""),
self.excel_safe(
"; ".join(f"{k}: {v}" for k, v in ad.params.items())
if isinstance(getattr(ad, "params", None), dict)
else (ad.params or "")
),
]

sheet.append(row)
Expand Down
83 changes: 80 additions & 3 deletions parser_cls.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def parse(self):
self.notifier.notify_many(ads=filtered_ads)
filtered_ads = self.parse_views(ads=filtered_ads)
filtered_ads = self.parse_description(ads=filtered_ads)
filtered_ads = self.parse_params(ads=filtered_ads)
filtered_ads = self.parse_phone(ads=filtered_ads)

if filtered_ads:
Expand Down Expand Up @@ -333,7 +334,62 @@ def count(value):

return description or None, total, today

def _enrich_ads(self, ads: list[Item], *, views: bool, description: bool) -> list[Item]:
@staticmethod
def _extract_params(payload: dict) -> dict[str, str] | None:
"""
Извлечение характеристик объявления («О помещении», параметры товара).
Поддерживает сценарии Beduin (mobile API scenario) и mobile.params.
"""
if not isinstance(payload, dict):
return None

def nested_dict(value, *keys):
for key in keys:
if not isinstance(value, dict):
return {}
value = value.get(key)
return value if isinstance(value, dict) else {}

params_dict: dict[str, str] = {}

# 1. Beduin scenario: success.view.scenario.beduin.main.params
card_params = nested_dict(
payload, "success", "view", "scenario", "beduin", "main", "params"
)
if isinstance(card_params, dict):
for widget_key, widget_data in card_params.items():
if isinstance(widget_data, dict):
items = widget_data.get("items")
if isinstance(items, list):
for item in items:
if isinstance(item, dict):
title = item.get("title") or item.get("name") or item.get("label")
desc = item.get("description") or item.get("value") or item.get("text")
if isinstance(title, str) and title.strip():
if isinstance(desc, (str, int, float)) and str(desc).strip():
params_dict[title.strip()] = str(desc).strip()

# 2. Mobile API: success.mobile.params / parameters / properties
mobile = nested_dict(payload, "success", "mobile")
for container in (mobile.get("params"), mobile.get("parameters"), mobile.get("properties")):
if isinstance(container, list):
for item in container:
if isinstance(item, dict):
title = item.get("title") or item.get("name") or item.get("label")
desc = item.get("description") or item.get("value") or item.get("text")
if isinstance(title, str) and title.strip():
if isinstance(desc, (str, int, float)) and str(desc).strip():
params_dict[title.strip()] = str(desc).strip()
elif isinstance(container, dict):
for k, v in container.items():
if isinstance(k, str) and k.strip() and isinstance(v, (str, int, float)) and str(v).strip():
params_dict[k.strip()] = str(v).strip()

return params_dict if params_dict else None

def _enrich_ads(
self, ads: list[Item], *, views: bool, description: bool, params: bool = False
) -> list[Item]:
total_ads = len(ads)
for index, ad in enumerate(ads, start=1):
if self.stop_event and self.stop_event.is_set():
Expand Down Expand Up @@ -361,6 +417,10 @@ def _enrich_ads(self, ads: list[Item], *, views: bool, description: bool) -> lis
)
if description and full_description:
ad.description = full_description
if params or getattr(self.config, "parse_params", False):
item_params = self._extract_params(payload)
if item_params:
ad.params = item_params
except Exception as err:
logger.warning(f"Ошибка при разборе карточки {ad.id}: {err}")
continue
Expand All @@ -373,15 +433,32 @@ def parse_views(self, ads: list[Item]) -> list[Item]:

logger.info("Начинаю парсинг просмотров")
return self._enrich_ads(
ads, views=True, description=getattr(self.config, "parse_description", False)
ads,
views=True,
description=getattr(self.config, "parse_description", False),
params=getattr(self.config, "parse_params", False),
)

def parse_description(self, ads: list[Item]) -> list[Item]:
if not getattr(self.config, "parse_description", False) or self.config.parse_views:
return ads

logger.info("Начинаю парсинг полного описания")
return self._enrich_ads(ads, views=False, description=True)
return self._enrich_ads(
ads,
views=False,
description=True,
params=getattr(self.config, "parse_params", False),
)

def parse_params(self, ads: list[Item]) -> list[Item]:
if not getattr(self.config, "parse_params", False):
return ads
if self.config.parse_views or getattr(self.config, "parse_description", False):
return ads

logger.info("Начинаю парсинг характеристик объявлений")
return self._enrich_ads(ads, views=False, description=False, params=True)

def parse_phone(self, ads: list[Item]) -> list[Item]:
if not self.config.parse_phone or self.config.parse_phone:
Expand Down
Loading
Loading