Como una de las principales plataformas de redes sociales, Twitter es el hogar de algunos de los contenidos más interesantes de Internet y tiene toneladas de datos útiles para las empresas que buscan comprender y ampliar sus mercados.
Aunque se puede acceder a estos datos de forma programática a través de la API de Twitter, su velocidad está limitada y el proceso de solicitud lleva mucho tiempo. Además, Twitter anunció recientemente el fin del acceso gratuito a la API y aumentó drásticamente sus costos, lo que hace que el método de la API sea inaccesible para un gran número de pequeñas y medianas empresas. Sin embargo, el raspado web puede ayudar a evitar estas molestias y extraer fácilmente lo que se necesita.
El raspado web es el proceso de capturar y almacenar grandes cantidades de datos de sitios y aplicaciones web con la ayuda de scripts automatizados o bots. En este artículo, se explicará cómo extraer datos de Twitter utilizando Python y Selenium, una combinación popular para el raspado web.
Raspado de Twitter con Selenium
Este tutorial tiene como finalidad, en primer lugar, explicar en qué consiste el raspado y, a continuación, mostrar cómo hacerlo paso a paso.
requisitos previos
Antes de empezar, se necesita una copia local de Python instalada en el sistema. La última distribución estable funcionará (que, en el momento de escribir este artículo, es la 3.11.2).
Una vez que Python esté instalado, será necesario instalar las siguientes dependencias a través de pip, el gestor de paquetes oficial de Python:
- Selenium
- Gestor de Webdriver
Se pueden ejecutar los siguientes comandos para instalar las dependencias:
pip install selenium
pip install webdriver_manager
Qué va a raspar
Decidir lo que se va a raspar es tan importante como implementar correctamente el script de raspado. Esto se debe a que Selenium proporcionará acceso a una página web completa de la aplicación de Twitter, que contiene una gran cantidad de datos, y todos ellos probablemente no sean útiles. Esto significa que se debe asegurar que se comprende y define claramente lo que se está buscando antes de empezar a escribir un script en Python.
Para el propósito de este tutorial, extraeremos los siguientes datos del perfil de un usuario:
- Nombre
- Nombre de usuario
- Ubicación
- Página web
- Fecha de alta
- Conteo de seguidores
- Conteo de seguidores
- Tweets
Raspado de un perfil de usuario
Para empezar a raspar una página de perfil de usuario, es necesario crear un nuevo archivo de secuencia de comandos Python llamado profile-page.py
. Se puede utilizar el siguiente comando para crearlo en sistemas *nix:
touch profile-page.py
En sistemas no *nix, se puede simplemente crear el archivo utilizando una aplicación de gestión de archivos (como el Explorador de Windows).
Configuración de Selenium
Después de crear un nuevo archivo de script Python, es necesario importar los siguientes módulos en el script:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
A continuación, se debe configurar un nuevo Selenium WebDriver (que es básicamente un navegador web automatizado que el script controlará):
driver= webdriver.Chrome(service=Service(ChromeDriverManager().install()))
Antes de cargar la página web y raspar la información, es necesario definir la URL de la página web. Como las URL de las páginas de perfil de Twitter dependen de los nombres de usuario, es necesario añadir el siguiente código al script para crear una URL de página de perfil a partir del nombre de usuario dado:
username = "bright_data"
URL = "https://twitter.com/" + username + "?lang=en"
A continuación, carga la página web:
driver.get(URL)
Espera a la carga de la página
No se puede proceder con el raspado de los datos de esta página hasta que se cargue completamente. Aunque existen algunos métodos deterministas para saber si una página HTML está completamente cargada (como comprobar document.readyState
), no es útil en el caso de una aplicación de una sola página (SPA) como Twitter. En este caso, es necesario esperar a que se completen las llamadas a la API del lado del cliente y a que los datos se muestren en la página web antes de poder rasparlos.
Para ello, es preciso añadir el siguiente fragmento de código al script:
try:
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, '[data-testid="tweetss"]')))
except WebDriverException:
print("Tweets did not appear! Proceeding after timeout")
Este código hará que el controlador web espere a que un elemento con un atributo data-testid="tweet"
se cargue en la página web antes de seguir adelante. La razón para elegir este elemento y atributo en particular es que este atributo sólo estará presente en los tweets bajo un perfil, y si los tweets se cargan, indica que el resto de la página también se ha cargado:
Nota: Es necesario tener cuidado al decidir cómo marcar la página como cargada. El fragmento de código anterior funcionaría para un perfil público con al menos un tweet. Sin embargo, fallará en todos los demás casos y se lanzará una
WebDriverException
. En tales casos, el script continuará después de esperar el tiempo de espera dado (que, en este caso, es de diez segundos).
Extracción de información
A esta altura, se puede comenzar con la parte más importante del tutorial: la extracción de información. Sin embargo, para extraer datos de la página web cargada, es necesario aprender la estructura de la página web que está raspando:
Nombre
Si se abre Chrome DevTools y se localiza el código fuente para el elemento name en la página, se debería ver algo como esto:
<span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">Bright Data</span>
El elemento name está envuelto en una etiqueta span
y tiene asignadas varias clases (obviamente) generadas aleatoriamente. Esto significa que no se puede confiar en estos nombres de clase para identificar la etiqueta contenedora del elemento nombre del usuario en la página de perfil. Será necesario buscar algo estático.
Si se asciende por la jerarquía en la fuente HTML para el elemento name, se encontrará una etiqueta div
que contiene tanto el nombre como el nombre de usuario (debajo de varias capas de span
s). La etiqueta de inicio para el contenedor div
tendrá este aspecto:
<div class="css-1dbjc4n r-6gpygo r-14gqq1x" data-testid="UserName">
Mientras que hay nombres de clase generados aleatoriamente, también existe otro atributo llamado data-testid
.data-testid
es un atributo HTML que se utiliza principalmente en pruebas de interfaz de usuario para identificar y localizar elementos HTML para ejecutar pruebas automatizadas. Se puede utilizar este atributo para seleccionar el contenedor div
que contiene el nombre del usuario. Sin embargo, también contiene el nombre de usuario (es decir, la cuenta de Twitter). Esto significa que será necesario dividir el texto en los saltos de línea y, a continuación, extraer el primer elemento (que es el nombre del usuario):
name = driver.find_element(By.CSS_SELECTOR,'div[data-testid="UserName"]').text.split('\n')[0]
Biografía, ubicación, sitio web y fecha de alta
De la misma manera en que identificó el selector correcto para el elemento nombre, necesita encontrar los selectores correctos para los otros puntos de datos. Notará que los elementos biografía, ubicación, sitio web y fecha de incorporación tienen data-testid
s adjuntos. Esto facilita la escritura de selectores CSS para encontrar los elementos y extraer sus datos:
bio = driver.find_element(By.CSS_SELECTOR,'div[data-testid="UserDescription"]').text
location = driver.find_element(By.CSS_SELECTOR,'span[data-testid="UserLocation"]').text
website = driver.find_element(By.CSS_SELECTOR,'a[data-testid="UserUrl"]').text
join_date = driver.find_element(By.CSS_SELECTOR,'span[data-testid="UserJoinDate"]').text
Seguidores y número de seguidores
Cuando se observan los recuentos de seguidores y seguidos, se aprecia que no tienen data-testid
s asociados, lo que significa que hay que idear algo creativo para identificarlos y seleccionarlos correctamente.
Subir en la jerarquía no ayuda, ya que ninguno de sus padres cercanos tiene valores de atributo estáticos asociados. En este caso, es necesario recurrir a XPath.
XPath significa XML Path Language y es un lenguaje utilizado para señalar (o crear referencias a) etiquetas en documentos XML. Puede escribir un selector usando XPath que busque un contenedor span
con el texto 'Following'
y luego suba un nivel en su jerarquía para localizar el recuento (ya que el texto 'Following'
y el valor del recuento están ambos envueltos en etiquetas contenedor-individuales):
following_count = driver.find_element(By.XPATH, "//span[contains(text(), 'Following')]/ancestor::a/span").text
Del mismo modo, también puede escribir un selector basado en XPath para el recuento de seguidores:
followers_count = driver.find_element(By.XPATH, "//span[contains(text(), 'Followers')]/ancestor::a/span").text
Tweets
Afortunadamente, cada tweet tiene un contenedor padre con un “tweet” con valor data-testid
(que se usó antes para comprobar si los tweets se habían cargado). Se puede utilizar el método find_elements()
en lugar del método find_element()
de Selenium para recoger todos los elementos que satisfacen el selector dado:
tweets = driver.find_elements(By.CSS_SELECTOR, '[data-testid="tweet"]')
Imprimiendo todo
Para imprimir todo lo que ha extraído en stdout
, utilice el siguiente código:
print("Name\t\t: " + name)
print("Bio\t\t: " + bio)
print("Location\t: " + location)
print("Website\t\t: " + website)
print("Joined on\t: " + join_date)
print("Following count\t: " + following_count)
print("Followers count\t: " + followers_count)
Para imprimir el contenido de los tweets, es necesario recorrer todos los tweets y extraer el texto del interior del contenedor de texto del tweet (un tweet tiene otros elementos, como avatar, nombre de usuario, hora y botones de acción, aparte del contenido principal). A continuación, se explica cómo utilizar un selector CSS para hacerlo:
for tweet in tweets:
tweet_text = tweet.find_element(By.CSS_SELECTOR,'div[data-testid="tweetText"]').text
print("Tweet text\t: " + tweet_text)
Ejecute el script con el siguiente comando:
python profile-page.py
Debería recibir una salida como esta:
Name : Bright Data
Bio : The World's #1 Web Data Platform
Location : We're everywhere!
Website : brdta.com/2VQYSWC
Joined on : Joined February 2016
Following count : 980
Followers count : 3,769
Tweet text : Happy BOO-RIM! Our offices transformed into a spooky "Bright Fright" wonderland today. The treats were to die for and the atmosphere was frightfully fun...
Check out these bone-chilling sights:
Tweet text : Our Bright Champions are honored each month, and today we are happy to present February's! Thank you for your exceptional work.
Sagi Tsaeiri (Junior BI Developer)
Or Dinoor (Compliance Manager)
Sergey Popov (R&D DevOps)
Tweet text : Omri Orgad, Chief Customer Officer at
@bright_data
, explores the benefits of outsourcing public web data collections for businesses using AI tools.
#WebData #ArtificialIntelligence
Click the link below to find out more
.
.
.
<output truncated>
Este es el código completo del script de raspado:
# import the required packages and libraries
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
# set up a new Selenium driver
driver= webdriver.Chrome(service=Service(ChromeDriverManager().install()))
# define the username of the profile to scrape and generate its URL
username = "bright_data"
URL = "https://twitter.com/" + username + "?lang=en"
# load the URL in the Selenium driver
driver.get(URL)
# wait for the webpage to be loaded
# PS: this considers a profile page to be loaded when at least one tweet has been loaded
# it might not work well for restricted profiles or public profiles with zero tweets
try:
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, '[data-testid="tweet"]')))
except WebDriverException:
print("Tweets did not appear! Proceeding after timeout")
# extract the information using either CSS selectors (and data-testid) or XPath
name = driver.find_element(By.CSS_SELECTOR,'div[data-testid="UserName"]').text.split('\n')[0]
bio = driver.find_element(By.CSS_SELECTOR,'div[data-testid="UserDescription"]').text
location = driver.find_element(By.CSS_SELECTOR,'span[data-testid="UserLocation"]').text
website = driver.find_element(By.CSS_SELECTOR,'a[data-testid="UserUrl"]').text
join_date = driver.find_element(By.CSS_SELECTOR,'span[data-testid="UserJoinDate"]').text
following_count = driver.find_element(By.XPATH, "//span[contains(text(), 'Following')]/ancestor::a/span").text
followers_count = driver.find_element(By.XPATH, "//span[contains(text(), 'Followers')]/ancestor::a/span").text
tweets = driver.find_elements(By.CSS_SELECTOR, '[data-testid="tweet"]')
# print the collected information
print("Name\t\t: " + name)
print("Bio\t\t: " + bio)
print("Location\t: " + location)
print("Website\t\t: " + website)
print("Joined on\t: " + join_date)
print("Following count\t: " + following_count)
print("Followers count\t: " + followers_count)
# print each collected tweet's text
for tweet in tweets:
tweet_text = tweet.find_element(By.CSS_SELECTOR,'div[data-testid="tweetText"]').text
print("Tweet text\t: " + tweet_text)
Raspado de Twitter con Bright Data
Aunque el raspado web ofrece una gran flexibilidad y control sobre la forma de extraer datos de páginas web, a veces puede ser difícil de configurar. En los casos en los que la aplicación web de destino carga la mayoría de sus datos de página a través de llamadas XHR después de cargar la página estática y hay muy pocos identificadores estáticos en HTML para localizar elementos (similar a lo que ha visto anteriormente en el caso de Twitter), puede ser complicado averiguar la configuración correcta.
En estos casos, Bright Data puede ayudar. Bright Data es una plataforma de datos web que puede ayudar a extraer enormes cantidades de datos no estructurados de Internet. Bright Data ofrece un producto para el raspado de datos de Twitter que es capaz de ayudarle a obtener una colección detallada de casi todos los puntos de datos posibles de las páginas web de Twitter.
Por ejemplo, a continuación se presentan las instrucciones para raspar el mismo perfil de usuario de Twitter utilizando Bright Data.
Comience por navegar al Panel de Control de Bright Data. Haga clic en el botón Ver productos de datos para ver las soluciones de raspado web que ofrece Bright Data:
A continuación, haga clic en Comenzar en la tarjeta del IDE de Web Scraper:
Bright Data le proporciona un IDE de Web Scraper que puede utilizar para crear su propio raspador desde cero o a partir de una plantilla base. Bright Data también ofrece una infraestructura de autoescalado y herramientas de depuración integradas para ayudar a empezar rápidamente.
Se solicitará que genere un raspador desde cero o que utilice una plantilla existente. Si desea empezar rápidamente, consulte la plantilla de búsqueda de hashtags de Twitter (que es la que utilizará aquí para configurar el IDE inicial). Haga clic en la opción de búsqueda de hashtags de Twitter:
Se debería poder ver el IDE completo en la pantalla con algo de código ya añadido al editor para poder empezar. Para utilizar este IDE en el raspado de páginas de perfil de Twitter, se elimina el código existente en el editor y se pega el siguiente código en él:
const start_time = new Date().getTime();
block(['*.png*', '*.jpg*', '*.mp4*', '*.mp3*', '*.svg*', '*.webp*', '*.woff*']);
// Set US ip address
country('us');
// Save the response data from a browser request
tag_response('profile', /\/UserTweets/)
// Store the website's URL here
let url = new URL('https://twitter.com/' + input["Username"]);
// function initialization
async function navigate_with_wait() {
navigate(url, { wait_until: 'domcontentloaded' });
try {
wait_network_idle({ ignore: [/accounts.google.com/, /twitter.com\/sw.js/, /twitter.com\/i\/jot/] })
} catch (e) { }
}
// calling navigate_with_wait function
navigate_with_wait()
// sometimes page does not load. If the "Try again" button exists in such case, try to click it and wait for results
let try_count = 0
while (el_exists('[value="Try again"]') && try_count++ <= 5) {
// wait_page_idle(4000)
if (el_exists('[value="Try again"]')) {
try { click('[value="Try again"]', { timeout: 1e3 }) } catch (e) { }
} else {
if (location.href.includes(url)) break
else navigate_2()
}
if (el_exists('[data-testid="empty_state_header_text"]')) navigate_2()
}
const gatherProfileInformation = (profile) => {
// Extract tweet-related information
let tweets = profile.data.user.result.timeline_v2.timeline.instructions[1].entries.flatMap(entry => {
if (!entry.content.itemContent)
return [];
let tweet = entry.content.itemContent.tweet_results.result
return {
"text": tweet.legacy.full_text,
"time": tweet.legacy.created_at,
"id": tweet.legacy.id_str,
"replies": tweet.legacy.reply_count,
"retweets": tweet.legacy.retweet_count,
"likes": tweet.legacy.favorite_count,
"hashtags": tweet.legacy.entities?.hashtags.toString(),
"tagged_users": tweet.legacy.entities?.user_mentions.toString(),
"isRetweeted": tweet.legacy.retweeted,
"views": tweet.views.count
}
})
// Extract profile information from first tweet
let profileDetails = profile.data.user.result.timeline_v2.timeline.instructions[1].entries[0].content.itemContent.tweet_results.result.core.user_results.result;
// Prepare the final object to be collected
let profileData = {
"profile_name": profileDetails.legacy.name,
"isVerified": profileDetails.legacy.verified, // Might need to swap with profileDetails.isBlueVerified
"bio": profileDetails.legacy.description,
"location": profileDetails.legacy.location,
"following": profileDetails.legacy.friends_count,
"followers": profileDetails.legacy.followers_count,
"website_url": profileDetails.legacy.entities?.url.urls[0].display_url || "",
"posts": profileDetails.legacy.statuses_count,
"media_count": profileDetails.legacy.media_count,
"profile_background_image_url": profileDetails.legacy.profile_image_url_https,
"handle": profileDetails.legacy.screen_name,
"collected_number_of_posts": tweets.length,
"posts_info": tweets
}
// Collect the data in the IDE
collect(profileData)
return null;
}
try {
if (el_is_visible('[data-testid="app-bar-close"]')) {
click('[data-testid="app-bar-close"]');
wait_hidden('[data-testid="app-bar-close"]');
}
// Scroll to the bottom of the page for all tweets to load
scroll_to('bottom');
// Parse the webpage data
const { profile } = parse();
// Collect profile information from the page
gatherProfileInformation(profile)
} catch (e) {
console.error(`Interaction warning (1 stage): ${e.message}`);
}
Hay comentarios en línea en el código anterior para ayudarle a entender lo que está sucediendo. La estructura básica es la siguiente
- Ir a la página del perfil
- Esperar a que se cargue la página
- Interceptar la respuesta de la API /
UserTweets
/ - Analizar la respuesta y extraer la información
Se deben eliminar los parámetros de entrada existentes y añadir un único parámetro de entrada “Nombre de usuario” en la sección de entrada de la parte inferior de la página. A continuación, es necesario proporcionarle un valor de entrada “bright_data“, por ejemplo. A continuación, se ejecuta el código haciendo clic en el botón de vista previa:
Los resultados tendrán este aspecto:
Aquí está la respuesta JSON detallada para referencia:
{
"profile_name": "Bright Data",
"isVerified": false,
"bio": "The World's #1 Web Data Platform",
"location": "We're everywhere!",
"following": 981,
"followers": 3970,
"website_url": "brdta.com/2VQYSWC",
"posts": 1749,
"media_count": 848,
"profile_background_image_url": "https://pbs.twimg.com/profile_images/1372153221146411008/U_ua34Q5_normal.jpg",
"handle": "bright_data",
"collected_number_of_posts": 40,
"posts_info": [
{
"text": "This week we will sponsor and attend @neudatalab's London Data Summit 2023. @omri_orgad, our CCO, will also participate in a panel discussion on the impact of artificial intelligence on the financial services industry. \nWe look forward to seeing you there! \n#ai #financialservices https://t.co/YtVOK4NuKY",
"time": "Mon Mar 27 14:31:22 +0000 2023",
"id": "1640360870143315969",
"replies": 0,
"retweets": 1,
"likes": 2,
"hashtags": "[object Object],[object Object]",
"tagged_users": "[object Object],[object Object]",
"isRetweeted": false,
"views": "386"
},
{
"text": "Is our Web Unlocker capable of bypassing multiple anti-bot solutions? That's the question that @webscrapingclub sought to answer! \nIn their latest blog post, they share their hands-on, step-by-step challenge and their conclusions.\nRead here: https://t.co/VwxcxGMLWm",
"time": "Thu Mar 23 11:35:32 +0000 2023",
"id": "1638867069587566593",
"replies": 0,
"retweets": 2,
"likes": 3,
"hashtags": "",
"tagged_users": "[object Object]",
"isRetweeted": false,
"views": "404"
},
]
}
Además de las capacidades de raspado web, Bright Data ofrece conjuntos de datos de redes sociales que contienen información altamente enriquecida basada en datos recopilados de sitios web de redes sociales como Twitter. Se pueden utilizar para obtener más información sobre el público objetivo, detectar tendencias, identificar a las personas influyentes y mucho más.
Conclusión
En este artículo, hemos mostrado cómo extraer información de Twitter utilizando Selenium. Si bien es posible extraer datos de esta manera, no es lo ideal, ya que puede ser complicado y llevar mucho tiempo. Es por eso que también hemos explicado cómo utilizar Bright Data, que es una solución más sencilla para el raspado de datos de Twitter.