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 familiagetUrls*(), etc.) fueron eliminadas; el mapa de reemplazos está en elNEWS.md.
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):
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:
https://x.com.auth_token y
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.
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:
# 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:
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.
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.
# 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*API())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):
dplyr::glimpse(tl)
#> Rows: 10
#> Columns: 31
#> $ fecha <dttm> 2026-10-20 18:00:27, 2026-10-20 18:00:06, 2026-10-20~
#> $ user <chr> "@NASA", "@NASA", "@NASA", "@NASA", "@NASA", "@NASA",~
#> $ texto <chr> "LIVE: Get a preview of two upcoming spacewalks set f~
#> $ idioma <chr> "en", "en", "en", "en", "en", "en", "en", "en", "en",~
#> $ respuestas <int> 41, 20, 151, 11, 83, 189, 169, 81, 88, 92
#> $ retweets <int> 79, 75, 305, 52, 120, 249, 264, 168, 172, 243
#> $ citas <int> 5, 8, 43, 3, 14, 25, 19, 10, 18, 10
#> $ megustas <int> 497, 511, 1949, 322, 1034, 1652, 2026, 1165, 1100, 21~
#> $ views <int> 227105, 224886, 286788, 90650, 405674, 404857, 100438~
#> $ emoticones <list> <>, <>, <>, <>, <"🇺", "🇿">, "✈", <>, <>, <>, <>
#> $ hashtags <list> <>, <>, <>, <>, <>, <>, <>, <>, <>, <>
#> $ menciones <list> "astro_anil", <>, "NASAMoonBase", <"NASAMoonBase", "N~
#> $ urls_externas <list> "https://x.com/i/broadcasts/1jxXgBNWypPJZ", "https://~
#> $ media <list> <>, <>, "https://video.twimg.com/amplify_video/211256~
#> $ media_tipo <list> <>, <>, "video", <>, "photo", "video", "photo", "pho~
#> $ es_retweet <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL~
#> $ es_cita <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL~
#> $ tweet_citado_id <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA
#> $ conversation_id <chr> "2112604875452223824", "2112604787539865876", "21125~
#> $ url <chr> "https://x.com/NASA/status/2112604875452223824", "ht~
#> $ tweet_id <chr> "2112604875452223824", "2112604787539865876", "21125~
#> $ user_id <chr> "11348282", "11348282", "11348282", "11348282", "1134~
#> $ respuesta_a_id <chr> NA, NA, NA, "2112560723603632217", NA, NA, NA, NA, NA~
#> $ respuesta_a_user <chr> NA, NA, NA, "@NASA", NA, NA, NA, NA, NA, NA
#> $ retweet_id <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA
#> $ guardados <int> 28, 38, 122, 25, 32, 67, 112, 50, 48, 131
#> $ fuente <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA
#> $ sensible <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS~
#> $ editado <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS~
#> $ lugar <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA
#> $ pais <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NALos tweets largos (más de 280 caracteres) vienen completos en
texto.
Latest / Top /
Media) con rango de fechasLa 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 recorre todos los operadores con ejemplos verificados,
incluidas las franjas horarias (since = "2026-10-20 12:00",
en UTC):
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
#> <chr> <dttm> <int> <int>
#> 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: 20getTweetsLiveAPI() 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:
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.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
#> <chr> <dttm> <chr>
#> 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.
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.
perfiles <- getUsersDataAPI("NASA")
#> Datos procesados y guardados.
#> # A tibble: 1 × 18
#> user nombre user_id descripcion seguidores siguiendo tweets favoritos
#> <chr> <chr> <chr> <chr> <int> <int> <int> <int>
#> 1 @NASA NASA 11348282 "Making the seemi… 92315363 115 74444 17032
#> # ℹ 10 more variables: verificado <lgl>, ubicacion <chr>, sitio_web <chr>,
#> # imagen_perfil <chr>, banner <chr>, fecha_creacion <dttm>, url <chr>,
#> # tweets_media <int>, protegida <lgl>, tipo_verificacion <chr>La ficha que X muestra en x.com/<cuenta>/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.
about <- getUsersAboutAPI(c("NASA", "elonmusk"))
#> # A tibble: 2 × 13
#> user nombre user_id pais_cuenta ubicacion_precisa cambios_usuario
#> <chr> <chr> <chr> <chr> <lgl> <int>
#> 1 @NASA NASA 11348282 <NA> TRUE 0
#> 2 @elonmusk Elon Musk 44196397 United States FALSE 0
#> # ℹ 7 more variables: ultimo_cambio_usuario <dttm>, afiliada_a <chr>,
#> # origen_cuenta <chr>, identidad_verificada <lgl>, verificada_desde <dttm>,
#> # fecha_creacion <dttm>, url <chr>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
#> <chr> <int>
#> 1 @rstats 1246
#> 2 @rstatstweet 28306
#> 3 @RStatsJobs 3956Por 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).
Las columnas tienen nombres estables, así que el post-procesamiento va directo, sin renombrar nada:
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):
Si tenés .rds viejos con HTML crudo (columna
art_html), extractTweetsData() los sigue
procesando sin necesidad de volver a scrapear:
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.