Extração de dados

Pare de Fazer Parsing de HTML: Extraindo Dados Estruturados de JSON-LD e Dados Incorporados

A maioria das páginas já traz dados legíveis por máquina. Como extrair JSON-LD e JSON incorporado em vez de seletores frágeis, e o que fazer quando os dados estão incompletos.

Matt Brown

Matt Brown

26 de setembro de 2026 · 10 min de leitura

A maioria dos scrapers é construída da mesma forma: abrir a página, encontrar o elemento que contém o preço, escrever um seletor CSS, repetir para cada campo. Funciona até o site passar por um redesign, renomear uma classe ou envolver o preço em um novo componente. Então o seletor não retorna nada ou, pior, retorna a coisa errada, e o pipeline continua rodando.

Muitas páginas já publicam os mesmos dados em um formato pensado para máquinas. Os mecanismos de busca pediram por isso, os sites forneceram, e isso está lá no código-fonte da página o tempo todo. Este tutorial mostra como encontrar esses dados, extraí-los com algumas dezenas de linhas de código e lidar com os casos em que estão ausentes ou incompletos, o que, como mostra o exemplo real abaixo, acontece com mais frequência do que a documentação sugere.

Principais conclusões

  • O JSON-LD apareceu em 41% das páginas no Web Almanac de 2024, um aumento em relação a 34% em 2022. Para produtos, artigos, eventos e organizações, geralmente é a fonte mais estável na página.
  • Dados estruturados mudam com muito menos frequência do que o layout da página, porque os sites dependem deles para resultados de busca. Seletores quebram em redesigns; dados estruturados geralmente sobrevivem a eles.
  • Nem sempre estão completos. Uma página de produto pode publicar registros completos para a variante exibida na tela e apenas links simples para todas as outras variantes. Valide cada registro.
  • O extrator mais robusto tenta primeiro dados estruturados, depois JSON incorporado e, por último, seletores CSS, e registra qual deles foi usado.

O que “dados estruturados” significa em uma página web

Existem quatro lugares onde dados legíveis por máquina costumam residir:

FonteComo se pareceConteúdo típico
JSON-LDblocos <script type="application/ld+json">objetos schema.org: Product, Offer, NewsArticle, Organization, Event, BreadcrumbList
Microdataatributos itemprop em elementos visíveisO mesmo vocabulário schema.org, espalhado pela marcação
Open Graph e meta tags<meta property="og:...">Título, descrição, imagem, às vezes preço
Estado de aplicação incorporadoUm grande objeto JSON que o JavaScript da página lê, como __NEXT_DATA__Frequentemente tudo que a página exibe, e mais

O Web Almanac de 2024 do HTTP Archive mediu a disseminação desses formatos na web: o JSON-LD cresceu “de 34% em 2022 para 41% em 2024”, a microdata se manteve estável em 26%, e RDFa e Open Graph, que incluem as tags de compartilhamento social que a maioria dos sites adiciona, apareceram em 66% e 64% das páginas. O JSON-LD é o primeiro a ser buscado, porque é um bloco de dados autocontido, em vez de atributos espalhados pelo layout.

Por que é mais confiável do que seletores

Um seletor CSS depende de como uma página se parece. Dados estruturados dependem do que uma página significa. Sites mudam constantemente a forma como suas páginas se parecem. Eles mudam com muito menos frequência o que seus dados estruturados dizem, porque isso alimenta os rich results nos mecanismos de busca, e quebrar isso tem um custo visível para o site.

Ele também falha de forma mais honesta. Um seletor que para de corresponder pode silenciosamente corresponder a um elemento diferente e retornar um valor errado, mas plausível, o tipo de erro descrito em a taxa de falha silenciosa. Um objeto JSON-LD ou contém um campo price, ou não contém, o que torna a validação simples.

Extraindo JSON-LD em Python

A biblioteca padrão é suficiente. Este extrator coleta cada bloco JSON-LD, tolera blocos quebrados e percorre os contêineres que os sites usam para aninhar objetos: listas, @graph e hasVariant, que o schema.org usa para variantes de produto.

import json
from html.parser import HTMLParser


class _Collector(HTMLParser):
    """Collect JSON-LD blocks and embedded JSON state from an HTML page."""

    def __init__(self):
        super().__init__()
        self.blocks, self._buf, self._kind = [], None, None

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "script" and a.get("type", "").lower() == "application/ld+json":
            self._buf, self._kind = [], "json-ld"
        elif tag == "script" and a.get("id") == "__NEXT_DATA__":
            self._buf, self._kind = [], "next-data"

    def handle_data(self, data):
        if self._buf is not None:
            self._buf.append(data)

    def handle_endtag(self, tag):
        if tag == "script" and self._buf is not None:
            raw = "".join(self._buf).strip()
            try:
                self.blocks.append((self._kind, json.loads(raw)))
            except json.JSONDecodeError:
                self.blocks.append((self._kind + "-invalid", raw[:200]))
            self._buf = self._kind = None


def _walk(node):
    """Yield every JSON-LD object, flattening lists and nested containers."""
    if isinstance(node, list):
        for item in node:
            yield from _walk(item)
    elif isinstance(node, dict):
        yield node
        for key in ("@graph", "mainEntity", "itemListElement", "hasVariant"):
            if key in node:
                yield from _walk(node[key])


def _types(obj):
    t = obj.get("@type", [])
    return {t} if isinstance(t, str) else set(t)


def jsonld_objects(html, wanted_type=None):
    """Every JSON-LD object on the page, optionally filtered by schema.org type."""
    collector = _Collector()
    collector.feed(html)
    objs = [o for kind, data in collector.blocks if kind == "json-ld" for o in _walk(data)]
    return [o for o in objs if wanted_type is None or wanted_type in _types(o)]

Em um artigo real do Guardian, jsonld_objects(html, "NewsArticle") retorna o título, os timestamps de publicação e modificação, e o autor, sem nenhum seletor. Somente esses timestamps já valem o esforço: são exatos, legíveis por máquina e consistentes em todos os artigos do site.

Normalizando produtos

Produtos precisam de um pouco mais de cuidado, porque os preços residem dentro de offers aninhados, às vezes como um único Offer e às vezes como um AggregateOffer com uma faixa de preço.

def products(html):
    """Return normalised product records found in a page's JSON-LD."""
    out = []
    for obj in jsonld_objects(html, "Product"):
        offers = obj.get("offers") or {}
        offer = offers[0] if isinstance(offers, list) and offers else offers
        if isinstance(offer, dict) and "AggregateOffer" in _types(offer):
            price = offer.get("lowPrice")
        else:
            price = offer.get("price") if isinstance(offer, dict) else None
        brand = obj.get("brand")
        out.append({
            "name": obj.get("name"),
            "sku": obj.get("sku") or obj.get("gtin13") or obj.get("mpn"),
            "brand": brand.get("name") if isinstance(brand, dict) else brand,
            "price": float(price) if price not in (None, "") else None,
            "currency": offer.get("priceCurrency") if isinstance(offer, dict) else None,
            "availability": (offer.get("availability") or "").rsplit("/", 1)[-1] if isinstance(offer, dict) else None,
        })
    return out

Executado em uma página com um produto e uma oferta padrão, retorna registros limpos como {"name": "Trail Runner", "sku": "TR-01", "brand": "Acme", "price": 89.0, "currency": "EUR", "availability": "InStock"}, independentemente de como a página está estilizada.

Quando os dados estruturados estão incompletos: um exemplo real

Os exemplos da documentação fazem isso parecer fácil. Páginas reais são mais confusas, e vale a pena mostrar uma.

Executamos o extrator na página de produto de um tênis popular em uma grande loja Shopify. O JSON-LD da página descrevia um ProductGroup, o tipo schema.org para um produto vendido em variantes, com o nome do produto, marca, descrição e imagens, e 49 variantes. Apenas 7 delas, os tamanhos da cor exibida na tela, eram produtos completos com um preço de $100.00 e um status de disponibilidade. As outras 42, cada outra cor e tamanho, eram referências simples: um tipo e uma URL, nada mais. A avaliação de nota ficava em um segundo bloco JSON-LD, separado.

Portanto, os dados estruturados descreviam a página, não o catálogo. Um pipeline que assumisse “todas as variantes estão no JSON-LD” teria precificado silenciosamente uma cor e não registrado nada para as demais.

A mesma loja também expõe uma representação JSON pública de cada produto, que correspondia: sete variantes para aquela cor, cada uma com um preço e um indicador de disponibilidade. Mas ali o preço vinha como 10000, em unidades menores. Um pipeline que misturasse as duas fontes sem normalizar teria registrado um tênis a $100 e o mesmo tênis a dez mil dólares.

Seguem-se três lições, e elas se aplicam bem além do Shopify:

  • Valide, não assuma. Um bloco JSON-LD que faz parse não é necessariamente um registro completo. Verifique se todos os campos de que você precisa estão presentes, e compare o que você obteve com o que esperava.
  • Siga as referências quando os dados estruturados forem parciais. URLs de variantes, JSON incorporado e endpoints públicos de produtos frequentemente preenchem as lacunas, e geralmente são mais limpos do que a página.
  • Normalize unidades explicitamente. Unidades menores, faixas de preço, preços com e sem impostos, e moeda, tudo isso precisa ser tratado no código, não por suposição.

Uma cadeia de fallback que registra sua origem

Junte as peças como uma cadeia: tente primeiro os dados estruturados, depois o JSON incorporado, depois os seletores, e mantenha um registro de qual deles produziu cada registro.

REQUIRED = ("name", "price", "currency")


def extract_product(html, embedded=None, css_fallback=None):
    for source, candidates in (
        ("json-ld", products(html)),
        ("embedded-json", embedded(html) if embedded else []),
        ("css", css_fallback(html) if css_fallback else []),
    ):
        for record in candidates:
            if all(record.get(f) not in (None, "") for f in REQUIRED):
                return {**record, "source": source}
    return None

O campo source prova seu valor rapidamente. Quando um site que sempre produzia registros json-ld passa a produzir registros css, seus dados estruturados mudaram ou desapareceram, e você quer saber disso antes que o fallback de seletor também quebre. É também uma entrada útil para um índice de saúde do alvo.

Fazendo isso com a Web Scraping API

Se você busca páginas através da Web Scraping API da Shifter, a mesma abordagem funciona sem que você precise rodar um navegador. O parâmetro extract_rules da API mapeia seletores CSS para campos JSON, e sua saída html retorna o HTML interno de um elemento, então uma regra que seleciona o script JSON-LD retorna o bloco bruto para você fazer o parse:

{
  "jsonld": { "selector": "script[type='application/ld+json']", "output": "html" }
}

Uma única regra retorna o primeiro elemento correspondente, então, para páginas com vários blocos JSON-LD, solicite o HTML completo e execute o extrator acima sobre ele. Combine qualquer uma das abordagens com render_js=1 para páginas que injetam seus dados estruturados via JavaScript, e use auto_parser=1 quando estiver buscando um endpoint JSON diretamente, como uma URL JSON de produto, para obter o corpo já parseado de volta. Campos ausentes retornam como null em vez de falhar a requisição, o que se encaixa no padrão de validar-depois-fallback descrito acima. A sintaxe completa está na documentação de regras de extração.

O que os dados estruturados não vão lhe dar

Dados estruturados descrevem o que o site escolheu publicar para os mecanismos de busca. Podem estar atrasados em relação à página visível, omitir campos que o site não se importa em expor, ou descrever a variante padrão em vez da que está na tela. Para preços, em particular, compare com a página visível em uma base de amostragem, porque um preço de JSON-LD desatualizado e um preço atual na página são ambos “corretos” de pontos de vista diferentes. E para sites que variam o conteúdo por localização do visitante, os dados estruturados também variam, então capture-os a partir do mercado que interessa a você, como abordado em scraping de preços de voos e hotéis.

Conclusão

Antes de escrever outro seletor, abra o código-fonte da página e procure por application/ld+json. Em uma grande parcela da web, os dados que você deseja já estão lá, rotulados com um vocabulário compartilhado, e muito menos propensos a mudar do que o layout ao redor deles.

Extraia-os primeiro, valide-os, use como fallback o JSON incorporado e depois os seletores quando estiverem incompletos, e registre de qual fonte cada registro veio. Seus extratores vão quebrar com menos frequência e, quando quebrarem, vão avisar você.

Fontes e referências

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $ 0,75/GB.

Começar