スクレイピングとクローリングの違いとは?コードで解説(Python)

ENGINEER

プログラムでWeb上から情報を取得する際によく登場する技術用語として、「スクレイピング」と「クローリング」があります。

両者はさまざまな本や解説サイトで登場している用語ですが、これらの用語の意味の違いを正しく理解していますか?

今回はスクレイピングとクローリングの違いを、実際のコードの比較を交えて解説します。

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

スクレイピングとクローリングって何が違うの?違いをコードで解説

スクレイビグは、情報を「抽出」すること

スクレイピング(scraping)とは、「こする」「削りとる」「剥離物」を意味する英単語です。

そこから転じて、「何らかのデータ構造から余分な情報を削りとり抽出すること」を、IT分野では「データスクレイピング」と呼ぶようになりました。

データスクレイピングは特に、プログラム間でやりとりされる人間が読むのに適さないデータから、人間が読めるレベルまで余分な情報を除去し、必要な情報のみ抽出する際によく用いられる言葉です。

なおこのデータスクレイピングのうち、Web上の情報に対象を絞ったものを「Webスクレイピング」と呼びます。

現在、国内のITエンジニア界隈で「スクレイピング」といったら、このWebスクレイピングを指している場合が多いです。

クローリングは、Web上を「巡回」すること

クローリング(crawling)とは、「徘徊」「這いずりまわること」「這うように進むこと」を意味する英単語です。泳法のクロールも、これと同じ意味ですね。

IT分野においては「Web上をプログラムで巡回すること(巡回してなにかをすること)」をクローリングと呼びます。「巡回」に重きが置かれている技術用語であり、単に情報の抽出を意味するスクレイピングとは異なります。なお、Web上を巡回するプログラムそのものは「クローラー」「ボット」「スパイダー」などと呼ばれています。

巡回して何をするかはそのクローリングの目的次第ですが、多くの場合は情報収集のために実行されており、「クローリング = Web上を巡回して情報収集すること」として扱われることがほとんどです。

代表的なところではGoogleの検索エンジンが、検索結果作成のためにクローラーを走らせています。その他にも、企業が機密情報の漏れを確認するためにクローラーを走らせている例もあります。

コードでみるWebスクレイピング

【Webスクレイピング入門編】サイト内構造を解析して狙った情報を抽出してみる

Workship MAGAZINEのENGINEERカテゴリの最新記事タイトルを、PythonでWebスクレイピングしてみましょう。


import requests
import re

# Webスクレイピングする対象ページのURL
target_url = "https://goworkship.com/magazine/engineer/"

# サイト管理者に分かるよう自身の連絡先などをUser-Agentに記載する
headers = {
    'User-Agent': 'foo-Bot/1.0 (xxxxfoo@xxmail.com)'
}

# 対象ページのhtml
html = requests.get(target_url, headers=headers).text

# 対象ページ中の最新記事のタイトルを抽出する
reg = 'article-title.+?>(.+?)<'
first_article_title = re.search(reg, html).group(1)

print(first_article_title)

Webスクレイピングでは、目的の情報を抽出するために事前の解析が重要です。今回は事前調査により、あらかじめ以下を判明させた上で実装しました。

  • Workship MAGAZINE内の記事タイトルは<h3 class=”article-title.”>記事タイトル</h3>という形式で記述されている
  • ENGINEERカテゴリの最新記事は、ENGINEERカテゴリページに表示される記事の中で最上位に記述されている

以上の情報をもとに、正規表現で‘article-title.+?>(.+?)<‘に一致する最初のテキストを取得するようにしました。

Webスクレイピングのコードの特徴は、このように事前の調査・解析で目的の情報がどこにどのような形式で記述されているのかを把握した上で、それを狙いうちにするように実装された抽出処理です。

このコードを実行すると、次のような結果を得られました。

scraping1

無事、Workship MAGAZINEのENGINEERカテゴリの最新記事タイトルをスクレイピングできましたね。

【Webスクレイピング実践編】PythonでWorkship MAGAZINEの記事タイトル一覧を作成する

先ほどは最新記事のタイトルを抜き出すだけのスクレイピングでしたが、今度は一歩掘り下げて、ENGINEERカテゴリの全記事タイトルを取得するスクレイピングをPythonで実装してみましょう。

ENGINEERカテゴリの記事をすべて取得するためには、複数のページを走査する必要があります。

事前の調査で、Workship MAGAZINEについて以下のことが判明しました。

  • ENGINEERカテゴリのURLはパラメータ”page”を渡すことで、指定のページを返却する
  • 1ページに最大21記事のタイトルが表示される
  • この21記事のうち、ページ下部には常に最新記事の一覧が4件と、トレンド記事の一覧が4件表示される(つまり上部13記事が重複のない純粋な記事一覧)

よって、URLパラメータ”page”に渡す値を変えながら全ページにアクセスし、各ページの上から13番目までの記事タイトルを抽出することで、ENGINEERカテゴリの記事タイトルをすべて取得できます。

これを踏まえて、早速コードを実装していきましょう。

なお、最初に紹介したWebスクレイピングのコードでは正規表現でタイトルを抜き出していましたが、今回はPythonでよく使われているHTML, XMLパーサー『BeautifulSoup』を利用します。BeautifulSoupはjQueryなどでもおなじみのCSSセレクタでHTMLの要素を取得するメソッドが用意されており、フロントを中心的に触ってきたエンジニアの方であればかなり扱いやすいライブラリになっています。


import requests
from bs4 import BeautifulSoup
import time

# ENGIINEERカテゴリの全記事のタイトルを格納する変数
all_title_list = []

# 指定するページ(以下while文中でカウントアップしていく)
page_index = 1

# 通信時のHTTPヘッダに設定する値
headers = {
    # User-Agentに設定する値
    # サイト管理者に分かるよう自身の連絡先などを記載する
    'User-Agent': 'foo-Bot/1.0 (xxxxfoo@xxmail.com)'
}

# 全てのページを探索するまでループ
while True:
    print(f'{page_index}ページ目解析開始')
    
    # アクセスする対象ページのURL
    url = f'https://goworkship.com/magazine/engineer/page/{page_index}/'
    
    # 対象ページのHTML
    html = requests.get(url, headers=headers).text
    
    # HTMLを解析したBeautifulSoupオブジェクト
    soup = BeautifulSoup(html, 'html.parser')
    
    # 対象ページの記事タイトルのHTML要素一覧をCSSセレクタで取得
    title_tag_list = soup.select('.article-title')
    
    # 記事タイトルのHTML要素が8つ以下の場合、これ以上記事は存在しないためループを抜ける
    # (最新記事一覧4件とトレンド記事一覧4件を除いて記事一覧が0件の状態)
    if (len(title_tag_list) <= 8):
        print('全ページ走査完了')
        break
    
    # ページ下部に常に表示される最新記事一覧4件とトレンド記事一覧4件を省く
    target_tag_list = title_tag_list[:-8]
    
    # 記事タイトル一覧
    title_list = [tag.string for tag in target_tag_list]
    
    # タイトル一覧をループ外の変数に格納
    all_title_list.extend(title_list)
    
    print(f'{page_index}ページ目解析終了')
    
    # ページのカウントアップ
    page_index += 1
    
    # 次のループに行く前に最低でも1秒以上待機する(サイトに負荷をかけないため)
    time.sleep(2)

# 全記事一覧を出力
for title in all_title_list:
    print(title)

これを実行すると、以下のように全記事のタイトルを取得できます。

scraping2

なお、上記コード中ではURLパラメータ”page”の値を変えながらENGINEERカテゴリ記事一覧の全ページを走査していますが、これはクローリングとは呼びません。

クローリングがプログラム実行中に自身で次にアクセスすべきURLを発見するのに対し、上記コードではあらかじめ『このアドレスにはどんなページがある』とわかった上でそこにアクセスするよう実装しています。

プログラム自身が次にアクセスするURLを発見するわけではないので、クローリングとは呼ばないのです。

コードでみるクローリング

【クローリング入門編】サイト構造を問わず次から次へと発見したページにアクセスしてみる

Workship MAGAZINEのTOPページからWeb上をクローリングして、HTMLを収集するPythonのコードを見てみましょう。


import re
import requests
import time
import random

# クローリングを開始するURL(Workship MagazineのTOPページ)
start_url = "https://goworkship.com/magazine/"

# サイト管理者に分かるよう自身の連絡先などをUser-Agentに記載する
headers = {
    'User-Agent': 'foo-Bot/1.0 (xxxxfoo@xxmail.com)'
}

# アクセスするURL(初期値はクローリングを開始するURL)
url = start_url

# HTMLの格納場所
html_list = []

for i in range(5):
    print(f'{i + 1}ページ目クローリング開始')

    # 対象ページのhtml
    html = requests.get(url, headers=headers).text

    # 取得したHTMLの格納
    html_list.append(html)

    # ページ中のaタグ内のURLを取得する
    url = random.choice(re.findall('<a.+?href="(https://.+?)".*?>', html))

    # 次のループに行く前に最低でも1秒以上待機する(サイトに負荷をかけないため)
    time.sleep(2)

# 収集したHTMLの出力
for i, html in enumerate(html_list):
    print(f'{i + 1}ページ取得結果')
    print(html)

HTMLの収集を行いながら、訪れたページ中のURLをランダムで取得してアクセスするようにしています。

クローリングはさまざまなWebサイトを自動で検出・アクセスする必要があります。そのためコードは以下の2つの特徴を持ちます。

  • 自身が次にアクセスするURLを発見できるようにする
  • 自身がアクセスする全てのWebページで正しく動作する汎用的な処理を行う

上記コードでは、どのページでもURLを取得できるよう一般的なaタグの記述を想定して‘<a.+?href=”(https://.+?)”.*?>’という正規表現を用いました。

前述のWebスクレイピングのコードでは、記事タイトルの抽出のためにWorkship MAGAZINEのサイト構造に特化した正規表現を用いましたが、クローリングの場合は逆にどのサイトでも正しく実行可能な汎用的なコードにする必要があるのです。(※解説のため単純化していますが、実際には相対パスの考慮などを行う必要があります)

上記コードを実行すると、次のような結果が得られます。

crawling1

なおコード中のURLを抽出する処理そのものは、Webスクレイピングにあたります。

クローリングとWebスクレイピングは相反する関係ではなく、Web上の情報収集を自動化する際に相互に利用しあう関係の技術なのです。

【クローリング実践編】 PythonでWorkship MAGAZINEを巡回してページURLを収集する

次に少しだけ本格的なクローリングを実装してみます。

特定のWebサイト内のURLを収集するクローラーの作成と、そのクローラーをWorkship MAGAZINE内で実行する処理を、Pythonで実装します。

なお作成するクローラーには、クローラーとしての基本的な巡回機能のほかに、次のような機能を実装させます。

  • robots.txtなどのクローラーのアクセス制限を判定する機能
  • 指定したWebサイト内のURL以外を除去する機能
  • 収集したURLの整形機能(URLフラグメントの除去、相対パスから絶対パスへの変換など)
  • 収集したURLの重複削除機能

今回は多数の機能を含むので、これら機能をもつクローラーをクラスとして定義します。

ではさっそく実装をはじめましょう。


import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from urllib.parse import urlparse
import time
import re
import urllib.robotparser


class WebsiteCrawler:
    """
        特定のWebサイト内のリンクを走査し、URLを収集するクローラー.
        走査するWebサイトとその際のユーザーエージェント、収集するURLの限界数を
        コンストラクタで設定して使用する
    """

    def __init__(self, website_url, user_agent, limit_number):
        """
           website_url: 走査するWebサイトのURL
           user_agent: ユーザエージェント名
           limit_number: 収集するURLの限界数
           all_url_list: 収集したURLを格納するリスト
           target_index: 走査する対象の要素番号
           robotparser: robots.txtを解析するパーサ
        """
        self.website_url = website_url
        self.user_agent = user_agent
        self.limit_number = limit_number
        self.all_url_list = [website_url]
        self.target_index = -1
        self.robotparser = urllib.robotparser.RobotFileParser()

    def __read_robots__(self):
        """
           走査対象のWebサイトのrobots.txtを読み込む
        """
        # クローリングを開始する先頭ページのURLの解析
        parsed = urlparse(self.website_url)

        # robots.txtの配置場所
        robots_url = f'{parsed.scheme}://{parsed.netloc}/robots.txt'

        # robots.txtの読み込み
        self.robotparser.set_url(robots_url)
        self.robotparser.read()
    
    def _clean_url_list(self, url_list):
        """
            収集したURLのリストから余分な情報を除外する
        """
        # 走査対象のWebサイトの外のURLを除去
        website_url_list = list(filter(
            lambda u: u.startswith(self.website_url), url_list
        ))

        # 各URL末尾のURLフラグメントを除去
        non_flagment_url_list = [(
            lambda u: re.match('(.*?)#.*?', u) or re.match('(.*)', u)
        )(url).group(1) for url in website_url_list]
        
        return non_flagment_url_list
    
    def _extract_url(self, beautiful_soup):
        """
            BeautifulSoup解析済みオブジェクトから余分な情報を除外したURLを抽出する
        """
        # rel属性がnofollowではないaタグ一覧を取得する
        a_tag_list = list(filter(
            lambda tag: not 'nofollow' in (tag.get('rel') or ''),
            beautiful_soup.select('a')
        ))

        # aタグ一覧からこのページのURL一覧を取得(href属性の値を取得)
        # この際相対パスを絶対パスに変換する
        url_list = [
            urljoin(self.website_url, tag.get('href')) for tag in a_tag_list
        ]

        # 余分な情報を除外したURL一覧を返ぎ却する
        return self._clean_url_list(url_list)

    def crawl(self):
        """
            インスタンス作成時に指定したWebサイトをクローリングし、
            収集したURLのリストを返却する。
        """
        print('クローリング開始')

        # robots.txtの読み込み
        self.__read_robots__()

        while True:

            # 走査対象のindexをカウントアップする
            self.target_index += 1

            # 走査を終えるかの判定
            len_all = len(self.all_url_list)
            if self.target_index >= len_all or len_all > self.limit_number:
                print('クローリング終了')
                break

            # 次のループに行く前に最低でも1秒以上待機する(サイトに負荷をかけないため)
            self.target_index and time.sleep(2)

            print(f'{self.target_index + 1}ページ目開始')

            # 走査対象のURL
            url = self.all_url_list[self.target_index]
            print(f'走査対象:{url}')

            # URLがrobots.txtでアクセス許可されているURLかどうかを判定する
            if not self.robotparser.can_fetch(self.user_agent, url):
                print(f'{url}はアクセスが許可されていません')
                continue  # 次ループへの移行
            
            # 通信結果
            headers = {'User-Agent': self.user_agent}
            res = requests.get(url, headers=headers)

            # 通信結果異常判定
            if res.status_code != 200:
                print(f'通信に失敗しました(ステータス:{res.status_code})')
                continue  # 次ループへの移行
            
            # 通信結果のHTMLを解析したBeautifulSoupオブジェクト
            soup = BeautifulSoup(res.text, 'html.parser')

            # robots meta判定
            for robots_meta in soup.select("meta[name='robots']"):
                if 'nofollow' in robots_meta['content']:
                    print(f'{url}内のリンクはアクセスが許可されていません')
                    continue  # 次ループへの移行
            
            # 解析済みBeautifulSoupオブジェクトから余分な情報を除去したURL一覧を抽出
            cleaned_url_list = self._extract_url(soup)

            print(f'このページで収集したURL件数:{len(cleaned_url_list)}')

            # 収集したURLの追加
            before_extend_num = len(self.all_url_list)
            self.all_url_list.extend(cleaned_url_list)

            # 重複したURLの除去
            before_duplicates_num = len(self.all_url_list)
            self.all_url_list = sorted(
                set(self.all_url_list), key=self.all_url_list.index
            )
            after_duplicates_num = len(self.all_url_list)

            print(f'重複除去件数:{before_duplicates_num - after_duplicates_num}')
            print(f'URL追加件数:{after_duplicates_num - before_extend_num}')
        
        return self.all_url_list

これで、特定のWebサイト内のURLを収集するクローラーのクラスを実装できました。

このクローラーのインスタンスを作成し、実装したcrawlメソッドを実行することで、クローリングを行うことができます。


# メイン処理の実行
if __name__ == '__main__':

    # 走査対象のURL
    target_website = 'https://goworkship.com/magazine/'

    # ユーザーエージェント
    user_agent = 'foo-Bot/1.0 (xxxxfoo@xxmail.com)'

    # 収集するURLの限界件数
    limit_number = 200

    # クローラーの作成
    crawler = WebsiteCrawler(target_website, user_agent, limit_number)

    # クローリングの実行
    result_url_list = crawler.crawl()

    # 収集したURLの出力
    print('-------- 結果出力 --------')
    for url in result_url_list:
        print(url)

    print(f'総件数:{len(result_url_list)}')

これでクローリングを実行できるようになりました。

インスタンス作成時に指定したユーザーエージェント名で、『全URLの走査を終える』か『収集したURLの件数が200件を越える』まで、URLの収集を行うことができます。

さっそく実行してみましょう。

crawling2

ちゃんと重複のないURLを収集してくれました。

まとめ:スクレイピングは「抽出」、クローリングは「巡回」

スクレイピングとクローリングはどちらも主に情報収集のために利用される技術ですが、スクレイピング は情報の「抽出(余分な情報の削りとり)」、クローリングはWeb上の「巡回」に焦点を置いた用語です。そして両者は相反する概念ではなく、クローリングの中でスクレイピングを行うなど、相互に利用しあう技術です。

今回ご紹介したコードでは利用しませんでしたが、Pythonには「Scrapy」という、複雑なクローリング・スクレイピングを行うクローラーを実装できるフレームワークも用意されています。またrequestsなどのHTTP通信用ライブラリは、JavaScript実行後のHTMLの状態を取得できませんが、これを解決するための「ヘッドレスブラウザ」などの技術も存在します(これはPythonに限った話ではありませんが)。

スクレイピング 、クローリングのための有用なライブラリや技術は、他にも多数あります。実装の際にはこれらのライブラリの利用を検討してみても良いでしょう。

 

こちらもおすすめ!▼

SHARE

RELATED

  • お問い合わせ
  • お問い合わせ
  • お問い合わせ