feat(shell): auto-commit con 31 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:55:16 +02:00
parent 1430039688
commit e1e9bb7499
31 changed files with 3917 additions and 0 deletions
@@ -0,0 +1,99 @@
---
name: scrape_tiktok_creative
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: impure
signature: "def scrape_tiktok_creative(country: str = 'ES', kind: str = 'hashtag', limit: int = 50, period: int = 7) -> list[dict]"
description: "Capta tendencias del TikTok Creative Center (hashtags, canciones, creadores y videos virales con metricas reales) via su API JSON interna creative_radar_api. Headers realistas con requests, paginacion, parseo tolerante a cambios de schema. Devuelve filas 1:1 con la tabla Postgres tiktok_trends. Impure: hace HTTP a un endpoint interno no publico que puede romperse o exigir anti-bot."
tags: [tiktok, social, trends, market-intel, datascience]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [requests]
params:
- name: country
desc: "Codigo ISO de pais del ranking (ej. 'ES', 'US', 'MX'). El Creative Center segmenta las tendencias por mercado. Default 'ES'."
- name: kind
desc: "Tipo de tendencia: 'hashtag' (default, el mas estable), 'song', 'creator' o 'video'. Cada uno usa un endpoint interno distinto. Empieza por hashtag si no estas seguro."
- name: limit
desc: "Numero maximo de filas a devolver. El endpoint pagina de 50 en 50; la funcion concatena paginas hasta alcanzar limit o agotar resultados. Default 50."
- name: period
desc: "Ventana temporal en dias. Solo acepta 7 (default), 30 o 120 — el endpoint rechaza otros valores con error de validacion."
output: "Lista de dicts con EXACTAMENTE las claves: country (str), kind (str), name (str|None), rank (int|None), views (int|None, BIGINT), growth_pct (float|None), industry (str|None), url (str|None). Mapea 1:1 con la tabla Postgres tiktok_trends (sin id/snapshot_date/scraped_at). Devuelve [] si el endpoint responde OK pero sin items para el segmento. Lanza ValueError (kind/period invalidos) o RuntimeError (403 anti-bot, HTTP de error, JSON invalido, code de error logico)."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/datascience/scrape_tiktok_creative.py"
notes: |
ESTRATEGIA: el Creative Center (ads.tiktok.com/business/creativecenter) es una
SPA JS-rendered, pero alimenta sus rankings desde una API interna de facto bajo
https://ads.tiktok.com/creative_radar_api/v1/popular_trend/... Esta funcion habla
directamente con ese endpoint con requests (mucho mas barato que un navegador
headless CUANDO responde). El parseo tolera variaciones del schema (data.list,
data.hashtags, data.items...) y nombres de campo distintos por kind.
REALISMO: en pruebas reales desde un entorno headless/datacenter el endpoint
respondio con code=40101 ("no permission") — rechazo anti-bot por falta de los
tokens de sesion firmados (anonymous-user-id, user-sign, timestamp) que la SPA
genera en cliente y que no se pueden falsear fuera del navegador. La funcion NO
inventa datos: en ese caso lanza RuntimeError con un mensaje claro. Se considera
el comportamiento esperado, no un bug de la funcion.
---
## Ejemplo
```python
from datascience.scrape_tiktok_creative import scrape_tiktok_creative
# Top 50 hashtags virales en Espana, ultimos 7 dias.
rows = scrape_tiktok_creative(country="ES", kind="hashtag", limit=50, period=7)
# rows[0] -> {
# "country": "ES", "kind": "hashtag", "name": "fyp", "rank": 1,
# "views": 12450000, "growth_pct": 42.0, "industry": "Entertainment",
# "url": "https://ads.tiktok.com/business/creativecenter/hashtag/fyp/pc/en"
# }
# Canciones en tendencia en US, ventana de 30 dias.
songs = scrape_tiktok_creative(country="US", kind="song", limit=20, period=30)
# Las filas casan 1:1 con un INSERT en la tabla Postgres tiktok_trends
# (sin id/snapshot_date/scraped_at, que los pone la BD).
```
## Cuando usarla
Usala cuando necesites market intelligence de TikTok: detectar hashtags, canciones,
creadores o productos virales por pais con metricas reales (views, ranking,
crecimiento) para alimentar la tabla `tiktok_trends`, un dashboard de tendencias o
un analisis de oportunidad de contenido. Empieza por `kind="hashtag"` (el endpoint
mas estable) antes de probar song/creator/video. Si el fetch HTTP devuelve
RuntimeError por anti-bot, baja al browser MCP/CDP del ecosistema.
## Gotchas
- **El endpoint interno NO es una API publica versionada.** `creative_radar_api/v1/popular_trend`
es un contrato de facto que TikTok cambia sin aviso: ruta, parametros, schema del
JSON y claves de campo pueden romperse en cualquier deploy. El parseo es tolerante
pero no inmune; si TikTok mueve la lista a otra ruta, la funcion devuelve [] o
lanza RuntimeError.
- **Anti-bot real y frecuente.** Desde IPs de datacenter o entornos headless el
endpoint suele responder `403` o `code=40101 (no permission)`. Los rankings se
sirven solo a clientes con los tokens de sesion firmados que la SPA genera en
navegador (`anonymous-user-id`, `user-sign`, `timestamp`). Esos tokens NO se
pueden falsear con requests. **Verificado en self-test: respondio code=40101.**
- **Alternativa robusta cuando el HTTP esta bloqueado:** usar el browser MCP/CDP del
ecosistema (regla `flow_replay.md`) navegando el Creative Center con una sesion de
chrome real, dejando que el cliente genere los tokens, y leyendo el JSON de la
respuesta XHR o el DOM renderizado. Es mas caro pero pasa el anti-bot.
- **No inventa datos.** Si no puede extraer de verdad, lanza una excepcion clara con
el codigo HTTP / code logico para diagnostico, en vez de devolver filas falsas.
- **growth_pct heuristico:** el Creative Center expresa el crecimiento como ratio
(0.42) o como porcentaje (42) segun campo/version; la funcion normaliza ratios en
[-1, 1] a porcentaje (*100). Si TikTok cambia la convencion, revisar `_row_from_item`.
- **Rate limiting:** la paginacion hace una request por pagina de 50. Para `limit`
altos puedes encadenar varias requests rapidas — anade backoff propio si scrapeas
muchos paises seguidos para no acelerar el bloqueo.