Como Fazer Scraping do Yahoo Finance em Python

Saiba como fazer scraping do Yahoo Finance a partir do seu JSON embutido em Python, validar cada ticker e migrar a tarefa para um servidor com a Bright Data.
40 min de leitura

O Yahoo Finance oferece cotações gratuitas, histórico de preços, demonstrações financeiras e estimativas de analistas, mas não tem API oficial. Suas páginas de cotação contêm esses dados como JSON dentro do HTML, então um pipeline Python pode lê-los sem um navegador.

Código de muitos tutoriais antigos falhava de 2 formas. Com seu User-Agent padrão, a biblioteca requests do Python recebia 429 Too Many Requests na primeira solicitação. Quando uma solicitação tinha sucesso, os seletores antigos não encontravam nada ou retornavam o preço do S&P 500 em vez do da ação.

Este guia mostra como fazer scraping do Yahoo Finance a partir desse JSON, evitar o 429 e migrar para um servidor.

Resumo

  • O 429 do Yahoo na primeira solicitação depende do cliente, não da taxa de solicitações. Personificando o Chrome, o curl_cffi solicitou 503 páginas de cotação do S&P 500 sem um 429.
  • Uma única solicitação retorna o histórico completo de preços de um ticker. Com period1=0, a aba de dados históricos da MSFT retornou cada barra diária desde 1986.
  • Valide cada página, mesmo quando retorna 200. FB, o antigo símbolo da Meta, retornou um ETF da ProShares, e o preço de Londres da Vodafone estava em pence.
  • Migre para um servidor trocando a função de busca. O Web Unlocker API da Bright Data adiciona rotação de IP e novas tentativas automáticas. Ou a Yahoo Finance Scraper API da Bright Data retorna registros estruturados.

Onde o Yahoo Finance guarda seus dados

Os números em uma página de cotação estão embutidos como JSON dentro do seu HTML. O Yahoo renderiza as páginas no servidor com SvelteKit, um framework JavaScript, e chama suas próprias APIs JSON durante a renderização. O SvelteKit insere cada resposta no HTML, para que o navegador possa reutilizá-la sem buscá-la novamente. Cada resposta está em sua própria tag script, que tem um atributo data-sveltekit-fetched e a URL da API de origem em data-url.

Uma tag da aba de resumo da MSFT se parece com isto, com seu corpo JSON abreviado:

<script type="application/json" data-sveltekit-fetched
  data-url="https://query1.finance.yahoo.com/v1/finance/quoteType/MSFT?formatted=true&amp;enablePrivateCompany=true&amp;overnightPrice=true&amp;lang=en-US&amp;region=US"
  data-hash="1ibomen" data-ttl="1800">
{"status":200,"statusText":"","headers":{"cache-control":"public, max-age=1800, stale-while-revalidate=1800"},"body":"{\"quoteType\":{\"result\":[{\"symbol\":\"MSFT\",\"quoteType\":\"EQUITY\",\"exchange\":\"NMS\",\"longName\":\"Microsoft Corporation\"}],\"error\":null}}"}
</script>

O campo body contém a resposta da API como uma string JSON. Fazer o parsing de uma tag precisa de 2 chamadas json.loads(). A primeira chamada faz o parsing do invólucro, e a segunda faz o parsing da string body. Cada aba de uma página de cotação embute um conjunto diferente de respostas de API:

Aba Resposta da API embutida O que você pode ler dela
/quote/MSFT/ v10/finance/quoteSummary e v7/finance/spark Preço, capitalização de mercado, P/L, dividendo, faixa de 52 semanas, metas de preço de analistas, até 24 horas de fechamentos de 5 minutos
/quote/MSFT/history/ v8/finance/chart Preços diários, semanais ou mensais, fechamento ajustado, desdobramentos, dividendos
/quote/MSFT/financials/, balance-sheet/, cash-flow/ ws/fundamentals-timeseries Itens anuais e dos últimos 12 meses
/quote/MSFT/analysis/ v10/finance/quoteSummary com earningsTrend Estimativas de LPA e receita, contagem de analistas, surpresas de lucros
/quote/MSFT/key-statistics/ ws/fundamentals-timeseries Capitalização de mercado trimestral, valor da empresa e índices de avaliação
/quote/MSFT/profile/ v10/finance/quoteSummary com assetProfile Principais executivos, número de funcionários, setor, registros na SEC
/quote/MSFT/holders/ v10/finance/quoteSummary com módulos de propriedade Detentores institucionais, de fundos e internos
/quote/MSFT/options/ v7/finance/options A cadeia de opções para o vencimento mais próximo, além da lista de datas de vencimento

Os hosts de API do Yahoo, query1.finance.yahoo.com e query2.finance.yahoo.com, servem o mesmo JSON, e a biblioteca de código aberto yfinance os chama diretamente. Nesses hosts, os endpoints quote, quoteSummary e options retornaram 401 quando uma solicitação não tinha crumb. Um crumb é um token que o Yahoo emite com cada cookie de sessão e espera como parâmetro de consulta crumb=.

O antigo download CSV retornou 401 User is not logged in. Em ambos os hosts de API, o robots.txt desautoriza todos os caminhos (Disallow: /). As respostas embutidas na página de cotação não precisam de cookie, crumb ou acesso ao host de API:

Diagrama de 2 formas de obter JSON do Yahoo Finance. Uma solicitação GET para finance.yahoo.com/quote/ retorna HTML com JSON embutido, não precisa de cookie ou crumb, e é permitida pelo robots.txt. Os hosts query1 e query2 precisam de um crumb em 3 endpoints, são desautorizados e podem retornar 429.

Os scripts leem apenas as páginas de cotação, então nunca chamam os hosts de API.

Muitos tutoriais antigos lêem preços com seletores CSS que não funcionam mais. Na página da MSFT, [data-symbol="MSFT"][data-field="regularMarketPrice"] e #quote-summary [data-test="PREV_CLOSE-value"] não encontraram nenhum elemento. O seletor menos específico fin-streamer[data-field="regularMarketPrice"] encontrou 9, e o primeiro resultado continha o preço do S&P 500 do resumo de mercado da página.

Outros tutoriais automatizam um navegador. O JSON embutido torna um navegador desnecessário para cotações, histórico diário, demonstrações anuais e estimativas, porque 1 solicitação HTTP por aba retorna os mesmos dados.

Se você usar um navegador, leia a resposta HTML bruta, porque o código do lado do cliente do SvelteKit remove cada tag script após lê-la. Em 2 de 4 carregamentos de página testados, page.content() retornou a página sem as tags. Em 1 deles, o scraper leu a página assim que o DOMContentLoaded disparou.

Por que o Yahoo Finance pode retornar 429 na primeira solicitação

As respostas do Yahoo dependiam do User-Agent do cliente e do handshake TLS, então um cliente pode receber um 429 na primeira solicitação. O cabeçalho server: ATS nas respostas identifica o Apache Traffic Server. Com suas configurações padrão, a biblioteca requests do Python recebeu um 429 na primeira solicitação. As respostas 429 tinham 2 formas:

  • Com o User-Agent padrão python-requests/2.34.2, toda pilha TLS na tabela abaixo recebeu uma resposta Edge: Too Many Requests de 23 bytes. A biblioteca requests do Python recebeu a mesma resposta em finance.yahoo.com.
  • Nos hosts de API, alguns User-Agents de navegador sobre a pilha TLS do Python receberam uma resposta Too Many Requests de 19 bytes.

A tabela mostra como o query1, o host de API que o yfinance usa, respondeu a solicitações de gráfico enviadas de 1 conexão residencial com pelo menos 1,5 segundo de intervalo. A biblioteca curl_cffi, um cliente HTTP Python, pode personificar o handshake TLS de um navegador. Cada linha combina uma pilha TLS com um cabeçalho User-Agent:

Pilha TLS Cabeçalho User-Agent Recebeu 429
Python requests (OpenSSL) python-requests/2.34.2, o padrão 5 de 5
Python requests (OpenSSL) Chrome 140 no macOS 26 de 26
Python requests (OpenSSL) Firefox 147 no Windows 16 de 17
Python requests (OpenSSL) Safari 26 no macOS 8 de 11
Python requests (OpenSSL) Chrome 150 no macOS 3 de 6
Python requests (OpenSSL) Chrome 150 no Windows 0 de 11
Python requests (OpenSSL) Mozilla/5.0 sozinho 0 de 11
curl_cffi (handshake Chrome, Firefox ou Safari) Chrome 140 ou 150, Firefox 147, Safari 26, ou Mozilla/5.0 0 de 80
curl_cffi (mesmos 3 handshakes) python-requests/2.34.2 15 de 15

Por que o handshake TLS importa?

A resposta do Yahoo dependia da pilha TLS, mesmo quando o User-Agent era o mesmo. Em um teste controlado, requests e curl_cffi enviaram o mesmo User-Agent (Chrome 140 no macOS) e cabeçalho Accept sobre HTTP/1.1 do mesmo endereço IP. Apenas o cliente HTTP e sua pilha TLS mudaram. Através da biblioteca requests do Python, 0 de 8 solicitações tiveram sucesso, e através da pilha TLS Chrome do curl_cffi, 8 de 8 tiveram.

As 2 pilhas já diferem antes de qualquer cabeçalho ser enviado. Uma impressão digital JA4, uma forma comum de fingerprinting TLS, resume o ClientHello, a primeira mensagem que um cliente envia em um handshake TLS. O handshake OpenSSL do Python produziu t13d1812h1_85036bcba153_b26ce05bbdd6, e o alvo chrome do curl_cffi produziu t13d1516h2_8daaf6152771_806a8c22fdea, que corresponde aos conjuntos de cifras do Chrome. Solicite https://tls.peet.ws/api/all do seu próprio cliente para ver o que ele envia.

Cada resultado dependia da combinação de pilha TLS e User-Agent. A string Mozilla/5.0 sozinha teve sucesso 11 de 11 vezes sobre o TLS do Python. O Chrome 140 no macOS falhou 26 de 26 vezes, e o Firefox 147 falhou 16 de 17 vezes. Então um User-Agent de navegador realista pode fazer a biblioteca requests do Python falhar com mais frequência.

Com um handshake TLS de navegador do curl_cffi, os User-Agents de navegador na tabela tiveram sucesso em 80 de 80 solicitações. Em um teste menor, o próprio finance.yahoo.com aceitou a string Chrome 140 sobre o TLS do Python e bloqueou a string padrão python-requests. O código do scraper usa curl_cffi em finance.yahoo.com também, então o User-Agent e o handshake TLS correspondem ao Chrome.

Se você enviar um User-Agent de navegador, envie um handshake TLS de navegador junto.

A taxa de solicitações importa?

Não para os 429s nas primeiras solicitações, que dependem do cliente. Com 8 threads, o curl_cffi solicitou todas as 503 páginas de cotação do S&P 500, depois 501 páginas de dados históricos, sem um 429. Esse é o volume de uma atualização diária do S&P 500. Se um 429 aparecer com volume maior, o código do scraper recua e tenta novamente.

Um trecho de limitação de taxa postado no rastreador de issues do yfinance limita o tráfego a 60 solicitações por minuto, 360 solicitações por hora e 8.000 solicitações por dia. Esses números vêm da página de limite de taxa da API DSP Traffic do Yahoo. Yahoo DSP é uma plataforma do lado da demanda para comprar anúncios, então o trecho usa os limites de outro produto.

A biblioteca yfinance também recebeu erros 429 do Yahoo, que levantou como YFRateLimitError('Too Many Requests. Rate limited. Try after a while.'). Após uma thread de issue sobre erros 429 receber mais de 140 comentários, os mantenedores do yfinance adotaram a mesma correção: um handshake TLS de navegador. A versão 0.2.58 (2025) migrou a biblioteca para curl_cffi, e a versão 1.7.0 cria sua sessão padrão com impersonate="chrome".

O código funciona a partir de um servidor em nuvem?

Sim, em 3 executores do GitHub Actions com endereços IP da Microsoft Azure. Em cada executor, a primeira solicitação do curl_cffi retornou uma página de cotação para cada um dos 20 símbolos, de ações e ETFs dos EUA a criptomoedas e listagens estrangeiras. Em uma segunda execução, cada executor solicitou todas as 503 páginas de cotação do S&P 500 sem um 429.

Os scripts retornaram o mesmo histórico, demonstrações e estimativas que retornaram em casa. Nos executores, a biblioteca requests do Python com seu User-Agent padrão recebeu a mesma resposta 429 de 23 bytes, enquanto o yfinance buscou o histórico diário completo da MSFT.

Falhas relatadas em nuvem envolveram clientes mais antigos e faixas de IP compartilhadas, então teste os scripts a partir do seu próprio servidor antes de agendar uma tarefa. Em 2025, antes de o yfinance migrar para curl_cffi, seus mantenedores removeram sua tarefa de teste do GitHub Actions após ela falhar ao buscar dados do Yahoo. Outro desenvolvedor rastreou respostas 401 “Invalid Crumb” até a faixa de IP compartilhada do nível gratuito do Render, uma plataforma em nuvem. As mesmas chamadas funcionaram a partir de um IP residencial.

Extraia o JSON embutido de uma página de cotação

Você precisa do Python 3.11 ou posterior e 5 pacotes. O código foi testado no Python 3.11, 3.13 e 3.14 com curl_cffi 0.16.3, beautifulsoup4 4.15.0, lxml 6.1.3, pandas 3.0.6 e requests 2.34.2. Instale-os com pip:

pip install curl_cffi beautifulsoup4 lxml pandas requests

Para fundamentos de scraping em Python, veja nosso guia de scraping de dados com Python. O módulo principal busca uma única aba, verifica se o Yahoo retornou uma página de cotação e retorna as respostas embutidas indexadas pela URL de sua API. Salve-o como yahoo_embedded.py, porque os scripts que leem as páginas do Yahoo dependem dele:

import json
import random
import threading
import time
from urllib.parse import quote

from bs4 import BeautifulSoup
from curl_cffi import requests

QUOTE_URL = "https://finance.yahoo.com/quote"
_local = threading.local()


class SymbolNotFound(Exception):
    """O Yahoo retornou sua página de busca ou um soft 404 em vez de uma página de cotação."""


def page_url(symbol: str, tab: str = "") -> str:
    return (
        f"{QUOTE_URL}/{quote(symbol.upper(), safe='')}/{tab}"  # ^GSPC vira %5EGSPC
    )


def embedded(html: str) -> dict[str, dict]:
    """Mapeia as URLs de API a partir das quais a página foi renderizada para seus corpos JSON."""
    soup = BeautifulSoup(html, "lxml")
    found = {}
    for tag in soup.select('script[type="application/json"][data-sveltekit-fetched]'):
        try:
            wrapper = json.loads(tag.string)
            found[tag["data-url"]] = json.loads(wrapper["body"])
        except (TypeError, KeyError, ValueError):
            continue  # uma resposta embutida cujo corpo não é JSON
    return found


def pick(found: dict[str, dict], path: str) -> dict:
    """Retorna a primeira resposta embutida cuja URL contém `path`."""
    for url, body in found.items():
        if path in url:
            return body
    raise LookupError(f"nenhuma resposta embutida para {path}")


def is_quote_page(html: str, symbol: str) -> bool:
    """True para uma página de cotação, False caso contrário, SymbolNotFound para um símbolo desconhecido."""
    found = embedded(html)
    if any("/v1/finance/lookup" in url for url in found):
        raise SymbolNotFound(symbol)
    for url, body in found.items():
        if "/v1/finance/quoteType/" in url:
            result = (body.get("quoteType") or {}).get("result") or [{}]
            if not result[0].get("exchange"):
                # Um soft 404, como para BRK.B (o Yahoo escreve BRK-B)
                raise SymbolNotFound(symbol)
            return True
    return False


def _session() -> requests.Session:
    # Uma sessão personificando o Chrome por thread, como o curl_cffi recomenda.
    if not hasattr(_local, "session"):
        _local.session = requests.Session(impersonate="chrome")
    return _local.session


def fetch_page(
    symbol: str, tab: str = "", params: dict | None = None, attempts: int = 4
) -> str:
    """Retorna o HTML de uma página de cotação, por exemplo tab="history/"."""
    for attempt in range(attempts):
        if attempt:
            time.sleep(2**attempt + random.random())  # recua antes de cada nova tentativa
        try:
            response = _session().get(
                page_url(symbol, tab), params=params, timeout=30, allow_redirects=False
            )
        except requests.RequestException:
            continue  # timeout ou conexão interrompida
        if response.is_redirect:
            location = response.headers.get("location", "")
            if "/lookup" in location:
                raise SymbolNotFound(symbol)
            # Por exemplo, uma aba que não existe
            raise LookupError(f"{symbol} {tab}: Yahoo redirecionou para {location}")
        if response.status_code == 200 and is_quote_page(response.text, symbol):
            return response.text
        if 400 <= response.status_code < 500 and response.status_code != 429:
            response.raise_for_status()
    # Toda tentativa expirou ou recebeu um 429, um 5xx, ou uma página sem dados de cotação
    raise RuntimeError(f"{symbol} {tab}: sem dados de cotação após {attempts} tentativas")


if __name__ == "__main__":
    from urllib.parse import urlsplit

    found = embedded(fetch_page("MSFT"))
    print(len(found), "respostas embutidas")
    for url in found:
        if url.startswith("https://"):
            print(urlsplit(url).netloc, urlsplit(url).path)

O módulo lida com 3 casos extremos:

  • allow_redirects=False para o cliente em um redirecionamento em vez de segui-lo. Um 302 para /lookup/ significa um símbolo desconhecido, então o módulo levanta SymbolNotFound. Ele não busca a página de busca, que o robots.txt desautoriza. Qualquer outro redirecionamento, como uma aba que um ETF não tem, levanta LookupError na primeira tentativa.
  • is_quote_page() verifica a resposta quoteType, porque um status 200 sozinho não prova que a página tem dados. Todas as 11 abas da MSFT a embutiram, incluindo news/. Um quoteType sem uma bolsa significa um soft 404, uma página com status 200 mas sem dados de cotação. O Yahoo retornou esse tipo de página para BRK.B, porque escreve o símbolo como BRK-B.
  • quote(symbol.upper(), safe='') coloca o símbolo em maiúsculas e codifica em percentual símbolos de índice como ^GSPC, porque ^ não é um caractere de URL válido até ser codificado.

Para MSFT, o módulo lista as APIs do Yahoo que a página chama:

23 respostas embutidas
query1.finance.yahoo.com /ws/obi-integration/v1/premiumTiersMeta
query1.finance.yahoo.com /v1/finance/quoteType/MSFT
query1.finance.yahoo.com /v7/finance/spark
query1.finance.yahoo.com /v6/finance/quote/marketSummary
query1.finance.yahoo.com /v7/finance/quote
query1.finance.yahoo.com /v7/finance/quote
query1.finance.yahoo.com /v6/finance/recommendationsbysymbol/MSFT
query1.finance.yahoo.com /v6/finance/markettime
query1.finance.yahoo.com /v7/finance/spark
query1.finance.yahoo.com /v10/finance/quoteSummary/MSFT
query1.finance.yahoo.com /v1/finance/trending/US
query1.finance.yahoo.com /v7/finance/quote
query1.finance.yahoo.com /v1/finance/screener/predefined/saved

As estatísticas da aba de resumo vêm de v10/finance/quoteSummary, que continha cerca de 223 KB de JSON em 17 módulos para a MSFT. A listagem mostra as 13 URLs absolutas de API, e as outras 10 respostas vieram dos próprios endpoints /xhr/ do Yahoo. A maioria das 23 fornece dados para widgets da página, como tickers em alta e resumos de mercado. A contagem varia: de executores em nuvem dos EUA, a mesma página embutiu 26 respostas, e as extras incluíram price-insights.

Faça o parsing dos campos de preço, avaliação e classificação

Muitos números dentro de quoteSummary são pares {"raw": 516.17, "fmt": "516.17"}, então um auxiliar mantém o valor bruto. Esta função retorna 20 campos de cotação. Salve-a como quote_summary.py, porque os scripts de estimativas e em lote a reutilizam:

import json

from yahoo_embedded import embedded, fetch_page, pick


def raw(module: dict, key: str):
    """Muitos números são pares {"raw": 516.17, "fmt": "516.17"}. Mantenha o valor bruto."""
    value = module.get(key)
    return value.get("raw") if isinstance(value, dict) else value


def quote_summary(symbol: str, fetch=fetch_page) -> dict:
    found = embedded(fetch(symbol))
    result = pick(found, "/v10/finance/quoteSummary/")["quoteSummary"]["result"][0]
    price = result.get("price", {})
    detail = result.get("summaryDetail", {})
    stats = result.get("defaultKeyStatistics", {})
    analysts = result.get("financialData", {})
    return {
        "symbol": price.get("symbol"),
        "name": price.get("longName") or price.get("shortName"),
        "quote_type": price.get("quoteType"),
        "currency": price.get("currency"),
        "price": raw(price, "regularMarketPrice"),
        "change_pct": raw(price, "regularMarketChangePercent"),
        "market_time": raw(price, "regularMarketTime"),
        "market_state": price.get("marketState"),
        "delay_minutes": price.get("exchangeDataDelayedBy"),
        "market_cap": raw(price, "marketCap"),
        "trailing_pe": raw(detail, "trailingPE"),
        "forward_pe": raw(detail, "forwardPE"),
        "dividend_yield": raw(detail, "dividendYield"),
        "beta": raw(detail, "beta"),
        "week52_low": raw(detail, "fiftyTwoWeekLow"),
        "week52_high": raw(detail, "fiftyTwoWeekHigh"),
        "eps_ttm": raw(stats, "trailingEps"),
        "target_mean": raw(analysts, "targetMeanPrice"),
        "recommendation": analysts.get("recommendationKey"),
        "reporting_currency": analysts.get("financialCurrency"),
    }


if __name__ == "__main__":
    print(json.dumps(quote_summary("MSFT"), indent=2))

Para MSFT, a função imprimiu valores brutos para todos os 20 campos após o fechamento do mercado:

{
  "symbol": "MSFT",
  "name": "Microsoft Corporation",
  "quote_type": "EQUITY",
  "currency": "USD",
  "price": 516.17,
  "change_pct": 0.0366316,
  "market_time": 1790366401,
  "market_state": "CLOSED",
  "delay_minutes": 0,
  "market_cap": 3832843862016,
  "trailing_pe": 28.739979,
  "forward_pe": 21.801512,
  "dividend_yield": 0.0076,
  "beta": 1.108,
  "week52_low": 349.2,
  "week52_high": 553.72,
  "eps_ttm": 17.96,
  "target_mean": 577.26135,
  "recommendation": "strong_buy",
  "reporting_currency": "USD"
}

A aba de resumo mostrou os mesmos números formatados, como uma capitalização de mercado de 3,833T, um P/L de 28,74 e um dividend yield de 0,76%:

Estatísticas da aba de resumo do Yahoo Finance para MSFT: fechamento anterior, abertura, lance, oferta, faixa do dia, faixa de 52 semanas, volume, capitalização de mercado 3.833T, beta, P/L 28.74, LPA 17.96, dividendo futuro 3.92 com rendimento de 0.76%, e meta de 1 ano 577.26.

O JSON contém os valores brutos em vez disso, como 3832843862016 para a capitalização de mercado de 3,833T.

Nos valores brutos, porcentagens são frações, então um change_pct de 0,0366316 é um movimento de 3,66%, e um dividend_yield de 0,0076 é 0,76%. A função lê cada módulo com .get() porque os módulos embutidos diferem por tipo de ativo. SPY, ^GSPC, BTC-USD e EURUSD=X retornaram preços sem o módulo financialData, então seus campos de analistas eram None. SPY incluiu fundProfile e topHoldings em vez disso.

Baixe o histórico completo de preços em 1 solicitação

A aba de dados históricos (history/) passa os parâmetros period1, period2 e frequency de sua própria URL para a solicitação de gráfico que embute. Ambos os parâmetros de período são timestamps Unix, então um period1 de 0 pede cada barra desde 1º de janeiro de 1970. O seletor de data então começa nessa data, e frequency=1d mostra linhas diárias:

Aba de dados históricos do Yahoo Finance para MSFT com um intervalo de datas começando em 01 de jan de 1970, Preços Históricos, Diário, e uma tabela de data, abertura, máxima, mínima, fechamento, fechamento ajustado e volume.

A linha mais recente da tabela corresponde à última linha que price_history() imprime abaixo.

A função price_history() converte a resposta de gráfico embutida em um DataFrame indexado por datas no fuso horário da bolsa. Salve-a como price_history.py:

import time

import pandas as pd

from yahoo_embedded import embedded, fetch_page, pick


def price_history(
    symbol: str, frequency: str = "1d", fetch=fetch_page
) -> tuple[pd.DataFrame, dict]:
    """Cada barra desde 1970 em 1d, 1wk ou 1mo, mais eventos de desdobramento e dividendo."""
    params = {"period1": 0, "period2": int(time.time()), "frequency": frequency}
    found = embedded(fetch(symbol, "history/", params))
    result = pick(found, "/v8/finance/chart/")["chart"]["result"][0]
    bars, meta = result["indicators"]["quote"][0], result["meta"]
    utc = pd.to_datetime(result["timestamp"], unit="s", utc=True)
    index = utc.tz_convert(meta["exchangeTimezoneName"]).tz_localize(None).normalize()
    frame = pd.DataFrame(
        {
            "open": bars["open"],
            "high": bars["high"],
            "low": bars["low"],
            "close": bars["close"],
            "adj_close": result["indicators"]["adjclose"][0]["adjclose"],
            "volume": bars["volume"],
        },
        index=index,
    )
    if frequency != "1d" and result["timestamp"][-1] == meta.get("regularMarketTime"):
        frame = frame.iloc[:-1]  # o Yahoo anexa a última sessão como uma barra extra
    return frame.dropna(how="all"), result.get("events", {})


if __name__ == "__main__":
    history, events = price_history("MSFT")
    print(history.tail(3).round(2).to_string())
    print(
        f"{len(history):,} barras diárias de {history.index[0]:%Y-%m-%d} "
        f"até {history.index[-1]:%Y-%m-%d}"
    )
    splits = sorted(
        pd.Timestamp(s["date"], unit="s").date()
        for s in events.get("splits", {}).values()
    )
    if splits:
        print(f"{len(splits)} desdobramentos, o último em {splits[-1]}")

Uma única solicitação retornou o histórico completo da MSFT:

              open    high     low   close  adj_close    volume
2026-09-23  500.76  509.44  495.88  500.59     500.59  19445600
2026-09-24  495.07  498.90  491.22  497.93     497.93  16657600
2026-09-25  499.04  519.40  497.25  516.17     516.17  38154200
10.213 barras diárias de 1986-03-13 até 2026-09-25
9 desdobramentos, o último em 2003-02-18

Através do host de API, o yfinance retornou as mesmas 10.213 barras para esse intervalo. Lide com estes 5 detalhes em um pipeline de histórico:

  • Nos testes, o histórico não começou antes de 1970. A página de ajuda do Yahoo sobre dados históricos diz que os preços geralmente não começam antes disso. A Coca-Cola é negociada desde 1919, mas seu histórico completo começou em 1970-01-02. Um period1 negativo retornou 404.
  • frequency suporta 1d, 1wk e 1mo. Em 1wk e 1mo, o Yahoo anexou a última sessão como uma barra extra, e a função a remove. Com essa barra removida, a última barra semanal ou mensal pode excluir a última sessão de negociação, então tire fechamentos recentes de barras diárias.
  • Valores intradiários como 1h, 5m e 1m retornaram barras diárias em vez disso. A aba de resumo embute até 24 horas de fechamentos de 5 minutos de /v7/finance/spark. Ela também embute uma segunda resposta spark para o resumo de mercado, então escolha a resposta cujo parâmetro symbols corresponde ao seu ticker. As páginas de cotação contêm apenas essas 24 horas de dados intradiários, porque o histórico intradiário mais longo vem dos hosts de API, que o robots.txt desautoriza.
  • O Yahoo ajusta close para desdobramentos e adj_close para desdobramentos e dividendos. Barras antigas mudam após cada novo desdobramento ou dividendo, então armazene os eventos de desdobramento e dividendo com cada download.
  • O tamanho da página cresce com o intervalo de datas. Com o intervalo de datas completo, a aba de dados históricos da MSFT tinha 4,9 MB de HTML, e a aba de resumo tinha cerca de 1,1 MB. Para uma atualização diária, defina period1 para um timestamp recente, e baixe o intervalo completo novamente após cada novo desdobramento ou dividendo.

Busque demonstrações financeiras e estimativas de analistas

Demonstrações e estimativas estão em abas diferentes, então cada uma recebe sua própria função.

Obtenha demonstrações financeiras anuais

As demonstrações financeiras vêm da resposta fundamentals-timeseries nas abas de demonstrações, balanço patrimonial e fluxo de caixa. Na aba de demonstrações da MSFT, essa resposta continha 96 séries anuais e 96 séries dos últimos 12 meses. statement() mantém as séries anuais e as organiza em uma tabela com 1 coluna por ano fiscal. Salve-a como statements.py:

import pandas as pd

from yahoo_embedded import embedded, fetch_page, pick


def statement(symbol: str, tab: str = "financials/", fetch=fetch_page) -> pd.DataFrame:
    """Itens anuais das abas financials/, balance-sheet/ ou cash-flow/."""
    found = embedded(fetch(symbol, tab))
    series = pick(found, "/fundamentals-timeseries/")["timeseries"]["result"]
    rows = []
    for item in series:
        name = item["meta"]["type"][0]  # por exemplo "annualTotalRevenue"
        if not name.startswith("annual"):
            continue  # ignora séries não anuais, como últimos 12 meses
        for point in item.get(name) or []:
            if point:
                rows.append(
                    {
                        "line_item": name.removeprefix("annual"),
                        "fiscal_year_end": point["asOfDate"],
                        "value": point["reportedValue"]["raw"],
                    }
                )
    table = pd.DataFrame(rows).pivot(
        index="line_item", columns="fiscal_year_end", values="value"
    )
    return table.sort_index(axis=1)


if __name__ == "__main__":
    income = statement("MSFT")
    billions = (
        income.loc[["TotalRevenue", "GrossProfit", "OperatingIncome", "NetIncome"]].T
        / 1e9
    )
    billions["DilutedEPS"] = income.loc["DilutedEPS"]
    print(billions.round(2).to_string())
    print(f"{len(income)} itens de linha")

O script imprimiu 4 anos fiscais da demonstração de resultados, em bilhões de dólares exceto LPA:

line_item        TotalRevenue  GrossProfit  OperatingIncome  NetIncome  DilutedEPS
fiscal_year_end                                                                   
2023-06-30             211.92       146.05            88.52      72.36        9.68
2024-06-30             245.12       171.01           109.43      88.14       11.80
2025-06-30             281.72       193.89           128.53     101.83       13.64
2026-06-30             331.84       225.46           155.24     133.75       17.95
47 itens de linha

Na aba de demonstrações, o Yahoo mostra os mesmos valores em milhares, então a receita de 331,84B na saída aparece como 331.839.000:

Aba de demonstrações financeiras do Yahoo Finance para MSFT com visões de Demonstração de Resultados, Balanço Patrimonial e Fluxo de Caixa, uma coluna TTM e 4 colunas de ano fiscal, e linhas de receita total até lucro líquido, em milhares.

As visões de Demonstração de Resultados, Balanço Patrimonial e Fluxo de Caixa são as abas financials/, balance-sheet/ e cash-flow/ que statement() lê.

Para MSFT, 47 das 96 séries anuais tinham valores. As séries vazias eram itens de linha como sinistros de seguro, dividendos preferenciais e impostos especiais. Para esses 4 anos, a função retornou 79 itens de linha de balance-sheet/ e 59 itens de linha de cash-flow/. Demonstrações trimestrais carregam apenas depois de você selecionar a opção Trimestral, e nenhum parâmetro de URL testado mudou isso.

Ignore os módulos incomeStatementHistory e balanceSheetHistory em quoteSummary. Suas linhas de receita tinham valores reais apenas para receita total e lucro líquido, tanto na versão anual quanto na trimestral. As linhas do balanço patrimonial tinham apenas datas. Outros campos de receita continham 0 como marcador de posição, então um parser que confia neles armazenaria um lucro bruto de 0.

Obtenha estimativas de analistas

A aba de análise embute earningsTrend, que contém LPA e receita de consenso para o trimestre atual, o próximo trimestre, o ano atual e o próximo ano. estimates() achata isso em 1 linha por período. Salve-a como estimates.py:

import pandas as pd

from quote_summary import raw
from yahoo_embedded import embedded, fetch_page, pick


def estimates(symbol: str, fetch=fetch_page) -> pd.DataFrame:
    """LPA e receita de consenso para o trimestre e ano atual e próximo."""
    found = embedded(fetch(symbol, "analysis/"))
    result = pick(found, "/v10/finance/quoteSummary/")["quoteSummary"]["result"][0]
    rows = []
    for period in result["earningsTrend"]["trend"]:
        eps, revenue = period["earningsEstimate"], period["revenueEstimate"]
        revenue_avg = raw(revenue, "avg")
        rows.append(
            {
                "period": period["period"],  # 0q, +1q, 0y, +1y
                "ends": period["endDate"],
                "eps_avg": raw(eps, "avg"),
                "eps_low": raw(eps, "low"),
                "eps_high": raw(eps, "high"),
                "eps_analysts": raw(eps, "numberOfAnalysts"),
                "revenue_avg_bn": None if revenue_avg is None else revenue_avg / 1e9,
                "revenue_analysts": raw(revenue, "numberOfAnalysts"),
            }
        )
    return pd.DataFrame(rows).set_index("period")


if __name__ == "__main__":
    print(estimates("MSFT").round(2).to_string())

Para MSFT, cada estimativa veio de 25 a 51 analistas:

              ends  eps_avg  eps_low  eps_high  eps_analysts  revenue_avg_bn  revenue_analysts
period                                                                                        
0q      2026-09-30     4.72     4.43      4.83            25           90.66                38
+1q     2026-12-31     4.82     4.72      5.02            25           95.49                37
0y      2027-06-30    19.76    19.21     20.70            33          391.04                50
+1y     2028-06-30    23.68    22.08     26.00            32          467.27                51

Na aba de análise, a tabela de Estimativa de Lucros mostra os mesmos números de LPA. Suas colunas Trim. Atual, Próx. Trim., Ano Atual e Próx. Ano correspondem às linhas 0q, +1q, 0y e +1y:

Aba de análise do Yahoo Finance para MSFT: a tabela de Estimativa de Lucros com a contagem de analistas e as estimativas médias, baixas e altas de LPA para o trimestre atual, próximo trimestre, ano atual e próximo ano.

A visão Normalizada da tabela mostra os mesmos números que earningsTrend contém.

A mesma solicitação também retorna epsTrend e epsRevisions para cada período, e earningsHistory com os últimos 4 trimestres reportados e suas porcentagens de surpresa.

Faça scraping do Yahoo Finance para centenas de tickers

Muitas listas de tickers em produção incluem símbolos antigos, índices e listagens estrangeiras, então seu script em lote precisa relatar problemas e continuar. Salve o próximo script como batch.py. Ele executa 8 threads por padrão, coleta símbolos ausentes e com falha separadamente, e converte unidades monetárias menores:

from concurrent.futures import ThreadPoolExecutor

import pandas as pd

from quote_summary import quote_summary
from yahoo_embedded import SymbolNotFound, fetch_page

# O Yahoo cota muitos preços de Londres, Johanesburgo e Tel Aviv em unidades menores.
# A capitalização de mercado permanece em unidades maiores.
MINOR_UNITS = {"GBp": ("GBP", 100), "ZAc": ("ZAR", 100), "ILA": ("ILS", 100)}
PRICE_FIELDS = ("price", "week52_low", "week52_high", "target_mean")


def to_major_units(row: dict) -> dict:
    unit = MINOR_UNITS.get(row["currency"])
    if unit is None:
        return row
    currency, divisor = unit
    prices = {key: row[key] / divisor for key in PRICE_FIELDS if row[key] is not None}
    return {**row, **prices, "currency": currency}


def collect(symbols: list[str], fetch=fetch_page, workers: int = 8):
    """Linhas de cotação para muitos símbolos, mais os símbolos ausentes ou com falha."""
    # " msft" e "MSFT" são o mesmo ticker
    symbols = list(dict.fromkeys(s.strip().upper() for s in symbols))
    missing, failed = [], {}

    def one(symbol: str) -> dict | None:
        try:
            return to_major_units(quote_summary(symbol, fetch=fetch))
        except SymbolNotFound:
            missing.append(symbol)
        except Exception as error:  # mantém o lote rodando, relata no final
            failed[symbol] = repr(error)
        return None

    with ThreadPoolExecutor(max_workers=workers) as pool:
        rows = [row for row in pool.map(one, symbols) if row]
    return pd.DataFrame(rows), missing, failed


if __name__ == "__main__":
    tickers = "AAPL MSFT SQ ^GSPC SPY BTC-USD 7203.T SAP.DE VOD.L NPN.JO".split()
    table, missing, failed = collect(tickers)
    cols = "symbol price currency market_cap delay_minutes reporting_currency".split()
    print(table[cols].to_string(index=False))
    print("ausentes:", missing, "| falhas:", failed)

Uma lista mista de 10 símbolos terminou em 2,5 segundos:

 symbol     price currency   market_cap  delay_minutes reporting_currency
   AAPL   341.070      USD 4.977637e+12              0                USD
   MSFT   516.170      USD 3.832844e+12              0                USD
  ^GSPC  7743.410      USD          NaN              0                NaN
    SPY   771.350      USD          NaN              0                NaN
BTC-USD 85011.320      USD 1.707212e+12              0                NaN
 7203.T  2989.500      JPY 3.540116e+13             20                JPY
 SAP.DE   185.920      EUR 2.145897e+11             15                EUR
  VOD.L     1.258      GBP 2.907280e+10             15                EUR
 NPN.JO   709.380      ZAR 5.307029e+11             15                USD
ausentes: ['SQ'] | falhas: {}

O lote relatou SQ como ausente porque a Block, Inc. mudou seu símbolo para XYZ. O Yahoo redireciona o símbolo antigo para sua página de busca, mas um símbolo reutilizado falha silenciosamente. FB, o antigo símbolo da Meta, retornou um ETF da ProShares com status 200, então compare cada name com sua lista de tickers. A página de cotação da FB parece qualquer outra página de cotação:

Título da página de cotação do Yahoo Finance para o símbolo FB, que mostra ProShares S&P 500 Dynamic Daily Buffer ETF com o ticker FB, uma cotação atrasada da Cboe US em USD.

Um símbolo reutilizado retorna uma página de cotação normal, então o nome é o sinal mais claro da mudança.

Verifique unidades, moedas e atrasos

Uma verificação de código de status não detectará mais 3 problemas:

  • Unidades monetárias menores. O Yahoo reportou o preço da VOD.L como 125,8 em GBp (pence) e sua capitalização de mercado como 29,07B em libras. O preço da NPN.JO era 70.938 em ZAc (centavos). to_major_units() divide apenas os campos de preço, então a capitalização de mercado mantém suas unidades maiores.
  • Moeda de relatório. Os valores das demonstrações usam a moeda de relatório da empresa, e os pontos de dados da série temporal incluem essa moeda em um campo currencyCode. A Vodafone (VOD.L) é negociada em pence mas reporta em euros, e a Naspers (NPN.JO) é negociada em rand mas reporta em dólares americanos. Índices podem misturar as 2 moedas. O P/L futuro da TEVA.TA era 43,5, enquanto a TEVA, sua listagem em Nova York, tinha 12,9. O Yahoo tinha dividido o preço em shekel da TEVA.TA por uma estimativa de LPA em dólares.
  • Atraso da bolsa. delay_minutes era 0 para as listagens dos EUA e Bitcoin. As cotações da Xetra, Londres e Johanesburgo tinham um atraso de 15 minutos, e a cotação de Tóquio tinha um atraso de 20 minutos. Mantenha delay_minutes e market_time com cada linha antes de comparar preços entre bolsas.

Execute lotes maiores

Com suas 8 threads padrão, collect() processou todos os 503 símbolos do S&P 500 em 74 segundos a partir da conexão residencial sem um 429 ou uma nova tentativa. Apenas BRK.B e BF.B estavam ausentes, porque a lista do S&P 500 da Wikipedia os escreve com pontos e o Yahoo usa BRK-B e BF-B. Com period1 definido para 7 dias atrás, todos os 501 históricos foram atualizados em 48 segundos. A página mediana tinha menos de 1 MB, comparado com 4,9 MB para o histórico completo da MSFT.

Com recuo exponencial, fetch_page() já tenta novamente timeouts, 429s, respostas 5xx e páginas sem dados de cotação. Nosso guia sobre como tentar novamente solicitações com falha em Python explica o recuo com o Retry do urllib3 e o Tenacity.

Se você migrar para asyncio, verifique primeiro o limite do seu cliente para o tamanho do cabeçalho de resposta. A resposta para a aba de resumo da MSFT tinha cerca de 37 KB de cabeçalhos, incluindo um Content-Security-Policy de 8.765 bytes e um cabeçalho Link de 27.039 bytes. Com configurações padrão, o aiohttp 3.14.3 levantou um ClientResponseError em uma página que o Yahoo havia servido com status 200. Sua mensagem começava com 400, message='Got more than 8190 bytes when reading.

Passar max_field_size=65536 para aiohttp.ClientSession corrigiu o erro. Tanto o httpx quanto o AsyncSession do curl_cffi lidaram com a página com configurações padrão. Nossa comparação entre requests, httpx e aiohttp explica as outras compensações entre eles.

Execute o mesmo parser a partir de um servidor com a Web Unlocker API

Nossa Web Unlocker API recebe uma URL e retorna a página. Ela lida com rotação de IP, resolução de CAPTCHA e novas tentativas automáticas em nossos servidores, então o mesmo parser roda a partir do seu servidor enquanto mantemos o desbloqueio. fetch_page_unlocker() recebe os mesmos argumentos symbol, tab e params que fetch_page(), então quote_summary(), price_history(), statement() e estimates() aceitam ambos através de seu parâmetro fetch. Salve-o como unlocker.py:

import os
from urllib.parse import urlencode

import requests

from yahoo_embedded import page_url

API_URL = "https://api.brightdata.com/request"


def fetch_page_unlocker(symbol: str, tab: str = "", params: dict | None = None) -> str:
    """Recebe os mesmos symbol, tab e params que fetch_page(), via Web Unlocker API."""
    url = page_url(symbol, tab) + (f"?{urlencode(params)}" if params else "")
    response = requests.post(
        API_URL,
        headers={"Authorization": f"Bearer {os.environ['BRIGHTDATA_API_KEY']}"},
        json={
            "zone": os.environ["BRIGHTDATA_UNLOCKER_ZONE"],
            "url": url,
            "format": "json",  # status, cabeçalhos e HTML do Yahoo, envolvidos em JSON
            "country": "us",
        },
        timeout=120,
    )
    response.raise_for_status()
    return response.json()["body"]

Ele lê sua chave de API e o nome da zona da Web Unlocker API em 2 variáveis de ambiente. Uma zona é uma configuração de produto nomeada em sua conta. Você encontrará a chave na página de configurações da conta e os passos para criar uma zona no guia rápido da Web Unlocker API. Assim que a zona existir, sua aba Visão Geral mostra ambos os valores:

Aba Visão Geral do Painel de Controle da Bright Data para uma zona da Web Unlocker API chamada web_unlocker, com Acesso direto à API selecionado e uma entrada API Keys que mostra uma chave mascarada com botões Mostrar e copiar.

O nome da zona, aqui web_unlocker, está no topo, e o botão de copiar embaixo de API Keys copia sua chave.

Defina ambos no seu shell antes de executar o próximo script:

export BRIGHTDATA_API_KEY="sua-chave-api"
export BRIGHTDATA_UNLOCKER_ZONE="nome-da-sua-zona"

Para rotear quote_summary(), price_history(), statement() ou estimates() através da Web Unlocker API, passe fetch_page_unlocker como o argumento fetch. Salve este script como unlocker_demo.py:

from price_history import price_history
from unlocker import fetch_page_unlocker

history, events = price_history("MSFT", fetch=fetch_page_unlocker)
print(
    f"{len(history):,} barras diárias de {history.index[0]:%Y-%m-%d} "
    f"até {history.index[-1]:%Y-%m-%d}"
)

Ele imprimiu o mesmo histórico que price_history.py:

10.213 barras diárias de 1986-03-13 até 2026-09-25

Uma única solicitação retornou a página completa de dados históricos, e o parser não precisou de mudanças para lê-la. O script em lote aceita a mesma função através de collect(tickers, fetch=fetch_page_unlocker).

Observe estes 3 detalhes:

  • Use "format": "json". A resposta então contém o código de status, cabeçalhos e HTML completo do Yahoo em um único objeto JSON. A função retorna o HTML de seu campo body.
  • Codifique caracteres especiais. page_url() já codifica em percentual símbolos de índice como ^GSPC como %5EGSPC, então toda URL que constrói é válida.
  • Ou use nosso SDK Python. Instale o brightdata-sdk com pip install brightdata-sdk, e passe response_format="json" para scrape_url() para obter o código de status do Yahoo junto com a página.

Cada aba é uma solicitação separada, e você paga apenas por solicitações bem-sucedidas.

Obtenha registros estruturados da Yahoo Finance Scraper API

Nossa Yahoo Finance Scraper API retorna um registro estruturado por ticker, e proxies, desbloqueio e novas tentativas já estão incluídos. É um scraper pré-construído em nossa Web Scraper API. Um registro combina os campos de resumo, perfil da empresa, principais executivos, demonstrações financeiras anuais e metas de preço de analistas, que a página do produto do scraper lista campo por campo. O Painel de Controle mostra os modos síncrono e assíncrono do scraper e o endpoint que o código abaixo chama:

Painel de Controle da Bright Data para a Yahoo Finance Scraper API, com as URLs de cotação MSFT, NVDA e SQ como entradas, modo Assíncrono selecionado, e um exemplo curl que chama o endpoint datasets/v3/trigger com o ID do dataset do scraper.

O exemplo curl do painel usa as mesmas URLs de cotação, endpoint de disparo e ID de dataset que scraper_api.py, e o modo Assíncrono funciona melhor para tarefas em lote agendadas.

Dispare uma tarefa e baixe os registros

Salve o próximo script como scraper_api.py. Ele dispara uma tarefa para uma lista de URLs, consulta seu progresso, baixa os registros e relata os símbolos que o Yahoo não lista mais:

import os
import time

import requests

API = "https://api.brightdata.com/datasets/v3"
DATASET_ID = "gd_lmrpz3vxmz972ghd7"  # informações comerciais do Yahoo Finance
HEADERS = {"Authorization": f"Bearer {os.environ['BRIGHTDATA_API_KEY']}"}
# A API retorna apenas estes campos, então a lista mantém input, error e error_code.
FIELDS = (
    "input|error|error_code|stock_ticker|closing_price|"
    "market_cap|pe_ratio|currency|timestamp"
)


def job_status(snapshot_id: str) -> str:
    progress = requests.get(
        f"{API}/progress/{snapshot_id}", headers=HEADERS, timeout=60
    )
    return progress.json()["status"]


def run_batch(urls: list[str], poll_seconds: int = 30) -> list[dict]:
    """Dispara uma tarefa de coleta, espera até ficar pronta e retorna seus registros."""
    trigger = requests.post(
        f"{API}/trigger",
        params={
            "dataset_id": DATASET_ID,
            "include_errors": "true",
            "custom_output_fields": FIELDS,
        },
        headers=HEADERS,
        json={"input": [{"url": url} for url in urls]},
        timeout=60,
    )
    trigger.raise_for_status()
    snapshot_id = trigger.json()["snapshot_id"]

    while job_status(snapshot_id) != "ready":
        time.sleep(poll_seconds)

    records = requests.get(
        f"{API}/snapshot/{snapshot_id}",
        params={"format": "json"},
        headers=HEADERS,
        timeout=300,
    )
    records.raise_for_status()
    return records.json()


def collect_records(urls: list[str]) -> tuple[list[dict], dict[str, str]]:
    """Executa uma tarefa e divide seus registros em resultados e erros por URL."""
    records = run_batch(list(dict.fromkeys(urls)))  # remove URLs duplicadas
    done = [record for record in records if not record.get("error")]
    errors = {
        record["input"]["url"]: f'{record.get("error_code")}: {record["error"]}'
        for record in records
        if record.get("error")
    }
    return done, errors


if __name__ == "__main__":
    urls = [f"https://finance.yahoo.com/quote/{s}/" for s in ("MSFT", "NVDA", "SQ")]
    records, errors = collect_records(urls)
    for record in records:
        print(record)
    print("erros:", errors)

Ele imprimiu 2 registros e um erro para SQ:

{'input': {'url': 'https://finance.yahoo.com/quote/MSFT/'}, 'stock_ticker': 'MSFT', 'closing_price': 516.17, 'market_cap': 3832843862016, 'pe_ratio': 28.739979, 'currency': 'USD', 'timestamp': '2026-09-27T16:15:44.789Z'}
{'input': {'url': 'https://finance.yahoo.com/quote/NVDA/'}, 'stock_ticker': 'NVDA', 'closing_price': 225.07, 'market_cap': 5434765213696, 'pe_ratio': 28.489874, 'currency': 'USD', 'timestamp': '2026-09-27T16:16:02.513Z'}
erros: {'https://finance.yahoo.com/quote/SQ/': 'dead_page: Target quote not found on lookup page'}

O scraper sinalizou SQ como dead_page porque o Yahoo redireciona SQ para sua página de busca, e fetch_page() levanta SymbolNotFound no mesmo caso.

Configure um pipeline agendado

Execute a Scraper API como uma tarefa em lote. Envie muitos tickers em cada chamada de disparo, depois consulte o endpoint de progresso ou espere por um webhook.

O endpoint síncrono /scrape retorna registros diretamente em sua resposta. Para uma tarefa mais longa, o endpoint retorna um snapshot_id para consultar em vez disso, como a referência da API síncrona descreve.

A chamada de disparo aceita algumas configurações que importam para um pipeline agendado. A referência da API assíncrona as lista, e o Painel de Controle oferece as mesmas opções em Configurações do scraper:

  • Relatório de erros. include_errors=true adiciona um error e um error_code a qualquer entrada sem resultado, como um símbolo que o Yahoo não lista mais. collect_records() relata essas entradas por URL.
  • Campos de saída. custom_output_fields mantém apenas os campos que você lista, separados por |, então adicione input, error e error_code para manter o relatório de erros.
  • Limites de registros. limit_per_input limita os registros por entrada, e limit_multiple_results limita os registros por tarefa. Ambos os limites controlam o custo de uma busca Descobrir por palavra-chave.
  • Entrega e webhooks. endpoint entrega os registros para sua URL de webhook. notify=true envia uma notificação quando a tarefa termina, então um agendador não precisa consultar /progress. Passe uncompressed_webhook=true para receber payloads de webhook não comprimidos. Configurações de entrega no painel também podem escrever resultados no Amazon S3, Google Cloud Storage ou Snowflake, como a documentação de entrega de dados descreve.
  • Agendamentos. O Painel de Controle pode executar a mesma coleta em uma programação recorrente, como uma atualização diária de tickers.

Em uma lista de tickers mais longa, o scraper também sinalizou MMC, BK e FI como dead_page. Essas empresas haviam mudado seus símbolos para MRSH, BNY e FISV. Você paga apenas por registros entregues com sucesso.

Busque por palavra-chave ou execute pela CLI

O modo Descobrir por palavra-chave encontra listagens que correspondem a uma palavra-chave em bolsas e tipos de ativos. Uma busca por WIX encontrou a Wix.com, Wickes Group (WIX.L) e contratos de opções WIX, e uma busca por MSFT também encontrou a listagem da Microsoft em Toronto e ETFs de ação única. Filtre em entity_type e stock_ticker para manter as listagens que deseja.

A Yahoo Finance Scraper API também roda de um terminal como brightdata pipelines yahoo_finance_business <url> na Bright Data CLI. Através da ferramenta web_data_yahoo_finance_business, um agente de IA pode buscar o registro para uma URL de cotação que você fornecer. A ferramenta roda em nosso Yahoo Finance MCP Server, depois de você ativar seu grupo de ferramentas finance.

Escolha a abordagem certa para seu pipeline

Comece lendo as páginas de cotação gratuitamente, e adicione a Web Unlocker API para tarefas de servidor agendadas. Para registros estruturados, use a Scraper API ou o Yahoo Finance Dataset:

Opção Melhor para Você lida com Preços
yfinance Pesquisa pessoal, como seu README afirma Atualizações de biblioteca, IPs bloqueados Gratuito
Páginas de cotação com curl_cffi Centenas de tickers por dia de casa ou um executor do GitHub Actions Parser, novas tentativas, IPs bloqueados Gratuito
Páginas de cotação com Web Unlocker API Tarefas de servidor agendadas que leem qualquer aba Parser Por solicitação bem-sucedida
Playwright com Browser API Demonstrações trimestrais e outros dados que aparecem após selecionar uma opção Script de navegador, parser Por GB de tráfego
Yahoo Finance Scraper API Registros analisados entregues em uma programação Lista de campos de saída Por registro entregue
Yahoo Finance Dataset Arquivos prontos de empresas, fundos e índices Configurações de entrega Por registro

Para estimar um orçamento mensal, conte 1 solicitação da Web Unlocker API por aba, por ticker, por atualização. A Yahoo Finance Scraper API precisa de 1 registro por ticker por atualização. Multiplique qualquer volume pelo preço atual na página de preços da Web Unlocker API ou na página de preços da Web Scraper API.

Para demonstrações trimestrais, um script Playwright ou Puppeteer pode selecionar a opção Trimestral da aba de demonstrações em nossa Browser API. Ela roda o Chromium na nuvem, e a resolução de CAPTCHA e a renderização de JavaScript já estão incluídas. Nosso guia sobre como fazer scraping de sites dinâmicos com Python mostra esse fluxo de trabalho passo a passo.

Nosso Yahoo Finance Dataset empacota o mesmo tipo de registros que a Scraper API em arquivos prontos JSON, NDJSON, CSV ou Parquet. Ele inclui ações, fundos mútuos, índices, ETFs, moedas e fundos do mercado monetário, então filtre em entity_type para manter as classes de ativos que deseja.

Você pode comprar um subconjunto com apenas os pontos de dados que precisa e obter atualizações em uma programação diária, semanal, mensal ou personalizada. Cada entrega pode conter apenas registros novos ou atualizados e ir diretamente para Amazon S3, Google Cloud, Azure, Snowflake ou SFTP. A página do dataset mostra a contagem atual de registros e preços, e você pode baixar um arquivo de amostra em JSON ou CSV antes de comprar.

Para outras fontes de dados de mercado, veja nossos guias sobre scraping de dados financeiros e scraping do Google Finance. Nossos outros scrapers e datasets oferecem mais dados de ações e outros resultados do Yahoo:

Considerações finais

As páginas de cotação do Yahoo Finance contêm suas próprias respostas de API como JSON, então 1 solicitação por aba retorna os dados sem navegador, cookie ou crumb. Corrija o cliente antes de adicionar atrasos, porque os 429s do Yahoo nas primeiras solicitações dependiam do handshake TLS e User-Agent do cliente. Faça o parsing das respostas embutidas, valide cada página, compare cada nome com sua lista de tickers, e converta unidades monetárias menores dentro do parser.

Para tarefas de servidor agendadas, roteie o mesmo parser através da Web Unlocker API, ou deixe a Yahoo Finance Scraper API retornar registros estruturados. Nossos créditos gratuitos mensais se aplicam a ambos, então você pode testar qualquer um com sua própria lista de tickers.

Perguntas frequentes

Existe uma API oficial do Yahoo Finance?

Não. O Yahoo diz que desativou o acesso via API aos dados de cotação porque seus fornecedores de dados “não autorizam atualmente” a redistribuição em forma de API. Os hosts de API internos que o yfinance usa não são documentados e são desautorizados no robots.txt. As páginas de cotação do Yahoo embutem os mesmos dados como JSON.

O yfinance ainda é utilizável?

Sim, de casa e 3 executores do GitHub Actions, o yfinance buscou o histórico diário completo da MSFT em menos de 1,5 segundos. Ele usa o handshake TLS do Chrome do curl_cffi. Seu README chama a API do Yahoo de “destinada apenas para uso pessoal”. Desenvolvedores relataram 401s e falhas de busca de alguns hosts em nuvem, então teste a partir do seu próprio servidor.

O Yahoo Finance bloqueia web scraping?

Sim, para alguns clientes. O Yahoo bloqueou o cliente requests padrão do Python na primeira solicitação. Com o handshake TLS do Chrome, o curl_cffi não recebeu 429s em uma conexão residencial ou 3 executores do GitHub Actions. Para tarefas agendadas, a Web Unlocker API da Bright Data busca as mesmas páginas com rotação de IP e novas tentativas automáticas.

É possível baixar dados do Yahoo Finance?

Sim, mas a página de ajuda do Yahoo limita o download CSV a assinantes pagos do Yahoo Finance. Em código, uma única solicitação à aba de dados históricos retornou cada barra diária da MSFT desde 1986. Para arquivos em massa, o Yahoo Finance Dataset da Bright Data entrega registros de empresas, fundos e índices na programação que você escolher.

A API do Yahoo Finance é gratuita?

Não oficialmente. Os endpoints internos do Yahoo não custam nada para chamar, mas não são documentados e são desautorizados para crawlers. A página de ajuda do Yahoo limita downloads CSV a assinantes pagos. A Yahoo Finance Scraper API da Bright Data retorna registros estruturados, e novas contas recebem créditos gratuitos mensais para testá-la.