--- title: "Scraping de X/Twitter con TweetScraperR" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Scraping de X/Twitter con TweetScraperR} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) ``` `TweetScraperR` recolecta datos de X/Twitter (tweets, usuarixs y metadatos) para análisis y visualización. Esta viñeta describe el **flujo de trabajo**, basado en consultas a la API GraphQL interna de X (datos estructurados, sin selectores HTML frágiles). Las salidas que se muestran son **capturas reales** obtenidas entre el 2026-10-18 y el 2026-10-20. > **Nota de versiones:** desde la **0.5.0** el paquete expone una superficie > única sobre el motor Node.js/Playwright (36 funciones en la 0.8, con el pool > de cuentas). Las funciones de > scraping por HTML (`getTweetsTimeline()`, `getTweetsData()`, > `getUsersData()`, la familia `getUrls*()`, etc.) fueron **eliminadas**; el > mapa de reemplazos está en el `NEWS.md`. ## 1. Instalar el motor (una vez por máquina) El motor de recolección corre sobre **Node.js (>= 18) + Playwright**. No se instala junto con el paquete: lo descargás vos, una sola vez, en un caché persistente del usuario (`tools::R_user_dir("TweetScraperR", "cache")`, que sobrevive a reinstalaciones del paquete): ```{r} library(TweetScraperR) # Instala el motor de Node.js/Playwright (una sola vez por máquina). installPlaywrightEngine() # Verifica que el motor quedó operativo. checkPlaywrightEngine() #> Motor Playwright OK — Playwright 1.64.1 | Node v24.20.0 ``` ## 2. Importar tu sesión X bloquea el login automatizado (detecta el navegador por *fingerprint*). Por eso la sesión se importa desde **tu navegador normal**, donde ya estás logueadx: 1. Iniciá sesión en en tu navegador habitual. 2. Abrí las *DevTools* (F12) → pestaña **Application / Almacenamiento** → **Cookies** → `https://x.com`. 3. Copiá los valores de las cookies **`auth_token`** y **`ct0`**. 4. Importalos: ```{r} importSessionX(auth_token = "tu_auth_token", ct0 = "tu_ct0") ``` La sesión queda guardada en disco y **todas las funciones la reusan** sin volver a loguearse. Cuando las cookies expiren, repetí el paso para reimportarlas. ### Varias cuentas: el pool X limita cada endpoint **por cuenta** en ventanas de 15 minutos (la búsqueda admite unas 50 consultas). Con varias cuentas, la recolección rota sola: la cuenta que agota el cupo queda en pausa hasta que X lo renueva y se sigue con la siguiente desde donde se cortó (retomando el cursor de paginación); la que X rechaza (sesión vencida o suspendida) se desactiva. Hay tres formas de cargar cuentas: ```{r} # 1) Asistente de consola: pide nombre, auth_token y ct0 (entrada oculta) y valida setupAccountsX() # 2) Carga masiva desde un CSV (separado por "," o ";", como lo guarda Excel) # columnas: account, auth_token, ct0 y, opcional, proxy addAccountsX("cuentas.csv") # 3) De a una, desde código importSessionX(cookies = "auth_token=...; ct0=...", account = "cuenta2") ``` Para revisar y administrar el pool: ```{r} checkAccountsX() # valida cada sesión y registra su @usuario real accountsX() # estado: activas, pausas por endpoint, requests, errores reactivateAccountX("cuenta2") resetLocksX() # quita las pausas de rate-limit removeAccountX("cuenta2") ``` `checkAccountsX()` desactiva las sesiones vencidas y, si la misma cuenta de X quedó cargada dos veces con nombres distintos, conserva solo la más nueva. Con una sola cuenta, al agotarse el cupo el paquete espera a que X lo renueve, como máximo `getOption("TweetScraperR.wait_timeout", 900)` segundos. ### Proxy (opcional) Un proxy hace que una cuenta salga a X por otra IP. No suma cupo (eso lo hace el pool), pero sirve si X bloquea tu IP o para que las cuentas no salgan todas de la misma conexión. Conviene una IP residencial dedicada y fija ("static residential" o "ISP"), usando la dirección **HTTP** del proveedor: Chromium no admite SOCKS5 con usuario y clave. ```{r} # Por cuenta (valida la cuenta a través del proxy nuevo) setProxyX("cuenta2", "http://usuario:clave@host:puerto") setProxyX("cuenta2", NULL) # quitarlo # Global, para las cuentas sin proxy propio options(TweetScraperR.proxy = "http://usuario:clave@host:puerto") # o TWEETSCRAPERR_PROXY=... en ~/.Renviron ``` ## 3. Recolectar datos (funciones `*API()`) ### 3.1 Timeline de unx usuarix ```{r} tl <- getUserTweetsAPI("NASA", n_tweets = 10) #> Recolectando timeline de @NASA... #> Tweets únicos recolectados: 10 #> Datos procesados y guardados. ``` Cada función de tweets devuelve el mismo `tibble` de **31 columnas** (salida real del 2026-10-20): ```{r} dplyr::glimpse(tl) #> Rows: 10 #> Columns: 31 #> $ fecha 2026-10-20 18:00:27, 2026-10-20 18:00:06, 2026-10-20~ #> $ user "@NASA", "@NASA", "@NASA", "@NASA", "@NASA", "@NASA",~ #> $ texto "LIVE: Get a preview of two upcoming spacewalks set f~ #> $ idioma "en", "en", "en", "en", "en", "en", "en", "en", "en",~ #> $ respuestas 41, 20, 151, 11, 83, 189, 169, 81, 88, 92 #> $ retweets 79, 75, 305, 52, 120, 249, 264, 168, 172, 243 #> $ citas 5, 8, 43, 3, 14, 25, 19, 10, 18, 10 #> $ megustas 497, 511, 1949, 322, 1034, 1652, 2026, 1165, 1100, 21~ #> $ views 227105, 224886, 286788, 90650, 405674, 404857, 100438~ #> $ emoticones <>, <>, <>, <>, <"🇺", "🇿">, "✈", <>, <>, <>, <> #> $ hashtags <>, <>, <>, <>, <>, <>, <>, <>, <>, <> #> $ menciones "astro_anil", <>, "NASAMoonBase", <"NASAMoonBase", "N~ #> $ urls_externas "https://x.com/i/broadcasts/1jxXgBNWypPJZ", "https://~ #> $ media <>, <>, "https://video.twimg.com/amplify_video/211256~ #> $ media_tipo <>, <>, "video", <>, "photo", "video", "photo", "pho~ #> $ es_retweet FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL~ #> $ es_cita FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL~ #> $ tweet_citado_id NA, NA, NA, NA, NA, NA, NA, NA, NA, NA #> $ conversation_id "2112604875452223824", "2112604787539865876", "21125~ #> $ url "https://x.com/NASA/status/2112604875452223824", "ht~ #> $ tweet_id "2112604875452223824", "2112604787539865876", "21125~ #> $ user_id "11348282", "11348282", "11348282", "11348282", "1134~ #> $ respuesta_a_id NA, NA, NA, "2112560723603632217", NA, NA, NA, NA, NA~ #> $ respuesta_a_user NA, NA, NA, "@NASA", NA, NA, NA, NA, NA, NA #> $ retweet_id NA, NA, NA, NA, NA, NA, NA, NA, NA, NA #> $ guardados 28, 38, 122, 25, 32, 67, 112, 50, 48, 131 #> $ fuente NA, NA, NA, NA, NA, NA, NA, NA, NA, NA #> $ sensible FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS~ #> $ editado FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS~ #> $ lugar NA, NA, NA, NA, NA, NA, NA, NA, NA, NA #> $ pais NA, NA, NA, NA, NA, NA, NA, NA, NA, NA ``` Los tweets largos (más de 280 caracteres) vienen completos en `texto`. ### 3.2 Timelines combinados (curaduría de investigación) ```{r} combinado <- getTweetsTimelinesAPI( c("sanchezcastejon", "agusnieto77"), n_tweets = 40 ) #> Recolectando timelines de 2 cuentas... #> @sanchezcastejon: 40 tweets #> @agusnieto77: 40 tweets #> Total combinado: 80 tweets de 2 cuentas. #> Datos procesados y guardados. ``` ### 3.3 Búsqueda (`Latest` / `Top` / `Media`) con rango de fechas La búsqueda soporta los operadores de X (`from:`, `lang:`, ...) y los argumentos `since`/`until` para acotar por días (formato `"YYYY-MM-DD"`, ambos extremos inclusivos). El artículo [Parámetros de búsqueda](https://agusnieto77.github.io/TweetScraperR/articles/busquedas.html) recorre todos los operadores con ejemplos verificados, incluidas las franjas horarias (`since = "2026-10-20 12:00"`, en UTC): ```{r} top <- getTweetsSearchAPI("Milei", n_tweets = 10, product = "Top") #> Buscando: Milei (Top) #> Tweets únicos recolectados: 10 #> Datos procesados y guardados. #> # A tibble: 10 × 31 #> user fecha megustas retweets #> #> 1 @JMilei 2026-10-15 17:54:43 11639 1558 #> 2 @JMilei 2026-10-07 13:04:03 32757 4481 #> 3 @Gianferreyra_ 2026-10-17 03:26:10 17298 1028 #> 4 @PinkuDraw 2026-10-16 04:49:39 64 9 #> 5 @JMilei 2026-10-16 21:08:15 5639 778 rango <- getTweetsSearchAPI( "Feijoo", n_tweets = 20, since = "2026-10-17", until = "2026-10-18" ) #> Buscando: Feijoo since:2026-10-17 until:2026-10-18 (Latest) #> Tweets únicos recolectados: 20 ``` #### Captura en vivo `getTweetsLiveAPI()` sigue una búsqueda "hacia adelante": repite la búsqueda `Latest` cada `interval` segundos (mínimo 60) durante `duration` y, desde la segunda ronda, pide solo lo publicado después del último tweet visto (`since_id:`). Con `save = TRUE` guarda lo acumulado después de cada ronda; Esc / Ctrl+C corta la captura y devuelve lo recolectado hasta ese momento: ```{r} vivo <- getTweetsLiveAPI("argentina lang:es", interval = 60, duration = 60, n_tweets = 20) #> Captura en vivo de: argentina lang:es (cada 60 s hasta las 18:26:30) #> 18:25:36 | ronda 1: 20 nuevos | total: 20 #> Ronda 2: se alcanzo el tope de 20 tweets; puede haber tweets sin recolectar. Conviene bajar `interval` o subir `n_tweets`. #> 18:26:37 | ronda 2: 20 nuevos | total: 40 #> Captura terminada: 40 tweets en 2 rondas. ``` ### 3.4 Respuestas / hilo de un tweet ```{r} hilo <- getTweetsRepliesAPI( "https://x.com/sanchezcastejon/status/2111426246517072370", n_tweets = 8 ) #> Recolectando respuestas de: https://x.com/sanchezcastejon/status/2111426246517072370 #> Tweets únicos recolectados: 8 #> Datos procesados y guardados. #> # A tibble: 8 × 31 (primeras 3 filas) #> user fecha texto #> #> 1 @sanchezcastejon 2026-10-17 11:57:00 "Feijóo es todo un personaje.\n\nDice que va..." #> 2 @Komainu_Ronin 2026-10-17 14:22:43 "@sanchezcastejon Habló, el galgo de Paiporta..." #> 3 @romanocisneros 2026-10-17 12:44:36 "@sanchezcastejon Si,sigue así chaval..." ``` La primera fila es siempre el tweet focal; el resto son sus respuestas. ### 3.5 Datos de tweets a partir de sus URLs ```{r} datos <- getTweetsDataAPI(c( "https://x.com/sanchezcastejon/status/2111426246517072370" )) #> Procesado: https://x.com/sanchezcastejon/status/2111426246517072370 #> Datos procesados y guardados. ``` ### 3.6 Media (fotos/videos) de unx usuarix ```{r} media <- getUserMediaAPI("sanchezcastejon", n_tweets = 15) #> Recolectando media de @sanchezcastejon... #> Tweets de media recolectados: 15 #> Datos procesados y guardados. ``` Las columnas `media` (URLs) y `media_tipo` (`"photo"` / `"video"`) describen cada adjunto. ### 3.7 Redes de usuarixs ```{r} seguidores <- getUserFollowersAPI("agusnieto77", n_users = 10) siguiendo <- getUserFollowingAPI("sanchezcastejon", n_users = 10) retweeters <- getTweetsRetweetsAPI( "https://x.com/sanchezcastejon/status/2111426246517072370", n_users = 10 ) ``` ### 3.8 Datos de perfil ```{r} perfiles <- getUsersDataAPI("NASA") #> Datos procesados y guardados. #> # A tibble: 1 × 18 #> user nombre user_id descripcion seguidores siguiendo tweets favoritos #> #> 1 @NASA NASA 11348282 "Making the seemi… 92315363 115 74444 17032 #> # ℹ 10 more variables: verificado , ubicacion , sitio_web , #> # imagen_perfil , banner , fecha_creacion , url , #> # tweets_media , protegida , tipo_verificacion ``` ### 3.9 "Acerca de esta cuenta" La ficha que X muestra en `x.com//about`: país desde el que opera la cuenta, cambios de nombre de usuarix y verificación. X no publica todos los campos para todas las cuentas. ```{r} about <- getUsersAboutAPI(c("NASA", "elonmusk")) #> # A tibble: 2 × 13 #> user nombre user_id pais_cuenta ubicacion_precisa cambios_usuario #> #> 1 @NASA NASA 11348282 TRUE 0 #> 2 @elonmusk Elon Musk 44196397 United States FALSE 0 #> # ℹ 7 more variables: ultimo_cambio_usuario , afiliada_a , #> # origen_cuenta , identidad_verificada , verificada_desde , #> # fecha_creacion , url ``` ### 3.10 Listas y búsqueda de cuentas ```{r} miembros <- getListMembersAPI("https://x.com/i/lists/53645372", n_users = 20) #> Recolectando miembros de la lista 53645372... #> Usuarios únicos recolectados: 20 lista <- getListTweetsAPI("53645372", n_tweets = 25) #> Recolectando tweets de la lista 53645372... #> Tweets únicos recolectados: 25 cuentas <- getUsersSearchAPI("rstats", n_users = 10) #> Buscando usuarixs: rstats #> Usuarios únicos recolectados: 10 cuentas[, c("user", "seguidores")] #> # A tibble: 10 × 2 #> user seguidores #> #> 1 @rstats 1246 #> 2 @rstatstweet 28306 #> 3 @RStatsJobs 3956 ``` Por defecto las funciones no escriben archivos (`save = FALSE`). Con `save = TRUE` guardan un `.rds` con marca de tiempo en `dir` (por defecto, el directorio de trabajo). ## 4. Visualización y análisis Las columnas tienen nombres estables, así que el post-procesamiento va directo, sin renombrar nada: ```{r} plotTime(tl) # serie temporal (columna fecha) plotWords(tl, max_words = 50) # nube de palabras (columna texto) plotEmojis(tl) # ranking de emojis (columna emoticones) getTweetsHashtags(tl) # hashtags detectados (columna hashtags) ``` Para análisis con IA (requieren `OPENAI_API_KEY`): ```{r} getTweetsSentiments(tl$texto) # sentimiento de cada texto getTweetsImagesAnalysis(tl$media) # qué muestran las imágenes ``` ## 5. Datos legacy ya guardados Si tenés `.rds` viejos con HTML crudo (columna `art_html`), `extractTweetsData()` los sigue procesando sin necesidad de volver a scrapear: ```{r} viejos <- readRDS("tweets_2024.rds") procesados <- extractTweetsData(viejos) ``` ## Uso responsable El scraping autenticado de X puede violar sus Términos de Servicio y derivar en la suspensión de la cuenta. El contenido recolectado está sujeto a los derechos de X y de sus autores. Usá cuentas dedicadas de investigación y respetá la normativa de protección de datos aplicable.