← Todos os posts

Monitoramento de Licitações: Os Editais Que Seu Scraper Nunca Viu

O monitoramento de licitações abaixo do limite da UE exige raspar uma cauda longa de pequenos portais. A falha mais custosa não é uma recusa, mas uma execução com status verde que não gravou nada.

€216.000. Abaixo desse valor, uma câmara municipal europeia que contrata serviços não precisa publicar sua licitação no TED, o registro central da UE.

Isso significa que os contratos que um pequeno concorrente pode realisticamente vencer estão em outro lugar: no portal de qualquer município, região, concessionária de serviços públicos ou ministério que esteja realizando a compra.

O desafio

Mais de 250.000 autoridades públicas na UE gastam cerca de €2,6 trilhões por ano em obras, suprimentos e serviços, segundo a contagem da Comissão Europeia. As diretrizes da UE cobrem apenas contratos acima dos limites, que a partir de 1º de janeiro de 2026 ficam em €140.000 para suprimentos e serviços do governo central, €216.000 para autoridades locais e regionais, e €5.404.000 para obras. Abaixo deles, aplicam-se regras nacionais e a publicação no TED é voluntária.

É por isso que o monitoramento de licitações se torna uma tarefa de scraping. Se você está desenvolvendo isso para uma consultoria de licitações ou uma construtora, a parte acima do limite é a mais fácil. O resto é uma cauda longa de portais que ninguém projetou para serem lidos por máquinas.

A BidPrime, que comercializa dados de licitações do setor público na América do Norte, chamou a versão americana disso de "milhares de fluxos de trabalho" em maio de 2026: portais que limitam o acesso automatizado, sistemas de compras que exigem uma conta ou sessão, anexos armazenados separadamente do edital, URLs que mudam sem aviso prévio. Nada nessa lista é exclusivo dos EUA.

Mas a falha que realmente prejudica no monitoramento de licitações não é o portal que rejeita o seu acesso. Essa aparece nos seus logs. O problema é a execução que retorna verde, mas sem dado algum.

O portal de um município pode responder a uma sessão expirada com uma página amigável em HTTP 200. Um aviso de manutenção pode ser renderizado no lugar da tabela de resultados. Uma busca que você enviou ontem pode entregar a página um novamente, porque a página dois só existia para o cookie que a solicitou. Seu parser encontra zero linhas e registra "nenhuma nova licitação hoje". E, diferente de um preço perdido, uma licitação perdida não volta: quando alguém nota a ausência, o prazo de envio já expirou e o edital não tem mais utilidade para o concorrente.

A abordagem: faça cada busca provar o que viu

Comece pelo que não precisa de scraping. A API do TED oferece acesso anônimo a avisos publicados, sem necessidade de chave, então extraia a parte acima do limite por lá. No Reino Unido, o Find a Tender é a plataforma central onde todas as autoridades contratantes publicam informações de compras públicas desde 24 de fevereiro de 2025, segundo o GOV.UK. Verifique o que um registro central já cobre antes de escrever um parser para um portal que ele espelha.

Para todo o restante, três regras sustentam a maior parte do trabalho.

1. Defina uma resposta real, incluindo a vazia. Uma página de resultados traz o cabeçalho da coluna ("Closing date", nas próprias palavras do portal). Um dia honestamente vazio traz a própria linha de estado vazio do portal. Qualquer outra coisa é uma lacuna, não um zero. Coloque ambas as strings na lista validate.data.accept da request, e uma página de sessão expirada falhará a request em vez de passar como um dia sem movimentação. Nossos mecanismos já se recusam a considerar uma página de verificação reconhecida como sucesso, mas nenhum mecanismo sabe como cada órgão público redige sua página de timeout (essa parte precisa vir de você). A correspondência é uma substring simples e sensível a maiúsculas e minúsculas, então copie o texto da página em vez de digitá-lo novamente. Abordamos a mecânica quando as regras de validação passaram a decidir o sucesso, e o mesmo problema sob a perspectiva de séries temporais em Quando um bloqueio parece um ponto de dados.

2. Verifique a contagem informada pelo portal. A maioria das listas de resultados informa quantos avisos correspondem ("1-20 de 143"). Faça o parse desse número e compare-o com as linhas armazenadas. Se divergirem, a execução não terminou, independentemente do que os status codes indicaram.

3. Encontre a rota uma vez e depois a reproduza. Portais paginados e vinculados a sessões costumam esperar páginas seguintes a partir do mesmo endereço e do mesmo navegador. Com uma plataforma como o FourA, o Auto encontra uma rota funcional para a primeira página e retorna sua sessão: o proxy ID, o user agent e os cookies da tentativa bem-sucedida. Cada página seguinte passa pelo Single nessa saída, que é o caminho de produção. O Auto é apenas o localizador de caminhos.

import math, re, requests

API = "https://api.foura.ai/api"
H = {"X-API-Key": "pk_live_..."}
LIST = "https://tenders.example-council.gov/open?page={}"
PER_PAGE = 20

# A real answer is the results table or the portal's own empty-state line.
# An expired session or a maintenance notice served at 200 is neither.
CHECK = {"data": {"accept": ["Closing date", "No open tenders"]}}

def fetch(endpoint, body):
    r = requests.post(f"{API}/{endpoint}", headers=H, json=body).json()
    if r.get("error"):
        raise RuntimeError(r["error"])  # a gap to refill, not a quiet day
    return r

# Auto finds a route that works and hands back its session...
first = fetch("auto", {"url": LIST.format(1), "validate": CHECK})
proxy, ua = first["session"]["proxy"], first["session"]["userAgent"]
pages = [first["data"]]

# ...and Single replays it on the same exit for every other page.
m = re.search(r"\d+-\d+ of (\d+)", pages[0])
total = int(m.group(1)) if m else 0
for n in range(2, math.ceil(total / PER_PAGE) + 1):
    pages.append(fetch("single", {
        "method": "GET", "url": LIST.format(n), "proxy": proxy,
        "headers": [["User-Agent", ua]], "validate": CHECK,
    })["data"])

Se o portal mantiver o estado da sua busca no servidor, envie o cookie de sessão na mesma lista headers; o Single retorna cada header Set-Cookie por salto de redirecionamento. Documentos de licitação geralmente ficam atrás de um link de download como PDFs, e o controle de redirecionamento e modo de buffer bruto do Single os traz de volta como bytes em vez de texto corrompido. Mais um hábito: consulte cada portal em seu próprio cronograma. Esses são servidores pequenos mantidos por conselhos pequenos, e vinte requests em uma única rajada parecem muito piores para eles do que vinte distribuídas ao longo de uma tarde.

Resultados

  • 25% a mais de licitações capturadas. A GroupBWT, uma empresa de serviços de scraping, relata isso para um cliente que monitora mais de 50 portais de compras públicas em relação a uma linha de base manual, "não porque existiam mais licitações, mas porque eles pararam de perdê-las".
  • Nenhum scraper para a parte acima do limite. Os avisos do TED vêm de uma API que não precisa de chave, portanto, cada hora de trabalho no parser vai para portais que não possuem API.
  • As lacunas aparecem enquanto o aviso ainda está aberto. Uma página que falha na regra de aceitação gera um erro no dia em que acontece, e o job tenta novamente em vez de armazenar um dia vazio.
  • Uma descoberta de rota por portal, não por página. A busca do Auto acontece uma vez; cada página subsequente é uma chamada Single na saída encontrada.

Apenas o primeiro item é um resultado medido; os outros três descrevem o que a configuração muda.

Principal Conclusão

No monitoramento de licitações, a linha mais cara que você pode armazenar é "nada de novo hoje", porque é a única que você não pode corrigir depois. Os preços voltam amanhã. Uma licitação que fechou enquanto seu scraper lia uma página de sessão expirada não volta.

Portanto, faça o job merecer esses zeros. Uma página de listagem que não consegue mostrar a tabela, a linha de estado vazio do próprio portal ou uma contagem correspondente ainda não disse nada a você, e tratar isso como uma resposta é como um concorrente acaba descobrindo sobre um contrato pelo aviso de adjudicação.