From ea91767d8b6b348a2a521b908043da96ce03b003 Mon Sep 17 00:00:00 2001 From: Silas Brito Date: Wed, 26 Aug 2026 18:05:07 -0300 Subject: [PATCH] fix: capturar div container pai do card para extrair notas e avaliacoes individuais com precisao e fallback no painel --- backend/app/services/scraper_service.py | 88 ++++++++++++++++++------- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/backend/app/services/scraper_service.py b/backend/app/services/scraper_service.py index b0275f1..c81bb38 100644 --- a/backend/app/services/scraper_service.py +++ b/backend/app/services/scraper_service.py @@ -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,