Commit inicial - upload de todos os arquivos da pasta
This commit is contained in:
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
OPENAI_API_KEY=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
|
||||
GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA
|
||||
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
OPENAI_API_KEY=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
|
||||
GEMINI_API_KEY=AIzaSyBEtSE6SpdOYXc0p5b5aepdcRuu53jHaFA
|
||||
120
Campanha-LP-Civil-Otimizada.md
Normal file
120
Campanha-LP-Civil-Otimizada.md
Normal file
@@ -0,0 +1,120 @@
|
||||
REGRAS OTIMIZADAS PARA GOOGLE ADS:
|
||||
|
||||
1. PALAVRAS-CHAVE (Fundo de Funil): Gere 20 termos com alta intenção de contratação. Use [Exata] e "Frase". Limite os termos a no máximo 25 caracteres para viabilizar o uso nos títulos.
|
||||
2. NEGATIVAS: Gere 20 termos que filtrem estudantes, curiosos e buscas gratuitas (ex: pdf, curso, modelo, jurisprudência, tcc).
|
||||
3. TÍTULOS (Headlines): Gere 15 títulos (máx. 30 caracteres). Regra de Ouro: A palavra-chave da Regra 1 deve aparecer de forma INTEGRAL e IDENTICA no título. Se a keyword for longa, o título será apenas ela.
|
||||
4. DESCRIÇÕES: Gere 4 descrições (máx. 90 caracteres). Devem terminar obrigatoriamente com um ponto final ou exclamação. Inclua uma Proposta Única de Valor (UVP).
|
||||
5. EXTENSÕES (Sitelinks & Callouts):
|
||||
- 4 Sitelinks (Título 25ch / Desc 35ch).
|
||||
- 6 Callouts (Frases de destaque, máx 25ch) focadas em autoridade e agilidade.
|
||||
6. POLÍTICAS EDITORIAIS:
|
||||
- Proibido: "Grátis" e sinônimos.
|
||||
- Proibido: CAIXA ALTA em palavras inteiras (exceto siglas como SP, OAB).
|
||||
- Proibido: Uso excessivo de pontuação (ex: !!!).
|
||||
|
||||
RETORNO: Apenas o JSON estruturado.
|
||||
|
||||
|
||||
|
||||
/////////////////////////
|
||||
{
|
||||
"campanha_google_ads": {
|
||||
"palavras_chave_fundo_de_funil": [
|
||||
"[advogado divórcio online]",
|
||||
"\"contratar advogado família\"",
|
||||
"[advogado guarda filhos]",
|
||||
"\"advogado pensão alimentícia\"",
|
||||
"[advogado inventário SP]",
|
||||
"\"advogado união estável\"",
|
||||
"[dissolução união estável]",
|
||||
"\"escritório direito civil\"",
|
||||
"[advogado para inventário]",
|
||||
"\"especialista em divórcio\"",
|
||||
"[ação de pensão alimentícia]",
|
||||
"\"advogado guarda de menores\"",
|
||||
"[advogado partilha de bens]",
|
||||
"\"consultoria jurídica família\"",
|
||||
"[divórcio consensual online]",
|
||||
"\"advogado civilista SP\"",
|
||||
"[inventário extrajudicial]",
|
||||
"\"advogado direito de família\"",
|
||||
"[pedir pensão alimentícia]",
|
||||
"\"advogado separação judicial\""
|
||||
],
|
||||
"palavras_chave_negativas": [
|
||||
"grátis (evitar buscas sem orçamento)",
|
||||
"gratuito (evitar defensoria pública)",
|
||||
"estágio (evitar candidatos a vaga)",
|
||||
"vagas (evitar candidatos a vaga)",
|
||||
"curso (evitar estudantes)",
|
||||
"faculdade (evitar estudantes)",
|
||||
"tcc (evitar estudantes)",
|
||||
"modelo de petição (evitar curiosos)",
|
||||
"jurisprudência (evitar estudantes)",
|
||||
"pdf (evitar buscas informativas)",
|
||||
"concurso (evitar concurseiros)",
|
||||
"trabalhista (fora do escopo civil)",
|
||||
"criminal (fora do escopo civil)",
|
||||
"previdenciário (fora do escopo civil)",
|
||||
"defensoria pública (baixa intenção)",
|
||||
"fórum (busca informativa)",
|
||||
"como fazer (busca informativa)",
|
||||
"lei (busca informativa)",
|
||||
"livro (evitar estudantes)",
|
||||
"apostila (evitar estudantes)"
|
||||
],
|
||||
"titulos_headlines": [
|
||||
"Advogado divórcio online",
|
||||
"Contratar advogado família",
|
||||
"Advogado guarda filhos",
|
||||
"Advogado pensão alimentícia",
|
||||
"Advogado inventário SP",
|
||||
"Advogado união estável",
|
||||
"Dissolução união estável",
|
||||
"Escritório direito civil",
|
||||
"Advogado para inventário",
|
||||
"Especialista em divórcio",
|
||||
"Ação de pensão alimentícia",
|
||||
"Advogado guarda de menores",
|
||||
"Advogado partilha de bens",
|
||||
"Consultoria jurídica família",
|
||||
"Divórcio consensual online"
|
||||
],
|
||||
"descricoes": [
|
||||
"Advocacia especializada em Direito Civil e Família. Atendimento 100% online e sigiloso.",
|
||||
"Resolva seu divórcio, guarda ou inventário de forma rápida e segura. Fale com um expert.",
|
||||
"Proteja seus direitos e sua família com especialistas. Soluções ágeis e atendimento humanizado.",
|
||||
"Mais de 10 anos de experiência em Direito de Família. Avalie seu caso com total sigilo."
|
||||
],
|
||||
"sitelinks": [
|
||||
{
|
||||
"titulo": "Divórcio e Separação",
|
||||
"desc_linha_1": "Encerre o caso de forma rápida.",
|
||||
"desc_linha_2": "Consensual ou litigioso online."
|
||||
},
|
||||
{
|
||||
"titulo": "Guarda e Pensão",
|
||||
"desc_linha_1": "Priorizando o bem-estar dos filhos.",
|
||||
"desc_linha_2": "Definição e revisão de valores."
|
||||
},
|
||||
{
|
||||
"titulo": "Inventário e Herança",
|
||||
"desc_linha_1": "Solução ágil na partilha de bens.",
|
||||
"desc_linha_2": "Via judicial ou extrajudicial."
|
||||
},
|
||||
{
|
||||
"titulo": "Fale com Especialista",
|
||||
"desc_linha_1": "Atendimento rápido via WhatsApp.",
|
||||
"desc_linha_2": "Tire suas dúvidas agora mesmo."
|
||||
}
|
||||
],
|
||||
"callouts": [
|
||||
"Atendimento 100% Online",
|
||||
"Sigilo Total Garantido",
|
||||
"10 Anos de Experiência",
|
||||
"Resposta Rápida",
|
||||
"Especialista em Família",
|
||||
"Solução Sem Burocracia"
|
||||
]
|
||||
}
|
||||
}
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8501
|
||||
|
||||
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
||||
5
Links Uteis.txt
Normal file
5
Links Uteis.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# Como criar uma Campanha - adriano gianini
|
||||
https://www.youtube.com/watch?v=4HMDAhl15cA
|
||||
|
||||
#Amanda Agostinho
|
||||
https://www.youtube.com/watch?v=XU1MWVk6HvQ
|
||||
55
Prompt.md
Normal file
55
Prompt.md
Normal file
@@ -0,0 +1,55 @@
|
||||
Objetivo: Landing Page (LP) já está bem otimizada para conversão, ela é a melhor fonte de "verdade" para o Google Ads. Usar Python para automatizar isso não só economiza tempo, mas garante que o anúncio seja um reflexo fiel do que o usuário vai encontrar na página (o que aumenta o seu **Índice de Qualidade**).
|
||||
|
||||
Aqui está como eu estruturaria essa aplicação Python para você:
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Arquitetura da Aplicação
|
||||
|
||||
Para construir isso "do zero", você pode dividir o script em quatro módulos principais:
|
||||
|
||||
### 1. Scraping (Extração de Conteúdo)
|
||||
|
||||
Use as bibliotecas `BeautifulSoup` ou `Selenium`.
|
||||
|
||||
* **O que pegar:** Títulos (`h1`, `h2`), textos de benefícios, CTAs (chamadas para ação) e a meta-description.
|
||||
* **Dica:** Foque em extrair a **proposta de valor** central.
|
||||
|
||||
### 2. Processamento com IA (O "Cérebro")
|
||||
|
||||
Em vez de tentar criar lógica de palavras-chave na mão, conecte o Python à **API do OpenAI**
|
||||
|
||||
* **Prompt de Ouro:** "Com base no texto desta LP [solicitar URL], gere: 10 variações de palavras-chave (fundo de funil), 3 títulos de 30 caracteres, 2 descrições de 90 caracteres e 4 sitelinks."
|
||||
|
||||
### 3. Estruturação dos Ativos
|
||||
|
||||
Organize os dados extraídos no formato que o Google Ads aceita (planilhas de upload em massa ou via API).
|
||||
|
||||
* **Palavras-chave:** Classifique por tipos de correspondência (Exata e Frase).
|
||||
* **Anúncios Responsivos (RSA):** Garanta que você tenha variações suficientes para o Google testar.
|
||||
|
||||
### 4. Exportação
|
||||
|
||||
Gere um arquivo `.csv` formatado para o **Google Ads Editor**. É muito mais seguro do que subir direto via API se você está começando do zero, pois permite uma revisão final antes de "dar o play".
|
||||
|
||||
---
|
||||
|
||||
## 🐍 Sugestão de Stack Tecnológica
|
||||
|
||||
| Componente | Ferramenta Recomendada |
|
||||
| --- | --- |
|
||||
| **Linguagem** | Python 3.10+ |
|
||||
| **Scraping** | `requests` + `BeautifulSoup4` |
|
||||
| **Inteligência** | `api oPENai` |
|
||||
| **Interface** | `Streamlit` (para você colar a URL e ver o resultado na tela) |
|
||||
| **Saída** | `pandas` (para gerar o CSV/Excel) |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Meu "Pulo do Gato" para você
|
||||
|
||||
Não peça apenas palavras-chave óbvias. Peça para a IA identificar **"Dores do Cliente"** no texto da sua LP e transformar essas dores em **Extensões de Frase de Destaque (Callouts)**.
|
||||
|
||||
**Exemplo:** Se na sua LP diz "Suporte em 5 minutos", o Python deve extrair isso automaticamente como um diferencial do anúncio.
|
||||
|
||||
---
|
||||
361
app.py
Normal file
361
app.py
Normal file
@@ -0,0 +1,361 @@
|
||||
"""
|
||||
Google Ads Generator - Interface Streamlit
|
||||
|
||||
Aplicação que extrai conteúdo de Landing Pages e gera automaticamente
|
||||
ativos de campanha para Google Ads usando IA (OpenAI ou Gemini).
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import streamlit as st
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from src.scraper import scrape_landing_page
|
||||
from src.ai_generator import generate_google_ads_assets, MODELS
|
||||
from src.exporter import (
|
||||
create_keywords_df,
|
||||
create_negative_keywords_df,
|
||||
create_ads_df,
|
||||
create_sitelinks_df,
|
||||
create_callouts_df,
|
||||
export_all_to_excel,
|
||||
)
|
||||
|
||||
# ─── Carregar variáveis de ambiente ───────────────────────────────
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _highlight_keywords(text: str, keywords: list[str]) -> str:
|
||||
"""Destaca em negrito as palavras-chave encontradas no texto."""
|
||||
result = text
|
||||
for kw in keywords:
|
||||
pattern = re.compile(re.escape(kw), re.IGNORECASE)
|
||||
match = pattern.search(result)
|
||||
if match:
|
||||
result = result[:match.start()] + f"**{match.group()}**" + result[match.end():]
|
||||
break # Destacar apenas a primeira keyword encontrada
|
||||
return result
|
||||
|
||||
# ─── Configuração da Página ───────────────────────────────────────
|
||||
st.set_page_config(
|
||||
page_title="Google Ads Generator",
|
||||
page_icon="📊",
|
||||
layout="wide",
|
||||
)
|
||||
|
||||
# ─── CSS Customizado ──────────────────────────────────────────────
|
||||
st.markdown("""
|
||||
<style>
|
||||
.main-header {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: #1a73e8;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.sub-header {
|
||||
font-size: 1.1rem;
|
||||
color: #5f6368;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.metric-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 1.2rem;
|
||||
text-align: center;
|
||||
border: 1px solid #e8eaed;
|
||||
}
|
||||
.metric-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1a73e8;
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 0.85rem;
|
||||
color: #5f6368;
|
||||
}
|
||||
.stTabs [data-baseweb="tab-list"] {
|
||||
gap: 8px;
|
||||
}
|
||||
.stTabs [data-baseweb="tab"] {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# ─── Header ───────────────────────────────────────────────────────
|
||||
st.markdown('<p class="main-header">Google Ads Generator</p>', unsafe_allow_html=True)
|
||||
st.markdown(
|
||||
'<p class="sub-header">Gere ativos de campanha automaticamente a partir da sua Landing Page</p>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
# ─── Sidebar ──────────────────────────────────────────────────────
|
||||
with st.sidebar:
|
||||
st.header("Configurações")
|
||||
|
||||
provider = st.selectbox(
|
||||
"Provider de IA",
|
||||
["OpenAI", "Gemini"],
|
||||
index=0,
|
||||
help="Escolha o provider de IA. As chaves devem estar configuradas no arquivo .env",
|
||||
)
|
||||
|
||||
model = st.selectbox(
|
||||
"Modelo",
|
||||
MODELS.get(provider, []),
|
||||
index=0,
|
||||
help="Modelo a ser utilizado para geração dos ativos.",
|
||||
)
|
||||
|
||||
# Status das chaves configuradas
|
||||
st.divider()
|
||||
openai_ok = bool(os.environ.get("OPENAI_API_KEY"))
|
||||
gemini_ok = bool(os.environ.get("GEMINI_API_KEY"))
|
||||
st.caption("Status das API Keys (.env):")
|
||||
st.markdown(f"- OpenAI: {'✅ Configurada' if openai_ok else '❌ Não encontrada'}")
|
||||
st.markdown(f"- Gemini: {'✅ Configurada' if gemini_ok else '❌ Não encontrada'}")
|
||||
|
||||
st.divider()
|
||||
|
||||
campaign_name = st.text_input(
|
||||
"Nome da Campanha",
|
||||
value="Campanha LP",
|
||||
help="Nome que aparecerá na coluna 'Campaign' do CSV.",
|
||||
)
|
||||
|
||||
ad_group = st.text_input(
|
||||
"Nome do Grupo de Anúncios",
|
||||
value="Grupo 1",
|
||||
help="Nome que aparecerá na coluna 'Ad Group' do CSV.",
|
||||
)
|
||||
|
||||
st.divider()
|
||||
st.caption("Desenvolvido com Streamlit + OpenAI + Gemini")
|
||||
|
||||
# ─── Área Principal ───────────────────────────────────────────────
|
||||
url = st.text_input(
|
||||
"URL da Landing Page",
|
||||
placeholder="https://www.seusite.com.br/landing-page",
|
||||
help="Cole a URL completa da sua Landing Page aqui.",
|
||||
)
|
||||
|
||||
col_btn, col_status = st.columns([1, 3])
|
||||
|
||||
with col_btn:
|
||||
generate_btn = st.button("Gerar Campanha", type="primary", use_container_width=True)
|
||||
|
||||
# ─── Lógica Principal ────────────────────────────────────────────
|
||||
if generate_btn:
|
||||
# Validações
|
||||
if not url:
|
||||
st.error("Por favor, insira a URL da Landing Page.")
|
||||
st.stop()
|
||||
|
||||
if not url.startswith(("http://", "https://")):
|
||||
st.error("A URL deve começar com http:// ou https://")
|
||||
st.stop()
|
||||
|
||||
# Verificar se a chave do provider selecionado está configurada
|
||||
if provider == "OpenAI" and not os.environ.get("OPENAI_API_KEY"):
|
||||
st.error("OPENAI_API_KEY não encontrada no arquivo .env. Configure antes de continuar.")
|
||||
st.stop()
|
||||
elif provider == "Gemini" and not os.environ.get("GEMINI_API_KEY"):
|
||||
st.error("GEMINI_API_KEY não encontrada no arquivo .env. Configure antes de continuar.")
|
||||
st.stop()
|
||||
|
||||
# Step 1: Scraping
|
||||
with st.status("Processando...", expanded=True) as status:
|
||||
st.write("Extraindo conteúdo da Landing Page...")
|
||||
try:
|
||||
lp_data = scrape_landing_page(url)
|
||||
except Exception as e:
|
||||
st.error(f"Erro ao acessar a URL: {e}")
|
||||
st.stop()
|
||||
|
||||
st.write(f"Conteúdo extraído: {len(lp_data['paragraphs'])} parágrafos, "
|
||||
f"{len(lp_data['h1'])} H1, {len(lp_data['h2'])} H2, "
|
||||
f"{len(lp_data.get('list_items', []))} itens de lista, "
|
||||
f"{len(lp_data['ctas'])} CTAs")
|
||||
|
||||
# Step 2: IA
|
||||
st.write(f"Gerando ativos com {provider} ({model})...")
|
||||
try:
|
||||
assets, prompts = generate_google_ads_assets(
|
||||
lp_content=lp_data["full_text"],
|
||||
provider=provider,
|
||||
model=model,
|
||||
)
|
||||
except ValueError as e:
|
||||
st.error(str(e))
|
||||
st.stop()
|
||||
except Exception as e:
|
||||
st.error(f"Erro ao gerar ativos: {e}")
|
||||
st.stop()
|
||||
|
||||
status.update(label="Concluído!", state="complete", expanded=False)
|
||||
|
||||
# Salvar no session_state
|
||||
st.session_state["assets"] = assets
|
||||
st.session_state["prompts"] = prompts
|
||||
st.session_state["lp_data"] = lp_data
|
||||
st.session_state["campaign_name"] = campaign_name
|
||||
st.session_state["ad_group"] = ad_group
|
||||
st.session_state["provider_used"] = provider
|
||||
st.session_state["model_used"] = model
|
||||
|
||||
# ─── Exibição dos Resultados ─────────────────────────────────────
|
||||
if "assets" in st.session_state:
|
||||
assets = st.session_state["assets"]
|
||||
prompts = st.session_state.get("prompts", {})
|
||||
camp = st.session_state.get("campaign_name", "Campanha LP")
|
||||
adg = st.session_state.get("ad_group", "Grupo 1")
|
||||
prov_used = st.session_state.get("provider_used", "")
|
||||
model_used = st.session_state.get("model_used", "")
|
||||
|
||||
st.divider()
|
||||
|
||||
# Métricas resumo
|
||||
col1, col2, col3, col4, col5, col6 = st.columns(6)
|
||||
|
||||
with col1:
|
||||
st.metric("Keywords", len(assets.get("keywords", [])))
|
||||
with col2:
|
||||
st.metric("Negativas", len(assets.get("negative_keywords", [])))
|
||||
with col3:
|
||||
st.metric("Títulos", len(assets.get("headlines", [])))
|
||||
with col4:
|
||||
st.metric("Descrições", len(assets.get("descriptions", [])))
|
||||
with col5:
|
||||
st.metric("Sitelinks", len(assets.get("sitelinks", [])))
|
||||
with col6:
|
||||
st.metric("Callouts", len(assets.get("callouts", [])))
|
||||
|
||||
st.divider()
|
||||
|
||||
# Abas com resultados
|
||||
tab_kw, tab_neg, tab_ads, tab_sl, tab_co, tab_prompts, tab_raw = st.tabs([
|
||||
"Keywords", "Negativas", "Anúncios RSA", "Sitelinks", "Callouts",
|
||||
"Prompts Utilizados", "Dados da LP",
|
||||
])
|
||||
|
||||
with tab_kw:
|
||||
st.subheader("Palavras-chave")
|
||||
kw_df = create_keywords_df(assets, camp, adg)
|
||||
if not kw_df.empty:
|
||||
st.dataframe(kw_df, use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.info("Nenhuma palavra-chave gerada.")
|
||||
|
||||
with tab_neg:
|
||||
st.subheader("Palavras-chave Negativas")
|
||||
nkw_df = create_negative_keywords_df(assets, camp)
|
||||
if not nkw_df.empty:
|
||||
st.dataframe(nkw_df, use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.info("Nenhuma palavra-chave negativa gerada.")
|
||||
|
||||
with tab_ads:
|
||||
st.subheader("Anúncio Responsivo de Pesquisa (RSA)")
|
||||
|
||||
st.write("**Títulos (Headlines):**")
|
||||
# Coletar palavras-chave para destacar nos títulos
|
||||
kw_list = sorted(
|
||||
[kw["keyword"].lower() for kw in assets.get("keywords", []) if kw.get("keyword")],
|
||||
key=len, reverse=True, # Maior primeiro para evitar match parcial
|
||||
)
|
||||
for i, h in enumerate(assets.get("headlines", []), 1):
|
||||
char_count = len(h)
|
||||
color = "green" if char_count <= 30 else "red"
|
||||
h_highlighted = _highlight_keywords(h, kw_list)
|
||||
st.markdown(f"{i}. {h_highlighted} — :{color}[{char_count} chars]")
|
||||
|
||||
st.write("**Descrições:**")
|
||||
for i, d in enumerate(assets.get("descriptions", []), 1):
|
||||
char_count = len(d)
|
||||
color = "green" if char_count <= 90 else "red"
|
||||
st.markdown(f"{i}. {d} — :{color}[{char_count} chars]")
|
||||
|
||||
st.divider()
|
||||
ads_df = create_ads_df(assets, camp, adg)
|
||||
if not ads_df.empty:
|
||||
st.dataframe(ads_df, use_container_width=True, hide_index=True)
|
||||
|
||||
with tab_sl:
|
||||
st.subheader("Sitelinks")
|
||||
sl_df = create_sitelinks_df(assets, camp)
|
||||
if not sl_df.empty:
|
||||
st.dataframe(sl_df, use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.info("Nenhum sitelink gerado.")
|
||||
|
||||
with tab_co:
|
||||
st.subheader("Callouts (Frases de Destaque)")
|
||||
co_df = create_callouts_df(assets, camp)
|
||||
if not co_df.empty:
|
||||
st.dataframe(co_df, use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.info("Nenhum callout gerado.")
|
||||
|
||||
with tab_prompts:
|
||||
st.subheader("Prompts Utilizados")
|
||||
st.caption(f"Provider: **{prov_used}** | Modelo: **{model_used}**")
|
||||
|
||||
st.markdown("#### Prompt de Sistema (System Prompt)")
|
||||
st.code(prompts.get("system_prompt", "N/A"), language=None)
|
||||
|
||||
st.markdown("#### Prompt do Usuário (User Prompt)")
|
||||
st.code(prompts.get("user_prompt", "N/A"), language=None)
|
||||
|
||||
with tab_raw:
|
||||
st.subheader("Dados Extraídos da Landing Page")
|
||||
lp_data = st.session_state.get("lp_data", {})
|
||||
if lp_data:
|
||||
st.write(f"**URL:** {lp_data.get('url', '')}")
|
||||
st.write(f"**Título:** {lp_data.get('title', '')}")
|
||||
st.write(f"**Meta Description:** {lp_data.get('meta_description', '')}")
|
||||
|
||||
with st.expander("Headings (H1, H2, H3)"):
|
||||
for h in lp_data.get("h1", []):
|
||||
st.write(f"**H1:** {h}")
|
||||
for h in lp_data.get("h2", []):
|
||||
st.write(f"**H2:** {h}")
|
||||
for h in lp_data.get("h3", []):
|
||||
st.write(f"**H3:** {h}")
|
||||
|
||||
with st.expander("Itens de Lista / Benefícios"):
|
||||
for li in lp_data.get("list_items", []):
|
||||
st.write(f"- {li}")
|
||||
|
||||
with st.expander("CTAs Encontrados"):
|
||||
for c in lp_data.get("ctas", []):
|
||||
st.write(f"- {c}")
|
||||
|
||||
with st.expander("Texto Completo Enviado à IA"):
|
||||
st.code(lp_data.get("full_text", ""), language=None)
|
||||
|
||||
# ─── Botões de Download ───────────────────────────────────────
|
||||
st.divider()
|
||||
st.subheader("Download dos Ativos")
|
||||
|
||||
col_dl1, col_dl2 = st.columns(2)
|
||||
|
||||
with col_dl1:
|
||||
excel_data = export_all_to_excel(assets, camp, adg)
|
||||
st.download_button(
|
||||
label="Baixar Excel (.xlsx)",
|
||||
data=excel_data,
|
||||
file_name="google_ads_assets.xlsx",
|
||||
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
use_container_width=True,
|
||||
)
|
||||
|
||||
with col_dl2:
|
||||
kw_csv = create_keywords_df(assets, camp, adg).to_csv(index=False)
|
||||
st.download_button(
|
||||
label="Baixar Keywords (.csv)",
|
||||
data=kw_csv,
|
||||
file_name="google_ads_keywords.csv",
|
||||
mime="text/csv",
|
||||
use_container_width=True,
|
||||
)
|
||||
30
github.bat
Normal file
30
github.bat
Normal file
@@ -0,0 +1,30 @@
|
||||
@echo off
|
||||
echo === INICIANDO UPLOAD PARA GITHUB ===
|
||||
|
||||
REM Inicializar repositório Git
|
||||
echo Inicializando repositorio Git...
|
||||
git init
|
||||
|
||||
REM Adicionar todos os arquivos
|
||||
echo Adicionando todos os arquivos...
|
||||
git add .
|
||||
|
||||
REM Fazer commit inicial
|
||||
echo Realizando commit inicial...
|
||||
git commit -m "Commit inicial - upload de todos os arquivos da pasta"
|
||||
|
||||
REM Adicionar repositório remoto
|
||||
echo Conectando ao repositorio remoto...
|
||||
git remote add origin https://gitea.aplicativopro.com/wander/Google-Ads.git
|
||||
|
||||
REM Definir branch principal
|
||||
echo Definindo branch principal como 'main'...
|
||||
git branch -M main
|
||||
|
||||
REM Fazer push para o GitHub
|
||||
echo Fazendo upload para o GitHub...
|
||||
git push -u origin main
|
||||
|
||||
echo === UPLOAD CONCLUIDO COM SUCESSO! ===
|
||||
|
||||
pause
|
||||
BIN
google_ads_assets.xlsx
Normal file
BIN
google_ads_assets.xlsx
Normal file
Binary file not shown.
21
google_ads_keywords (1).csv
Normal file
21
google_ads_keywords (1).csv
Normal file
@@ -0,0 +1,21 @@
|
||||
Campaign,Ad Group,Keyword,Match Type,Status
|
||||
Campanha LP,Grupo 1,[advogado divórcio online],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado guarda filhos""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado pensão alimentícia""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[divórcio extrajudicial online],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado inventário SP""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[calcular pensão alimentícia],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""divórcio amigável online""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado união estável SP""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[contratar advogado divórcio],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado especialista família""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[consulta advogado divórcio],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado guarda compartilhada""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[honorários advogado divórcio],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado pensão alimentícia SP""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[divórcio rápido online],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado inventário judicial""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[custo advogado divórcio],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado especialista em família""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[falar com advogado divórcio],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advogado divórcio preço""",Phrase,Enabled
|
||||
|
16
google_ads_keywords.csv
Normal file
16
google_ads_keywords.csv
Normal file
@@ -0,0 +1,16 @@
|
||||
Campaign,Ad Group,Keyword,Match Type,Status
|
||||
Campanha LP,Grupo 1,[advogados trabalhistas],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""consultoria jurídica online""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[direitos do trabalhador],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""demissão injusta""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[horas extras não pagas],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""assédio moral advogado""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[rescisão de contrato],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""consultoria gratuita advogados""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[direito do trabalho],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""advocacia online""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[defesa de direitos trabalhistas],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""avaliação jurídica gratuita""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[advogado demissão],Exact,Enabled
|
||||
Campanha LP,Grupo 1,"""horas extras advogado""",Phrase,Enabled
|
||||
Campanha LP,Grupo 1,[consultar advogado online],Exact,Enabled
|
||||
|
56
planejamento.md
Normal file
56
planejamento.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Google Ads Generator from LP
|
||||
|
||||
## Arquitetura do Projeto
|
||||
|
||||
O projeto seguirá a estrutura modular sugerida:
|
||||
1. **Interface (Streamlit):** Entrada da URL e Chave da API, exibição dos resultados e botão de download.
|
||||
2. **Scraper (BeautifulSoup):** Extração de texto visível, títulos e meta tags da URL fornecida.
|
||||
3. **IA (OpenAI API):** Geração de palavras-chave, títulos, descrições e extensões com base no conteúdo extraído.
|
||||
4. **Processamento e Exportação (Pandas):** Formatação dos dados para CSV compatível com Google Ads Editor.
|
||||
|
||||
## Passos de Implementação
|
||||
|
||||
### 1. Configuração do Ambiente
|
||||
- Criar arquivo `requirements.txt` com as dependências: `streamlit`, `pandas`, `requests`, `beautifulsoup4`, `openai`, `python-dotenv`.
|
||||
- Configurar estrutura de pastas.
|
||||
|
||||
### 2. Módulo de Scraping (`src/scraper.py`)
|
||||
- Criar função `scrape_landing_page(url)` que retorna um dicionário com:
|
||||
- Título da página (`title`)
|
||||
- Meta description
|
||||
- Headings (`h1`, `h2`, `h3`)
|
||||
- Texto principal (parágrafos relevantes)
|
||||
- Texto de botões/CTAs
|
||||
|
||||
### 3. Módulo de IA (`src/ai_generator.py`)
|
||||
- Configurar cliente OpenAI.
|
||||
- Criar prompts específicos para:
|
||||
- **Palavras-chave:** Fundo de funil, com tipos de correspondência (Exata, Frase).
|
||||
- **Palavras-chave Negativas:** Identificar termos irrelevantes ou que atraiam tráfego desqualificado com base no conteúdo da LP.
|
||||
- **Anúncios Responsivos (RSA):** 15 títulos (30 chars) e 4 descrições (90 chars).
|
||||
- **Sitelinks:** 4 variações com texto e descrição.
|
||||
- **Callouts (Frases de Destaque):** Extração de "Dores do Cliente" e diferenciais (o "Pulo do Gato").
|
||||
|
||||
### 4. Módulo de Exportação (`src/exporter.py`)
|
||||
- Criar função para estruturar os dados em um DataFrame do Pandas.
|
||||
- Mapear colunas para o formato padrão do Google Ads Editor (ex: `Campaign`, `Ad Group`, `Keyword`, `Headline 1`, etc.).
|
||||
- Gerar CSV para download.
|
||||
|
||||
### 5. Interface do Usuário (`app.py`)
|
||||
- Criar layout com Streamlit.
|
||||
- Campo para input da URL.
|
||||
- Campo para input da OpenAI API Key (opcional se usar .env, mas bom para interface).
|
||||
- Botão "Gerar Campanha".
|
||||
- Exibição dos resultados em abas (Keywords, Negative Keywords, Ads, Extensions).
|
||||
- Botão de download do CSV final.
|
||||
|
||||
OpenAI (do mais barato ao mais caro):
|
||||
gpt-4.1-mini -- mais recente e barato
|
||||
gpt-4o-mini -- otimo custo-beneficio
|
||||
gpt-4.1 -- mais capaz, custo moderado
|
||||
gpt-4o -- alta qualidade
|
||||
Gemini (do mais barato ao mais caro):
|
||||
gemini-2.0-flash-lite -- mais barato do Gemini
|
||||
gemini-2.0-flash -- equilibrio custo/qualidade
|
||||
gemini-2.5-flash-lite -- mais recente
|
||||
gemini-1.5-flash -- estavel e confiavel
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
streamlit
|
||||
pandas
|
||||
requests
|
||||
beautifulsoup4
|
||||
openai
|
||||
python-dotenv
|
||||
openpyxl
|
||||
google-genai
|
||||
selenium
|
||||
webdriver-manager
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/ai_generator.cpython-310.pyc
Normal file
BIN
src/__pycache__/ai_generator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/exporter.cpython-310.pyc
Normal file
BIN
src/__pycache__/exporter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/scraper.cpython-310.pyc
Normal file
BIN
src/__pycache__/scraper.cpython-310.pyc
Normal file
Binary file not shown.
263
src/ai_generator.py
Normal file
263
src/ai_generator.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
Módulo de IA - Geração de ativos para Google Ads via OpenAI ou Gemini.
|
||||
|
||||
Recebe o conteúdo extraído da LP e gera palavras-chave,
|
||||
anúncios responsivos, sitelinks, callouts e palavras-chave negativas.
|
||||
Suporta múltiplos providers de IA (OpenAI e Google Gemini).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
from openai import OpenAI
|
||||
from google import genai
|
||||
|
||||
|
||||
# ─── Modelos disponíveis por provider ─────────────────────────────
|
||||
MODELS = {
|
||||
"OpenAI": ["gpt-4.1-mini", "gpt-4o-mini", "gpt-4.1", "gpt-4o"],
|
||||
"Gemini": ["gemini-2.0-flash-lite", "gemini-2.0-flash", "gemini-2.5-flash-lite", "gemini-1.5-flash"],
|
||||
}
|
||||
|
||||
|
||||
def generate_google_ads_assets(
|
||||
lp_content: str,
|
||||
provider: str = "OpenAI",
|
||||
model: str = "gpt-4o-mini",
|
||||
) -> tuple[dict, dict]:
|
||||
"""
|
||||
Gera todos os ativos de campanha Google Ads a partir do conteúdo da LP.
|
||||
|
||||
Args:
|
||||
lp_content: Texto completo extraído da LP (full_text do scraper).
|
||||
provider: Provider de IA ("OpenAI" ou "Gemini").
|
||||
model: Modelo a ser usado (deve ser compatível com o provider).
|
||||
|
||||
Returns:
|
||||
Tupla com:
|
||||
- assets: Dicionário com keywords, negative_keywords, headlines, etc.
|
||||
- prompts: Dicionário com system_prompt e user_prompt usados.
|
||||
"""
|
||||
system_prompt = _build_system_prompt()
|
||||
user_prompt = _build_user_prompt(lp_content)
|
||||
|
||||
prompts = {
|
||||
"system_prompt": system_prompt,
|
||||
"user_prompt": user_prompt,
|
||||
}
|
||||
|
||||
if provider == "OpenAI":
|
||||
raw_response = _call_openai(system_prompt, user_prompt, model)
|
||||
elif provider == "Gemini":
|
||||
raw_response = _call_gemini(system_prompt, user_prompt, model)
|
||||
else:
|
||||
raise ValueError(f"Provider não suportado: {provider}")
|
||||
|
||||
# Limpar possíveis blocos de código markdown
|
||||
raw_response = _clean_json_response(raw_response)
|
||||
|
||||
result = json.loads(raw_response)
|
||||
assets = _validate_and_normalize(result)
|
||||
|
||||
return assets, prompts
|
||||
|
||||
|
||||
def _call_openai(system_prompt: str, user_prompt: str, model: str) -> str:
|
||||
"""Chama a API da OpenAI e retorna a resposta bruta."""
|
||||
api_key = os.environ.get("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError("OPENAI_API_KEY não encontrada no arquivo .env")
|
||||
|
||||
client = OpenAI(api_key=api_key)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=4000,
|
||||
)
|
||||
|
||||
return response.choices[0].message.content.strip()
|
||||
|
||||
|
||||
def _call_gemini(system_prompt: str, user_prompt: str, model: str, max_retries: int = 2) -> str:
|
||||
"""
|
||||
Chama a API do Google Gemini e retorna a resposta bruta.
|
||||
Faz retry automático em caso de rate limit (429).
|
||||
"""
|
||||
api_key = os.environ.get("GEMINI_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError("GEMINI_API_KEY não encontrada no arquivo .env")
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=user_prompt,
|
||||
config=genai.types.GenerateContentConfig(
|
||||
system_instruction=system_prompt,
|
||||
temperature=0.7,
|
||||
max_output_tokens=4000,
|
||||
),
|
||||
)
|
||||
return response.text.strip()
|
||||
|
||||
except Exception as e:
|
||||
error_str = str(e)
|
||||
if "429" in error_str or "RESOURCE_EXHAUSTED" in error_str:
|
||||
# Extrair tempo de retry sugerido pela API
|
||||
wait_match = re.search(r"retry.*?(\d+)", error_str, re.IGNORECASE)
|
||||
wait_seconds = int(wait_match.group(1)) if wait_match else 45
|
||||
|
||||
if attempt < max_retries:
|
||||
time.sleep(wait_seconds)
|
||||
continue
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Gemini API: Limite de requisições excedido (Free Tier). "
|
||||
f"Aguarde ~{wait_seconds}s e tente novamente, ou mude para um "
|
||||
f"plano pago em https://ai.google.dev/pricing. "
|
||||
f"Alternativa: use o provider OpenAI."
|
||||
) from e
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def _clean_json_response(raw: str) -> str:
|
||||
"""Remove blocos de código markdown e extrai apenas o JSON."""
|
||||
raw = raw.strip()
|
||||
if raw.startswith("```"):
|
||||
raw = raw.split("\n", 1)[1]
|
||||
if raw.endswith("```"):
|
||||
raw = raw[:-3]
|
||||
raw = raw.strip()
|
||||
return raw
|
||||
|
||||
|
||||
def _build_system_prompt() -> str:
|
||||
"""Constrói o prompt de sistema."""
|
||||
return (
|
||||
"Você é um especialista em Google Ads com mais de 10 anos de experiência. "
|
||||
"Seu trabalho é gerar ativos de campanha de alta performance a partir do "
|
||||
"conteúdo de Landing Pages. Responda SEMPRE em formato JSON válido, "
|
||||
"sem markdown, sem blocos de código. Apenas o JSON puro."
|
||||
)
|
||||
|
||||
|
||||
def _build_user_prompt(lp_content: str) -> str:
|
||||
"""Constrói o prompt do usuário para geração de ativos."""
|
||||
return f"""Com base no conteúdo desta Landing Page, gere ativos completos para uma campanha de Google Ads.
|
||||
|
||||
=== CONTEÚDO DA LANDING PAGE ===
|
||||
{lp_content}
|
||||
=== FIM DO CONTEÚDO ===
|
||||
|
||||
Gere o seguinte em formato JSON:
|
||||
|
||||
{{
|
||||
"keywords": [
|
||||
{{"keyword": "exemplo de palavra-chave", "match_type": "Exact"}},
|
||||
{{"keyword": "outro exemplo", "match_type": "Phrase"}}
|
||||
],
|
||||
"negative_keywords": [
|
||||
{{"keyword": "grátis", "reason": "Atrai tráfego que não converte"}},
|
||||
{{"keyword": "como fazer", "reason": "Topo de funil, não converte"}}
|
||||
],
|
||||
"headlines": [
|
||||
"Título com até 30 caracteres"
|
||||
],
|
||||
"descriptions": [
|
||||
"Descrição com até 90 caracteres que destaca benefícios e inclui CTA"
|
||||
],
|
||||
"sitelinks": [
|
||||
{{"title": "Título do Sitelink", "description1": "Linha 1", "description2": "Linha 2"}}
|
||||
],
|
||||
"callouts": [
|
||||
"Frase de destaque curta"
|
||||
]
|
||||
}}
|
||||
|
||||
REGRAS OTIMIZADAS PARA GOOGLE ADS:
|
||||
|
||||
1. PALAVRAS-CHAVE (Fundo de Funil): Gere 20 termos com alta intenção de contratação. Use [Exata] e "Frase". Limite os termos a no máximo 25 caracteres para viabilizar o uso nos títulos.
|
||||
2. NEGATIVAS: Gere 20 termos que filtrem estudantes, curiosos e buscas gratuitas (ex: pdf, curso, modelo, jurisprudência, tcc).
|
||||
3. TÍTULOS (Headlines): Gere 15 títulos (máx. 30 caracteres). Regra de Ouro: A palavra-chave da Regra 1 deve aparecer de forma INTEGRAL e IDENTICA no título. Se a keyword for longa, o título será apenas ela.
|
||||
4. DESCRIÇÕES: Gere 4 descrições (máx. 90 caracteres). Devem terminar obrigatoriamente com um ponto final ou exclamação. Inclua uma Proposta Única de Valor (UVP).
|
||||
5. EXTENSÕES (Sitelinks & Callouts):
|
||||
- 4 Sitelinks (Título 25ch / Desc 35ch).
|
||||
- 6 Callouts (Frases de destaque, máx 25ch) focadas em autoridade e agilidade.
|
||||
6. POLÍTICAS EDITORIAIS:
|
||||
- Proibido: "Grátis" e sinônimos.
|
||||
- Proibido: CAIXA ALTA em palavras inteiras (exceto siglas como SP, OAB).
|
||||
- Proibido: Uso excessivo de pontuação (ex: !!!).
|
||||
|
||||
RETORNO: Apenas o JSON estruturado."""
|
||||
|
||||
def _validate_and_normalize(data: dict) -> dict:
|
||||
"""Valida e normaliza os dados retornados pela IA."""
|
||||
result = {
|
||||
"keywords": [],
|
||||
"negative_keywords": [],
|
||||
"headlines": [],
|
||||
"descriptions": [],
|
||||
"sitelinks": [],
|
||||
"callouts": [],
|
||||
}
|
||||
|
||||
# Keywords
|
||||
for kw in data.get("keywords", []):
|
||||
if isinstance(kw, dict) and "keyword" in kw:
|
||||
result["keywords"].append({
|
||||
"keyword": kw["keyword"],
|
||||
"match_type": kw.get("match_type", "Phrase"),
|
||||
})
|
||||
elif isinstance(kw, str):
|
||||
result["keywords"].append({"keyword": kw, "match_type": "Phrase"})
|
||||
|
||||
# Negative Keywords
|
||||
for nkw in data.get("negative_keywords", []):
|
||||
if isinstance(nkw, dict) and "keyword" in nkw:
|
||||
result["negative_keywords"].append({
|
||||
"keyword": nkw["keyword"],
|
||||
"reason": nkw.get("reason", ""),
|
||||
})
|
||||
elif isinstance(nkw, str):
|
||||
result["negative_keywords"].append({"keyword": nkw, "reason": ""})
|
||||
|
||||
# Headlines - garantir limite de 30 chars
|
||||
for h in data.get("headlines", []):
|
||||
if isinstance(h, str) and len(h) <= 30:
|
||||
result["headlines"].append(h)
|
||||
elif isinstance(h, str):
|
||||
result["headlines"].append(h[:30])
|
||||
|
||||
# Descriptions - garantir limite de 90 chars
|
||||
for d in data.get("descriptions", []):
|
||||
if isinstance(d, str) and len(d) <= 90:
|
||||
result["descriptions"].append(d)
|
||||
elif isinstance(d, str):
|
||||
result["descriptions"].append(d[:90])
|
||||
|
||||
# Sitelinks
|
||||
for sl in data.get("sitelinks", []):
|
||||
if isinstance(sl, dict) and "title" in sl:
|
||||
result["sitelinks"].append({
|
||||
"title": sl.get("title", "")[:25],
|
||||
"description1": sl.get("description1", "")[:35],
|
||||
"description2": sl.get("description2", "")[:35],
|
||||
})
|
||||
|
||||
# Callouts
|
||||
for c in data.get("callouts", []):
|
||||
if isinstance(c, str) and len(c) <= 25:
|
||||
result["callouts"].append(c)
|
||||
elif isinstance(c, str):
|
||||
result["callouts"].append(c[:25])
|
||||
|
||||
return result
|
||||
163
src/exporter.py
Normal file
163
src/exporter.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Módulo de Exportação - Gera arquivos CSV compatíveis com Google Ads Editor.
|
||||
|
||||
Recebe os ativos gerados pela IA e os formata em DataFrames do Pandas,
|
||||
prontos para importação no Google Ads Editor.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def create_keywords_df(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> pd.DataFrame:
|
||||
"""Cria DataFrame de palavras-chave no formato Google Ads Editor."""
|
||||
rows = []
|
||||
for kw in assets.get("keywords", []):
|
||||
match_type = kw.get("match_type", "Phrase")
|
||||
keyword = kw.get("keyword", "")
|
||||
|
||||
# Formatar keyword conforme tipo de correspondência
|
||||
if match_type == "Exact":
|
||||
formatted_kw = f"[{keyword}]"
|
||||
elif match_type == "Phrase":
|
||||
formatted_kw = f'"{keyword}"'
|
||||
else:
|
||||
formatted_kw = keyword
|
||||
|
||||
rows.append({
|
||||
"Campaign": campaign_name,
|
||||
"Ad Group": ad_group,
|
||||
"Keyword": formatted_kw,
|
||||
"Match Type": match_type,
|
||||
"Status": "Enabled",
|
||||
})
|
||||
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def create_negative_keywords_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
|
||||
"""Cria DataFrame de palavras-chave negativas no formato Google Ads Editor."""
|
||||
rows = []
|
||||
for nkw in assets.get("negative_keywords", []):
|
||||
rows.append({
|
||||
"Campaign": campaign_name,
|
||||
"Keyword": nkw.get("keyword", ""),
|
||||
"Criterion Type": "Negative",
|
||||
"Reason": nkw.get("reason", ""),
|
||||
})
|
||||
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def create_ads_df(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> pd.DataFrame:
|
||||
"""Cria DataFrame do anúncio responsivo (RSA) no formato Google Ads Editor."""
|
||||
headlines = assets.get("headlines", [])
|
||||
descriptions = assets.get("descriptions", [])
|
||||
|
||||
row = {
|
||||
"Campaign": campaign_name,
|
||||
"Ad Group": ad_group,
|
||||
"Ad Type": "Responsive Search Ad",
|
||||
}
|
||||
|
||||
# Preencher até 15 headlines
|
||||
for i, h in enumerate(headlines[:15], start=1):
|
||||
row[f"Headline {i}"] = h
|
||||
|
||||
# Preencher até 4 descrições
|
||||
for i, d in enumerate(descriptions[:4], start=1):
|
||||
row[f"Description {i}"] = d
|
||||
|
||||
return pd.DataFrame([row])
|
||||
|
||||
|
||||
def create_sitelinks_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
|
||||
"""Cria DataFrame de sitelinks no formato Google Ads Editor."""
|
||||
rows = []
|
||||
for sl in assets.get("sitelinks", []):
|
||||
rows.append({
|
||||
"Campaign": campaign_name,
|
||||
"Sitelink Text": sl.get("title", ""),
|
||||
"Description Line 1": sl.get("description1", ""),
|
||||
"Description Line 2": sl.get("description2", ""),
|
||||
})
|
||||
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def create_callouts_df(assets: dict, campaign_name: str = "Campanha LP") -> pd.DataFrame:
|
||||
"""Cria DataFrame de callouts no formato Google Ads Editor."""
|
||||
rows = []
|
||||
for c in assets.get("callouts", []):
|
||||
rows.append({
|
||||
"Campaign": campaign_name,
|
||||
"Callout Text": c,
|
||||
})
|
||||
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def export_all_to_excel(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> BytesIO:
|
||||
"""
|
||||
Exporta todos os ativos em um único arquivo Excel com múltiplas abas.
|
||||
|
||||
Returns:
|
||||
BytesIO com o conteúdo do arquivo Excel.
|
||||
"""
|
||||
output = BytesIO()
|
||||
|
||||
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
||||
kw_df = create_keywords_df(assets, campaign_name, ad_group)
|
||||
if not kw_df.empty:
|
||||
kw_df.to_excel(writer, sheet_name="Keywords", index=False)
|
||||
|
||||
nkw_df = create_negative_keywords_df(assets, campaign_name)
|
||||
if not nkw_df.empty:
|
||||
nkw_df.to_excel(writer, sheet_name="Negative Keywords", index=False)
|
||||
|
||||
ads_df = create_ads_df(assets, campaign_name, ad_group)
|
||||
if not ads_df.empty:
|
||||
ads_df.to_excel(writer, sheet_name="Ads RSA", index=False)
|
||||
|
||||
sl_df = create_sitelinks_df(assets, campaign_name)
|
||||
if not sl_df.empty:
|
||||
sl_df.to_excel(writer, sheet_name="Sitelinks", index=False)
|
||||
|
||||
co_df = create_callouts_df(assets, campaign_name)
|
||||
if not co_df.empty:
|
||||
co_df.to_excel(writer, sheet_name="Callouts", index=False)
|
||||
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
|
||||
def export_all_to_csv(assets: dict, campaign_name: str = "Campanha LP", ad_group: str = "Grupo 1") -> dict[str, str]:
|
||||
"""
|
||||
Exporta todos os ativos como strings CSV separadas.
|
||||
|
||||
Returns:
|
||||
Dicionário com nome da aba -> conteúdo CSV.
|
||||
"""
|
||||
csvs = {}
|
||||
|
||||
kw_df = create_keywords_df(assets, campaign_name, ad_group)
|
||||
if not kw_df.empty:
|
||||
csvs["keywords"] = kw_df.to_csv(index=False)
|
||||
|
||||
nkw_df = create_negative_keywords_df(assets, campaign_name)
|
||||
if not nkw_df.empty:
|
||||
csvs["negative_keywords"] = nkw_df.to_csv(index=False)
|
||||
|
||||
ads_df = create_ads_df(assets, campaign_name, ad_group)
|
||||
if not ads_df.empty:
|
||||
csvs["ads"] = ads_df.to_csv(index=False)
|
||||
|
||||
sl_df = create_sitelinks_df(assets, campaign_name)
|
||||
if not sl_df.empty:
|
||||
csvs["sitelinks"] = sl_df.to_csv(index=False)
|
||||
|
||||
co_df = create_callouts_df(assets, campaign_name)
|
||||
if not co_df.empty:
|
||||
csvs["callouts"] = co_df.to_csv(index=False)
|
||||
|
||||
return csvs
|
||||
265
src/scraper.py
Normal file
265
src/scraper.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
Módulo de Scraping - Extração de conteúdo de Landing Pages.
|
||||
|
||||
Utiliza Selenium (Chrome headless) + BeautifulSoup4 para extrair
|
||||
títulos, textos, CTAs e meta tags de uma Landing Page fornecida via URL.
|
||||
O Selenium renderiza o JavaScript antes da extração, garantindo que
|
||||
todo o conteúdo dinâmico seja capturado.
|
||||
"""
|
||||
|
||||
import time
|
||||
from bs4 import BeautifulSoup
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
|
||||
|
||||
def _create_driver() -> webdriver.Chrome:
|
||||
"""Cria e retorna um driver Chrome headless."""
|
||||
options = Options()
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
options.add_argument("--disable-extensions")
|
||||
options.add_argument("--disable-infobars")
|
||||
options.add_argument(
|
||||
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/120.0.0.0 Safari/537.36"
|
||||
)
|
||||
# Suprimir logs do Chrome
|
||||
options.add_argument("--log-level=3")
|
||||
options.add_experimental_option("excludeSwitches", ["enable-logging"])
|
||||
|
||||
service = Service(ChromeDriverManager().install())
|
||||
driver = webdriver.Chrome(service=service, options=options)
|
||||
return driver
|
||||
|
||||
|
||||
def scrape_landing_page(url: str, wait_seconds: int = 5) -> dict:
|
||||
"""
|
||||
Faz scraping de uma Landing Page usando Selenium (Chrome headless)
|
||||
e retorna um dicionário com os elementos relevantes para geração de anúncios.
|
||||
|
||||
Args:
|
||||
url: URL completa da Landing Page.
|
||||
wait_seconds: Segundos para aguardar o carregamento do JS.
|
||||
|
||||
Returns:
|
||||
Dicionário com as chaves:
|
||||
- url: URL original
|
||||
- title: Título da página (<title>)
|
||||
- meta_description: Conteúdo da meta description
|
||||
- h1: Lista de textos dos <h1>
|
||||
- h2: Lista de textos dos <h2>
|
||||
- h3: Lista de textos dos <h3>
|
||||
- paragraphs: Lista dos parágrafos principais
|
||||
- ctas: Lista de textos de botões e links de ação
|
||||
- full_text: Texto completo concatenado (para envio à IA)
|
||||
"""
|
||||
driver = _create_driver()
|
||||
|
||||
try:
|
||||
driver.get(url)
|
||||
|
||||
# Aguardar o body estar presente (sinal de que a página carregou)
|
||||
WebDriverWait(driver, 15).until(
|
||||
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
||||
)
|
||||
|
||||
# Aguardar tempo extra para JavaScript renderizar conteúdo dinâmico
|
||||
time.sleep(wait_seconds)
|
||||
|
||||
# Scroll até o final para disparar lazy-loading
|
||||
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
||||
time.sleep(2)
|
||||
|
||||
# Capturar HTML completo após renderização
|
||||
page_source = driver.page_source
|
||||
|
||||
finally:
|
||||
driver.quit()
|
||||
|
||||
# Parsear com BeautifulSoup
|
||||
soup = BeautifulSoup(page_source, "html.parser")
|
||||
|
||||
# Remover scripts e styles para limpeza do texto
|
||||
for tag in soup(["script", "style", "noscript", "iframe", "svg"]):
|
||||
tag.decompose()
|
||||
|
||||
# Título da página
|
||||
title = soup.title.string.strip() if soup.title and soup.title.string else ""
|
||||
|
||||
# Meta description
|
||||
meta_desc_tag = soup.find("meta", attrs={"name": "description"})
|
||||
meta_description = ""
|
||||
if meta_desc_tag and meta_desc_tag.get("content"):
|
||||
meta_description = meta_desc_tag["content"].strip()
|
||||
|
||||
# Headings
|
||||
h1_tags = [tag.get_text(strip=True) for tag in soup.find_all("h1") if tag.get_text(strip=True)]
|
||||
h2_tags = [tag.get_text(strip=True) for tag in soup.find_all("h2") if tag.get_text(strip=True)]
|
||||
h3_tags = [tag.get_text(strip=True) for tag in soup.find_all("h3") if tag.get_text(strip=True)]
|
||||
|
||||
# Parágrafos relevantes (ignora parágrafos muito curtos)
|
||||
paragraphs = [
|
||||
tag.get_text(strip=True)
|
||||
for tag in soup.find_all("p")
|
||||
if tag.get_text(strip=True) and len(tag.get_text(strip=True)) > 20
|
||||
]
|
||||
|
||||
# Lista items (muitas LPs usam <li> para benefícios)
|
||||
list_items = [
|
||||
tag.get_text(strip=True)
|
||||
for tag in soup.find_all("li")
|
||||
if tag.get_text(strip=True) and 10 < len(tag.get_text(strip=True)) < 200
|
||||
]
|
||||
|
||||
# Spans e divs com texto significativo (para LPs que não usam <p>)
|
||||
extra_texts = _extract_visible_text_blocks(soup)
|
||||
|
||||
# CTAs - botões e links com texto de ação
|
||||
ctas = _extract_ctas(soup)
|
||||
|
||||
# Texto completo para enviar à IA
|
||||
full_text = _build_full_text(
|
||||
title, meta_description, h1_tags, h2_tags, h3_tags,
|
||||
paragraphs, list_items, extra_texts, ctas,
|
||||
)
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"title": title,
|
||||
"meta_description": meta_description,
|
||||
"h1": h1_tags,
|
||||
"h2": h2_tags,
|
||||
"h3": h3_tags,
|
||||
"paragraphs": paragraphs,
|
||||
"list_items": list_items,
|
||||
"ctas": ctas,
|
||||
"full_text": full_text,
|
||||
}
|
||||
|
||||
|
||||
def _extract_visible_text_blocks(soup: BeautifulSoup) -> list[str]:
|
||||
"""
|
||||
Extrai blocos de texto visível de divs e spans que não estão
|
||||
dentro de tags semânticas (p, h1-h6, li, button, a).
|
||||
Útil para LPs que usam divs customizados para conteúdo.
|
||||
"""
|
||||
semantic_tags = {"p", "h1", "h2", "h3", "h4", "h5", "h6", "li", "button", "a", "input", "label"}
|
||||
texts = []
|
||||
seen = set()
|
||||
|
||||
for tag in soup.find_all(["div", "span"]):
|
||||
# Pular se tem filhos que são tags semânticas
|
||||
if tag.find(list(semantic_tags)):
|
||||
continue
|
||||
|
||||
text = tag.get_text(strip=True)
|
||||
if text and 30 < len(text) < 500 and text not in seen:
|
||||
seen.add(text)
|
||||
texts.append(text)
|
||||
|
||||
return texts[:15] # Limitar para não poluir
|
||||
|
||||
|
||||
def _extract_ctas(soup: BeautifulSoup) -> list[str]:
|
||||
"""Extrai textos de botões e links que parecem ser CTAs."""
|
||||
ctas = []
|
||||
|
||||
# Botões
|
||||
for btn in soup.find_all("button"):
|
||||
text = btn.get_text(strip=True)
|
||||
if text and len(text) < 80:
|
||||
ctas.append(text)
|
||||
|
||||
# Inputs do tipo submit
|
||||
for inp in soup.find_all("input", attrs={"type": "submit"}):
|
||||
value = inp.get("value", "").strip()
|
||||
if value:
|
||||
ctas.append(value)
|
||||
|
||||
# Links com classes comuns de CTA ou texto indicativo
|
||||
cta_keywords = [
|
||||
"btn", "button", "cta", "action", "comprar", "contratar",
|
||||
"saiba", "agendar", "solicitar", "falar", "whatsapp",
|
||||
"contato", "orcamento", "orçamento", "consulta", "agende",
|
||||
]
|
||||
for a_tag in soup.find_all("a"):
|
||||
classes = " ".join(a_tag.get("class", [])).lower()
|
||||
href = (a_tag.get("href") or "").lower()
|
||||
text = a_tag.get_text(strip=True)
|
||||
if text and len(text) < 80:
|
||||
if any(kw in classes for kw in cta_keywords):
|
||||
ctas.append(text)
|
||||
elif any(kw in text.lower() for kw in cta_keywords):
|
||||
ctas.append(text)
|
||||
elif "whatsapp" in href or "wa.me" in href:
|
||||
ctas.append(text)
|
||||
|
||||
# Remover duplicados mantendo ordem
|
||||
seen = set()
|
||||
unique_ctas = []
|
||||
for cta in ctas:
|
||||
if cta not in seen:
|
||||
seen.add(cta)
|
||||
unique_ctas.append(cta)
|
||||
|
||||
return unique_ctas
|
||||
|
||||
|
||||
def _build_full_text(
|
||||
title: str,
|
||||
meta_description: str,
|
||||
h1: list[str],
|
||||
h2: list[str],
|
||||
h3: list[str],
|
||||
paragraphs: list[str],
|
||||
list_items: list[str],
|
||||
extra_texts: list[str],
|
||||
ctas: list[str],
|
||||
) -> str:
|
||||
"""Monta um texto completo e estruturado da LP para envio à IA."""
|
||||
parts = []
|
||||
|
||||
if title:
|
||||
parts.append(f"TÍTULO DA PÁGINA: {title}")
|
||||
if meta_description:
|
||||
parts.append(f"META DESCRIPTION: {meta_description}")
|
||||
|
||||
if h1:
|
||||
parts.append("TÍTULOS PRINCIPAIS (H1):")
|
||||
parts.extend(f" - {h}" for h in h1)
|
||||
|
||||
if h2:
|
||||
parts.append("SUBTÍTULOS (H2):")
|
||||
parts.extend(f" - {h}" for h in h2)
|
||||
|
||||
if h3:
|
||||
parts.append("SUBTÍTULOS (H3):")
|
||||
parts.extend(f" - {h}" for h in h3)
|
||||
|
||||
if paragraphs:
|
||||
parts.append("TEXTOS PRINCIPAIS:")
|
||||
parts.extend(f" {p}" for p in paragraphs[:25])
|
||||
|
||||
if list_items:
|
||||
parts.append("ITENS DE LISTA / BENEFÍCIOS:")
|
||||
parts.extend(f" - {li}" for li in list_items[:15])
|
||||
|
||||
if extra_texts:
|
||||
parts.append("OUTROS TEXTOS VISÍVEIS:")
|
||||
parts.extend(f" {t}" for t in extra_texts[:10])
|
||||
|
||||
if ctas:
|
||||
parts.append("CHAMADAS PARA AÇÃO (CTAs):")
|
||||
parts.extend(f" - {c}" for c in ctas)
|
||||
|
||||
return "\n".join(parts)
|
||||
Reference in New Issue
Block a user