feat(tradein-sweep): --require-house-number filter (default on) (#536)
This commit is contained in:
parent
bc594e3f79
commit
499ca76aed
1 changed files with 68 additions and 12 deletions
|
|
@ -69,22 +69,34 @@ def _addresses_to_sweep(
|
||||||
limit: int | None,
|
limit: int | None,
|
||||||
skip_recent_days: int,
|
skip_recent_days: int,
|
||||||
single_address: str | None,
|
single_address: str | None,
|
||||||
|
require_house_number: bool = True,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Pull unique active EKB yandex addresses, optionally skipping recently-cached ones."""
|
"""Pull unique active EKB yandex addresses, optionally skipping recently-cached ones.
|
||||||
|
|
||||||
|
require_house_number: filter to addresses containing a house number. Yandex's
|
||||||
|
valuation endpoint needs a specific building — bare street names ('Академическая
|
||||||
|
улица') return 0 items. Heuristic: address must contain a digit after a comma
|
||||||
|
OR end with a digit token. Default True.
|
||||||
|
"""
|
||||||
if single_address:
|
if single_address:
|
||||||
return [single_address.strip()]
|
return [single_address.strip()]
|
||||||
|
|
||||||
|
# Postgres regex for house-number tokens — used when require_house_number=True.
|
||||||
|
# Matches ", 12" / ", 12к3" / ", 5Б" anywhere in the address.
|
||||||
|
house_num_clause = " AND address ~ ',\\s*\\d+' " if require_house_number else ""
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
if skip_recent_days > 0:
|
if skip_recent_days > 0:
|
||||||
sql = text(
|
sql = text(
|
||||||
"""
|
f"""
|
||||||
SELECT DISTINCT l.address
|
SELECT DISTINCT l.address
|
||||||
FROM listings l
|
FROM listings l
|
||||||
WHERE l.source = 'yandex'
|
WHERE l.source = 'yandex'
|
||||||
AND l.region_code = 66
|
AND l.region_code = 66
|
||||||
AND l.is_active = TRUE
|
AND l.is_active = TRUE
|
||||||
AND l.address IS NOT NULL
|
AND l.address IS NOT NULL
|
||||||
|
{house_num_clause}
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM external_valuations ev
|
SELECT 1 FROM external_valuations ev
|
||||||
WHERE ev.source = 'yandex_valuation'
|
WHERE ev.source = 'yandex_valuation'
|
||||||
|
|
@ -97,13 +109,14 @@ def _addresses_to_sweep(
|
||||||
rows = db.execute(sql, {"days": skip_recent_days}).mappings().all()
|
rows = db.execute(sql, {"days": skip_recent_days}).mappings().all()
|
||||||
else:
|
else:
|
||||||
sql = text(
|
sql = text(
|
||||||
"""
|
f"""
|
||||||
SELECT DISTINCT address
|
SELECT DISTINCT address
|
||||||
FROM listings
|
FROM listings
|
||||||
WHERE source = 'yandex'
|
WHERE source = 'yandex'
|
||||||
AND region_code = 66
|
AND region_code = 66
|
||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
AND address IS NOT NULL
|
AND address IS NOT NULL
|
||||||
|
{house_num_clause}
|
||||||
ORDER BY address
|
ORDER BY address
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
@ -124,17 +137,23 @@ async def _sweep_one_address(
|
||||||
) -> tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
"""Fetch+save up to max_pages for one address.
|
"""Fetch+save up to max_pages for one address.
|
||||||
|
|
||||||
|
Pagination stop conditions (whichever fires first):
|
||||||
|
1. Page returns 0 items (empty / past last page)
|
||||||
|
2. Cumulative fetched >= total_objects from page-1 header (when known)
|
||||||
|
3. max_pages reached
|
||||||
|
|
||||||
Returns (items_fetched_total, items_saved_total).
|
Returns (items_fetched_total, items_saved_total).
|
||||||
"""
|
"""
|
||||||
total_fetched = 0
|
total_fetched = 0
|
||||||
total_saved = 0
|
total_saved = 0
|
||||||
|
total_objects_expected: int | None = None
|
||||||
|
seen_keys: set[tuple] = set() # dedup within sweep (page overlap protection)
|
||||||
|
|
||||||
for page in range(1, max_pages + 1):
|
for page in range(1, max_pages + 1):
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
# _get_or_fetch_yandex_valuation_cached signature accepts address+offer_*;
|
# _get_or_fetch_yandex_valuation_cached only fetches page 1 (no `page`
|
||||||
# page is not in its signature — for paginated walk we must call the
|
# arg); cache key includes address only. Pages >= 2 go direct to scraper.
|
||||||
# scraper directly. Use the cached helper for page=1 only, then call
|
|
||||||
# YandexValuationScraper.fetch_house_history for pages >= 2.
|
|
||||||
if page == 1:
|
if page == 1:
|
||||||
result = await _get_or_fetch_yandex_valuation_cached(
|
result = await _get_or_fetch_yandex_valuation_cached(
|
||||||
db,
|
db,
|
||||||
|
|
@ -153,15 +172,32 @@ async def _sweep_one_address(
|
||||||
if result is None or not result.history_items:
|
if result is None or not result.history_items:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Capture total_objects on first response (subsequent pages may not carry it)
|
||||||
|
if total_objects_expected is None and result.house.total_objects:
|
||||||
|
total_objects_expected = result.house.total_objects
|
||||||
|
|
||||||
|
# Dedup against prior pages (Yandex sometimes repeats items at page boundaries)
|
||||||
|
new_items = []
|
||||||
|
for it in result.history_items:
|
||||||
|
key = (it.publish_date, it.area_m2, it.floor, it.start_price)
|
||||||
|
if key not in seen_keys:
|
||||||
|
seen_keys.add(key)
|
||||||
|
new_items.append(it)
|
||||||
|
|
||||||
saved = _save_yandex_history_items(db, result)
|
saved = _save_yandex_history_items(db, result)
|
||||||
total_fetched += len(result.history_items)
|
total_fetched += len(new_items)
|
||||||
total_saved += saved
|
total_saved += saved
|
||||||
|
exp_str = f"/{total_objects_expected}" if total_objects_expected is not None else ""
|
||||||
print(
|
print(
|
||||||
f" page={page}/{max_pages}: items={len(result.history_items)} saved={saved}",
|
f" page={page}/{max_pages}: items={len(result.history_items)} "
|
||||||
|
f"new={len(new_items)} saved={saved} cum={total_fetched}{exp_str}",
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
# Stop early if Yandex doesn't paginate further (less than full page returned)
|
# Stop if we've collected at least what the header promised
|
||||||
if len(result.history_items) < 13:
|
if total_objects_expected is not None and total_fetched >= total_objects_expected:
|
||||||
|
break
|
||||||
|
# Or if this page yielded zero NEW items (pure duplicates → end of feed)
|
||||||
|
if not new_items:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
|
|
@ -213,9 +249,29 @@ async def main() -> None:
|
||||||
default=None,
|
default=None,
|
||||||
help="Single address mode — sweep only this address",
|
help="Single address mode — sweep only this address",
|
||||||
)
|
)
|
||||||
|
p.add_argument(
|
||||||
|
"--require-house-number",
|
||||||
|
dest="require_house_number",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="Skip addresses without a house number (default: True). "
|
||||||
|
"Yandex valuation endpoint needs a specific building; street-only addresses "
|
||||||
|
"return 0 items. Use --no-require-house-number to disable.",
|
||||||
|
)
|
||||||
|
p.add_argument(
|
||||||
|
"--no-require-house-number",
|
||||||
|
dest="require_house_number",
|
||||||
|
action="store_false",
|
||||||
|
help="Include all addresses (sweep street-only entries too — most return 0).",
|
||||||
|
)
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
addresses = _addresses_to_sweep(args.limit, args.skip_recent_days, args.address)
|
addresses = _addresses_to_sweep(
|
||||||
|
args.limit,
|
||||||
|
args.skip_recent_days,
|
||||||
|
args.address,
|
||||||
|
require_house_number=args.require_house_number,
|
||||||
|
)
|
||||||
if not addresses:
|
if not addresses:
|
||||||
print("No addresses to sweep (all recently cached or none match query).", flush=True)
|
print("No addresses to sweep (all recently cached or none match query).", flush=True)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue