Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Acervo

Pipeline para montar um acervo navegável de criativos de anúncios que estão no ar — coletados da Biblioteca de Anúncios da Meta, classificados automaticamente e publicados como uma página estática única, com filtros e análise.

Serve para media buyer, estrategista de criativo e dono de e-commerce que quer parar de "achar" o que o concorrente está fazendo e passar a ler o que o mercado bancou com dinheiro.


O problema que ele resolve

A Biblioteca de Anúncios da Meta é pública, mas é péssima de analisar: sem filtro cruzado, sem comparação entre marcas, sem histórico, e o scroll infinito derrete depois de 30 cards. Espiar 50 concorrentes na mão é inviável.

Este pipeline varre dezenas de páginas de anunciante, extrai o JSON por trás dos cards, classifica cada criativo em vários eixos e gera uma página onde dá para perguntar coisas como:

  • que criativos de short os concorrentes mantêm no ar há mais de 60 dias?
  • quem está vendendo (link para PDP) e quem só está levando gente para o perfil?
  • marcas do meu porte duram mais ou menos que os gigantes?
  • esse nicho vende com desconto ou com desejo?

O que ele mede — e o que ele NÃO mede

Isto é a parte mais importante do projeto. Leia antes de usar.

A Meta não publica gasto, impressões, alcance nem CTR de anúncio comercial. Os campos existem na resposta e vêm vazios (spend: null, impressions_index: -1). Qualquer ferramenta que diga "o concorrente gastou R$ X" está estimando.

Este pipeline usa só os dois sinais que são reais:

Sinal O que é Por que importa
Dias no ar o criativo está ligado há X dias e continua ativo ninguém deixa criativo ruim pagando CPM — longevidade é a única prova pública de que a peça se paga
Variações quantas versões do mesmo criativo rodam em paralelo (collation_count) ninguém produz 8 variações de uma peça que não performa — mede verba alocada

Há um terceiro sinal, mais sutil: a varredura pede à Biblioteca ordenação por total_impressions, e o Meta ordena no servidor. O número é secreto, mas a ordem é dele — a posição na lista vira um ranking de impressões utilizável (comparável apenas dentro da mesma marca, e enviesado para o criativo mais antigo, porque é acumulado).

Ressalva que evita conclusão errada: longevidade sozinha não prova vencedor. Anúncio antigo com 1 variação só, numa conta com pouquíssimos anúncios ativos, costuma ser peça esquecida ligada com verba mínima. O sinal forte é longevidade + variações + conta com volume, os três juntos.


Como funciona

descobre_ids.py ──► cdp.py ──► descobre_ids.py extrai ──► monta_marcas.py
   (gera buscas)   (coleta)      (page_id por marca)      (universo final)
                                                                │
                              ┌─────────────────────────────────┘
                              ▼
     varre.py gera ──► cdp.py ──► varre.py parseia ──► analisa.py
      (URLs/marca)    (coleta)     (extrai os ads)    (classifica + thumbs)
                                                                │
                              ┌─────────────────────────────────┘
                              ▼
              sintetiza.py ──► escreve_analise.py ──► gera_pagina.py
               (estatística)     (tabelas + textos)     (HTML final)

O coletor (cdp.py)

A Biblioteca não cede a curl (403 / login wall) e não cede mais ao --dump-dom do Chrome headless: a partir do Chrome ~150 o novo headless ignora --virtual-time-budget e trava no polling da página — o dump nunca sai.

O cdp.py sobe uma instância de Chrome com --remote-debugging-port e conversa por CDP (Chrome DevTools Protocol) via WebSocket: navega, espera, rola para disparar o scroll infinito, e lê o DOM na hora que quiser. Cerca de 15s por busca e 30s por página, contra 90–150s do método antigo — e uma instância só serve todas as marcas.

O parser (parser.py)

Não faz scraping de HTML renderizado. Ancora em "ad_archive_id" dentro dos <script> e recupera o objeto JSON inteiro por brace matching. De lá saem page_id, is_active, start_date, collation_count, snapshot.display_format, cta_text, cta_type, title, body.text, link_url, publisher_platform, page_like_count

Os eixos de classificação (analisa.py)

Cada criativo é etiquetado em:

  • Ângulo — Oferta & Desconto, Produto & Qualidade, Identidade & Comunidade, Lançamento…
  • Hook — o mecanismo da primeira linha (pergunta, comando, prova, dor…)
  • Produção — creator/UGC, produção de marca, catálogo automatizado, voz de cliente
  • Objetivo aparente — conversão-venda, tráfego-site, tráfego-perfil do Instagram, mensagem, loja física… inferido de cta_type + destino do link (a Biblioteca não publica o objetivo da campanha)
  • Placement — só Instagram / Instagram+Facebook / todos (Advantage+)
  • Validação — Comprovado / Consolidado / Em tração / Recente
  • Produto (opcional) — categoria da peça, detectada pelo slug do link do anúncio

A página (gera_pagina.py)

HTML estático único, sem build e sem dependência de CDN além das fontes. Nav sticky com scrollspy, filtros cruzados client-side, toolbar com os filtros ativos removíveis, modal de detalhe, ordenação (impressões / dias / variações / recentes) e as thumbs baixadas localmente — as URLs do fbcdn expiram em horas, então sem cópia local o acervo fica sem imagem em poucos dias.

?noanim=1 desliga animações e content-visibility (útil para QA e print).


Uso

Requisitos

  • Python 3.9+
  • Google Chrome instalado
  • pip install websockets
  • macOS/Linux (o caminho do Chrome está no topo do cdp.py)

Passo a passo

# 0. estrutura: o pipeline grava em ../<saida>/criativos/
#    acervo/            <- este repositório (scripts)
#    meu-cliente/       <- pasta irmã, criada automaticamente

cp exemplo/textos.exemplo.py acervo/textos.py
cp exemplo/tiers.exemplo.json acervo/tiers.json
# edite CONFIG no topo de acervo/gera_pagina.py (nome do cliente e pasta de saída)

# 1. descoberta: edite CANDIDATAS e CATEGORIAS em descobre_ids.py com o SEU nicho
python3 acervo/descobre_ids.py gera /tmp/buscas.json
python3 acervo/cdp.py /tmp/buscas.json /tmp/dumps
python3 acervo/descobre_ids.py extrai /tmp/dumps

# 2. universo: classifique cada marca em tiers.json (par/escala/pequena/tema/voce)
python3 acervo/monta_marcas.py

# 3. varredura
python3 acervo/varre.py gera /tmp/alvos.json
python3 acervo/cdp.py /tmp/alvos.json /tmp/dumps
python3 acervo/varre.py parseia /tmp/dumps

# 4. análise e página
python3 acervo/analisa.py        # classifica + baixa e otimiza as thumbs
python3 acervo/sintetiza.py      # estatísticas -> stats.json  (LEIA a saída!)
python3 acervo/escreve_analise.py
python3 acervo/gera_pagina.py

Entre o passo 3 e o 4, vale rodar garimpa_pequenas.py sobre os dumps: ele encontra marcas do seu porte que não estavam em lista nenhuma, filtrando por page_like_count e por vocabulário do nicho.

Escreva textos.py depois de ler a saída do sintetiza.py, nunca antes. As tabelas são geradas dos dados; os textos são só a interpretação. Número escrito à mão em textos.py não se atualiza na próxima varredura e vira mentira silenciosa.


Pegadinhas que custaram tempo

Estão aqui para você não repetir:

  1. Chrome headless em paralelo devolve dump de 0 byte. O coletor é sequencial de propósito. Se você "otimizar" isso, os dumps voltam vazios e parece bug da técnica.

  2. until ! pgrep -f "cdp.py" nunca termina — a linha de comando do próprio shell contém a string e o pgrep acha a si mesmo. Use pgrep -f "[c]dp.py".

  3. Busca por keyword é ruidosa. q=<marca> casa com o texto do anúncio, não com o anunciante. Sempre confira o page_name retornado contra seus aliases.

  4. Antes de reportar "essa marca não anuncia", faça teste de controle. Dump vazio e dump de estado-vazio têm tamanho parecido. Procure a string "Nenhum anúncio corresponde" e rode uma página sabidamente ativa com os mesmos parâmetros.

    E se o teste de controle passar mesmo assim, suspeite do country. A busca por page_id filtra por país de veiculação, não só por anunciante — uma marca internacional pode não ter nenhuma campanha voltada pro país que você consultou hoje e ainda assim estar rodando globalmente. country=BR numa marca só-brasileira é seguro; a mesma query numa marca de outro país pode devolver zero anúncios com uma marca gigante e ativa do outro lado. Já aconteceu: 12 de 14 marcas internacionais "zeraram" com country=BR e voltaram normais com country=ALL — nenhuma delas tinha parado de anunciar. Pra marca de fora, sempre reconfira com country=ALL antes de concluir "inativa".

  5. display:flex numa classe anula o atributo [hidden] (o display:none do user-agent perde para qualquer classe). Sem .cr[hidden]{display:none!important} o filtro marca tudo certo e nada some da tela.

  6. overflow-x:hidden no body quebra window.scrollTo, âncoras e scrollspy (promove o body a scroll container). E overflow-x:clip é pior: clip num eixo força o outro a clip e o viewport perde o scroll inteiro.

  7. aspect-ratio perde para o flex se min-height for auto (o padrão do flex item): criativo 9:16 estoura enquanto o 1:1 fica certo. Precisa de min-height:0.

  8. html.escape(str(s or "")) engole zero0.0 é falsy e vira "", exibindo "%" sem número. E truncar depois do escape corta entidade no meio (&amp;&am).

  9. scrollIntoView({block:'nearest'}) num link dentro do nav sticky briga com o scroll da página. O scrollspy usa isso pra manter o item ativo visível no nav horizontal (.tn-links{overflow-x:auto}) — mas o link também está dentro de position:sticky, e nesse caso o navegador não trata block:'nearest' como "não mexe": ele tenta reposicionar o ancestral sticky no eixo vertical a cada troca de seção, e isso trava/pula o scroll da página inteira (mais perceptível em mobile, onde o nav sempre precisa rolar horizontalmente). Fix: não usar scrollIntoView nisso — calcular o delta com getBoundingClientRect() e rolar só o container do nav via tnLinks.scrollTo({left:...}), nunca window/body.

E a regra que vale mais que todas: ao verificar um filtro, meça o que RENDERIZA (offsetParent !== null), não a propriedade que o seu código acabou de escrever (!el.hidden). Um teste que só confirma a linha anterior do próprio código não pode falhar — e por isso não prova nada.


O que este repositório NÃO contém

  • Dados de clientes. Os acervos gerados (marcas, criativos, análises, thumbs) são trabalho de cliente e ficam de fora.
  • Credenciais. Os scripts de deploy (FTP) não estão aqui. Publique como preferir — é só um diretório com index.html, acervo.json e media/.

Aviso de uso

Os criativos coletados pertencem aos respectivos anunciantes e vêm de uma base pública (a Biblioteca de Anúncios da Meta). O acervo gerado é material de referência e análise — publique com noindex e sem sugerir endosso das marcas exibidas. Respeite os Termos da Meta ao coletar.

Licença

MIT — veja LICENSE.

About

Pipeline de inteligência de criativo: varre a Biblioteca de Anúncios da Meta, classifica e publica um acervo navegável. Mede só o que é real — dias no ar e variações.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages