fix: capturar div container pai do card para extrair notas e avaliacoes individuais com precisao e fallback no painel
This commit is contained in:
@@ -13,11 +13,10 @@ logger = logging.getLogger(__name__)
|
||||
class ScraperService:
|
||||
|
||||
@staticmethod
|
||||
def _extract_card_rating_and_reviews(card) -> tuple:
|
||||
def _extract_card_rating_and_reviews(card, detail_panel=None) -> tuple:
|
||||
"""
|
||||
Extrai a nota (rating) e a quantidade total de avaliações estritamente
|
||||
escopadas dentro do container do próprio card do estabelecimento.
|
||||
Evita a repetição de dados globais ou de cabeçalho.
|
||||
Extrai a nota (rating) e a quantidade total de avaliações do container
|
||||
exclusivo do estabelecimento, com fallback para o painel de detalhes.
|
||||
"""
|
||||
rating = 0.0
|
||||
total_avaliacoes = 0
|
||||
@@ -26,10 +25,24 @@ class ScraperService:
|
||||
return rating, total_avaliacoes
|
||||
|
||||
try:
|
||||
# 1. Procurar aria-labels contendo informações de estrelas/avaliações no próprio card ou em seus filhos
|
||||
aria_elems = card.query_selector_all("[aria-label*='estrela'], [aria-label*='star'], [aria-label*='avaliaç']")
|
||||
# 1. Obter o elemento container pai do lugar no feed (div.Nv251d ou ancestral do card link)
|
||||
container = card
|
||||
try:
|
||||
parent_handle = card.evaluate_handle(
|
||||
"el => el.closest('div.Nv251d') || el.closest('div[role=\"article\"]') || el.closest('div.THtW3d') || el.parentElement.parentElement"
|
||||
)
|
||||
if parent_handle:
|
||||
elem = parent_handle.as_element()
|
||||
if elem:
|
||||
container = elem
|
||||
except Exception:
|
||||
container = card
|
||||
|
||||
# 2. Procurar aria-labels no container do lugar
|
||||
aria_elems = container.query_selector_all("[aria-label*='estrela'], [aria-label*='star'], [aria-label*='avaliaç']")
|
||||
container_label = container.get_attribute("aria-label") or ""
|
||||
card_label = card.get_attribute("aria-label") or ""
|
||||
combined_aria = " ".join([e.get_attribute("aria-label") or "" for e in aria_elems] + [card_label])
|
||||
combined_aria = " ".join([e.get_attribute("aria-label") or "" for e in aria_elems] + [container_label, card_label])
|
||||
|
||||
r_match = re.search(r'([1-5][.,]\d)\s*(?:estrelas?|stars?)', combined_aria, re.IGNORECASE)
|
||||
if r_match:
|
||||
@@ -45,14 +58,14 @@ class ScraperService:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 2. Extrair do texto bruto (inner_text) exclusivo do card do estabelecimento
|
||||
card_text = card.inner_text() or ""
|
||||
# 3. Ler o texto bruto (inner_text) do container do lugar
|
||||
container_text = container.inner_text() or card.inner_text() or ""
|
||||
|
||||
if rating == 0.0:
|
||||
# No Google Maps a nota no card vem em formatos como "4,8 (120)" ou "4.8"
|
||||
m = re.search(r'([1-5][.,]\d)\s*(?:\(|\s*★|\s*estrelas?)', card_text, re.IGNORECASE)
|
||||
# O rating no card do Maps vem como "4,8 (120)" ou "4.8 ★"
|
||||
m = re.search(r'([1-5][.,]\d)\s*(?:\(|\s*★|\s*estrelas?)', container_text, re.IGNORECASE)
|
||||
if not m:
|
||||
m = re.search(r'([1-5][.,]\d)', card_text)
|
||||
m = re.search(r'([1-5][.,]\d)', container_text)
|
||||
if m:
|
||||
try:
|
||||
val = float(m.group(1).replace(',', '.'))
|
||||
@@ -63,19 +76,48 @@ class ScraperService:
|
||||
|
||||
if total_avaliacoes == 0:
|
||||
# O total de avaliações no card vem entre parênteses "(120)" ou "(1.250)"
|
||||
m = re.search(r'\(([\d.,]+)\)', card_text)
|
||||
m = re.search(r'\(([\d.,]+)\)', container_text)
|
||||
if m:
|
||||
try:
|
||||
total_avaliacoes = int(re.sub(r'\D', '', m.group(1)))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
m = re.search(r'([\d.,]+)\s*avaliaç', card_text, re.IGNORECASE)
|
||||
m = re.search(r'([\d.,]+)\s*avaliaç', container_text, re.IGNORECASE)
|
||||
if m:
|
||||
try:
|
||||
total_avaliacoes = int(re.sub(r'\D', '', m.group(1)))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 4. Fallback para o detail_panel se o container do card não possuir a nota
|
||||
if (rating == 0.0 or total_avaliacoes == 0) and detail_panel:
|
||||
detail_text = detail_panel.inner_text() or ""
|
||||
|
||||
if rating == 0.0:
|
||||
star_elem = detail_panel.query_selector("div.F7L8d span, span.ceYyJf, [aria-label*='estrelas']")
|
||||
if star_elem:
|
||||
st_txt = star_elem.get_attribute("aria-label") or star_elem.inner_text()
|
||||
st_m = re.search(r'([1-5][.,]\d)', st_txt)
|
||||
if st_m:
|
||||
try:
|
||||
rating = float(st_m.group(1).replace(',', '.'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if total_avaliacoes == 0:
|
||||
rev_elem = detail_panel.query_selector("button[aria-label*='avaliaç'], span[aria-label*='avaliaç']")
|
||||
if rev_elem:
|
||||
rv_txt = rev_elem.get_attribute("aria-label") or rev_elem.inner_text()
|
||||
rv_m = re.search(r'([\d.,]+)\s*avaliaç', rv_txt, re.IGNORECASE)
|
||||
if rv_m:
|
||||
try:
|
||||
cnt = int(re.sub(r'\D', '', rv_m.group(1)))
|
||||
if cnt < 20000:
|
||||
total_avaliacoes = cnt
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao extrair rating/avaliacoes do card: {e}")
|
||||
|
||||
@@ -162,10 +204,7 @@ class ScraperService:
|
||||
continue
|
||||
seen_urls.add(href)
|
||||
|
||||
# 1. Extração da Nota e Avaliações estritamente escopadas no Card do lugar
|
||||
rating, total_avaliacoes = ScraperService._extract_card_rating_and_reviews(card)
|
||||
|
||||
# 2. Clicar no card para abrir o painel lateral de detalhes de telefone, endereço e website
|
||||
# Clicar no card para abrir o painel lateral de detalhes no Maps
|
||||
try:
|
||||
card.click()
|
||||
time.sleep(1.2)
|
||||
@@ -175,7 +214,10 @@ class ScraperService:
|
||||
detail_panel = page.query_selector("div[role='main']") or page.query_selector("div.m6QEbd")
|
||||
detail_text = detail_panel.inner_text() if detail_panel else card.inner_text()
|
||||
|
||||
# 3. Nome da Empresa (priorizar aria-label do card ou h1 dentro do detail_panel)
|
||||
# 1. Extração da Nota e Avaliações estritamente escopadas no Container do lugar
|
||||
rating, total_avaliacoes = ScraperService._extract_card_rating_and_reviews(card, detail_panel)
|
||||
|
||||
# 2. Nome da Empresa (priorizar aria-label do card ou h1 dentro do detail_panel)
|
||||
nome_empresa = (card.get_attribute("aria-label") or "").strip()
|
||||
if nome_empresa.lower() in ('resultados', 'resultado', ''):
|
||||
nome_empresa = ""
|
||||
@@ -197,7 +239,7 @@ class ScraperService:
|
||||
if not nome_empresa or nome_empresa.lower() in ('resultados', 'resultado'):
|
||||
continue
|
||||
|
||||
# 4. Telefone detalhado
|
||||
# 3. Telefone detalhado
|
||||
telefone_raw = ""
|
||||
phone_btn = page.query_selector("button[data-tooltip*='telefone'], button[data-item-id*='phone'], [aria-label*='Telefone'], [aria-label*='Ligar']")
|
||||
if phone_btn:
|
||||
@@ -211,7 +253,7 @@ class ScraperService:
|
||||
if phone_match:
|
||||
telefone_raw = phone_match.group(1)
|
||||
|
||||
# 5. Endereço Completo
|
||||
# 4. Endereço Completo
|
||||
endereco = ""
|
||||
addr_btn = page.query_selector("button[data-tooltip*='endereço'], button[data-item-id*='address'], [aria-label*='Endereço']")
|
||||
if addr_btn:
|
||||
@@ -228,7 +270,7 @@ class ScraperService:
|
||||
endereco = line_str
|
||||
break
|
||||
|
||||
# 6. Website
|
||||
# 5. Website
|
||||
website = ""
|
||||
site_btn = page.query_selector("a[data-tooltip*='site'], a[data-item-id*='authority']")
|
||||
if site_btn:
|
||||
@@ -276,7 +318,7 @@ class ScraperService:
|
||||
if site_btn:
|
||||
website = site_btn.get_attribute("href") or ""
|
||||
|
||||
rating, total_avaliacoes = ScraperService._extract_card_rating_and_reviews(page.query_selector("h1"))
|
||||
rating, total_avaliacoes = ScraperService._extract_card_rating_and_reviews(page.query_selector("h1"), detail_panel)
|
||||
|
||||
results.append({
|
||||
'nome_empresa': nome_empresa,
|
||||
|
||||
Reference in New Issue
Block a user