Como automatizo a coleta de métricas de playlists do YouTube com Python

Passo a passo prático para extrair IDs, títulos e visualizações via YouTube Data API, consolidar em CSV e agendar rotinas diárias para decisões de conteúdo.

Como automatizo a coleta de métricas de playlists do YouTube com Python

Em muitos freelas e projetos de growth, eu preciso levantar dados de conteúdo com rapidez, confiabilidade e zero clique manual. Um caso clássico, extrair títulos, IDs e visualizações de todos os vídeos de uma ou várias playlists do YouTube para alimentar um dashboard, validar hipóteses de SEO, medir retenção por série e priorizar próximos roteiros. Abaixo está meu passo a passo completo, com código de produção, boas práticas e dicas para agendar a automação.

Visão geral do que vamos construir

  1. Autentico no YouTube Data API com OAuth em modo desktop e salvo o token para evitar relogar a cada execução.
  2. Busco todosos vídeos de uma playlist, com paginação.
  3. Consulto em lote as estatísticasde cada vídeo, em especial 'viewCount', e trago também título, data e link.
  4. Entrego um CSV consolidado, pronto para Power BI, Google Sheets ou Metabase, e explico como agendar a rotina. 

Pré-requisitos

  • Python 3.10+
  • Instalar dependências:
    • pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib python-dateutil

No Google Cloud Console, crie um projeto, ative YouTube Data API v3e gere credenciais OAuth Client IDdo tipo Desktop app. Baixe o JSON e salve na raiz do projeto como 'client_secret.json'.

Adicione ao '.gitignore': 'client_secret.json' e 'token.json'.

Código completo, pronto para usar (Aceita URL da playlistou ID. Faz paginação automática, busca estatísticas em lote e grava CSV.):

from _future_ import annotations

import csv

import os

from typing import List, Dict, Iterable

from datetime import datetime

from dateutil import tz

from googleapiclient.discovery import build

from googleapiclient.errors import HttpError

from google.oauth2.credentials import Credentials

from google_auth_oauthlib.flow import InstalledAppFlow

from google.auth.transport.requests import Request

# Escopo somente leitura

SCOPES = ["https://www.googleapis.com/auth/youtube.readonly"]

def authenticate(api_name: str = "youtube", api_version: str = "v3"):

    creds = None

    if os.path.exists("token.json"):

        creds = Credentials.from_authorized_user_file("token.json", SCOPES)

    if not creds or not creds.valid:

        if creds and creds.expired and creds.refresh_token:

            creds.refresh(Request())

        else:

            flow = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES)

            # Em ambiente local, uso um servidor local para o fluxo OAuth

            creds = flow.run_local_server(port=0)

        with open("token.json", "w") as token:

            token.write(creds.to_json())

    return build(api_name, api_version, credentials=creds)

def extract_playlist_id(url_or_id: str) -> str:

    if "list=" in url_or_id:

        from urllib.parse import urlparse, parse_qs

        q = parse_qs(urlparse(url_or_id).query)

        pl = q.get("list", [""])[0]

        if not pl:

            raise ValueError("Não foi possível extrair o parâmetro 'list' da URL.")

        return pl

    return url_or_id.strip()

def chunked(iterable: Iterable[str], n: int) -> Iterable[List[str]]:

    bucket = []

    for item in iterable:

        bucket.append(item)

        if len(bucket) == n:

            yield bucket

            bucket = []

    if bucket:

        yield bucket

def get_playlist_video_ids(youtube, playlist_id: str) -> List[str]:

    video_ids: List[str] = []

    page_token = None

    while True:

        resp = youtube.playlistItems().list(

            part="contentDetails",

            playlistId=playlist_id,

            maxResults=50,

            pageToken=page_token

        ).execute()

        for item in resp.get("items", []):

            vid = item["contentDetails"]["videoId"]

            video_ids.append(vid)

        page_token = resp.get("nextPageToken")

        if not page_token:

            break

    # remove duplicados preservando a ordem

    seen = set()

    deduped = []

    for v in video_ids:

        if v not in seen:

            deduped.append(v)

            seen.add(v)

    return deduped

def get_video_stats(youtube, video_ids: List[str]) -> List[Dict]:

    results: List[Dict] = []

    for batch in chunked(video_ids, 50):

        resp = youtube.videos().list(

            part="statistics,snippet",

            id=",".join(batch),

            maxResults=50

        ).execute()

        for item in resp.get("items", []):

            stats = item.get("statistics", {})

            snippet = item.get("snippet", {})

            results.append({

                "video_id": item["id"],

                "title": snippet.get("title", "").strip(),

                "views": int(stats.get("viewCount", 0)),

                "published_at": snippet.get("publishedAt", ""),

                "url": f"https://www.youtube.com/watch?v={item['id']}"

            })

    return results

def save_to_csv(rows: List[Dict], out_path: str) -> None:

    if not rows:

        print("Nenhum dado para salvar.")

        return

    fieldnames = ["video_id", "title", "views", "published_at", "url"]

    with open(out_path, "w", newline="", encoding="utf-8") as f:

        writer = csv.DictWriter(f, fieldnames=fieldnames)

        writer.writeheader()

        writer.writerows(rows)

def br_time_iso_to_local(iso: str) -> str:

       if not iso:

        return ""

    utc = datetime.fromisoformat(iso.replace("Z", "+00:00"))

    local = utc.astimezone(tz.gettz("America/Sao_Paulo"))

    return local.strftime("%Y-%m-%d %H:%M:%S")

def run(playlists: List[str], out_csv: str = "youtube_playlist_stats.csv") -> None:

    youtube = authenticate()

    all_rows: List[Dict] = []

    for raw in playlists:

        pl_id = extract_playlist_id(raw)

        print(f"Coletando IDs da playlist {pl_id}...")

        ids = get_playlist_video_ids(youtube, pl_id)

        if not ids:

            print(f"Playlist {pl_id} sem vídeos públicos.")

            continue

        print(f"Consultando estatísticas de {len(ids)} vídeos...")

        rows = get_video_stats(youtube, ids)

        for r in rows:

            r["published_at"] = br_time_iso_to_local(r["published_at"])

        all_rows.extend(rows)

    # Ordeno por views desc para facilitar análise

    all_rows.sort(key=lambda r: r["views"], reverse=True)

    save_to_csv(all_rows, out_csv)

    print(f"Concluído, {len(all_rows)} vídeos salvos em {out_csv}")

if _name_ == "_main_":

    # Exemplos, pode misturar URLs e IDs

    playlists_de_trabalho = [

        # "https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxx",

        # "https://www.youtube.com/watch?v=XXXXXXXX&list=PLyyyyyyyyyyyyyyyy",

        # "PLzzzzzzzzzzzzzzzzzz"

    ]

    if not playlists_de_trabalho:

        print("Defina suas playlists em 'playlists_de_trabalho' e rode novamente.")

    else:

        try:

            run(playlists_de_trabalho)

        except HttpError as e:

            print(f"Erro na API, detalhe: {e}")

Como uso no dia a dia

Colei a URL da playlist no array 'playlists_de_trabalho'.

Rodei o script uma vez, fiz login no fluxo OAuth e o 'token.json' ficou guardado, então nas próximas execuções o login é automático.

O CSV sai ordenado por visualizações, com título e link, pronto para pivot, análise de pareto ou enriquecimento em um BI.

Pontos de atenção que sempre considero

Paginação. A API devolve no máximo 50 itens por página, então é obrigatório usar 'nextPageToken', como no meu código.

Lote máximo de IDs. Em 'videos().list', mando até 50 IDs por vez, por isso uso a função 'chunked'.

Quotas e backoff. Em picos, a API pode responder 'quotaExceeded' ou 'rateLimitExceeded'. Eu capturo 'HttpError' e, em cenários de operação 24x7, uso backoff exponencial com 'time.sleep' progressivo.

Privacidade. Vídeos privados ou removidos não retornam dados completos, e podem reduzir o total coletado.

Segurança. Nunca commito 'client_secret.json' nem 'token.json'. Ambos ficam fora do repositório e protegidos por permissões de arquivo.

API Key x OAuth. Para leitura pública simples, uma API key funciona, porém eu padronizo OAuth, já que me permite escalar para casos que exigem escopos de leitura de dados do canal do cliente, quando ele me autoriza.

Extendendo o freela para gerar mais valor

Título no lugar do ID. Meu código já inclui 'title', então dá para gerar ranking de vídeos por desempenho por período.

Filtro temporal. Basta filtrar 'published_at' por intervalo, por exemplo, últimos 90 dias para olhar tração recente.

Exportação. Já entrego CSV, mas frequentemente jogo direto para Google Sheets com 'gspread', o que facilita o trabalho do time de conteúdo.

Múltiplas playlists. É comum o cliente ter séries por produto, então aceito várias playlists de uma vez e consolido no mesmo CSV.

BI. O CSV alimenta um dashboard com métricas chave, como visualizações por série, top 10 vídeos por período, e tendência semanal.

Como eu agendo a automação

Escolho conforme o ambiente do cliente.

Windows, Agendador de Tarefas

1. Abra o Agendador de Tarefas, crie uma tarefa básica.

2. Acionador diário às 9h.

3. Ação, Iniciar um programa.

   Programa: caminho do Python, por exemplo 'C:\Python312\python.exe'

   Argumentos: caminho do script, por exemplo '"C:\projetos\yt\coleta.py"'

   Iniciar em: pasta do projeto, para garantir que encontre os JSONs.

Linux e macOS, cron:

crontab -e

# Executa às 09:00 todos os dias

0 9 /usr/bin/python3 /home/usuario/projetos/yt/coleta.py >> /home/usuario/projetos/yt/log.txt 2>&1

Se prefiro dentro do próprio Python, uso a biblioteca 'schedule' para intervalos simples, lembrando que o processo precisa ficar em execução.

Check rápido do resultado

O CSV gerado terá colunas:

video_id,title,views,published_at,url

dQw4w9WgXcQ,Minha Aula 01,12345,2025-08-01 10:05:10,https://www.youtube.com/watch?v=dQw4w9WgXcQ

Com isso, eu atendo o requisito do freela em minutos, padronizo a coleta para qualquer playlist e ainda deixo o cliente com um processo agendado e auditável.