fix: isolamento estrito de nota e total de avaliacoes dentro do container especifico de cada card do Maps
This commit is contained in:
@@ -13,57 +13,71 @@ logger = logging.getLogger(__name__)
|
|||||||
class ScraperService:
|
class ScraperService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_rating_and_reviews(detail_panel, text_fallback: str = "") -> tuple:
|
def _extract_card_rating_and_reviews(card) -> tuple:
|
||||||
"""
|
"""
|
||||||
Extrai a nota de avaliação (ex: 4.8) e o total de avaliações (ex: 150)
|
Extrai a nota (rating) e a quantidade total de avaliações estritamente
|
||||||
específicos de cada estabelecimento no Google Maps.
|
escopadas dentro do container do próprio card do estabelecimento.
|
||||||
|
Evita a repetição de dados globais ou de cabeçalho.
|
||||||
"""
|
"""
|
||||||
rating = 0.0
|
rating = 0.0
|
||||||
total_avaliacoes = 0
|
total_avaliacoes = 0
|
||||||
|
|
||||||
if detail_panel:
|
if not card:
|
||||||
# 1. Procurar elemento específico de nota (ex: <span aria-label="4,8 estrelas"> ou <span class="ceYyJf">)
|
return rating, total_avaliacoes
|
||||||
star_elem = detail_panel.query_selector("[aria-label*='estrela'], [aria-label*='star'], span.ceYyJf, div.F7L8d")
|
|
||||||
if star_elem:
|
try:
|
||||||
aria_txt = star_elem.get_attribute("aria-label") or star_elem.inner_text()
|
# 1. Procurar aria-labels contendo informações de estrelas/avaliações no próprio card ou em seus filhos
|
||||||
m = re.search(r'([1-5][.,]\d)', aria_txt)
|
aria_elems = card.query_selector_all("[aria-label*='estrela'], [aria-label*='star'], [aria-label*='avaliaç']")
|
||||||
|
card_label = card.get_attribute("aria-label") or ""
|
||||||
|
combined_aria = " ".join([e.get_attribute("aria-label") or "" for e in aria_elems] + [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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 2. Extrair do texto bruto (inner_text) exclusivo do card do estabelecimento
|
||||||
|
card_text = 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)
|
||||||
|
if not m:
|
||||||
|
m = re.search(r'([1-5][.,]\d)', card_text)
|
||||||
if m:
|
if m:
|
||||||
try:
|
try:
|
||||||
rating = float(m.group(1).replace(',', '.'))
|
val = float(m.group(1).replace(',', '.'))
|
||||||
|
if 1.0 <= val <= 5.0:
|
||||||
|
rating = val
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 2. Procurar elemento específico de total de avaliações (ex: <button aria-label="1.250 avaliações">)
|
if total_avaliacoes == 0:
|
||||||
rev_elem = detail_panel.query_selector("button[aria-label*='avaliaç'], span[aria-label*='avaliaç'], [aria-label*='avaliações']")
|
# O total de avaliações no card vem entre parênteses "(120)" ou "(1.250)"
|
||||||
if rev_elem:
|
m = re.search(r'\(([\d.,]+)\)', card_text)
|
||||||
rev_txt = rev_elem.get_attribute("aria-label") or rev_elem.inner_text()
|
|
||||||
m = re.search(r'([\d.,]+)\s*avaliaç', rev_txt, re.IGNORECASE)
|
|
||||||
if m:
|
if m:
|
||||||
try:
|
try:
|
||||||
total_avaliacoes = int(re.sub(r'\D', '', m.group(1)))
|
total_avaliacoes = int(re.sub(r'\D', '', m.group(1)))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
# Fallback usando o texto bruto do painel do lugar
|
m = re.search(r'([\d.,]+)\s*avaliaç', card_text, re.IGNORECASE)
|
||||||
if rating == 0.0 and text_fallback:
|
if m:
|
||||||
m = re.search(r'([1-5][.,]\d)\s*(?:estrelas?|★|\s*\(|\s*·)', text_fallback, re.IGNORECASE)
|
try:
|
||||||
if not m:
|
total_avaliacoes = int(re.sub(r'\D', '', m.group(1)))
|
||||||
m = re.search(r'([1-5][.,]\d)', text_fallback)
|
except ValueError:
|
||||||
if m:
|
pass
|
||||||
try:
|
except Exception as e:
|
||||||
rating = float(m.group(1).replace(',', '.'))
|
logger.error(f"Erro ao extrair rating/avaliacoes do card: {e}")
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if total_avaliacoes == 0 and text_fallback:
|
|
||||||
m = re.search(r'([\d.,]+)\s*avaliaç', text_fallback, re.IGNORECASE)
|
|
||||||
if not m:
|
|
||||||
m = re.search(r'\(([\d.,]+)\)', text_fallback)
|
|
||||||
if m:
|
|
||||||
try:
|
|
||||||
total_avaliacoes = int(re.sub(r'\D', '', m.group(1)))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return rating, total_avaliacoes
|
return rating, total_avaliacoes
|
||||||
|
|
||||||
@@ -72,7 +86,7 @@ class ScraperService:
|
|||||||
"""
|
"""
|
||||||
Executa o Playwright em modo headless para buscar no Google Maps.
|
Executa o Playwright em modo headless para buscar no Google Maps.
|
||||||
Clica em cada estabelecimento para abrir o painel lateral de detalhes
|
Clica em cada estabelecimento para abrir o painel lateral de detalhes
|
||||||
e extrair dados completos: Telefone, Endereço completo, Website e Rating.
|
e extrai dados completos: Telefone, Endereço completo, Website e Rating individual.
|
||||||
"""
|
"""
|
||||||
results = []
|
results = []
|
||||||
try:
|
try:
|
||||||
@@ -125,7 +139,6 @@ class ScraperService:
|
|||||||
has_feed = False
|
has_feed = False
|
||||||
logger.info("Feed de múltiplos resultados não localizado. Verificando painel de resultado único.")
|
logger.info("Feed de múltiplos resultados não localizado. Verificando painel de resultado único.")
|
||||||
|
|
||||||
# Se houver feed de múltiplos resultados, fazer scroll progressivo
|
|
||||||
if has_feed:
|
if has_feed:
|
||||||
for _ in range(min(6, (max_results // 3) + 2)):
|
for _ in range(min(6, (max_results // 3) + 2)):
|
||||||
page.evaluate(f"""
|
page.evaluate(f"""
|
||||||
@@ -149,7 +162,10 @@ class ScraperService:
|
|||||||
continue
|
continue
|
||||||
seen_urls.add(href)
|
seen_urls.add(href)
|
||||||
|
|
||||||
# Clicar no card para abrir o painel lateral de detalhes no Maps
|
# 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
|
||||||
try:
|
try:
|
||||||
card.click()
|
card.click()
|
||||||
time.sleep(1.2)
|
time.sleep(1.2)
|
||||||
@@ -159,7 +175,7 @@ class ScraperService:
|
|||||||
detail_panel = page.query_selector("div[role='main']") or page.query_selector("div.m6QEbd")
|
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()
|
detail_text = detail_panel.inner_text() if detail_panel else card.inner_text()
|
||||||
|
|
||||||
# 1. Nome da Empresa (priorizar aria-label do card ou h1 dentro do detail_panel)
|
# 3. Nome da Empresa (priorizar aria-label do card ou h1 dentro do detail_panel)
|
||||||
nome_empresa = (card.get_attribute("aria-label") or "").strip()
|
nome_empresa = (card.get_attribute("aria-label") or "").strip()
|
||||||
if nome_empresa.lower() in ('resultados', 'resultado', ''):
|
if nome_empresa.lower() in ('resultados', 'resultado', ''):
|
||||||
nome_empresa = ""
|
nome_empresa = ""
|
||||||
@@ -181,7 +197,7 @@ class ScraperService:
|
|||||||
if not nome_empresa or nome_empresa.lower() in ('resultados', 'resultado'):
|
if not nome_empresa or nome_empresa.lower() in ('resultados', 'resultado'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 2. Telefone detalhado
|
# 4. Telefone detalhado
|
||||||
telefone_raw = ""
|
telefone_raw = ""
|
||||||
phone_btn = page.query_selector("button[data-tooltip*='telefone'], button[data-item-id*='phone'], [aria-label*='Telefone'], [aria-label*='Ligar']")
|
phone_btn = page.query_selector("button[data-tooltip*='telefone'], button[data-item-id*='phone'], [aria-label*='Telefone'], [aria-label*='Ligar']")
|
||||||
if phone_btn:
|
if phone_btn:
|
||||||
@@ -195,7 +211,7 @@ class ScraperService:
|
|||||||
if phone_match:
|
if phone_match:
|
||||||
telefone_raw = phone_match.group(1)
|
telefone_raw = phone_match.group(1)
|
||||||
|
|
||||||
# 3. Endereço Completo
|
# 5. Endereço Completo
|
||||||
endereco = ""
|
endereco = ""
|
||||||
addr_btn = page.query_selector("button[data-tooltip*='endereço'], button[data-item-id*='address'], [aria-label*='Endereço']")
|
addr_btn = page.query_selector("button[data-tooltip*='endereço'], button[data-item-id*='address'], [aria-label*='Endereço']")
|
||||||
if addr_btn:
|
if addr_btn:
|
||||||
@@ -212,15 +228,12 @@ class ScraperService:
|
|||||||
endereco = line_str
|
endereco = line_str
|
||||||
break
|
break
|
||||||
|
|
||||||
# 4. Website
|
# 6. Website
|
||||||
website = ""
|
website = ""
|
||||||
site_btn = page.query_selector("a[data-tooltip*='site'], a[data-item-id*='authority']")
|
site_btn = page.query_selector("a[data-tooltip*='site'], a[data-item-id*='authority']")
|
||||||
if site_btn:
|
if site_btn:
|
||||||
website = site_btn.get_attribute("href") or ""
|
website = site_btn.get_attribute("href") or ""
|
||||||
|
|
||||||
# 5. Rating e Avaliações reais por estabelecimento
|
|
||||||
rating, total_avaliacoes = ScraperService._extract_rating_and_reviews(detail_panel, detail_text)
|
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
'nome_empresa': nome_empresa,
|
'nome_empresa': nome_empresa,
|
||||||
'telefone': telefone_raw,
|
'telefone': telefone_raw,
|
||||||
@@ -263,7 +276,7 @@ class ScraperService:
|
|||||||
if site_btn:
|
if site_btn:
|
||||||
website = site_btn.get_attribute("href") or ""
|
website = site_btn.get_attribute("href") or ""
|
||||||
|
|
||||||
rating, total_avaliacoes = ScraperService._extract_rating_and_reviews(detail_panel, detail_text)
|
rating, total_avaliacoes = ScraperService._extract_card_rating_and_reviews(page.query_selector("h1"))
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
'nome_empresa': nome_empresa,
|
'nome_empresa': nome_empresa,
|
||||||
|
|||||||
Reference in New Issue
Block a user