feat: scripts de projectos vindos do Hub (podcast, alojadamaria, clip, ocr, etc.)
Movidos do vault Hub para centralizar scripts. Hub mantem symlinks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Captura screenshots de alojadamaria.com para auditoria visual SEO/UX
|
||||
"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
|
||||
BASE_URL = "https://alojadamaria.com/"
|
||||
OUTPUT_DIR = "/media/ealmeida/Dados/Hub/03-Propostas/ALojaDaMaria/screenshots/alojadamaria"
|
||||
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
VIEWPORTS = {
|
||||
"desktop": {"width": 1440, "height": 900},
|
||||
"mobile": {"width": 375, "height": 812},
|
||||
}
|
||||
|
||||
PAGES = {
|
||||
"homepage": BASE_URL,
|
||||
"categoria": BASE_URL + "product-category/novidades/",
|
||||
"contacto": BASE_URL + "contactos/",
|
||||
}
|
||||
|
||||
def capturar(page, url, nome, viewport):
|
||||
"""Captura acima da dobra e página completa"""
|
||||
print(f" -> A capturar: {nome} ({viewport['width']}x{viewport['height']})")
|
||||
try:
|
||||
page.goto(url, wait_until="networkidle", timeout=30000)
|
||||
time.sleep(2)
|
||||
|
||||
# Fechar pop-ups comuns (cookie consent, newsletter)
|
||||
for selector in [
|
||||
"button[class*='close']",
|
||||
"button[class*='dismiss']",
|
||||
"[class*='cookie'] button",
|
||||
"[id*='cookie'] button",
|
||||
"[class*='popup-close']",
|
||||
".pum-close",
|
||||
"button[aria-label*='Close']",
|
||||
"button[aria-label*='close']",
|
||||
]:
|
||||
try:
|
||||
el = page.query_selector(selector)
|
||||
if el and el.is_visible():
|
||||
el.click()
|
||||
time.sleep(0.5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Above the fold (viewport apenas)
|
||||
page.screenshot(
|
||||
path=f"{OUTPUT_DIR}/{nome}_atf.png",
|
||||
full_page=False,
|
||||
clip={"x": 0, "y": 0, "width": viewport["width"], "height": viewport["height"]},
|
||||
)
|
||||
|
||||
# Página completa
|
||||
page.screenshot(
|
||||
path=f"{OUTPUT_DIR}/{nome}_full.png",
|
||||
full_page=True,
|
||||
)
|
||||
|
||||
# Recolher metadados
|
||||
title = page.title()
|
||||
h1_els = page.query_selector_all("h1")
|
||||
h1_texts = [el.inner_text().strip() for el in h1_els if el.is_visible()]
|
||||
|
||||
nav_visible = bool(page.query_selector("nav, [class*='nav'], [class*='menu']"))
|
||||
|
||||
ctas = []
|
||||
for sel in ["a[class*='btn'], a[class*='button'], button[class*='btn'], .add-to-cart, [class*='cta']"]:
|
||||
els = page.query_selector_all(sel)
|
||||
for el in els[:5]:
|
||||
try:
|
||||
if el.is_visible():
|
||||
ctas.append(el.inner_text().strip()[:50])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
popup_visible = bool(page.query_selector(".pum-overlay, [class*='popup'][style*='display: block'], [class*='modal'][style*='display: block']"))
|
||||
|
||||
# Dimensões do logo
|
||||
logo = page.query_selector("img[class*='logo'], a[class*='logo'] img, header img, .site-logo img")
|
||||
logo_info = None
|
||||
if logo:
|
||||
try:
|
||||
bb = logo.bounding_box()
|
||||
logo_info = bb
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"title": title,
|
||||
"h1": h1_texts,
|
||||
"nav_visible": nav_visible,
|
||||
"ctas_sample": ctas[:8],
|
||||
"popup_detected": popup_visible,
|
||||
"logo_bounding_box": logo_info,
|
||||
}
|
||||
except Exception as e:
|
||||
print(f" ERRO: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
resultados = {}
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
|
||||
for device_name, viewport in VIEWPORTS.items():
|
||||
print(f"\n[{device_name.upper()}] {viewport['width']}x{viewport['height']}")
|
||||
context = browser.new_context(
|
||||
viewport=viewport,
|
||||
user_agent="Mozilla/5.0 (compatible; AuditBot/1.0)",
|
||||
locale="pt-PT",
|
||||
)
|
||||
page = context.new_page()
|
||||
|
||||
for page_name, url in PAGES.items():
|
||||
chave = f"{device_name}_{page_name}"
|
||||
print(f" Página: {page_name}")
|
||||
dados = capturar(page, url, chave, viewport)
|
||||
resultados[chave] = dados
|
||||
|
||||
context.close()
|
||||
|
||||
browser.close()
|
||||
|
||||
with open(f"{OUTPUT_DIR}/metadados.json", "w", encoding="utf-8") as f:
|
||||
json.dump(resultados, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("\nCaptura concluída. Ficheiros em:", OUTPUT_DIR)
|
||||
return resultados
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Captura detalhes adicionais: hero CTA, produto, footer, barra anúncio
|
||||
"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
OUTPUT_DIR = "/media/ealmeida/Dados/Hub/03-Propostas/ALojaDaMaria/screenshots/alojadamaria"
|
||||
BASE_URL = "https://alojadamaria.com/"
|
||||
|
||||
def crop(page, path, clip):
|
||||
page.screenshot(path=path, clip=clip, full_page=False)
|
||||
print(f" Guardado: {path}")
|
||||
|
||||
def main():
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
|
||||
# --- Desktop 1440px ---
|
||||
ctx = browser.new_context(viewport={"width": 1440, "height": 900}, locale="pt-PT")
|
||||
page = ctx.new_page()
|
||||
page.goto(BASE_URL, wait_until="networkidle", timeout=40000)
|
||||
time.sleep(2)
|
||||
|
||||
# Hero completo com CTA visível
|
||||
page.screenshot(path=f"{OUTPUT_DIR}/desktop_hero_zoom.png",
|
||||
clip={"x": 0, "y": 0, "width": 1440, "height": 600})
|
||||
print(" Hero desktop guardado")
|
||||
|
||||
# Header/nav
|
||||
page.screenshot(path=f"{OUTPUT_DIR}/desktop_header.png",
|
||||
clip={"x": 0, "y": 0, "width": 1440, "height": 80})
|
||||
print(" Header desktop guardado")
|
||||
|
||||
# Barra topo (announcement bar)
|
||||
page.screenshot(path=f"{OUTPUT_DIR}/desktop_announcebar.png",
|
||||
clip={"x": 0, "y": 0, "width": 1440, "height": 35})
|
||||
print(" Barra anúncio guardada")
|
||||
|
||||
# Produtos (scroll para secção)
|
||||
page.evaluate("window.scrollTo(0, 700)")
|
||||
time.sleep(1)
|
||||
page.screenshot(path=f"{OUTPUT_DIR}/desktop_produtos.png",
|
||||
clip={"x": 0, "y": 0, "width": 1440, "height": 900})
|
||||
print(" Produtos desktop guardados")
|
||||
|
||||
# Footer
|
||||
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
time.sleep(1)
|
||||
page.screenshot(path=f"{OUTPUT_DIR}/desktop_footer.png",
|
||||
clip={"x": 0, "y": 0, "width": 1440, "height": 900})
|
||||
print(" Footer desktop guardado")
|
||||
|
||||
ctx.close()
|
||||
|
||||
# --- Mobile 375px ---
|
||||
ctx_m = browser.new_context(viewport={"width": 375, "height": 812}, locale="pt-PT")
|
||||
page_m = ctx_m.new_page()
|
||||
page_m.goto(BASE_URL, wait_until="networkidle", timeout=40000)
|
||||
time.sleep(2)
|
||||
|
||||
# Header mobile
|
||||
page_m.screenshot(path=f"{OUTPUT_DIR}/mobile_header.png",
|
||||
clip={"x": 0, "y": 0, "width": 375, "height": 120})
|
||||
print(" Header mobile guardado")
|
||||
|
||||
# Hero mobile
|
||||
page_m.screenshot(path=f"{OUTPUT_DIR}/mobile_hero.png",
|
||||
clip={"x": 0, "y": 0, "width": 375, "height": 500})
|
||||
print(" Hero mobile guardado")
|
||||
|
||||
# Produtos mobile
|
||||
page_m.evaluate("window.scrollTo(0, 500)")
|
||||
time.sleep(1)
|
||||
page_m.screenshot(path=f"{OUTPUT_DIR}/mobile_produtos.png",
|
||||
clip={"x": 0, "y": 0, "width": 375, "height": 812})
|
||||
print(" Produtos mobile guardados")
|
||||
|
||||
# Footer mobile
|
||||
page_m.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
time.sleep(1)
|
||||
page_m.screenshot(path=f"{OUTPUT_DIR}/mobile_footer.png",
|
||||
clip={"x": 0, "y": 0, "width": 375, "height": 812})
|
||||
print(" Footer mobile guardado")
|
||||
|
||||
# Tentar obter URL de produto real
|
||||
links = page_m.query_selector_all("a[href*='product']")
|
||||
product_url = None
|
||||
for l in links:
|
||||
href = l.get_attribute("href")
|
||||
if href and "product-category" not in href and "alojadamaria.com/product" in href:
|
||||
product_url = href
|
||||
break
|
||||
|
||||
if product_url:
|
||||
print(f"\n URL produto encontrado: {product_url}")
|
||||
page_m.goto(product_url, wait_until="networkidle", timeout=30000)
|
||||
time.sleep(2)
|
||||
page_m.screenshot(path=f"{OUTPUT_DIR}/mobile_produto_detalhe_atf.png",
|
||||
full_page=False)
|
||||
page_m.screenshot(path=f"{OUTPUT_DIR}/mobile_produto_detalhe_full.png",
|
||||
full_page=True)
|
||||
print(" Produto detalhe mobile guardado")
|
||||
|
||||
# Desktop produto
|
||||
ctx_d2 = browser.new_context(viewport={"width": 1440, "height": 900}, locale="pt-PT")
|
||||
page_d2 = ctx_d2.new_page()
|
||||
page_d2.goto(product_url, wait_until="networkidle", timeout=30000)
|
||||
time.sleep(2)
|
||||
page_d2.screenshot(path=f"{OUTPUT_DIR}/desktop_produto_detalhe_atf.png",
|
||||
full_page=False)
|
||||
page_d2.screenshot(path=f"{OUTPUT_DIR}/desktop_produto_detalhe_full.png",
|
||||
full_page=True)
|
||||
print(" Produto detalhe desktop guardado")
|
||||
ctx_d2.close()
|
||||
|
||||
ctx_m.close()
|
||||
browser.close()
|
||||
print("\nCapturas extra concluídas.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de captura e análise visual SEO para descomplicar.pt
|
||||
Analisa: capturas desktop/mobile, above-the-fold, imagens, CTAs
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
URL = "https://descomplicar.pt"
|
||||
SCREENSHOTS_DIR = "/media/ealmeida/Dados/Hub/03-Propostas/ALojaDaMaria/screenshots"
|
||||
|
||||
VIEWPORTS = {
|
||||
"desktop": {"width": 1920, "height": 1080},
|
||||
"laptop": {"width": 1366, "height": 768},
|
||||
"tablet": {"width": 768, "height": 1024},
|
||||
"mobile": {"width": 375, "height": 812},
|
||||
}
|
||||
|
||||
|
||||
def capture(url, output_path, viewport_width=1920, viewport_height=1080):
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page(viewport={"width": viewport_width, "height": viewport_height})
|
||||
page.goto(url, wait_until="networkidle", timeout=30000)
|
||||
page.screenshot(path=output_path, full_page=False)
|
||||
browser.close()
|
||||
|
||||
|
||||
def analyse_page(url):
|
||||
results = {}
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
|
||||
# --- Desktop 1920x1080 ---
|
||||
page = browser.new_page(viewport=VIEWPORTS["desktop"])
|
||||
page.goto(url, wait_until="networkidle", timeout=30000)
|
||||
page.screenshot(
|
||||
path=f"{SCREENSHOTS_DIR}/desktop_1920.png", full_page=False
|
||||
)
|
||||
page.screenshot(
|
||||
path=f"{SCREENSHOTS_DIR}/desktop_1920_full.png", full_page=True
|
||||
)
|
||||
|
||||
# Dados above-the-fold (desktop)
|
||||
atf = page.evaluate("""() => {
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
|
||||
// H1
|
||||
const h1s = Array.from(document.querySelectorAll('h1'));
|
||||
const h1Visible = h1s.filter(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.top >= 0 && r.bottom <= vh && r.width > 0;
|
||||
});
|
||||
|
||||
// CTAs (botões e links com texto de acção)
|
||||
const ctaKeywords = /contacto|falar|orçamento|começar|saber mais|ver mais|agendar|demo|serviços|get started|contact/i;
|
||||
const allBtns = Array.from(document.querySelectorAll('a, button'));
|
||||
const ctasAtf = allBtns.filter(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.top >= 0 && r.bottom <= vh && r.width > 0 && ctaKeywords.test(el.textContent);
|
||||
}).map(el => ({text: el.textContent.trim().substring(0,60), tag: el.tagName, top: Math.round(el.getBoundingClientRect().top)}));
|
||||
|
||||
// Value proposition (primeiro parágrafo/subtítulo visível)
|
||||
const textEls = Array.from(document.querySelectorAll('h2, h3, p, .subtitle, .hero-text, [class*="hero"] p, [class*="tagline"]'));
|
||||
const vpEl = textEls.find(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.top >= 0 && r.bottom <= vh && el.textContent.trim().length > 30;
|
||||
});
|
||||
|
||||
// Sinais de confiança (logos, testimonials, reviews)
|
||||
const trustSelectors = '[class*="client"], [class*="partner"], [class*="logo"], [class*="review"], [class*="testim"], [class*="trust"], .stars, [class*="rating"]';
|
||||
const trustEls = Array.from(document.querySelectorAll(trustSelectors));
|
||||
const trustAtf = trustEls.filter(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.top >= 0 && r.bottom <= vh && r.width > 0;
|
||||
}).length;
|
||||
|
||||
return {
|
||||
viewport: {width: vw, height: vh},
|
||||
h1Count: h1s.length,
|
||||
h1Texts: h1s.map(el => ({text: el.textContent.trim().substring(0,100), visible: h1Visible.includes(el)})),
|
||||
h1AboveFold: h1Visible.length,
|
||||
ctasAboveFold: ctasAtf,
|
||||
valueProposition: vpEl ? vpEl.textContent.trim().substring(0,200) : null,
|
||||
trustSignalsAboveFold: trustAtf,
|
||||
};
|
||||
}""")
|
||||
|
||||
# Análise de imagens
|
||||
images = page.evaluate("""() => {
|
||||
return Array.from(document.querySelectorAll('img')).map(img => ({
|
||||
src: img.src.substring(0, 120),
|
||||
alt: img.alt,
|
||||
hasAlt: img.alt.trim().length > 0,
|
||||
loading: img.loading,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
hasWidthAttr: img.hasAttribute('width'),
|
||||
hasHeightAttr: img.hasAttribute('height'),
|
||||
isWebP: img.src.includes('.webp'),
|
||||
isAvif: img.src.includes('.avif'),
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
rect: (() => { const r = img.getBoundingClientRect(); return {top: Math.round(r.top), visible: r.width > 0}; })()
|
||||
}));
|
||||
}""")
|
||||
|
||||
# Dados de meta SEO
|
||||
meta_seo = page.evaluate("""() => {
|
||||
const getMeta = (name) => {
|
||||
const el = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`);
|
||||
return el ? el.getAttribute('content') : null;
|
||||
};
|
||||
return {
|
||||
title: document.title,
|
||||
metaDescription: getMeta('description'),
|
||||
ogTitle: getMeta('og:title'),
|
||||
ogDescription: getMeta('og:description'),
|
||||
ogImage: getMeta('og:image'),
|
||||
canonical: (() => { const l = document.querySelector('link[rel="canonical"]'); return l ? l.href : null; })(),
|
||||
lang: document.documentElement.lang,
|
||||
h2Count: document.querySelectorAll('h2').length,
|
||||
h3Count: document.querySelectorAll('h3').length,
|
||||
};
|
||||
}""")
|
||||
|
||||
# Desempenho básico (recursos)
|
||||
perf = page.evaluate("""() => {
|
||||
const entries = performance.getEntriesByType('resource');
|
||||
const imgs = entries.filter(e => e.initiatorType === 'img');
|
||||
const scripts = entries.filter(e => e.initiatorType === 'script');
|
||||
const styles = entries.filter(e => e.initiatorType === 'link' || e.initiatorType === 'css');
|
||||
return {
|
||||
totalResources: entries.length,
|
||||
imgCount: imgs.length,
|
||||
scriptCount: scripts.length,
|
||||
styleCount: styles.length,
|
||||
};
|
||||
}""")
|
||||
|
||||
results["desktop_atf"] = atf
|
||||
results["images"] = images
|
||||
results["meta_seo"] = meta_seo
|
||||
results["perf"] = perf
|
||||
|
||||
# --- Mobile 375x812 ---
|
||||
mobile_page = browser.new_page(
|
||||
viewport=VIEWPORTS["mobile"],
|
||||
user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1"
|
||||
)
|
||||
mobile_page.goto(url, wait_until="networkidle", timeout=30000)
|
||||
mobile_page.screenshot(
|
||||
path=f"{SCREENSHOTS_DIR}/mobile_375.png", full_page=False
|
||||
)
|
||||
mobile_page.screenshot(
|
||||
path=f"{SCREENSHOTS_DIR}/mobile_375_full.png", full_page=True
|
||||
)
|
||||
|
||||
mobile_checks = mobile_page.evaluate("""() => {
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const docWidth = document.documentElement.scrollWidth;
|
||||
|
||||
// Verificar overflow horizontal
|
||||
const hasHorizontalScroll = docWidth > vw;
|
||||
|
||||
// Navegação móvel
|
||||
const nav = document.querySelector('nav, [class*="nav"], [class*="menu"], header');
|
||||
const navVisible = nav ? nav.getBoundingClientRect().width > 0 : false;
|
||||
const hamburger = document.querySelector('[class*="hamburger"], [class*="toggle"], [class*="burger"], .menu-icon, [aria-label*="menu"], [aria-label*="Menu"]');
|
||||
|
||||
// Tamanho dos tap targets (mínimo 48x48px)
|
||||
const allTapTargets = Array.from(document.querySelectorAll('a, button, input, select, textarea'));
|
||||
const smallTargets = allTapTargets.filter(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.width > 0 && r.height > 0 && (r.width < 44 || r.height < 44);
|
||||
}).slice(0, 10).map(el => ({
|
||||
tag: el.tagName,
|
||||
text: el.textContent.trim().substring(0, 40),
|
||||
w: Math.round(el.getBoundingClientRect().width),
|
||||
h: Math.round(el.getBoundingClientRect().height)
|
||||
}));
|
||||
|
||||
// Tamanho de fonte base
|
||||
const bodyFontSize = parseFloat(window.getComputedStyle(document.body).fontSize);
|
||||
|
||||
// H1 visível no mobile
|
||||
const h1s = Array.from(document.querySelectorAll('h1'));
|
||||
const h1MobileVisible = h1s.filter(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.top >= 0 && r.bottom <= vh && r.width > 0;
|
||||
});
|
||||
|
||||
// CTAs mobile
|
||||
const ctaKeywords = /contacto|falar|orçamento|começar|saber mais|ver mais|agendar|demo|serviços/i;
|
||||
const ctasMobile = Array.from(document.querySelectorAll('a, button')).filter(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.top >= 0 && r.bottom <= vh && r.width > 0 && ctaKeywords.test(el.textContent);
|
||||
}).map(el => ({text: el.textContent.trim().substring(0,50), w: Math.round(el.getBoundingClientRect().width), h: Math.round(el.getBoundingClientRect().height)}));
|
||||
|
||||
return {
|
||||
viewport: {width: vw, height: vh},
|
||||
documentWidth: docWidth,
|
||||
hasHorizontalScroll,
|
||||
navVisible,
|
||||
hasHamburger: !!hamburger,
|
||||
hamburgerClass: hamburger ? hamburger.className.substring(0,60) : null,
|
||||
smallTapTargets: smallTargets,
|
||||
smallTapTargetCount: smallTargets.length,
|
||||
bodyFontSize,
|
||||
h1AboveFoldMobile: h1MobileVisible.length,
|
||||
h1TextMobile: h1MobileVisible[0] ? h1MobileVisible[0].textContent.trim().substring(0,100) : null,
|
||||
ctasMobileAtf: ctasMobile,
|
||||
};
|
||||
}""")
|
||||
|
||||
results["mobile"] = mobile_checks
|
||||
|
||||
# --- Laptop 1366x768 ---
|
||||
laptop_page = browser.new_page(viewport=VIEWPORTS["laptop"])
|
||||
laptop_page.goto(url, wait_until="networkidle", timeout=30000)
|
||||
laptop_page.screenshot(
|
||||
path=f"{SCREENSHOTS_DIR}/laptop_1366.png", full_page=False
|
||||
)
|
||||
|
||||
browser.close()
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("A capturar screenshots e analisar descomplicar.pt...")
|
||||
data = analyse_page(URL)
|
||||
|
||||
output_file = f"{SCREENSHOTS_DIR}/analysis_data.json"
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Análise concluída. Dados guardados em: {output_file}")
|
||||
print(f"Screenshots em: {SCREENSHOTS_DIR}/")
|
||||
print("\n--- RESUMO ---")
|
||||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
Reference in New Issue
Block a user