Dragon Arrow written by Tatsuya Nakaji, all rights reserved animated-dragon-image-0164

pythonで動的スクレイピング (scrapy + chromedriver)

updated on 2021-04-10

pythonで動的スクレイピング




scrapy, selenium, chromedriverとは


Scrapy

Python でクローラーを実装するためのフレームワーク

設定を簡単に変更できたり、コマンドからスクレイピングを実行できるので、非常に開発が楽

Beautifulsoupやlxmlなどのスクレイピングライブラリと違い、scrapyはスクレイピングを行うコマンド生成からログのパスやレベルの設定、並列処理など、1アプリケーションとして丸ごとの機能を生成してくれる。


Selenium

Selenium は Web ブラウザの操作を自動化するためのフレームワーク


Chrome Driver

ブラウザをプログラムで動かす為のドライバー


SeleniumとChromeDriver の2つを組みあわせることで、以下のことが可能になる

・スクレイピング

・ブラウザの自動操作(次へボタンや購入ボタンなどを自動で押すなど)

・システムの自動テスト

・非同期サイトのスクレイピング


ライブラリのインストール


筆者環境 python: 3.9


$ pip install chromedriver-binary = "==89.0.4389.23.0" # ここは、自分のchromeのバージョンに合わせる
$ pip install Scrapy
$ pip install selenium
$ pip install twisted # 任意だが、インターネット接続時のエラー処理で使える



Headlessモードと画面ありモード



Headlessモード

import chromedriver_binary
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options


class QuotesSpider(scrapy.Spider):
    # 一意なスパイダーの定義
    name = "school_picker"


    def __init__(self, *args, **kwargs):
        """
        :param args:
        :param kwargs:
        """
        super(QuotesSpider, self).__init__(*args, **kwargs)

        # headless mode の場合
      # headless start
        # Chrome Optionを設定
        options = Options()
        # headlessで必要な項目
        options.add_argument('--headless')
        options.add_argument('--disable-gpu')
        # headlessで不要な項目
        options.add_argument('--disable-desktop-notifications')
        options.add_argument('--disable-extensions')
        options.add_argument('--no-sandbox')
        # シングルプロセス
        options.add_argument('--single-process')
        # HTML5のApplication Cacheを無効
        options.add_argument('--disable-application-cache')
        # SSLセキュリティ証明書のエラーページを非表示
        options.add_argument('--ignore-certificate-errors')
        # ウィンドウ最大化
        options.add_argument('--start-maximized')
        self.driver = Chrome(options=options)
        headless end
...


画面ありモード


Chromeのoption引数に何も指定しなければ良い (デフォルトの状態)

self.driver = Chrome()


ページから要素を取得


スクレイピングの際に、条件から、要素を取得する関数

  • find_element_by_id
  • find_element_by_name
  • find_element_by_xpath
  • find_element_by_link_text
  • find_element_by_partial_link_text
  • find_element_by_tag_name
  • find_element_by_class_name
  • find_element_by_css_selector

複数の要素を見つけるには(これらのメソッドはリストを返す)

  • find_elements_by_name
  • find_elements_by_xpath
  • find_elements_by_link_text
  • find_elements_by_partial_link_text
  • find_elements_by_tag_name
  • find_elements_by_class_name
  • find_elements_by_css_selector


例:

クラス名で要素の取得

1件

self.driver.find_element_by_class_name('class-name') # クラス名 'class-name'の要素を取得

全件

self.driver.find_elements_by_class_name('class-name') 


HTMLタグで要素の取得

1件

find_element_by_tag_name('span') # spanタグの要素を取得

全件

find_elements_by_tag_name('span')


idで要素の取得

1件

find_element_by_id('element-id') # id属性が'element-id'の要素を取得

全件

find_elements_by_id('element-id')


複数の条件から要素を取得

elementById = self.driver.find_element_by_id('element-id')
elementByIdAndClass = facultiesInfoDiv.find_elements_by_class_name('element-class')


テキストを取得

spanTag = find_element_by_tag_name('span')
spanText = spanTag.text
print(spanText)



テキストフィールドに入力


send_keysに指定した文字を入力できる


例:

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select, WebDriverWait
import sys


class QuotesSpider(scrapy.Spider):

def login(self, response):
    form = self.driver.find_element_by_tag_name('form')
    self.driver.find_element_by_name('login_id').send_keys('mypage_login_id')
    self.driver.find_element_by_name('login_password').send_keys('mypage_login_password')
    form.submit()


またはjsの関数を実行する (こっちの方が早いのでおすすめ)

loginId = "mypage_login_id"
loginPass = "mypage_login_password"

self.driver.execute_script(
    "document.getElementsById('login_id').value=arguments[0];", loginId
)
self.driver.execute_script(
    "document.getElementsById('login_password').value=arguments[0];", loginPass
)


リンクを新規タブで開く


self.driver.execute_script('arguments[0].target="_blank"',element)
self.driver.execute_script('arguments[0].click()', element)

で 新規タブでボタンやリンクを開くことができる。


facultyPage = find_element_by_tag_name('a')
actions = ActionChains(self.driver)
actions.move_to_element(facultyPage)
actions.perform()
self.driver.execute_script('arguments[0].target="_blank"',facultyPage)
self.driver.execute_script('arguments[0].click()', facultyPage)



タブを閉じる




一番最初に開いたタブ (一番左に開いているタブ)のみ残し、他は全て閉じる

while len(self.driver.window_handles) > 1:
    self.driver.switch_to.window(self.driver.window_handles[-1])
    self.driver.close()
    self.driver.implicitly_wait(3)
    self.driver.switch_to.window(self.driver.window_handles[0])



HTTPレスポンスエラー




from twisted.internet.error import DNSLookupError
from twisted.internet.error import TimeoutError, TCPTimedOutError


...
def start_requests(self):
    for url in self.start_urls:
        yield Request(
            url,
            callback=self.parse,
            errback=self._errback
        )

def _errback(self, failure):
    """
    リクエスト処理で例外が発生したとき(HTTPレスポンスステータスコードが200以外)のエラーバック
    :param failure:
    """
    # log all failures
    self.logger.error(repr(failure))

    if failure.check(HttpError):
        # these exceptions come from HttpError spider middleware
        # you can get the non-200 response
        response = failure.value.response
        self.logger.error('HttpError on %s', response.url)
    elif failure.check(DNSLookupError):
        # DNS lookup failed
        request = failure.request
        self.logger.error('DNSLookupError on %s', request.url)
    elif failure.check(TimeoutError, TCPTimedOutError):
        request = failure.request
        self.logger.error('TimeoutError on %s', request.url)



キャプチャの取得



import scrapy
from io import BytesIO
from PIL import Image
import sys

class QuotesSpider(scrapy.Spider):
...

    def saveScreenShot(self, file_name):
        """
        スクリーンショットをJPGに変換して保存
        :param file_name:
        """
        # 画面全体をスクリーンショット
        driver = self.driver
        w = driver.execute_script('return document.body.scrollWidth')
        h = driver.execute_script('return document.body.scrollHeight')
        driver.set_window_size(w, h)
        png_image = driver.get_screenshot_as_png()

        # PNGからJPGに変換
        try:
            with Image.open(BytesIO(png_image)) as image:
                rgb_image = image.convert('RGB')
                rgb_image.save('path_to_your_image_directory/' + file_name + '.jpg', quality = 95)
        except OSError:
            self.logger.error('Exception: 見出し記事の取得に失敗しました')
            sys.exit(1)


        self.saveScreenShot('myPage') # 'path_to_your_image_directory/myPage.jpg' が保存される


改行文字の入力


例: 'paraKeyWords'というキーワードを持つテキストエリアに対して、改行を含む入力を行いたいとき


おすすめのやり方 (推奨)

keywords = 'キーワード1\r\nキーワード2\r\nキーワード3'
self.driver.execute_script(
    "document.getElementsByName('paraKeywords')[0].value=arguments[0];", keywords
)


一応、以下の方法でもできますが、改行文字を2重でエスケープしているように見えてレビュワーが混乱するので、基本的にやめた方が良いです。

terarailでは以下のやり方ばかり紹介しているユーザーを多く見かけしますが、よくないやり方なので、真似せず、上記やり方(jsの関数argmentsを使った手法)を取ってください。(上記の方がわかりやすいし、早いです)


非推奨のやり方 (レビュワーの混乱をうむ)

バックスラッシュをエスケープではなく文字として認識させる為にバックスラッシュをエスケープするというもの

keywords = 'キーワード1\r\nキーワード2\r\nキーワード3'
driver.execute_script('document.getElementsByName(\'paraKeywords\')[0].value=\'%s\';' % keywords.replace('\r','\\r').replace('\n','\\n')


よくある間違い

改行文字が認識されず、エラーになる

keywords = 'キーワード1\r\nキーワード2\r\nキーワード3'
driver.execute_script('document.getElementsByName(\'paraKeywords\')[0].value=\'%s\';' % keywords