Compare commits
No commits in common. "8d516d07814e7233209908bfac7cd1ad5ba39cae" and "f82ee32f50a0c54b063618635c167a06d4d3932d" have entirely different histories.
8d516d0781
...
f82ee32f50
2 changed files with 13 additions and 62 deletions
|
|
@ -263,15 +263,6 @@ _TOTAL_COUNT_RE = re.compile(
|
||||||
flags=re.I,
|
flags=re.I,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Маркер валидной ПУСТОЙ выборки. На 0-результатной странице (напр. студии с
|
|
||||||
# pmin=17M, которых нет) Avito отдаёт HTTP 200 БЕЗ `page-title/count`, но с
|
|
||||||
# текстом «ничего не найдено» И ~50 fallback-«похожими» объявлениями (НЕ
|
|
||||||
# подходящими под ценовой фильтр). Без этого маркера _extract_total_count
|
|
||||||
# возвращал None → resilience-логика дробила пустой брекет + ротировала IP
|
|
||||||
# (прод-инцидент run 209: 14+ мин в пустых верхних брекетах, errors=1058).
|
|
||||||
# Lowercase substring «ничего не найден» ловит «найдено»/«найдены»/любой регистр.
|
|
||||||
_AVITO_NO_RESULTS_MARKERS = ("ничего не найден",)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_firewall_page(html: str) -> bool:
|
def _is_firewall_page(html: str) -> bool:
|
||||||
"""True если Avito вернул firewall-страницу IP-блока (на HTTP 200)."""
|
"""True если Avito вернул firewall-страницу IP-блока (на HTTP 200)."""
|
||||||
|
|
@ -650,31 +641,21 @@ class AvitoScraper(BaseScraper):
|
||||||
Это аналог cian totalOffers — gate для бисекции (total > cap → split).
|
Это аналог cian totalOffers — gate для бисекции (total > cap → split).
|
||||||
Парсинг: текст span → убрать NBSP (\xa0)/пробелы/` ` → int.
|
Парсинг: текст span → убрать NBSP (\xa0)/пробелы/` ` → int.
|
||||||
|
|
||||||
Порядок: сначала `page-title/count` (его несёт ТОЛЬКО непустая выборка),
|
|
||||||
потом no-results-маркер → 0 (валидная пустая выборка: брекет скипается без
|
|
||||||
split/rotation и без скрапа ~50 fallback-«похожих»), и только при отсутствии
|
|
||||||
обоих — None (реально битая/captcha/firewall/незагруженная страница).
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int при успехе (включая 0 на no-results-странице), либо None при
|
int при успехе (включая 0), либо None при captcha/firewall/missing span.
|
||||||
captcha/firewall/незагруженной странице (ни count, ни no-results).
|
|
||||||
"""
|
"""
|
||||||
m = _TOTAL_COUNT_RE.search(html)
|
m = _TOTAL_COUNT_RE.search(html)
|
||||||
if m is not None:
|
if m is None:
|
||||||
raw = m.group("count")
|
return None
|
||||||
# Убрать NBSP (\xa0), -entity и обычные пробелы-разделители разрядов.
|
raw = m.group("count")
|
||||||
cleaned = raw.replace("\xa0", "").replace(" ", "").replace(" ", "")
|
# Убрать NBSP (\xa0), -entity и обычные пробелы-разделители разрядов.
|
||||||
if cleaned:
|
cleaned = raw.replace("\xa0", "").replace(" ", "").replace(" ", "")
|
||||||
try:
|
if not cleaned:
|
||||||
return int(cleaned)
|
return None
|
||||||
except (TypeError, ValueError):
|
try:
|
||||||
pass
|
return int(cleaned)
|
||||||
# Счётчик не найден/не распарсился. Проверяем no-results-маркер: его наличие
|
except (TypeError, ValueError):
|
||||||
# = валидная пустая выборка (total=0), несмотря на ~50 fallback-объявлений.
|
return None
|
||||||
lowered = html.lower()
|
|
||||||
if any(marker in lowered for marker in _AVITO_NO_RESULTS_MARKERS):
|
|
||||||
return 0
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def _fetch_rooms_page_html(
|
async def _fetch_rooms_page_html(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -110,41 +110,11 @@ def test_extract_total_count_zero(scraper: AvitoScraper) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_extract_total_count_missing_returns_none(scraper: AvitoScraper) -> None:
|
def test_extract_total_count_missing_returns_none(scraper: AvitoScraper) -> None:
|
||||||
"""Captcha/firewall (нет span, нет no-results-маркера) → None."""
|
"""Captcha/firewall (нет span) → None."""
|
||||||
html = "<html><head><title>Доступ ограничен</title></head></html>"
|
html = "<html><head><title>Доступ ограничен</title></head></html>"
|
||||||
assert scraper._extract_total_count(html) is None
|
assert scraper._extract_total_count(html) is None
|
||||||
|
|
||||||
|
|
||||||
def test_extract_total_count_no_results_marker_returns_zero(scraper: AvitoScraper) -> None:
|
|
||||||
"""Валидная пустая выборка: нет `page-title/count`, но есть «ничего не найдено» → 0.
|
|
||||||
|
|
||||||
Даже если на странице есть fallback-«похожие» объявления (item-id'ы) — это
|
|
||||||
не подходящие под фильтр листинги, total всё равно 0. Это предотвращает
|
|
||||||
дробление пустого брекета + ротацию IP (прод-инцидент run 209).
|
|
||||||
"""
|
|
||||||
html = (
|
|
||||||
'<div data-marker="item" data-item-id="1234567"></div>'
|
|
||||||
'<div data-marker="item" data-item-id="7654321"></div>'
|
|
||||||
"<p>По вашему запросу ничего не найдено</p>"
|
|
||||||
)
|
|
||||||
assert scraper._extract_total_count(html) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_total_count_no_results_marker_case_insensitive(scraper: AvitoScraper) -> None:
|
|
||||||
"""Маркер регистронезависим: «Ничего не найдено» с заглавной → тоже 0."""
|
|
||||||
html = "<div><h2>Ничего не найдено</h2><p>Попробуйте изменить фильтры</p></div>"
|
|
||||||
assert scraper._extract_total_count(html) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_total_count_count_wins_over_no_results(scraper: AvitoScraper) -> None:
|
|
||||||
"""Непустая выборка с count имеет приоритет, даже если где-то ниже есть маркер."""
|
|
||||||
html = (
|
|
||||||
f"<div><h1>Квартиры</h1>{_REAL_COUNT_SPAN}</div>"
|
|
||||||
"<footer>Если ничего не найдено, измените запрос</footer>"
|
|
||||||
)
|
|
||||||
assert scraper._extract_total_count(html) == 1178
|
|
||||||
|
|
||||||
|
|
||||||
# ── Бисекция: split / paginate / dedup ────────────────────────────────────────
|
# ── Бисекция: split / paginate / dedup ────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue