From 52c2ba90d61e31b3b02b3706cf9cf6851ca3d2b1 Mon Sep 17 00:00:00 2001 From: Silas Brito Date: Wed, 26 Aug 2026 18:16:12 -0300 Subject: [PATCH] fix: captura precisa de notas e total de comentarios reais por estabelecimento no Google Maps --- backend/app/services/scraper_service.py | 78 +++++++++++-------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/backend/app/services/scraper_service.py b/backend/app/services/scraper_service.py index c81bb38..bbb83d3 100644 --- a/backend/app/services/scraper_service.py +++ b/backend/app/services/scraper_service.py @@ -16,7 +16,7 @@ class ScraperService: def _extract_card_rating_and_reviews(card, detail_panel=None) -> tuple: """ Extrai a nota (rating) e a quantidade total de avaliações do container - exclusivo do estabelecimento, com fallback para o painel de detalhes. + exclusivo do estabelecimento (ex: "4,5 estrelas 56.778 comentários" ou "4,5(56.778)"). """ rating = 0.0 total_avaliacoes = 0 @@ -38,59 +38,51 @@ class ScraperService: 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ç']") + # 2. Checar ARIA labels do container (ex: "4,5 estrelas 56.778 comentários") + aria_elems = container.query_selector_all("[aria-label]") 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] + [container_label, card_label]) + all_labels = [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: - try: - rating = float(r_match.group(1).replace(',', '.')) - except ValueError: - pass + for lbl in all_labels: + m = re.search(r'([1-5][.,]\d)\s*(?:estrelas?|stars?)\s*([\d.,]+)', lbl, re.IGNORECASE) + if m: + try: + r_val = float(m.group(1).replace(',', '.')) + c_val = int(re.sub(r'\D', '', m.group(2))) + if 1.0 <= r_val <= 5.0: + rating = r_val + total_avaliacoes = c_val + return rating, total_avaliacoes + except ValueError: + pass - rev_match = re.search(r'([\d.,]+)\s*avaliaç', combined_aria, re.IGNORECASE) - if rev_match: - try: - total_avaliacoes = int(re.sub(r'\D', '', rev_match.group(1))) - except ValueError: - pass - - # 3. Ler o texto bruto (inner_text) do container do lugar + # 3. Ler linhas de texto do container (ex: "4,5(56.778)" ou "3,8(360)") container_text = container.inner_text() or card.inner_text() or "" + for line in container_text.split('\n'): + line = line.strip() - if rating == 0.0: - # 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)', container_text) - if m: + # Testar formato oficial "4,5(56.778)" + m_line = re.search(r'([1-5][.,]\d)\s*\(([\d.,]+)\)', line) + if m_line: try: - val = float(m.group(1).replace(',', '.')) - if 1.0 <= val <= 5.0: - rating = val + rating = float(m_line.group(1).replace(',', '.')) + total_avaliacoes = int(re.sub(r'\D', '', m_line.group(2))) + return rating, total_avaliacoes except ValueError: pass - if total_avaliacoes == 0: - # O total de avaliações no card vem entre parênteses "(120)" ou "(1.250)" - 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ç', container_text, re.IGNORECASE) - if m: + if rating == 0.0: + m_rat = re.search(r'^([1-5][.,]\d)', line) + if m_rat: try: - total_avaliacoes = int(re.sub(r'\D', '', m.group(1))) + val = float(m_rat.group(1).replace(',', '.')) + if 1.0 <= val <= 5.0: + rating = val except ValueError: pass - # 4. Fallback para o detail_panel se o container do card não possuir a nota + # 4. Fallback no detail_panel da empresa clicada caso o card na lista não exiba a contagem if (rating == 0.0 or total_avaliacoes == 0) and detail_panel: detail_text = detail_panel.inner_text() or "" @@ -106,14 +98,14 @@ class ScraperService: pass if total_avaliacoes == 0: - rev_elem = detail_panel.query_selector("button[aria-label*='avaliaç'], span[aria-label*='avaliaç']") + rev_elem = detail_panel.query_selector("button[aria-label*='avaliaç'], button[aria-label*='comentário'], span[aria-label*='comentário']") 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) + rv_m = re.search(r'([\d.,]+)\s*(?:comentários|avaliações)', rv_txt, re.IGNORECASE) if rv_m: try: cnt = int(re.sub(r'\D', '', rv_m.group(1))) - if cnt < 20000: + if cnt < 200000: total_avaliacoes = cnt except ValueError: pass