naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Python、seleniumを使ってグーグル検索結果スクレイピング

プログラミング関連 約302日前
2017年2月12日12:48
seleniumを使い、グーグル検索結果のスクレイピングを行います。
今回は主に、3つの部分を取得します。

まずは検索結果の部分と...


広告の部分...


そして、関連キーワード...


これらを、ページ毎に取得します。
まずはソースコードです。

gscrapy.py
from collections import namedtuple
import time
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.keys import Keys


# 検索結果を1件ずつ格納する名前付きタプル
SearchResultRow = namedtuple(
    'SearchResultRow',
    ['title', 'url', 'display_url', 'dis']
)

# 広告を1件ずつ格納する名前付きタプル
AdResultRow = namedtuple(
    'AdResultRow',
    ['title', 'url', 'display_url', 'dis1', 'dis2', 'dis3']
)

# 関連キーワードを1件ずつ格納する名前付きタプル
RelationResultRow = namedtuple(
    'RelationResultRow',
    ['word', 'url']
)


def get_text_or_none(element, num):
    """
    <div class='spam'>
      <a>リンク1-1</a>
      <a>リンク1-2</a>
      <a>リンク1-3</a>
    </div>
    <div class='spam'>
      <a>リンク2-1</a>
    </div>
    のようなHTMLから、テキストを取得する際に使います

    for row in driver.find_elements_by_css_selector('div.spam'):
        a_element = row.find_elements_by_tag_name('a')
        a1 = get_elements_of_one(a_element, 0)
        a2 = get_elements_of_one(a_element, 1)
        a3 = get_elements_of_one(a_element, 2)
    """

    try:
        return element[num].text
    except IndexError:
        return ''


class GoogleScrapy:

    def __init__(self, keyword, end=1, default_wait=5):
        self.url = 'https://www.google.co.jp?pws=0'
        self.keyword = keyword
        self.end = end
        self.default_wait = default_wait
        self.driver = None
        self.searches = [[] for x in range(end)]
        self.ads = [[] for x in range(end)]
        self.relations = [[] for x in range(end)]

    def enter_keyword(self):
        """キーワードを入力し、エンターを押す"""

        self.driver.get(self.url)
        self.driver.find_element_by_id('lst-ib').send_keys(self.keyword)
        self.driver.find_element_by_id('lst-ib').send_keys(Keys.RETURN)

    def next_page(self):
        """次のページへ移動する"""

        self.driver.find_element_by_css_selector('a#pnnext').click()
        time.sleep(self.default_wait)

    def get_search(self, page):
        """通常の検索結果を取得する"""

        all_search = self.driver.find_elements_by_class_name('rc')
        for data in all_search:
            title = data.find_element_by_tag_name('h3').text
            url = data.find_element_by_css_selector(
                'h3 > a').get_attribute('href')
            display_url = data.find_element_by_tag_name('cite').text
            try:
                dis = data.find_element_by_class_name('st').text
            except NoSuchElementException:
                dis = ''

            result = SearchResultRow(title, url, display_url, dis)
            self.searches[page].append(result)

    def get_ad(self, page):
        """広告を取得する"""

        all_ads = self.driver.find_elements_by_class_name('ads-ad')
        for ads in all_ads:
            title = ads.find_element_by_tag_name('h3').text
            url = ads.find_elements_by_css_selector(
                'h3 > a')[1].get_attribute('href')
            display_url = ads.find_element_by_tag_name('cite').text

            dis_element = ads.find_elements_by_class_name('ellip')
            dis1 = get_text_or_none(dis_element, 0)
            dis2 = get_text_or_none(dis_element, 1)
            dis3 = get_text_or_none(dis_element, 2)
            result = AdResultRow(title, url, display_url, dis1, dis2, dis3)
            self.ads[page].append(result)

    def get_relation(self, page):
        """関連する検索キーワードを取得します"""

        all_relation = self.driver.find_elements_by_css_selector('p._e4b > a')
        for relation in all_relation:
            word = relation.text
            url = relation.get_attribute('href')
            result = RelationResultRow(word, url)
            self.relations[page].append(result)

    def start(self):
        """ ブラウザを立ち上げ、各種データの取得を開始する"""
        try:
            self.driver = webdriver.Firefox()
            self.driver.implicitly_wait(self.default_wait)
            self.enter_keyword()
            for page in range(self.end):
                self.get_search(page)
                self.get_ad(page)
                self.get_relation(page)
                self.next_page()
        finally:
            self.driver.quit()


main.py
from gscrapy import GoogleScrapy

google = GoogleScrapy('プログラミング', end=3)
google.start()

print('検索結果 全ページ')
print('-'*30)
for page_num, rows in enumerate(google.searches, 1):
    print('-'*30)
    print('{0}ページ目'.format(page_num))
    print('-'*30)
    for row in rows:
        print(row.title)

print('-'*30)
print('広告 1ページのみ')
print('-'*30)
for row in google.ads[0]:
    print(row.title)

print('-'*30)
print('関連キー 3ページのみ')
print('-'*30)
for row in google.relations[2]:
    print(row.word)


実行結果。上手くとれました。
C:\MyMercurial\test\testpython>python main.py
検索結果 全ページ
------------------------------
------------------------------
1ページ目
------------------------------
プログラミング (コンピュータ) - Wikipedia
【完全保存版】プログラミング初心者が最初にやるべき10のコト | 侍 ...
初心者に捧ぐ!プログラミングを独学で勉強する最強入門バイブル | TECH ...
プログラミング (コンピュータ) - Wikipedia
ゲームで遊ぶだけ!気付いたらプログラミングできるようになれるサービス14 ...
頼むからプログラミングを学ばないでくれ | TechCrunch Japan
ASCII.jp - プログラミング+
Progate | プログラミングの入門なら基礎から学べるProgate[プロゲート]
人気の「プログラミング」動画 4,154本 - ニコニコ動画 - niconico
初心者から中級者まで正しく学べるプログラミングの勉強法 - いつ俺 ...
------------------------------
2ページ目
------------------------------
1ヶ月でプログラミングを習得する方法とは | TechAcademyマガジン
プログラミング勉強を加速させる7つの習慣 - Qiita
主要なプログラミング言語8種をざっくり解説 - shi3zの長文日記
これからプログラミングを学ぼうとする君へ | Social Change!
プログラミング言語「ドリトル」
ドットインストール - 3分動画でマスターする初心者向けプログラミング学習 ...
プログラミングのIT勉強会・セミナー・イベント情報 - dots.[ドッツ]
Why!?プログラミング [技術 小5~6・中・高]|NHK for School
Amazon.co.jp 売れ筋ランキング: プログラミング の中で最も人気のある ...
情報処理学会 プログラミング研究会
------------------------------
3ページ目
------------------------------
プログラミング - Wikibooks
プログラミング:Geekなぺーじ
Cプログラミング診断室
プログラミング アーカイブ - Code部Code部 - CodeCamp
プログラミング(1 / 4) - インプレスブックス - 本、雑誌と関連Webサービス
プログラミングやソフト開発のコース - lynda.com - Lynda.jp
将来成功するためにプログラミングを学ぶことがなぜナンセンスなのか : 深 ...
編集長の眼 - 「プログラミングで論理思考が育つ」は本当か:ITpro
プログラミング 回答受付中の質問 - Yahoo!知恵袋 - Yahoo! JAPAN
20代の第二新卒・フリーターのためのプログラミング学習/就職支援 ...
------------------------------
広告 1ページのみ
------------------------------
札幌のプログラミング教室
英語とプログラミングを学ぶ - セブ島で未経験から世界へ
------------------------------
関連キー 3ページのみ
------------------------------
プログラミング言語
プログラミング 入門
android プログラミング
構造化プログラミング
プログラミング 基礎
webプログラミング
mac プログラミング
ipad プログラミング
プログラミング 独学
プログラミング 初心者


main.pyをシンプルにすると、こんな感じになります。
from gscrapy import GoogleScrapy

google = GoogleScrapy('プログラミング', end=3)
google.start()

# 検索結果を全て取得
for rows in google.searches:
    for row in rows:
        print(row.title)

# 1ページ目の広告
for row in google.ads[0]:
    print(row.title)

# 3ページ目の関連キーワード
for row in google.relations[2]:
    print(row.word)


これらは、取得したデータを格納する名前付きタプルです。
単純なタプルや辞書よりは柔軟に使え、軽量です。
クラスでも良かったっちゃ良かったんですが、今回は面倒なので名前付きタプルを採用しました。
# 検索結果を1件ずつ格納する名前付きタプル
SearchResultRow = namedtuple(
    'SearchResultRow',
    ['title', 'url', 'display_url', 'dis']
)

# 広告を1件ずつ格納する名前付きタプル
AdResultRow = namedtuple(
    'AdResultRow',
    ['title', 'url', 'display_url', 'dis1', 'dis2', 'dis3']
)

# 関連キーワードを1件ずつ格納する名前付きタプル
RelationResultRow = namedtuple(
    'RelationResultRow',
    ['word', 'url']
)


これはスクレイピングをしてると、たまにほしくなる関数です。
def get_text_or_none(element, num):
    """
    <div class='spam'>
      <a>リンク1-1</a>
      <a>リンク1-2</a>
      <a>リンク1-3</a>
    </div>
    <div class='spam'>
      <a>リンク2-1</a>
    </div>
    のようなHTMLから、テキストを取得する際に使います

    for row in driver.find_elements_by_css_selector('div.spam'):
        a_element = row.find_elements_by_tag_name('a')
        a1 = get_elements_of_one(a_element, 0)
        a2 = get_elements_of_one(a_element, 1)
        a3 = get_elements_of_one(a_element, 2)
    """

    try:
        return element[num].text
    except IndexError:
        return ''


div.spamが1件のデータになっており、aタグのテキストを
それぞれa1、a2、a3という変数に格納したいとしましょう。
    <div class='spam'>
      <a>リンク1-1</a>
      <a>リンク1-2</a>
      <a>リンク1-3</a>
    </div>
...以下、div.spamの繰り返し


しかし、ページやデータによってはaタグが一つしかない、一つもない、ということもあります。
    <div class='spam'>
      <a>リンク1-1</a>
      <a>リンク1-2</a>
      <a>リンク1-3</a>
    </div>
    <div class='spam'>
    </div>
    <div class='spam'>
      <a>リンク3-1</a>
    </div>


このような場合に使えるのがget_text_or_none関数です。
これであれば、aタグが無い部分は空文字が入り、a1~a3という変数に正しく格納できます。
for row in driver.find_elements_by_css_selector('div.spam'):
    a_element = row.find_elements_by_tag_name('a')
    a1 = get_elements_of_one(a_element, 0)
    a2 = get_elements_of_one(a_element, 1)
    a3 = get_elements_of_one(a_element, 2)



メインとなるクラスです。
class GoogleScrapy:

    def __init__(self, keyword, end=1, default_wait=5):
        self.url = 'https://www.google.co.jp?pws=0'
        self.keyword = keyword
        self.end = end
        self.default_wait = default_wait
        self.driver = None
        self.searches = [[] for x in range(end)]
        self.ads = [[] for x in range(end)]
        self.relations = [[] for x in range(end)]


pws=0を加えると、パーソナライズ検索を無効にできます。
スクレイピングするPCによって取得データが違うと困りますからね。
self.url = 'https://www.google.co.jp?pws=0'


それぞれ検索結果、広告、関連キーワードを格納するリストです。
        self.searches = [[] for x in range(end)]
        self.ads = [[] for x in range(end)]
        self.relations = [[] for x in range(end)]


最終的なsearches等には、以下のようにデータを格納することにしました。
なので、予めその用意をしています。
今にして思うと、空リストにしておいて、データを追加するときにリストをappendする形でもよかったです。
[
[data, data, data, data,],  # 1ページ目のデータ
[data, data, data, data,],  # 2ページ目のデータ
[data, data, data, data,],  # 3ページ目のデータ
]



startメソッドです。
ブラウザを立ち上げ、待ち時間を設定し、google検索を行い、指定したページ(end)まで必要なデータを取得します。
driverを使う際は、finally等で必ず閉じるようにしておきましょう。
    def start(self):
        """ ブラウザを立ち上げ、各種データの取得を開始する"""
        try:
            self.driver = webdriver.Firefox()
            self.driver.implicitly_wait(self.default_wait)
            self.enter_keyword()
            for page in range(self.end):
                self.get_search(page)
                self.get_ad(page)
                self.get_relation(page)
                self.next_page()
        finally:
            self.driver.quit()


見てのとおりの、google検索にキーワードを入れてエンターするメソッドです。
    def enter_keyword(self):
        """キーワードを入力し、エンターを押す"""

        self.driver.get(self.url)
        self.driver.find_element_by_id('lst-ib').send_keys(self.keyword)
        self.driver.find_element_by_id('lst-ib').send_keys(Keys.RETURN)


これは次ページへ移動するメソッド。
time.sleep()を入れて画面の描画を少し待つようにしています。
    def next_page(self):
        """次のページへ移動する"""

        self.driver.find_element_by_css_selector('a#pnnext').click()
        time.sleep(self.default_wait)



これが検索結果を取得するメソッドです。
長いですが、やっていることは単純です。
    def get_search(self, page):
        """通常の検索結果を取得する"""

        all_search = self.driver.find_elements_by_class_name('rc')
        for data in all_search:
            title = data.find_element_by_tag_name('h3').text
            url = data.find_element_by_css_selector(
                'h3 > a').get_attribute('href')
            display_url = data.find_element_by_tag_name('cite').text
            try:
                dis = data.find_element_by_class_name('st').text
            except NoSuchElementException:
                dis = ''

            result = SearchResultRow(title, url, display_url, dis)
            self.searches[page].append(result)


このtry~exceptはよく利用します。
NoSuchElementExceptionを使うと良いです。
            try:
                dis = data.find_element_by_class_name('st').text
            except NoSuchElementException:
                dis = ''


こっちは広告の取得。これも思ったより面倒でした。
    def get_ad(self, page):
        """広告を取得する"""

        all_ads = self.driver.find_elements_by_class_name('ads-ad')
        for ads in all_ads:
            title = ads.find_element_by_tag_name('h3').text
            url = ads.find_elements_by_css_selector(
                'h3 > a')[1].get_attribute('href')
            display_url = ads.find_element_by_tag_name('cite').text

            dis_element = ads.find_elements_by_class_name('ellip')
            dis1 = get_text_or_none(dis_element, 0)
            dis2 = get_text_or_none(dis_element, 1)
            dis3 = get_text_or_none(dis_element, 2)
            result = AdResultRow(title, url, display_url, dis1, dis2, dis3)
            self.ads[page].append(result)


関連キーワード。これは割と楽でした。
    def get_relation(self, page):
        """関連する検索キーワードを取得します"""

        all_relation = self.driver.find_elements_by_css_selector('p._e4b > a')
        for relation in all_relation:
            word = relation.text
            url = relation.get_attribute('href')
            result = RelationResultRow(word, url)
            self.relations[page].append(result)



今回は使用しませんでしたが、seleniumを使っていると一時的にimplicitly_waitを変えたい、というケースがあります。
そのような場合、単純に以下のようにしてもいいのですが
driver.implicitly_wait(0)
処理
driver.implicitly_wait(DEFAULT_WAIT)



私は以下のようにすることもあります。なかなか見やすいのではないでしょうか。
from contextlib import contextmanager


@contextmanager
def change_wait(time=0):
    """一時的に、implicitly_waitを変更する

    with change_wait(0.5):
        処理...
    """

    try:
        driver.implicitly_wait(time)
        yield
    finally:
        driver.implicitly_wait(DEFAULT_WAIT)

with change_wailt():
    処理



change_waitを使うサンプルです。
このブログの見出しを取得しますが、あえて
title = row.find_element_by_tag_name('h23').text
と、あり得ないタグを指定しています。
from contextlib import contextmanager
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

try:
    driver = webdriver.Firefox()
    driver.implicitly_wait(5)
    driver.get('https://torina.top/')
    for row in driver.find_elements_by_class_name('card'):
        try:
            title = row.find_element_by_tag_name('h23').text
        except NoSuchElementException:
            title = 'みつからなかった'
        print(title)
finally:
    driver.quit()


当然出力結果はこうなるのですが、implicitly_waitで5秒と指定しているために
5秒待つ→見つからんかった→5秒→見つからない→5秒...となってしまっています。これは時間の無駄です。
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった


早速change_waitを使用します。
from contextlib import contextmanager
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

@contextmanager
def change_wait(time=0):
    """一時的に、implicitly_waitを変更する

    with change_wait(0):
        処理...
    """

    try:
        driver.implicitly_wait(time)
        yield
    finally:
        driver.implicitly_wait(5)

try:
    driver = webdriver.Firefox()
    driver.implicitly_wait(5)
    driver.get('https://torina.top/')
    for row in driver.find_elements_by_class_name('card'):
        # 追加
        with change_wait():
            try:
                title = row.find_element_by_tag_name('h23').text
            except NoSuchElementException:
                title = 'みつからなかった'
        print(title)
finally:
    driver.quit()


今回は素早く表示されました。
with文なので、柔軟に使用することができるでしょう。
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった
みつからなかった


他には、デコレータを利用する方法もありそうです。
from functools import wraps
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException


def change_wait(time=0):
    def _change_wait(func):
        @wraps(func)
        def __change_wait(*args, **kwargs):
            driver.implicitly_wait(time)
            result = func(*args, **kwargs)
            driver.implicitly_wait(5)
            return result
        return __change_wait
    return _change_wait


@change_wait(0)
def get_title(row):
    try:
        title = row.find_element_by_tag_name('h23').text
    except NoSuchElementException:
        title = 'みつからなかった'
    return title


try:
    driver = webdriver.Firefox()
    driver.implicitly_wait(5)
    driver.get('https://torina.top/')
    for row in driver.find_elements_by_class_name('card'):
        title = get_title(row)
        print(title)
finally:
    driver.quit()