見出し画像

【初心者向け】Pythonでスクレイピングする環境を作る⑩ crawler と scraper を分ける

Naruhiko です。

前回まで、crawler.py を作ってきました。
合わせてスクレイピングの注意点やテストのやり方なども一緒に説明しました。

今回は、今まで作ってきた crawler のスクレイピング部分を切り離し、モジュール化してみようと思います。

クローリングとスクレイピングの違いは?

クローリングと言ったり、スクレイピングと言ったりしますよね。
その2つの違いはなんでしょうか。

・クローリングは巡回する機能。
・スクレイピングは抽出する機能。

というような感じです。

これを考えると、作成した crawler.py は、
Google検索結果から抽出したURLを巡回して、
巡回先サイトのデータを抽出するという両方が合わさった機能ということになります。

ということで、このクローリング部分と、スクレイピング部分を分けてみたいと思います。

今の状態の crawler.py はこの様になっています。

import time
from urllib.robotparser import RobotFileParser
from urllib.parse import urlparse
from logging import basicConfig, getLogger, DEBUG

import requests
from bs4 import BeautifulSoup


# ログフォーマットを定義
formatter = "[%(asctime)s][%(name)s][%(levelname)s]%(message)s"
# ログレベル
basicConfig(level=DEBUG, format=formatter)

class Crawler:
    """ Crawler
    self.user_agent : ユーザーエージェントの情報を設定
    self.engine : 検索サイトを設定

    get_robot_txt(url) : robots.txt の中身を精査
        url: robots.txt を確認するサイトURL
        return True, False

    get_html(url, params=None, headers=None) : サイトの情報を取得
        url: データを取得するサイトのURL
        [params]: 検索サイトのパラメーター {x: param}
        [headers]: カスタムヘッダー情報

    get_search_url(word, engine="google") : 検索サイトで検索結果の一覧を取得
        word: 検索するワード
        [engine]: 使用する検索サイト(デフォルトは google)
    """

    def __init__(self, engine="google"):
        # User-Agent
        self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
                           AppleWebKit/537.36 (KHTML, like Gecko) \
                           Chrome/69.0.3497.100"
        # engine
        self.engine = engine

    def get_robots_txt(self, url):
        """ get_robots_txt
        url: robots.txt を確認するサイトURL
        """
        logger = getLogger("get_robots_txt()")
        try:
            # robots.txt 用パーサー
            rp = RobotFileParser()
            # robots の url 取得
            parsed_url = urlparse(url)
            robots_url = "{0.scheme}://{0.netloc}/robots.txt".format(parsed_url)
            # robots.txt 取得
            rp.set_url(robots_url)
            rp.read()
            # 取得していいか確認
            return rp.can_fetch("*", url)
        except Exception as e:
            logger.warning(e)
            return False

    def get_html(self, url, params=None, headers=None):
        """ get_html
        url: データを取得するサイトのURL
        [params]: 検索サイトのパラメーター {x: param}
        [headers]: カスタムヘッダー情報
        """
        logger = getLogger("get_html()")
        try:
            # 待機
            time.sleep(5)
            # データ取得
            resp = requests.get(url, params=params, headers=headers)
            resp.encoding = resp.apparent_encoding
            # 要素の抽出
            soup = BeautifulSoup(resp.text, "html.parser")
            return soup
        except Exception as e:
            logger.warning(e)
            return None

    def get_search_url(self, word):
        """ get_search_url
        word: 検索するワード
        [engine]: 使用する検索サイト(デフォルトは google)
        """
        logger = getLogger("get_search_url()")
        try:
            if self.engine == "google":
                # google 検索
                search_url = "https://www.google.co.jp/search"
                search_params = {"q": word}
                search_headers = {"User-Agent": self.user_agent}
                # データ取得
                soup = self.get_html(search_url, search_params, search_headers)
                if soup != None:
                    tags = soup.select(".r > a")
                    urls = [tag.get("href") for tag in tags]
                    return urls
                else:
                    raise Exception("No Data")
            else:
                raise Exception("No Engine")
        except Exception as e:
            logger.warning(e)
            return None


if __name__ == '__main__':
    try:
        # インスタンス化
        crawler = Crawler()
        urls = crawler.get_search_url("python")
        if urls != None:
            for url in urls:
                if crawler.get_robots_txt(url):
                    soup = crawler.get_html(url)
                    print(soup.title.get_text())
                else:
                    print("クロールが拒否されました [{}]".format(url))
        else:
            print("取得できませんでした")
    except Exception as e:
        print("エラーになりました")

スクレイピングに分ける部分は scraper.py に分ける

スクレイピングである、データの取得と抽出する部分は、

・get_robots_txt()
・get_html()

になります。

get_search_url() はクローリングさせる部分ですので、こちらと分けてしまいましょう。

まず、新しいファイルを作成します。
crawler.py と同じ場所に scraper.py を新規に作成しましょう。

scraper.py にこの2つのメソッドで使用するモジュールをインポートします。

import time
from urllib.robotparser import RobotFileParser
from urllib.parse import urlparse

import requests
from bs4 import BeautifulSoup

scraper.py に移動するメソッドのクラス名は Scraper にしましょう。

class Scraper:

そして、2つのメソッドをコピーします。
ただし、logger に関しては、crawler.py の方のみ使うことにして、
scraper.py では使わず、エラー内容は raise でエラーを返すようにします。

import time
from urllib.robotparser import RobotFileParser
from urllib.parse import urlparse

import requests
from bs4 import BeautifulSoup

class Scraper:
    """ Scraper
    get_robot_txt(url) : robots.txt の中身を精査
        url: robots.txt を確認するサイトURL
        return True, False
    get_html(url, params=None, headers=None) : サイトの情報を取得
        url: データを取得するサイトのURL
        [params]: 検索サイトのパラメーター {x: param}
        [headers]: カスタムヘッダー情報
    """

    def get_robots_txt(self, url):
        """ get_robots_txt
        url: robots.txt を確認するサイトURL
        """
        try:
            # robots.txt 用パーサー
            rp = RobotFileParser()
            # robots の url 取得
            parsed_url = urlparse(url)
            robots_url = "{0.scheme}://{0.netloc}/robots.txt".format(parsed_url)
            # robots.txt 取得
            rp.set_url(robots_url)
            rp.read()
            # 取得していいか確認
            return rp.can_fetch("*", url)
        except Exception as e:
            raise Exception(e.args[0])

    def get_html(self, url, params=None, headers=None):
        """ get_html
        url: データを取得するサイトのURL
        [params]: 検索サイトのパラメーター {x: param}
        [headers]: カスタムヘッダー情報
        """
        try:
            # 待機
            time.sleep(5)
            # データ取得
            resp = requests.get(url, params=params, headers=headers)
            resp.encoding = resp.apparent_encoding
            # 要素の抽出
            soup = BeautifulSoup(resp.text, "html.parser")
            return soup
        except Exception as e:
            raise Exception(e.args[0])

エラーを返すようにしたのは、
・ログを返すようにしていない
・予期せぬエラーの場合はきちんとエラー処理とするべき
・エラーとして返すことでメソッドを呼び出した側で柔軟な対応ができる

というような考えをもっているからです。
ここでの考え方は人それぞれだと思います。
今後の拡張に合わせて考えて、どのような動作をイメージしているかで決めていいと思います。

Scraper クラスをクラスメソッドにする

Scraper クラスを作りましたが、これをモジュールとして使えるようにクラスメソッドにしてみましょう。

今のコードの状態だと、インスタンス変数を作成してメソッドを読み出す形になります。

from scraper import Scraper

scraper = Scraper()

text = scraper.get_html("https://www.google.com")
print(text.title.get_text())

出力される値はタイトルの「Google」がプリントされます。

ここでは、scraper 変数に Scraper クラスのインスタンスを作成して
そのインスタンスから get_html メソッドを起動しています。

これをインスタンス変数にせずに、そのままクラスからメソッドを起動できるようにしてみたいと思います。

クラスメソッドにするためには、@classmethod を使用します。
メソッドの頭にこれを入れることでクラスメソッドとして使用できます。
さらに、クラス自身を示す変数 self を cls に変更する必要があります。

@classmethod
def get_robots_txt(cls, url):

@classmethod
def get_html(cls, url, params=None, headers=None):

メソッドの中の self も cls に変更します。
ただ、今回のコードでは使用するところはありませんでした。

こうすることで、メソッドの使用方法が変わります。

from scraper import Scraper

text = Scraper.get_html("https://www.google.com")
print(text.title.get_text())

やっていることは同じですが、インスタンス変数 scraper を作成せずに、
Scraper クラスから直接メソッドを実行出来るようになっています。

このやり方はモジュールを実行するやり方と似ていますね。
外部からモジュールをインポートしてそれを使用する形になりました。

編集後の scraper.py のコードはこの様になっています。

import time
from urllib.robotparser import RobotFileParser
from urllib.parse import urlparse

import requests
from bs4 import BeautifulSoup

class Scraper:
    """ Scraper
    get_robot_txt(url) : robots.txt の中身を精査
        url: robots.txt を確認するサイトURL
        return True, False

    get_html(url, params=None, headers=None) : サイトの情報を取得
        url: データを取得するサイトのURL
        [params]: 検索サイトのパラメーター {x: param}
        [headers]: カスタムヘッダー情報
    """

    @classmethod
    def get_robots_txt(cls, url):
        """ get_robots_txt
        url: robots.txt を確認するサイトURL
        """
        try:
            # robots.txt 用パーサー
            rp = RobotFileParser()
            # robots の url 取得
            parsed_url = urlparse(url)
            robots_url = "{0.scheme}://{0.netloc}/robots.txt".format(parsed_url)
            # robots.txt 取得
            rp.set_url(robots_url)
            rp.read()
            # 取得していいか確認
            return rp.can_fetch("*", url)
        except Exception as e:
            raise Exception(e.args[0])

    @classmethod
    def get_html(cls, url, params=None, headers=None):
        """ get_html
        url: データを取得するサイトのURL
        [params]: 検索サイトのパラメーター {x: param}
        [headers]: カスタムヘッダー情報
        """
        try:
            # 待機
            time.sleep(5)
            # データ取得
            resp = requests.get(url, params=params, headers=headers)
            resp.encoding = resp.apparent_encoding
            # 要素の抽出
            soup = BeautifulSoup(resp.text, "html.parser")
            return soup
        except Exception as e:
            raise Exception(e.args[0])

crawler.py の修正

スクレイピング周りを scraper.py に移動しました。
それをふまえて crawler.py を修正していきます。

まずは、モジュールのインポートです。
必要なインポートは logger だけになります。
合わせて今回切り分けた scraper.py もインポートします。

from logging import basicConfig, getLogger, DEBUG

from scraper import Scraper

Scraperを使う部分をクラスからメソッド実行出来るようにします。

soup = self.get_html(search_url, search_params, search_headers)
soup = Scraper.get_html(search_url, search_params, search_headers)

ここも編集します。

if crawler.get_robots_txt(url):
    soup = crawler.get_html(url)
if Scraper.get_robots_txt(url):
    soup = Scraper.get_html(url)

あとは、scraper.py に移動したメソッドを削除して完成です。

最後に

今回作成した crawler.py はこちらです。

from logging import basicConfig, getLogger, DEBUG

from scraper import Scraper


# ログフォーマットを定義
formatter = "[%(asctime)s][%(name)s][%(levelname)s]%(message)s"
# ログレベル
basicConfig(level=DEBUG, format=formatter)


class Crawler:
    """ Crawler
    self.user_agent : ユーザーエージェントの情報を設定
    self.engine : 検索サイトを設定

    get_search_url(word, engine="google") : 検索サイトで検索結果の一覧を取得
        word: 検索するワード
        [engine]: 使用する検索サイト(デフォルトは google)
    """

    def __init__(self, engine="google"):
        # User-Agent
        self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
                           AppleWebKit/537.36 (KHTML, like Gecko) \
                           Chrome/69.0.3497.100"
        # engine
        self.engine = engine

    def get_search_url(self, word):
        """ get_search_url
        word: 検索するワード
        [engine]: 使用する検索サイト(デフォルトは google)
        """
        logger = getLogger("get_search_url()")
        try:
            if self.engine == "google":
                # google 検索
                search_url = "https://www.google.co.jp/search"
                search_params = {"q": word}
                search_headers = {"User-Agent": self.user_agent}
                # データ取得
                soup = Scraper.get_html(search_url, search_params, search_headers)
                if soup != None:
                    tags = soup.select(".r > a")
                    urls = [tag.get("href") for tag in tags]
                    return urls
                else:
                    raise Exception("No Data")
            else:
                raise Exception("No Engine")
        except Exception as e:
            logger.warning(e)
            return None


if __name__ == '__main__':
    try:
        # インスタンス化
        crawler = Crawler()
        urls = crawler.get_search_url("python")
        if urls != None:
            for url in urls:
                if Scraper.get_robots_txt(url):
                    soup = Scraper.get_html(url)
                    print(soup.title.get_text())
                else:
                    print("クロールが拒否されました [{}]".format(url))
        else:
            print("取得できませんでした")
    except Exception as e:
        print("エラーになりました")

ファイルは以下の2つです。
・crawler.py
・scraper.py

---

気に入っていただけたら、フォローや好きをお願いします!

note 連載目次

【初心者向け】Pythonでスクレイピングする環境を作る① はじめに
【初心者向け】Pythonでスクレイピングする環境を作る② Dockerの使い方
【初心者向け】Pythonでスクレイピングする環境を作る③ VSCodeでDocker環境を構築する
【初心者向け】Pythonでスクレイピングする環境を作る④ requestsでデータを取得してみる
【初心者向け】Pythonでスクレイピングする環境を作る⑤ Google検索をしてみる
【初心者向け】Pythonでスクレイピングする環境を作る⑥ スクレイピングでの注意事項
【初心者向け】Pythonでスクレイピングする環境を作る⑦ 検索結果のページのタイトルを取得する
【初心者向け】Pythonでスクレイピングする環境を作る⑧ クラスにまとめてみる
【初心者向け】Pythonでスクレイピングする環境を作る⑨ テストしてみる
【初心者向け】Pythonでスクレイピングする環境を作る⑩ crawler と scraper を分ける

ここから先は

0字

¥ 100

この記事が気に入ったらサポートをしてみませんか?