極楽とんぼのロボット製作記

情報工学系大学院生がロボットとその周辺技術や身の回りの出来事について紹介するブログ

seleniumで始めるウェブスクレイピング(Steamで遊ぶ)

Seleniumでウェブスクレイピングをする方向けの記事です。

この記事で分かること

  1. seleniumの環境構築
  2. XPATHでの要素の指定方法
  3. seleniumでページ上の要素を取得する方法
  4. seleniumを使う上でのTIps

実行環境は以下の通りです。

Python 3.6.3 |Anaconda custom (64-bit)
>>> selenium.__version__
'3.12.0'

ウェブドライバーにはChromeDriver(2.4.0)を使用します。

環境構築

seleniumのインストール

Pythonは基本的にPython3の環境であれば問題ありません。 pipでseleniumのインストールを行います。

$ pip install selenium

インストールが完了したらimportできるかも確認しましょう。

$ python
>>> import selenium
>>> selenium.__version__
'3.12.0'

WebDriverのダウンロード

以下のリンクから最新のChromeDriverをダウンロードします。ダウンロードする際は自分のOSに合ったものを選びます。
Downloads - ChromeDriver - WebDriver for Chrome

ダウンロードしたらzipファイルを展開して適当な場所に置きます。
例)

$ unzip chromedriver_linux64.zip
$ mkdir ~/webdrivers
$ mv chromedriver ~/webdrivers/

ダウンロードできたらウェブドライバーをロードしてみましょう。 Pythonで以下のコードを書いて実行します。

from selenium import webdriver
chrome_driver_path = "/home/username/webdrivers/chromedriver"
driver = webdriver.Chrome(chrome_driver_path)

ここで以下のようなエラーがでる場合はGoogleChromeをアップデートする必要があります。

selenium.common.exceptions.SessionNotCreatedException: 
Message: session not created exception: Chrome version must be >= 66.0.3359.0

以下のコマンドを実行してGoogleChromeのアップデートしましょう。

$ sudo apt --only-upgrade install google-chrome-stable

Steamをウェブスクレイピングしてみよう!

SteamのSummerSaleが終わってしまい、これからまたSteamの瞬間的なSaleを追いかけることになりそうです。せっかくですのでSteamを題材にウェブスクレイピングしてみようと思います。

Steamで検索する

キーワードをSteamで検索するプログラムを作成します。プログラムの流れは検索窓を見つける、検索窓に入力する、検索ボタンをクリックするという3段階です。

要素を見つける

まず検索窓を指定してやる必要があります。ブラウザで Welcome to Steam を開いて、検索窓を右クリックして検証(Firefoxの場合は「要素を調査」)をクリックします。 f:id:gokuraku104robot:20180705205454p:plain:w500
するとウェブページのソースコードが見れます。ハイライトされているのが検索欄のソースコードです。ソースコードのハイライトされている部分を右クリックしてCopy→Copy elementをクリックします。 検索窓のソースは以下のようになっています。

<input id="store_nav_search_term" name="term" type="text" class="default" placeholder="ストアを検索する" size="22" autocomplete="off">
XPATHで要素を指定する

今、見つけた要素を使って検索窓に文字を入力します。 要素を指定するにはclassやidなどで指定したり、cssセレクタを使う方法などがありますが、今回は汎用性の高いXPATHを使う方法で指定します。

XPATHとは

XML Path Language(XPATH)はマークアップ言語XMLに準拠した文書の特定の部分を指定する言語構文である。 XML Path Language - Wikipedia

例えば次のようなhtmlの文書があった時、

<html>
...
  <body>
    <h1>test</h1>
    <div class="hoge">
      <span class="piyo">foo</span>
    </div>
  </body>
</html>

<span class="piyo">foo</span>の部分を指定するには

/html/body/div/span[@class='piyo']

のように階層を指定します。毎回、フルのパスを書くのは面倒なので通常は省略して表記します。//を頭につけると<html>の中の全てのタグを表すことができます。

//span[@class='piyo']

この指定方法ではspanタグでclassの要素がpiyoであるものが指定されます。場合によっては一意に定まらないので、その際はもう1つ上の階層から指定したり、複数の要素を指定したりします。

今回の検索窓は<input id="store_nav_search_term" name="term" type="text" class="default" placeholder="ストアを検索する" size="22" autocomplete="off">ですので、これを指定するXPATHは

//input[@id='store_nav_search_term'

となります。 同じように検索ボタンのコードをブラウザの検証から探します。検索ボタンは

<img src="https://steamstore-a.akamaihd.net/public/images/blank.gif">

ですので、XPATHは

//img[@src='https://steamstore-a.akamaihd.net/public/images/blank.gif']

となります。

では以下のコードを実行して、steamで検索させてみましょう。

from selenium import webdriver
chrome_driver_path = "/home/tombo/webdrivers/chromedriver"  #chromedriverへのパス
driver = webdriver.Chrome(chrome_driver_path)     #chromedriverの読み込み
steam_url = "https://store.steampowered.com/"    #SteamのURL
search_word = "Undertale"    #検索キーワード

driver.get(steam_url)    #SteamのURLを開く
search_element = driver.find_element_by_xpath("//input[@id='store_nav_search_term']")    #検索窓を指定
search_element.send_keys(search_word)    #要素にキーワードを入力
search_button = driver.find_element_by_xpath("//img[@src='https://steamstore-a.akamaihd.net/public/images/blank.gif']")   #検索ボタンを指定
search_button.click()    #要素をクリック
input()    #すぐに閉じてしまうのでキー入力を待機させます
driver.quit()

売上上位のゲームのリストを得る

Steamの売上上位のゲームのリストを作成します。下記のリンクのページにあるゲームのリストからゲームタイトル、価格、ゲームへのリンクを取得します。
Steam Search
手始めにゲームのリストが載っているソースをコピーしてきます。

<div id="search_result_container">

    <div class="search_rule"></div>

    

        <!-- List Items -->
        <div>
            <!-- Extra empty div to hack around lame IE7 layout bug -->
            <div></div>
            <!-- End Extra empty div -->
                                                        <!---リストの1つ目--->
                            <a href="https://store.steampowered.com/app/381210/Dead_by_Daylight/?snr=1_7_7_topsellers_150_1" data-ds-appid="381210" onmouseover="GameHover( this, event, 'global_hover', {&quot;type&quot;:&quot;app&quot;,&quot;id&quot;:381210,&quot;public&quot;:1,&quot;v6&quot;:1} );" onmouseout="HideGameHover( this, event, 'global_hover' )" class="search_result_row ds_collapse_flag app_impression_tracked">
                    <div class="col search_capsule"><img src="https://steamcdn-a.akamaihd.net/steam/apps/381210/capsule_sm_120.jpg?t=1528898731"></div>
                    <div class="responsive_search_name_combined">
                        <div class="col search_name ellipsis">
                            <span class="title">Dead by Daylight</span>
                            <p>
                                <span class="platform_img win"></span>                           </p>
                        </div>
                        <div class="col search_released responsive_secondrow">2016年6月14日</div>
                        <div class="col search_reviewscore responsive_secondrow">
                                                            <span class="search_review_summary positive" data-tooltip-html="ほぼ好評<br>このゲームのユーザーレビュー 101,107 件中 77% が好評です">
                                </span>
                                                    </div>


                        <div class="col search_price_discount_combined responsive_secondrow">
                            <div class="col search_discount responsive_secondrow">
                                
                            </div>
                            <div class="col search_price  responsive_secondrow">
                                ¥ 1,980                            </div>
                        </div>
                    </div>


                    <div style="clear: left;"></div>
                </a>
                                                        <!---リストの2つ目--->
                            <a href="....

ページの構造としては<div id="search_result_container"> <div>...の中に<a href="...>として商品のURLやタイトルが入っています。(trで作ってくれればスクレイピングしやすかったのですが...)まずは商品の情報が入っている<a>で囲まれている部分をまとめて取得します。このときのXPATHは

//div[@id='search_result_container']/div/a

となります。このXPATHを使ってブラウザで検索してみると25件ヒットすると思われます。 f:id:gokuraku104robot:20180707150720p:plain:w500
このように複数の要素を指定して読み込みたいときはfind_elements関数を使用します。

driver.find_elements_by_xpath("//div[@id='search_result_container']/div/a")

find_elementsは指定に該当する要素のリストを返します。

以下のコードを実行してsteamの売上上位の25個の商品のURLが取得できているか確認してみてください。

from selenium import webdriver

chrome_driver_path = "/home/tombo/webdrivers/chromedriver"
driver = webdriver.Chrome(chrome_driver_path)
steam_url = "https://store.steampowered.com/search/?filter=topsellers"

driver.get(steam_url)
topseller_elements = driver.find_elements_by_xpath("//div[@id='search_result_container']/div/a")
print(len(topseller_elements))
for top_element in topseller_elements:
    print(top_element.get_attribute("href"))

driver.quit()

URLが取得できたので、次はゲームのタイトルを取得します。ゲームタイトルまでのXPATHは今、取得したURLを利用すると指定しやすいです。XPATHのhrefの部分に今取得したURLを使います。

game_url = top_element.get_attribute("href")
game_title_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/span[@class='title']")

次にゲームの価格を取得します。ゲームの価格はセールになっている場合となっていない場合とでhtmlのコードが異なっているため、それに対応するように書きます。 セールでない時のゲーム価格は

<div class="col search_price  responsive_secondrow">
¥ 1,980
</div>

のようなコードで、セール時には

<div class="col search_price discounted responsive_secondrow">
<span style="color: #888888;"><strike>¥ 1,520</strike></span><br>¥ 1,368
</div>

のようなコードになります。今回は例外処理でそれぞれの処理を書きました。

    try:    #セールでない時
        game_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price  responsive_secondrow']")
        game_regular_price = int(re.sub(r'\D','',game_price_element.text))    #正規表現を用いて数字のみを抜き出し、int型にキャスト
        game_discount_price = None
    except:    #セールの時
        #通常価格とセール価格の両方が文字列として入ってしまっている
        game_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price discounted responsive_secondrow']")

        #通常価格のみを抜き出す
        game_regular_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price discounted responsive_secondrow']/span/strike")
        game_regular_price = int(re.sub(r'\D','',game_regular_price_element.text))    #正規表現を用いて数字のみを抜き出し、int型にキャスト

        #2つの価格が入っている文字列から通常価格を消す
        discount_price_text = game_price_element.text.replace(game_regular_price_element.text,"")
        game_discount_price = int(re.sub(r'\D','',discount_price_text))    #正規表現を用いて数字のみを抜き出し、int型にキャスト
    print(game_regular_price)
    print(game_discount_price)

最後に売上上位のゲームのリストをcsvに保存するプログラムです。

from selenium import webdriver
import re
import csv

chrome_driver_path = "/home/tombo/webdrivers/chromedriver"
driver = webdriver.Chrome(chrome_driver_path)
steam_url = "https://store.steampowered.com/search/?filter=topsellers"
driver.get(steam_url)

topseller_elements = driver.find_elements_by_xpath("//div[@id='search_result_container']/div/a")

fileobj = open("steam_topsale_games.csv","w")
dataWriter = csv.writer(fileobj)

for top_element in topseller_elements:
    game_url = top_element.get_attribute("href")
    game_title_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/span[@class='title']")
    game_title = game_title_element.text
    try:
        game_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price  responsive_secondrow']")
        game_regular_price = int(re.sub(r'\D','',game_price_element.text))
        game_discount_price = None
    except:
        game_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price discounted responsive_secondrow']")
        game_regular_price_element = driver.find_element_by_xpath(f"//a[@href='{game_url}']/div/div/div[@class='col search_price discounted responsive_secondrow']/span/strike")
        game_regular_price = int(re.sub(r'\D','',game_regular_price_element.text))
        discount_price_text = game_price_element.text.replace(game_regular_price_element.text,"")
        game_discount_price = int(re.sub(r'\D','',discount_price_text))
    game_data_list = list()
    game_data_list.append(game_title)
    game_data_list.append(game_url)
    game_data_list.append(game_regular_price)
    game_data_list.append(game_discount_price)

    dataWriter.writerow(game_data_list)


driver.quit()
fileobj.close()

Selenium TIps

seleniumのwait

明示的な待機

次のようにして要素を取得してボタンをクリックできるようになるまで待機することができます。

from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, 'someid')))
暗黙的な待機

暗黙的な待機は一度設定すれば、すべてに適応されます。要素が利用できるまで待ったり、Javascriptの実行終了まで待ったり、ページのロードを待ったりすることができます。

driver.implicitly_wait(10)    #すぐに利用できない要素が利用できるようになるまで10秒待つ
driver.set_script_timeout(10)   #javascript実行が終了するまで10秒待つ
driver.set_page_load_timeout(10)    #ページが完全にロードされるまで最大で10秒待つ

ボタンを押すときはスクロール

ボタンを押そうとしているのに

selenium.common.exceptions.WebDriverException: Message: unknown error: Element is not clickable at point

などと表示されて押せないことはありませんか? seleniumは画面外にあるボタンを押すにはその場所までスクロールして画面内にもってくる必要があります。location_once_scrolled_into_vieを使うことで要素の場所までスクロールすることができます。

driver.find_element_by_xpath(next_page_xpath).location_once_scrolled_into_vie
driver.find_element_by_xpath(next_page_xpath).click()

headlessモード

いちいちブラウザが立ち上がるのが面倒な場合はheadlessモードがおすすめです。ブラウザを立ち上げずにスクレイピングができます。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--headless')
driver = webdriver.Chrome(chrome_driver_path,chrome_options=options)

便利なのですが、一部javascriptが動かない時もあり、スクレイピングしたい要素が見えなくなってしまうという場合もあります。そんな時はvirtualdisplayを使いましょう。 まずはxvfbとpyvirtualdisplayをインストールします。

$ sudo apt install xvfb
$ pip install pyvirtualdisplay

コードを書くときには以下のようにします。visibleが0で非表示、1で表示です。

from pyvirtualdisplay import Display
window_width = 1600
window_height = 900
display = Display(visible=0, size=(window_width,window_height))
display.start()
driver = webdriver.Chrome(chrome_driver_path)
driver.set_window_size(window_width,window_height)
#######何らかの処理#######
driver.quit()
display.stop()

参考サイト

PythonでWebスクレイピングする時の知見をまとめておく - Stimulator
Selenium API(逆引き)
selenium reference
seleniumのReference(日本語)
python - Selenium working with Chrome, but not headless Chrome - Stack Overflow seleniumにてButtonがクリックできない時の対処法