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


