All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m6s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m45s
101 lines
5.2 KiB
Python
101 lines
5.2 KiB
Python
"""#2669: Cian отдаёт просмотры фразой — разбор обязан доставать из неё числа.
|
||
|
||
Замер на проде до правки: `listings.views_total` пуст у ВСЕХ 21 799 cian-строк
|
||
(0 из 1 571 detail-обогащённых), при том что у avito 12 350/12 352, у domklik
|
||
6 296/6 296, у yandex 1 146/1 210 — потеря ровно одна и только у Cian.
|
||
|
||
Корень: `_parse_views` звала `int()` по всей фразе
|
||
`"146 просмотров, 8 за сегодня"` → ValueError → None (except в самой функции).
|
||
|
||
Тесты гоняют настоящий сохранённый ответ (fixtures/cian_flat_330982715.html)
|
||
через настоящий `fetch_detail` + перебирают формы фразы.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
from unittest.mock import AsyncMock, MagicMock
|
||
|
||
import pytest
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||
|
||
from scraper_kit.providers.cian.detail import _parse_views, fetch_detail
|
||
|
||
_FIXTURE = os.path.join(os.path.dirname(__file__), "fixtures", "cian_flat_330982715.html")
|
||
|
||
# Дословно из фикстуры: "stats":{"totalViewsFormattedString":"146 просмотров, 8 за сегодня"}
|
||
_FIXTURE_PHRASE = "146 просмотров, 8 за сегодня"
|
||
|
||
|
||
def _fixture_html() -> str:
|
||
with open(_FIXTURE, encoding="utf-8") as fh:
|
||
return fh.read()
|
||
|
||
|
||
def test_fixture_still_carries_the_phrase() -> None:
|
||
"""Страховка: тесты ниже бессмысленны, если фикстура перестала содержать фразу."""
|
||
assert f'"totalViewsFormattedString":"{_FIXTURE_PHRASE}"' in _fixture_html()
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("phrase", "expected"),
|
||
[
|
||
(_FIXTURE_PHRASE, (146, 8)), # форма из сохранённого ответа
|
||
("1 просмотр", (1, None)), # единственное число, хвоста нет
|
||
("2 просмотра", (2, None)),
|
||
("0 просмотров", (0, None)), # ноль — это 0, а не «нет данных»
|
||
("1 234 просмотра", (1234, None)), # разделитель тысяч — обычный пробел
|
||
("1\xa0234 просмотра", (1234, None)), # ... и NBSP
|
||
("12 345 просмотров, 1\xa0234 за сегодня", (12345, 1234)), # narrow NBSP
|
||
("1 234", (1234, None)), # голое число (форма из старого докстринга)
|
||
("", (None, None)),
|
||
(None, (None, None)),
|
||
("просмотров нет", (None, None)), # без цифр — пусто, а не 0
|
||
("нет данных", (None, None)), # ответ площадки «нечего показать»
|
||
],
|
||
)
|
||
def test_parse_views_forms(phrase: str | None, expected: tuple[int | None, int | None]) -> None:
|
||
assert _parse_views(phrase) == expected
|
||
|
||
|
||
def test_unparsed_phrase_is_logged_with_itself(caplog: pytest.LogCaptureFixture) -> None:
|
||
"""Провал разбора обязан быть отличим от «Циан не прислал» (эпик #2674).
|
||
|
||
Прошлая пропажа держалась месяцами ровно потому, что в журнале не было ни
|
||
строки: пустая колонка одинаково выглядела и при сломанном разборе, и при
|
||
молчании площадки. Поэтому в warning уходит САМА фраза.
|
||
"""
|
||
with caplog.at_level(logging.WARNING, logger="scraper_kit.providers.cian.detail"):
|
||
assert _parse_views("нет данных") == (None, None)
|
||
|
||
assert "нет данных" in caplog.text, "неразобранная фраза не видна в журнале"
|
||
|
||
|
||
def test_absent_phrase_is_not_logged(caplog: pytest.LogCaptureFixture) -> None:
|
||
"""Циан не прислал ключ вовсе — это не поломка разбора, шуметь не о чем."""
|
||
with caplog.at_level(logging.WARNING, logger="scraper_kit.providers.cian.detail"):
|
||
assert _parse_views(None) == (None, None)
|
||
|
||
assert caplog.text == ""
|
||
|
||
|
||
def test_no_warning_when_today_tail_absent(caplog: pytest.LogCaptureFixture) -> None:
|
||
"""«0 просмотров» без хвоста «за сегодня» — штатная форма, не повод для warning."""
|
||
with caplog.at_level(logging.WARNING, logger="scraper_kit.providers.cian.detail"):
|
||
assert _parse_views("0 просмотров") == (0, None)
|
||
|
||
assert caplog.text == ""
|
||
|
||
|
||
async def test_fetch_detail_fills_views_from_real_saved_page() -> None:
|
||
"""Настоящая сохранённая страница → views_total/views_today непусты (#2669)."""
|
||
fetcher = MagicMock()
|
||
fetcher.fetch = AsyncMock(return_value=_fixture_html())
|
||
|
||
result = await fetch_detail("https://ekb.cian.ru/sale/flat/330982715/", browser_fetcher=fetcher)
|
||
|
||
assert result is not None
|
||
assert result.views_total == 146, "views_total снова теряется — колонка останется пустой"
|
||
assert result.views_today == 8
|