#!/usr/bin/env python3 """Read-only extraction helper for the Cruzeiro ranking. The database is opened in a consistent read transaction so WAL content is included. The script only prints a TSV and never updates the database. """ from __future__ import annotations import gzip import argparse import re import sqlite3 import sys from pathlib import Path from bs4 import BeautifulSoup ROOT = Path(__file__).resolve().parents[2] DB_PATH = ROOT / "dfimoveis_data" / "dfimoveis.sqlite3" DATA_ROOT = DB_PATH.parent SEARCH_FRAGMENT = "/cruzeiro/novo/" def clean(value: str | None) -> str: return re.sub(r"\s+", " ", value or "").strip() def sanitize_description(value: str) -> str: value = re.sub(r"\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b", "[email]", value) value = re.sub( r"(?:\(\d{2}\)|\b\d{2}\b)\s*(?:\d[\s.-]*){5,11}\d", "[telefone]", value, ) value = re.sub(r"\bCRECI\b.{0,30}", "", value, flags=re.IGNORECASE) return clean(value) def parse_html(relative_path: str) -> tuple[str, str, str]: with gzip.open(DATA_ROOT / relative_path, "rt", encoding="utf-8", errors="replace") as fh: soup = BeautifulSoup(fh.read(), "lxml") description_node = soup.select_one("div.assined-imv") description = "" if description_node: for node in description_node.select("span, a, button"): node.decompose() description = sanitize_description(description_node.get_text(" ", strip=True)) details: list[str] = [] for item in soup.select("ul.details-text li"): text = clean(item.get_text(" ", strip=True)) if text and text not in details: details.append(text) address_node = soup.select_one('[itemprop="address"]') address = clean(address_node.get_text(" ", strip=True) if address_node else "") return description, " | ".join(details), address def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--ids-only", action="store_true") args = parser.parse_args() connection = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True) connection.execute("PRAGMA query_only = ON") connection.execute("BEGIN") rows = connection.execute( """ SELECT l.listing_id, l.url, l.price_brl, l.condominium_brl, l.iptu_brl, l.area_m2, l.bedrooms, l.suites, l.parking_spaces, l.raw_html_path, COUNT(p.source_url) AS photo_count FROM listings AS l JOIN listing_searches AS s USING (listing_id) LEFT JOIN photos AS p USING (listing_id) WHERE instr(s.search_url, ?) > 0 AND l.inactive_at IS NULL GROUP BY l.listing_id ORDER BY l.price_brl, l.listing_id LIMIT 100 """, (SEARCH_FRAGMENT,), ).fetchall() connection.close() if args.ids_only: for row in rows: print(row[0]) return 0 print( "listing_id\tprice_brl\tcondominium_brl\tiptu_brl\tarea_m2\tbedrooms\t" "suites\tparking_spaces\tphoto_count\taddress\tdetails\tdescription\turl" ) for row in rows: ( listing_id, url, price, condominium, iptu, area, bedrooms, suites, parking, raw_path, photo_count, ) = row description, details, address = parse_html(raw_path) values = ( listing_id, price, condominium, iptu, area, bedrooms, suites, parking, photo_count, address, details, description, url, ) print("\t".join("" if value is None else clean(str(value)) for value in values)) return 0 if __name__ == "__main__": sys.exit(main())