Seleniumで動的スクレイピング(2023/11/25時点Colab動作確認済)

*念のため付記しておくと、サーバーに負荷をかけるようなことや倫理的に望ましくないデータ取得はやめましょう。

JavaScriptなどでクライアント側での描画を行うWebサイト(SpotifyとかTikTokとか)は、BeautifulSoup4を使った静的なHTML取得ではスクレイピングができない。じゃあSeleniumでやればいいんじゃないのという話になるのだが、SeleniumはColabとの相性がいまいちよろしくない。ローカルで走るコードが走らなかったりする。

たとえばPATHに配置されているChromedriverは落としてきたバイナリとバージョンが違うだの、

WARNING:selenium.webdriver.common.selenium_manager:The chromedriver version (114.0.5735.90) detected in PATH at /usr/local/bin/chromedriver might not be compatible with the detected chrome version (119.0.6045.159); currently, chromedriver 119.0.6045.105 is recommended for chrome 119.*, so it is advised to delete the driver in PATH and retry

これに限らずかなりうるさく、5月に公開したコードがもう動かなくなっている。そこで誰かがwrapper (jpjacobpadilla/Google-Colab-Selenium)を作ってくれていた。ありがたい。

早速使ってみる。基本的にはpipでインストールでき、wrapperなのでパラメータなどはseleniumのものを流用できる。

!pip install google_colab_selenium

あとは今インストールしたパッケージとSeleniumを読み出してあげる。僕はその他自分が使うものも適当にインポートしています。

import google_colab_selenium as gs
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
import time
import pandas as pd
import numpy as np

試しにひとつURLを指定し、今回はTikTokの情報を取得してみる。Optionsとかは割とざっくばらんに指定しています。

url = 'https://www.tiktok.com/music/アイドル-7216137897747941378'

options = Options()
options.add_argument("--headless")
options.add_argument('--disable-dev-shm-usage')
options.add_argument("--no-sandbox")
options.add_argument("--lang=ja")
options.add_argument("--window-size=1920,1080")
options.add_argument("--disable-popup-blocking")
options.add_argument("--ignore-certificate-errors")
options.add_argument("--incognito")
driver = gs.Chrome(options=options)

driver.get(url)
time.sleep(10)

page    = driver.title
title   = driver.find_element(By.XPATH, value="//h1[@data-e2e='music-title']").text
creator = driver.find_element(By.XPATH, value="//h2[@data-e2e='music-creator']").text
cnt     = driver.find_element(By.XPATH, value="//h2[@data-e2e='music-video-count']").text
driver.quit()

print(page, title, creator, cnt)

こんな感じ。走らせると、初回だけChromeのインストール、そして次からはInitializeが行われる。chromedriverはあくまで動的なので、quitするとインスタンスが終了してデータも取得できなくなる。よってquit前に必要な情報は取得しておく必要がある。

TikTokの音源ページでは、必要な情報は割とわかりやすくタグにまとまっている。

たとえば、h1, h2タグあたりで

  • <h1 data-e2e=’music-title’>タグ:曲名
  • <h2 data-e2e=’music-creator’>タグ:クリエーター名
  • <h2 data-e2e=’music-video-count’>タグ:曲が使われている動画数

が取得できる。

これらを適当に関数にまとめてあげれば…

def count_one_video_used(driver, video_id = 'オリジナル楽曲-れるりり-7242640629258341122'):
    print('処理開始:', video_id)
    url = 'https://www.tiktok.com/music/' + video_id
    driver.get(url)
    time.sleep(np.random.randint(5,12)) # JavaScriptの描画を5-12秒ランダムに待つ

    page=None; title=None; creator=None; cnt=None
    page    = driver.title
    title   = driver.find_element(By.XPATH, value="//h1[@data-e2e='music-title']").text
    creator = driver.find_element(By.XPATH, value="//h2[@data-e2e='music-creator']").text
    cnt     = driver.find_element(By.XPATH, value="//h2[@data-e2e='music-video-count']").text
    d = pd.DataFrame({
        'video_id': [str(video_id)],
        'page' : [str(page)],
        'title': [str(title)],
        'creator': [str(creator)],
        'count': [str(cnt)]
    })
    print('結果:', str(video_id), str(page), str(title), str(creator), str(cnt))
    return d

def count_all_video_used(ids):
    options = Options()
    options.add_argument("--headless")
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument("--no-sandbox")
    options.add_argument("--lang=ja")
    options.add_argument("--window-size=1920,1080")  # Set the window size
    #options.add_argument("--disable-infobars")  # Disable the infobars
    options.add_argument("--disable-popup-blocking")  # Disable pop-ups
    options.add_argument("--ignore-certificate-errors")  # Ignore certificate errors
    options.add_argument("--incognito")  # Use Chrome in incognito mode
    driver = gs.Chrome(options=options)

    try:
        for id in ids:
            _d = count_one_video_used(driver=driver, video_id=id)
            d  = _d if id==ids[0] else pd.concat([d, _d], axis=0)
            time.sleep(np.random.randint(1,4)) # 1-4秒ランダムに待機
    finally:
        driver.quit()

    d['num'] = d['count'].apply(lambda x: x.split(' ')[0].replace('K','').replace('M','')).astype(float)
    d['_count'] = 1
    d.loc[ d['count'].str.find('K')>=0, '_count' ] = 1000
    d.loc[ d['count'].str.find('M')>=0, '_count' ] = 1000000
    d['count'] = d['num'] * d['_count']
    return d.drop(columns=['num', '_count'])

そして実際の実行として、

ids = [
    'オリジナル楽曲-れるりり-7242640629258341122',
    'アイドル-ラスサビ-愛してる-ver-7223079274210084866',
    'アイドル-7216137897747941378'
    ]
count_all_video_used(ids)

こんな感じでpandasのDataFrameとして取得できる。各動画の再生数やLike数なども似た方法で取得できる。