Будучи одной из самых популярных социальных сетей, Twitter может предложить не только обилие интересного контента, но и массу полезной информации для бизнеса, ставящего цель выявить основные тенденции рынка, чтобы расширить на нем свое присутствие.
Хотя вы можете получить доступ к этим данным программным способом с помощью API «Твиттера», он ограничен по скорости, а сам процесс подачи заявки занимает много времени. Более того, не так давно Twitter не только отменил бесплатный доступ к API, но и значительно увеличил расценки на него. Для многих средних и небольших компаний это нововведение по сути это перекрыло доступ к данному методу получения информации. Однако веб-скрапинг может помочь вам решить эту проблему и легко извлечь любые необходимые вам данные.
Веб-скрапинг представляет собой процесс сбора и дальнейшего хранения больших объемов данных с сайтов и веб-приложений, выполняемый с помощью автоматизированных скриптов или ботов. В этой статье вы узнаете, как выполнить веб-скрапинг определенной информации из Twitter, используя Python и Selenium — довольно популярную комбинацию для реализации задач подобного рода.
Веб-скрапинг данных из Twitter с помощью Selenium
В этом руководстве мы сначала объясним, что нужно скрапить, а затем покажем, шаг за шагом, как именно это делать.
Предварительные условия
Но, прежде чем начать, вам понадобится установить на свою операционную систему Python. Подойдет последний стабильный дистрибутив (на момент написания статьи это версия 3.11.2).
Успешно справившись с Python, далее приступайте к установке приведенных ниже зависимости с помощью pip, официального менеджера пакетов Python:
- Selenium
- Webdriver Manager
Вам нужно выполнить следующие команды для установки зависимостей:
pip install selenium
pip install webdriver_manager
Что вы будете скрапить
Решение о том, что именно скрапить, не менее важно, чем правильная реализация самого сценария веб-скрапинга. Это связано с тем, что Selenium предоставит вам доступ ко всем данным, которые присутствуют на той или иной странице Twitter. А она содержит огромное количество данных, не все из которых, скорее всего, вам нужны. Это значит, что следует четко определиться с тем, что именно вы хотите найти, прежде чем начать писать сценарий на Python.
В рамках данного руководства вы извлечете следующие данные из профиля пользователя:
- Имя
- Никнейм
- Местонахождение
- Сайт
- Дата регистрации
- Количество читаемых пользователей
- Количество подписчиков
- Твиты
Скрапинг профиля пользователя
Чтобы выполнить веб-скрапинг страницы профиля пользователя, для начала необходимо создать новый файл Python с именем profile-page.py
. В системах *nix для его создания можно использовать следующую команду:
touch profile-page.py
Если же у вас не *nix-система, можете просто создать файл с помощью приложения файлового менеджера (например, Windows Explorer).
Настройка Selenium
После создания нового файла Python вам понадобится импортировать следующие модули:
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
Затем вам нужно настроить новый Selenium WebDriver (который, по сути, является автоматизированным веб-браузером, которым будет управлять ваш скрипт):
driver= webdriver.Chrome(service=Service(ChromeDriverManager().install()))
Прежде чем загружать страницу и приступать к веб-скрапингу, необходимо определить ее URL. Поскольку в адресе профиля в Twitter используется имя пользователя, вам необходимо добавить следующий код в ваш сценарий. Это позволит создать URL страницы профиля на основе заданного имени пользователя:
username = "bright_data"
URL = "https://twitter.com/" + username + "?lang=en"
Затем загрузите страницу:
driver.get(URL)
Ожидание загрузки страницы
Вы не сможете приступить к веб-скрапингу данных со страницы, пока она полностью не загрузится. Хотя существует несколько детерминированных методов определения полной загрузки HTML-страницы (например, проверка document.readyState
), они не пригодятся в случае такого одностраничного приложения (SPA) как Twitter. В этой ситуации, чтобы приступить к веб-скрапингу, необходимо дождаться завершения вызовов API на стороне клиента и отображения данных на странице.
Этот код заставит веб-драйвер подождать, пока элемент с атрибутом data-testid=”tweet” не будет загружен, прежде чем двигаться дальше. Причина выбора именно этого элемента и атрибута заключается в том, что он присутствует только в твитах под профилем, и если они загружены, значит, остальная часть страницы тоже загружена:
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")
Этот код заставит веб-драйвер подождать, пока элемент с атрибутом data-testid="tweet"
не будет загружен, прежде чем двигаться дальше. Причина выбора именно этого элемента и атрибута заключается в том, что он присутствует только в твитах под профилем, и если они загружены, значит, остальная часть страницы тоже загружена:
Важный момент: внимательно выбирайте способ пометить страницу как загруженную. Предыдущий фрагмент кода будет работать для публичного профиля с хотя бы одним твитом. Однако во всех остальных случаях он не сработает и будет выброшена ошибка
WebDriverException
. В таких случаях сценарий продолжит работу после ожидания заданного тайм-аута (который в данном случае составляет десять секунд).
Извлечение информации
Теперь вы готовы к самой важной части пошагового руководства — извлечению информации. Однако, чтобы выполнить веб-скрапинг данных из загруженной страницы, вам необходимо внимательно изучить ее структуру:
Имя
Если вы откроете Chrome DevTools и найдете исходный код элемента name на странице, вы должны увидеть что-то вроде этого:
<span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">Bright Data</span>
Элемент name обернут в тег span
и ему присвоен целый ряд очевидно случайно сгенерированных классов. Это означает, что вы не можете полагаться на эти имена классов, чтобы определить тег контейнера для элемента имени пользователя на странице профиля. Вам придется поискать что-нибудь более статичное.
Если подняться немного вверх по иерархии HTML-кода для элемента name, можно найти тег div
, который содержит и имя, и фамилию пользователя (под несколькими слоями тегов span
). Начальный тег для контейнера div
будет выглядеть следующим образом:
<div class="css-1dbjc4n r-6gpygo r-14gqq1x" data-testid="UserName">
Хотя имена классов генерируются случайным образом, у него также есть еще один HTML-атрибут под названием data-testid
. Он используется в основном при тестировании пользовательского интерфейса для идентификации и определения местоположения различных HTML-элементов в рамках автоматизированных тестов. Вы можете использовать этот атрибут для выбора контейнера div
, который содержит имя пользователя. Однако он также содержит никнейм. Это значит, что вам нужно разделить текст в месте разрыва строки, а затем извлечь первый элемент (который и является именем пользователя):
name = driver.find_element(By.CSS_SELECTOR,'div[data-testid="UserName"]').text.split('\n')[0]
Bio, Location, Website, и Join Date
Точно так же, как вы определили правильный селектор для элемента имени, вам нужно найти правильные селекторы для других точек данных. Вы заметите, что элементы bio, location, website и join date имеют прикрепленные к ним data-testids
. Это позволяет легко написать CSS-селекторы для поиска элементов и извлечения их данных:
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
Количество подписчиков и читаемых пользователей
Когда вы посмотрите на количество последователей и подписчиков, вы заметите, что к ним не прикреплены данные-тесты, а значит, вам придется придумать что-то креативное, чтобы правильно их определить и выбрать.
Подъем вверх по HTML-иерархии не поможет, поскольку ни один из их близких родителей не имеет статических значений привязанных к ним атрибутов. В этом случае необходимо обратиться к XPath.
XPath расшифровывается как XML Path Language — специальный язык, используемый для указания (или создания ссылок) на теги в XML-документах. С помощью XPath можно написать селектор, который ищет контейнер span
с текстом 'Following'
, а затем поднимается на один уровень в его иерархии, чтобы найти значение count (поскольку текст 'Following'
и значение count обернуты в отдельные теги контейнера):
following_count = driver.find_element(By.XPATH, "//span[contains(text(), 'Following')]/ancestor::a/span").text
Аналогично можно написать селектор на основе XPath для количества подписчиков:
followers_count = driver.find_element(By.XPATH, "//span[contains(text(), 'Followers')]/ancestor::a/span").text
Твиты
К счастью, каждый твит имеет родительский контейнер со значением “tweet” для атрибута data-testid
(который вы использовали ранее для проверки загрузки твитов). Вы можете использовать метод find_elements()
вместо метода find_element()
из Selenium, чтобы собрать все элементы, удовлетворяющие заданному селектору:
tweets = driver.find_elements(By.CSS_SELECTOR, '[data-testid="tweet"]')
Распечатка всего
Чтобы вывести все извлеченное на экран (stdout
), используйте следующий код:
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)
Чтобы распечатать содержимое твитов, необходимо просмотреть их и извлечь текст из контейнера каждого сообщения (помимо него, в твите есть и другие элементы — такие, как аватар, никнейм, время и кнопки действий). Вот как можно использовать для этого селектор CSS:
for tweet in tweets:
tweet_text = tweet.find_element(By.CSS_SELECTOR,'div[data-testid="tweetText"]').text
print("Tweet text\t: " + tweet_text)
Запустите скрипт с помощью следующей команды:
python profile-page.py
И вы должны получить результат, подобный этому:
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>
Вот полный код скрипта для веб-скрапинга:
# 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)
Скрапинг «Твиттера» с помощью Bright Data
Хотя веб-скрапинг предоставляет большую гибкость и контроль над тем, как вы извлекаете данные со страниц, иногда он трудоемок на стадии настройки скрипта. В тех случаях, когда целевое веб-приложение загружает большую часть данных страницы через вызовы XHR после загрузки статической страницы, может быть сложно определить правильную конфигурацию. Это связано с тем, что в HTML довольно мало статических идентификаторов для определения местоположения элементов, как мы в этом уже убедились ранее в примере с Twitter.
В таких случаях на помощь может прийти компания Bright Data. Bright Data — это онлайн-платформа, которая оказывает профессиональную помощь в извлечении неструктурированных данных из интернета. В том числе, Bright Data предлагает и качественный инструмент для веб-скрапинга «Твиттера», который позволяет получить подробную коллекцию практически всех возможных точек данных на страницах Twitter.
Например, ниже приведена инструкция, как выполнить веб-скрапинг того же профиля пользователя в Twitter с помощью инструмента Bright Data.
Начните с перехода в панель управления Bright Data. Нажмите на кнопку Просмотр продуктов данных, чтобы просмотреть решения для веб-скрейпинга, предлагаемые Bright Data:
Далее нажмите кнопку Get started в Web Scraper IDE:
Bright Data предоставляет IDE Web Scraper, который можно использовать для создания собственного веб-скрапера с нуля или на основе готового базового шаблона. Bright Data также предлагает инфраструктуру для автоматического масштабирования и встроенные инструменты отладки, чтобы помочь вам как можно быстрее приступить к работе.
Вам будет предложено либо создать скрапер с нуля, либо использовать уже существующий шаблон. Если хотите побыстрее приступить к работе, ознакомьтесь с шаблоном поиска по хэштегам «Твиттера» (именно его вы будете использовать здесь для первоначальной настройки IDE). Нажмите на опцию поиска по хэштегам Twitter:
Вы должны увидеть на экране IDE с некоторым кодом, уже добавленным в редактор. Чтобы использовать IDE для сканирования страниц профиля Twitter, замените скрипт на следующий:
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}`);
}
В коде присутствуют встроенные комментарии, которые упрощают его понимание. Основная структура выглядит следующим образом:
- Перейдите на страницу профиля
- Подождите, пока страница загрузится
- Перехватите ответ от API
/UserTweets/
- Спарсите ответ и извлеките информацию
Вам нужно заменить предустановленные входные параметры на «Имя пользователя» в разделе ввода в нижней части страницы. Затем нужно будет указать ему входное значение, например, “bright_data“. После этого запустите код, нажав на кнопку предварительного просмотра:
Результат будет выглядеть следующим образом:
Вот подробный ответ в формате JSON:
{
"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"
},
]
}
В дополнение к возможностям веб-скрапинга Bright Data предлагает наборы данных социальных сетей, которые содержат качественную информацию, основанную на данных, собранных на сайтах таких социальных сетей, как Twitter. Вы можете использовать их, чтобы узнать больше о своей целевой аудитории, определить главные тренды, выявить главных инфлюенсеров и многое другое!
Заключение
Из этой статьи вы узнали, как получить информацию из Twitter с помощью Selenium. Хотя этот способ подходит для сбора данных, он далеко не идеален, поскольку он может быть достаточно сложным и отнимать довольно много времени. Поэтому, как вы также узнали, лучше воспользоваться инструментом для веб-скрапинга от Bright Data, который является гораздо более простым решением для парсинга данных из Twitter.