python – Javascriptページをseleniumでスクレイピング

python – Javascriptページをseleniumでスクレイピング

JavaScriptは、今日のWebでは、最も普及していて、サポートが行き届いているクライアント側のスクリプト言語です。

ユーザ追跡情報、ページをリロードせずにフォームサブミット、マルチメディアの埋め込み、オンラインゲーム全体のパワーアップなどに使われています。見たところは単純なページが、複数のJavaScriptを含むこともよくあります。

1. Seleniumを用いてPythonでJavaScriptを実行

Selenium(http://www.seleniumhq.org/)は、もともとはWebサイトテストのために開発された、強力なWebスクレイピングツールです。最近では、ブラウザに表示される通りの、正確な記述がWebサイトに必要なときに使われます。Seleniumは、ブラウザがWebサイトをロードし、必要なデータを取り出し、スクリーンショットを撮ったり、Webサイトである種の動作を起こしたりすることを自動化するために使われます。

Seleniumには自前のWebブラウザはありません。実行にはサードパーティのブラウザと統合する必要があります。例えば、FirefoxでSeleniumを実行すると、文字通り、Firefoxインスタンスがスクリーン上で開いて、Webサイトをナビゲートして、コードで指定した動作を行うのを目にします。これは見ていて面白いかもしれませんが、私はスクリプトをバックグラウンドで静かに実行するほうが好きですので、Chromeのヘッドレスモードを使います。

ヘッドレスブラウザは、Webサイトをメモリ上にロードして、そのページでJavaScriptを実行しますが、Webサイトをユーザに表示することはしません。SeleniumとヘッドレスChromeを組み合わせると、クッキー、JavaScript、ヘッダ、その他必要なものすべてをあたかも普通の画面表示のブラウザを使っているかのように簡単に扱える非常に強力なWebスクレイパーを実行することができます。

SeleniumライブラリはWebサイト(https://pypi.python.org/pypi/selenium)からまたは、pipのようなサードパーティインストーラを使ってコマンドラインからインストールできます。

Chrome WebDriverは、ChromeDriver Webサイト(http://chromedriver.chromium.org/downloads)からダウンロードできます。ChromeDriverは、Python専用のライブラリではなく、Chrome実行に際して使われる独立したアプリケーションなので、ダウンロードしてインストールして使う必要があり、pipではインストールできません。

Seleniumライブラリは、WebDriverオブジェクトを呼び出すAPIです。これはダウンロードしたWebDriverアプリケーションを表すか、そのインタフェースとなるPythonオブジェクトであることに注意してださい。同じ用語で(Pythonオブジェクトとアプリケーションそのものの)2つを表していますが、両者の概念をきちんと区別することが重要です。

WebDriverオブジェクトは、Webサイトをロードできるという点では、ブラウザに似ていますが、BeautifulSoupオブジェクトのようにページ要素を見つけ、ページ上の要素とやり取り(テキストを送る、クリックするなど)し、Webスクレイパーを駆動する他の作業もします。

次のコードは、テキストページのAjaxの「壁」の後ろにあるテキストを取り出します。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

chrome_options = Options()
chrome_options.add_argument("--headless")
driver = webdriver.Chrome(
    executable_path='drivers/chromedriver', options=chrome_options)

driver.get('http://pythonscraping.com/pages/javascript/ajaxDemo.html')
try:
    element = WebDriverWait(driver, 10).until(
                            EC.presence_of_element_located((By.ID, 'loadedButton')))
finally:
    print(driver.find_element_by_id('content').text)
    driver.close()

これは、Chromeライブラリを使って、新しいSelenium WebDriverを作り、WebDriverにページをロードし、実行を3秒間止めてから(ロードしたと期待する)コンテンツをページから取り出すよう指示します。

すべてが正しく構成されていれば、スクリプトは数秒経ってから実行して次のテキストを結果として返します。

Here is some important text you want to retrieve!
A button to click!

2. JavaScriptのScraping(ロケータ)

ロケータはセレクタではないことに注意してください。ロケータはByオブジェクトを用いた抽象的なクエリ言語で、セレクタを作るのを含めてさまざまなことに使われています。

上記コードでは、ロケータがloadedButtonというIDの要素を探すのに使われます。

ロケータはWebDriver関数find_elementを用いてセレクタを作るのにも使うことができます。

print(driver.find_element(By.ID, 'content').text)

これは、もちろん、コード例での行と機能的に等価です。

print(driver.find_element_by_id('content').text)

次のようなロケータ選択戦略がByオブジェクトで使われます。

ID例で用いられたように、HTMLのid属性で要素を見つける。
CLASS_NAMEHTMLのclass属性で要素を見つける。この関数はなぜCLASS_NAMEで、単にCLASSでないのか。.classが予約メソッドであるSeleniumのJavaライブラリでobject.CLASSという形式が問題を生じるからだ。異なる言語の間でSelenium構文を統一するために、CLASS_NAMEが用いられた。
CSS_SELECTOR#idName.classNametagNameという表記を用いて、classid, またはtag名で要素を探す。
LINK_TEXTテキストでHTML <a>タグを見つける。例えば、「Next」と言うリンクは、(By.LINK_TEXT, "Next")を用いて探せる。
PARTIAL_LINK_TEXTLINK_TEXTと同様だが、部分文字列で合致する。
NAMEname属性でHTMLタグを見つける。これはHTMLフォームに役立つ。
TAG_NAMEタグ名でHTMLタグを見つける。
XPATHXPath式を用いて合致要素を選択する。

3. JavaScriptのScraping(XPath構文)

XPath(XML Pathの省略形)はXML文書の内部をナビゲートし選択するのに使われるクエリ言語です。1999年にW3Cによって定められ、Python、Java、C#などの言語でXML文書を扱うときに使われています。

BeautifulSoupはXPathをサポートしていませんが、ScrapyやSeleniumなど他の多くのライブラリがサポートしています。HTML文書よりも一般的なXML文書を処理できるように設計されており、(mytag#idnameのように)CSSセレクタと同じように使えます。

XPath構文には4つの主要概念があります。

  • ルート節点と非ルート節点
    • /divは、文書のルートにあるときだけ、div節点を選ぶ
    • //divは、文書のどこにあっても、すべてのdiv節点を選ぶ
  • 属性選択
    • //@hrefは、属性hrefを持つどんな節点でも選ぶ
    • //a[@href='http://google.com']は、文書内の、Googleを指しているすべてのリンクを選ぶ
  • 位置で節点選択
    • //a[3]は、文書の第3リンクを選ぶ
    • //table[last()]は、文書の最後のテーブルを選ぶ
    • //a[position() < 3]は、文書の最初の2つのリンクを選ぶ
  • アスタリスク(*)は文字または節点の任意の集合とマッチして、さまざまな状況で使える
    • //table/tr/*は、すべてのテーブルのtrタグのすべての子を選ぶ(これは、thタグとtdタグの両方を用いてセルを選ぶのによい)
    • //div[@*]は、任意の属性を持つすべてのdivタグを選ぶ

もちろん、XPath構文には多くの高度の機能があります。年数を重ねて、XPathは、ブール論理、(position()のような)関数、その他、ここで取り上げていないさまざまな演算子を含む比較的複雑なクエリ言語へと発展してきました。

詳細はMicrosoftのXPath構文ページ(https://msdn.microsoft.com/en-us/enus/library/ms256471)を参照してください。

4. リダイレクトの処理

クライアント側リダイレクトは、ページコンテンツが送られる前のサーバで実行されるリダイレクトとは異なり、ブラウザでJavaScriptによって実行されるページリダイレクトです。

Webブラウザでページを見ているときには、違いを見つけるのは困難なことがあります。リダイレクトは高速なので、ロード時に遅延が気付かれず、クライアント側リダイレクトを実際はサーバ側リダイレクトと思ってしまうこともあります。

Seleniumは、他のJavaScript実行を扱うのと同様にこのJavaScriptリダイレクトを扱えます。しかし、これらのリダイレクトで主たる問題は、いつページ実行を止めるか、すなわち、ページがリダイレクトしたことをどのようにして通知するかです。http://pythonscraping.com/pages/javascript/redirectDemo1.htmlにおけるデモページは、2秒間停止後、この種のリダイレクトの例を示します。

このリダイレクトを、ページを最初にロードしたときに、DOMの要素を「注視」して、SeleniumがStaleElementReferenceExceptionを投げるまで、すなわち、その要素がもはやページのDOMに付随せず、サイトがリダイレクトされるまで、その要素を繰り返し呼び出すという巧妙な方法で検知できます。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import StaleElementReferenceException
import time

def waitForLoad(driver):
  elem = driver.find_element_by_tag_name("html")
  count = 0
  while True:
    count += 1
    if count > 20:
      print('Timing out after 10 seconds and returning')
      return
    time.sleep(.5)
    try:
      elem == driver.find_element_by_tag_name('html')
    except StaleElementReferenceException:
      return
chrome_options = Options()
chrome_options.add_argument("--headless")
driver = webdriver.Chrome(
    executable_path='drivers/chromedriver', options=chrome_options)
driver.get('http://pythonscraping.com/pages/javascript/redirectDemo1.html')
waitForLoad(driver)
print(driver.page_source)
driver.close()

このスクリプトはページを0.5秒ごとにチェックして、10秒後にタイムアウトします。チェックする時間やタイムアウトの時間は、必要に応じて変更できます。

別の方法は、URLが変化していないかどうか、あるいは特定のURLにならないかどうかページの現在のURLをチェックするループを書くことです。

要素が出現または消滅するのを待機することは、Seleniumでよくあるタスクです。以前のボタンをロードする例で使われたのと同じWebDriverWait関数を使うことができます。次のコードでは、15秒間のタイムアウトとページ本体のコンテンツのXPathセレクタを使って同じことをします。

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException

chrome_options = Options()
chrome_options.add_argument("--headless")
driver = webdriver.Chrome(
    executable_path='drivers/chromedriver', options=chrome_options)

driver.get('http://pythonscraping.com/pages/javascript/redirectDemo1.html')
try:
    bodyElement = WebDriverWait(driver, 15).until(EC.presence_of_element_located(
        (By.XPATH, '//body[contains(text()="This is the page you are looking for!")]')))
    print(bodyElement.text)
except TimeoutException:
    print('Did not find the element')

まとめ

今回は、python – Javascriptページをseleniumでスクレイピングについてでした。サイトがJavaScriptを使っているということだけで、伝統的なWebスクレイピングツールが役に立たないわけではありません。

JavaScriptの目的は、ブラウザでレンダリングされるHTMLとCSSコードを生成することであったり、HTTPリクエストとレスポンスを通じてサーバと動的に通信することであったりするわけです。

Seleniumを使えば、他のWebサイトのコードでやるのと同様に、ページのHTMLとCSSとを読んでパースすることができますし、これまでの章の技法を使ってHTTPリクエストとレスポンスを送ったり処理したりすることが、Seleniumなしでもできます。

さらに、JavaScriptはWebスクレイパーにとっては好都合なところもあります。JavaScriptが使用されているということは、「ブラウザ側でのコンテンツマネジメントシステム」が、データをより直接的に取得できる有用なAPIを公開しているようなものだからです。

(Visited 2,697 times, 1 visits today)