主要なソーシャルメディアプラットフォームであるTwitterには、インターネット上で最も興味深いコンテンツがあり、市場を理解して拡大したいと考えている企業にとって有用なデータが大量にあります。
このデータにはTwitter APIを介してプログラム的にアクセスすることができますが、レートが制限されており、申請手続きには時間がかかります。さらに、Twitterは最近、無料APIアクセスの終了を発表し、APIコストを大幅に引き上げたため、多くの中小企業にとってAPI方式は利用できなくなっています。しかし、ウェブスクレイピングを使えば、こうした煩わしさを回避し、必要なものを簡単に抽出できます。
ウェブスクレイピングとは、自動化されたスクリプトやボットの助けを借りて、ウェブサイトやウェブアプリから大量のデータを取得し、保存するプロセスのことです。この記事では、ウェブスクレイピングで人気の組み合わせであるPythonとSeleniumを使って、Twitterデータをスクレイピングする方法を学びます。
Seleniumを使ってTwitterをスクレイピングする
このチュートリアルでは、まず何をスクレイピングするかを説明し、次にその方法をステップバイステップで紹介します。
前提条件
このチュートリアルでは、まず何をスクレイピングするかを説明し、次にその方法をステップバイステップで紹介します。
Pythonをインストールしたら、Pythonの公式パッケージマネージャであるpipを使って次の依存関係をインストールする必要があります。
- Selenium
- Webdriver Manager
以下のコマンドを実行して、依存関係をインストールできます。
pip install selenium
pip install webdriver_manager
何をスクレイピングするか
何をスクレイピングするかを決めることは、スクレイピングスクリプトを正しく実装することに劣らず重要です。これは、Seleniumを使用すると、Twitterアプリの完全なウェブページへのアクセスが提供されるためです。これには大量のデータが含まれており、おそらくそのすべてが役に立たないからです。つまり、Pythonスクリプトを書き始める前に、自分が求めているものを明確に理解し、定義しておく必要があります。
このチュートリアルでは、ユーザープロフィールから以下のデータを抽出します。
- 名前
- ユーザ名
- 所在地
- ウェブサイト
- 参加日
- フォロー数
- フォロワー数
- ツイート
ユーザープロフィールのスクレイピング
ユーザープロフィールページのスクレイピングを開始するには、profile-page.py
という名前の新しいPythonスクリプトファイルを作成する必要があります。*nixシステムでは、次のコマンドで作成できます。
touch profile-page.py
非*nixシステムでは、ファイルマネージャーアプリケーション(Windowsエクスプローラーなど)を使用して、ファイルを簡単に作成できます。
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はユーザー名に依存しているため、以下のコードをスクリプトに追加して、指定したユーザー名からプロフィールページのURLを作成する必要があります。
username = "bright_data"
URL = "https://twitter.com/" + username + "?lang=en"
次に、ウェブページを読み込みます。
driver.get(URL)
ページの読み込みを待機する
このページが完全に読み込まれるまで、データのスクレイピングを続行することはできません。HTMLページが完全に読み込まれたかどうかを知るための決定的な方法はいくつかありますが(document.readyState
をチェックするなど)、Twitterのようなシングルページアプリケーション(SPA)の場合、それは役に立ちません。この場合、クライアント側のAPI呼び出しが完了し、データがウェブページにレンダリングされるのを待ってからスクレイピングする必要があります。
そのためには、スクリプトに以下のコードを追加する必要があります。
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"
属性を持つ要素がウェブページにロードされるまで、ウェブドライバに待機させてから先に進みます。この特定の要素と属性を選んだ理由は、この属性がプロフィール下のツイートにのみ存在し、ツイートが読み込まれると、ページの他の部分も読み込まれたことを示すためです。
注記: ページが読み込み済みであることを示すマークを決定するときには、注意が必要です。前のコードスニペットは、少なくとも1つのツイートを含む公開プロフィールに対して機能します。ただし、それ以外の場合はすべて失敗し、
WebDriverException
がスローされます。このような場合、スクリプトは指定されたタイムアウト時間(この場合は10秒)待った後で続行されます。
情報を抽出する
これで、このチュートリアルの最も重要な部分である「情報を抽出する」準備が整いました。ただし、読み込んだウェブページからデータを抽出するためには、スクレイピングするウェブページの構造を学ぶ必要があります。
名前
Chrome DevToolsを開いて、ページ内の名前要素のソースコードを探すと、次のような内容が表示されるはずです。
<span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">Bright Data</span>
名前要素はspan
タグで囲まれ、(明らかに)ランダムに生成されたクラスが多数が割り当てられます。これは、プロフィールページのユーザーの名前要素のコンテナタグを特定するために、これらのクラス名を頼りにすることはできないことを意味します。何か静的なものを探す必要があります。
名前要素のHTMLソースの階層を上に進むとdiv
タグがあり、その中に名前とユーザー名の両方が見つかります(span
の数階層下)。div
コンテナの開始タグは、次のようになります。
<div class="css-1dbjc4n r-6gpygo r-14gqq1x" data-testid="UserName">
ランダムに生成されるクラス名がある一方で、data-testid
という別の属性もあります。data-testid
は、主にUIテストで使用されるHTML属性で、自動テストを実行するためのHTML要素を識別して、位置を特定します。この属性を使って、ユーザーの名前を含むdiv
コンテナを選択できます。ただし、ユーザー名(つまりTwitterのハンドル)も含まれます。つまり、テキストを改行位置で分割し、最初の項目(ユーザーの名前)を抽出する必要があります。
name = driver.find_element(By.CSS_SELECTOR,'div[data-testid="UserName"]').text.split('\n')[0]
経歴、所在地、ウェブサイト、参加日
名前要素の正しいセレクタを特定したのと同じ方法で、他のデータポイントの正しいセレクタを見つける必要があります。経歴、所在地、ウェブサイト、参加日などの要素には、すべてdata-testid
が添付されていることがわかります。そのため、要素を見つけ、そのデータを抽出するための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
フォロワー数とフォロー数
フォロワー数やフォロー数を見ると、data-testid
が付いていないことがわかります。つまり、正しく識別して選択するためには、何か工夫をしなければならないのです。
近い親には静的属性値が付加されていないため、階層を上に上がっても解決しません。この場合、XPathに目を向ける必要があります。
XPathはXML Path Languageの略で、XMLドキュメント内のタグを指す(またはタグへの参照を作成する)ために使用される言語です。XPathを使用して、テキスト 'Following'
を含む span
コンテナを検索し、その階層内を1段上に移動してカウントを見つけるセレクタを作成できます(テキスト 'Following'
とカウント値はどちらも個別のコンテナタグで囲まれているため)。
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
ツイート
幸い、各ツイートにはdata-testid
値「tweet」を持つ親コンテナがあります(以前、ツイートが読み込まれたかどうかの確認に使用しました)。Seleniumのfind_elements()
メソッドの代わりにfind_element()
メソッドを使用すると、指定されたセレクタを満たすすべての要素を収集できます。
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を使用したTwitterスクレイピング
ウェブスクレイピングを使用すると、ウェブページからデータを抽出する方法を非常に柔軟に制御できますが、設定が難しい場合もあります。ターゲットとなるウェブアプリが、静的ページの読み込み後にXHR呼び出しを介してページデータの大半を読み込み、HTML内に要素を特定するための静的識別子がほとんどない場合(先ほど見たTwitterの場合と同様)、適切な構成を把握するのは難しいことがあります。
このような場合、Bright Dataが役に立ちます。Bright Dataは、インターネットから膨大な量の非構造化データを抽出するのに役立つウェブデータプラットフォームです。Bright DataはTwitterデータをスクレイピングするための製品を提供しており、これはTwitterのウェブページから入手可能なデータポイントほぼすべての詳細なコレクションを取得するのに役立ちます。
例えば、Bright Dataを使って同じTwitterユーザープロフィールをスクレイピングする方法は以下の通りです。
まず、Bright Dataコントロールパネルに移動します。データ製品の表示ボタンをクリックして、Bright Dataが提供するウェブスクレイピングソリューションを表示します。
次に、Web Scraper IDEカード上の始めるをクリックします。
Bright Dataの提供するWeb Scraper IDEを使用して、ゼロから、またはベースラインテンプレートから独自のスクレイパーを作成できます。またBright Dataは、すぐに使い始めるのに役立つ自動スケーリングインフラストラクチャと内蔵のデバッグツールも提供します。
スクレイパーをゼロから作成するか、既存のテンプレートを使用するかを選択するよう求められます。すぐに始めたい場合は、Twitterのハッシュタグ検索のテンプレートをご覧ください(ここでは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}`);
}
上記のコードには、何が起こっているかを理解するのに役立つインラインコメントがあります。基本構成は以下の通りです。
- プロフィールページに移動する
- ページが読み込まれるのを待つ
/UserTweets/
APIからのレスポンスをインターセプトする- レスポンスを解析して情報を抽出する
既存の入力パラメータを削除し、ページ下部の入力セクションに1つの入力パラメータ「Username」を追加する必要があります。次に、例えば「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などのソーシャルメディアウェブサイトから収集されたデータをもとに高度に充実した情報を伝送するソーシャルメディアデータセットを提供します。これらを使って、ターゲットオーディエンスの情報を収集し、トレンドを把握し、注目を集めているインフルエンサーを特定するなど、さまざまことができます!
まとめ
この記事では、Seleniumを使用してTwitterから情報をスクレイピングする方法を学びました。この方法でデータをスクレイピングすることは可能ですが、複雑で時間がかかるため、理想的ではありません。そのため、TwitterデータをよりシンプルにスクレイピングできるBright Dataの使用方法も学びました。