SeleniumでTwitterをスクレイピングする【Python】

SeleniumでTwitterをスクレイピングする【Python】 プログラミング

Twitterのスクレイピングに興味がありますか?
それなら、まずは次の記事をご覧ください。

上の記事では、本記事を読む上で必ず理解しておくべき内容を記載しています。
面倒だとは思いますが、流し読みでも構いませんので一読を。

では、この記事の内容に話を戻しましょう。
ズバリ、Twitterをスクレイピングする方法を解説しています。
コードも載せています。

本記事の内容

  • Twitterのスクレイピング戦略
  • JavaScriptによる動的コンテンツの読み込み
  • スクロールによる追加コンテンツの読み込み
  • IDやCLASS名で特定困難なタグ構成への対応
  • 完成したコード

それでは、上記に沿って解説を行っていきます。

Twitterのスクレイピング戦略

戦略とは、大袈裟ですね。
仕様だと思ってください。

まず、初めに言っておきます。
関連記事でも言ったかもしれませんが。
Twitterは、最高レベルのスクレイピング防御力を誇ります。

私は、スクレイピングが本業(?)ではありません。
でも、かれこれ10年ほどの経験があります。

PHPで対応可能なら、PHPを利用。
PHP単独で無理なら、PhantomJSの力を借りていました。

現在は、Pythonでスクレイピングをしています。
理由は簡単。
Seleniumを用いるからです。

Seleniumに関しては、次の記事でまとめています。

話を戦略に戻しましょう。
戦略は、「ブラウザを使ってTwitterを見るように、スクレイピングを行う」です。

よって、本当にブラウザを動かす必要があります。
そのためには、Seleniumが必須です。

Seleniumなしでは、Twitterをスクレイピングはできません。
技術的には可能かもしれませんが、かなり面倒になるでしょう。

なぜ、Seleniumが必須なのか?
それは、以下の理由です。

  • JavaScriptにより動的コンテンツを表示している
  • スクロールにより追加コンテンツを表示している(改ページの概念がない)

動的コンテンツは、Seleniumなしでも何とかなります。
でも、スクロール処理はSeleniumの力が必要です。
他には、Node.jsと言う手があるかもしれませんけどね。

では、Seleniumを使ってTwitterを攻略していきます。
上記であげた2点がポイントです。

JavaScriptによる動的コンテンツの読み込み

「JavaScriptにより動的コンテンツを表示している」に対応します。

まずは、ツイートのタグ構成を確認しましょう。
各ツイートは、articleタグで表現されています。

これらは、jsで読み込んだコンテンツです。
動的コンテンツ(js)に対応していないと、プログラムでは認識ができません。

そして、プログラムで認識するタイミングも問題となります。
早すぎるとarticleタグは一つも認識できません。

よって、articleタグが出てくるまで待つ必要があります。
それを以下のコードで実現しています。
なお、コード全体は最後にまとめて載せています。

    # articleタグが読み込まれるまで待機(最大15秒)
    WebDriverWait(driver, 15).until(EC.visibility_of_element_located((By.TAG_NAME, 'article')))

このコードにより、articleタグが出てくるまで読み込み(処理)を待機するのです。
スゴイですね~

動的コンテンツをスクレイピングするのは、以下の2つが必要な要素となります。

  • jsに対応している
  • 読み込み完了のタイミングを細かく設定できる

Seleniumだと、これらに簡単に対応可能です。

スクロールによる追加コンテンツの読み込み

「スクロールにより追加コンテンツを表示している(改ページの概念がない)」に対応します。

Twitterをスクレイピングする上では、最大の難関です。
スクロール毎に何個かツイートを読み込んで表示しています。

ページにアクセスした時点では、5個のツイートが読み込まれています。
このとき、DOM上には、articleタグが5個存在しています。

そして、スクロールすると追加でツイートが何個か読み込まれます。
もっと正確に言いいます。
表示されているarticleタグの末尾付近までスクロールすると、新たに追加でツイートが読み込まれます。

なお、末尾までスクロールするとツイートが何個か飛ばされてしまいます。
ある程度の閾値があるはずですが、そこまでは把握できませんでした。

そのため、ツイートが歯抜けにならないように小細工が必要になります。
末尾ではなく、末尾から2個目を対象とします。
そうすれば、ツイートが歯抜けになることはありません。

これらの動きをまとめます。

  • 末尾から2個目のarticleタグを認識する
  • 末尾から2個目のarticleタグまでスクロールする

プログラム上で、上記が実現できないとダメなのです。
Seleniumなら、これが可能になります。

末尾から2個目のarticleタグを認識する

これは単純です。

    # 最後の要素の一つ前までスクロール
    elems_article = driver.find_elements_by_tag_name('article')
    last_elem = elems_article[-2]

末尾から2個目のarticleタグまでスクロールする

コードだけなら、以下で済みます。
last_elemは上記で認識したモノです。

    actions = ActionChains(driver);
    actions.move_to_element(last_elem);
    actions.perform();

なお、articleタグが出てくる仕組みは、単純な話ではありません。
スクロールしてarticleタグが増えれば、それまで存在していたaritcleはDOMから消えていきます。

スクロールする度、articleタグから情報を取得する必要があるのです。
スクロールをやり尽くした後に、まとめてarticleタグから情報を取得ということができません。

これを知ったとき、Twitterの本気を理解しました。
「本当にスクレイピングさせたくないのだなー」と。

IDやCLASS名で特定困難なタグ構成への対応

これは、Seleniumを使う必要がありません。
そのため、BeautifulSoupを使っています。

やはり、ここでもTwitterはスクレイピングをさせたくないようです。
基本的には、タグにはIDが指定されていません。

そして、CLASS名は「css-1dbjc4n」のように命名ルールがわからないモノです。
よって、CLASS名などで要素を特定することが、基本的には不可能です。

できるとしたら、何番目のaタグなどの指定になります。

例えば、ツイートの内容を取得するコードは以下のようになります。
まずは、role=groupのdivタグ(これが1つだけだったのでラッキー)の親要素を特定します。
その要素の(子供である)最初の要素のテキストを取得という流れになります。

    # 投稿
    base_elem = soup.find("div", role="group").parent
    tweet = base_elem.find("div").text

だから、泥臭いコーディングになってしまいます。
もともとスクレイピングは泥臭い作業ですが、さらにドロドロという感じです。

完成したコード

完成したコードは以下です。
ここでは、一つ一つは説明しません。
わからない部分がある方は、紹介した過去記事を参考にしてください。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from bs4 import BeautifulSoup
import time
import pandas as pd

twitter_id = "seolabo85"
file_path = "./data/" + twitter_id + ".json"
scroll_count = 5
scroll_wait_time = 2

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
}

# tweet
def get_tweet(twitter_id):
    
    url = 'https://twitter.com/' + twitter_id
    
    id_list = []
    tweet_list = []
    # ヘッドレスモードでブラウザを起動
    options = Options()
    options.add_argument('--headless')
    
    # ブラウザーを起動
    driver = webdriver.Chrome("chromedriver.exeのパス", options=options)
    driver.get(url)

    # articleタグが読み込まれるまで待機(最大15秒)
    WebDriverWait(driver, 15).until(EC.visibility_of_element_located((By.TAG_NAME, 'article')))
    
    # 指定回数スクロール
    for i in range(scroll_count):
        id_list, tweet_list = get_article(url, id_list, tweet_list, driver)
        
        # スクロール=ページ移動
        scroll_to_elem(driver)
        
        # ○秒間待つ(サイトに負荷を与えないと同時にコンテンツの読み込み待ち)
        time.sleep(scroll_wait_time)  
    
    # ブラウザ停止
    driver.quit()
    
    return tweet_list

def scroll_to_elem(driver):
    
    # 最後の要素の一つ前までスクロール
    elems_article = driver.find_elements_by_tag_name('article')
    last_elem = elems_article[-2]
    
    actions = ActionChains(driver);
    actions.move_to_element(last_elem);
    actions.perform();
    
def get_info_of_article(data):
    
    soup = BeautifulSoup(data, features='lxml')
    elems_a = soup.find_all("a")
    
    # 名前
    name_str = elems_a[1].text
    
    id = ''
    name = ''
    if name_str:
        name_list = name_str.split('@')
        name = name_list[0]
        id = name_list[1]
        
    # リンク
    link = elems_a[2].get("href")
    
    # 投稿日時
    datetime = elems_a[2].find("time").get("datetime")
    
    # 投稿
    base_elem = soup.find("div", role="group").parent
    tweet = base_elem.find("div").text
    
    info = {}
    info["user_id"] = id
    info["user_name"] = name
    info["link"] = link
    info["datetime"] = datetime
    info["tweet"] = tweet
    
    return info

def get_article(url, id_list, tweet_list, driver):

    elems_article = driver.find_elements_by_tag_name('article')

    for elem_article in elems_article:
        tag = elem_article.get_attribute('innerHTML')
        elems_a = elem_article.find_elements_by_tag_name('a')
        
        href_2 = elems_a[1].get_attribute("href")
        href_tweet = ""
        
        if href_2 == url:
            # tweet
            href_tweet = elems_a[2].get_attribute("href")
            
            if href_tweet in id_list:
                print("重複")
            else:
                # tweet情報取得
                info = get_info_of_article(tag)
                
                id_list.append(href_tweet)
                tweet_list.append(info)
                
    return id_list, tweet_list


if __name__ == '__main__':
    # tweet情報をlist型で取得
    tweet_list = get_tweet(twitter_id)
    # データフレームに変換
    df = pd.DataFrame(tweet_list)
    # jsonとして保存
    df.to_json(file_path, orient='records')

次の部分は、説明しておきます。

twitter_id = "seolabo85"
file_path = "./data/" + twitter_id + ".json"
scroll_count = 5
scroll_wait_time = 2

twitter_id

スクレイピング対象のアカウントの識別コードです。
https://twitter.com/seolabo85

上記アカウントであれば、「seolabo85」の部分です。

file_path

スクレイピングした内容をjsonファイルに保存します。
そのjsonファイルのパスの設定です。

scroll_count

スクロール回数です。
5が設定されていれば、追加で5回のコンテンツ読み込みが実施されます。

ちなみに1000回と指定した場合、819個のツイートが取得できました。
ただし、ツイートの長さ(画像ありなし)に依存するため、一概にこうだとは言えません。

scroll_wait_time

スクロールを何秒間隔で行うかという設定です。
これは、ページアクセスを何秒間隔で行うかと同じ意味です。

1秒以上開ければ、問題はないと考えています。
しかし、今回は2秒の設定にしています。

動的コンテンツがDOMに反映されることも加味すれば、2秒ぐらいが妥当です。
2秒だと、かなり優しいスクレイピングです。

2秒だと、人間が実際にブラウザで見るときより、遅い可能性すらあり得ます。
一気にスクロールをしますよね。
多分、それよりは優しいアクセスになっています。

タイトルとURLをコピーしました