LoginSignup
20
32

More than 5 years have passed since last update.

ScrapyによるWebスクレイピング

Last updated at Posted at 2017-10-07

前置き

O'Reilly Japan の「PythonとJavaScriptではじめるデータビジュアライゼーション」を参考に、勉強をしています。

今回は、Scrapyを使ってWebページのクローリング、スクレイピングを実施します。

Scrapyとは

Scrapyは、スクレイピングとクローリングに有用な機能を持つアプリケーションフレームワークです。
データマイニング, 情報処理, アーカイブなどの幅広い用途に活用することができます。

Scrapyのインストール

以下のコマンドでScrapyをインストールします。

pip install scrapy

Scapyプロジェクトの作成

新しいプロジェクトを作成します。
(フォルダが作成されるので、適切なディレクトリに移動してから実行)

$ scrapy startproject akutagawa_prize
New Scrapy project 'akutagawa_prize', using template directory '/Users/Amatsuka/.pyenv/versions/3.6.1/lib/python3.6/site-packages/scrapy/templates/project', created in:
    /Users/Amatsuka/dev/Python/project/akutagawa_prize

You can start your first spider with:
    cd akutagawa_prize
    scrapy genspider example example.com

作成されたプロジェクトのディレクトリツリーは、下記のようになっています。

$ tree
.
└── akutagawa_prize
    ├── akutagawa_prize
    │   ├── __init__.py
    │   ├── __pycache__
    │   ├── items.py
    │   ├── middlewares.py
    │   ├── pipelines.py
    │   ├── settings.py
    │   └── spiders
    │       ├── __init__.py
    │       └── __pycache__
    └── scrapy.cfg

Scapyシェル

Scrapyはコマンドラインシェルを提供しています。URLからレスポンスコンテキストを作成し、そのコンテキスト内でxpath対象の取得を試しつつ、スクレイピングを進めることができます。

URLからレスポンスコンテキストを作成

日本文学振興会の「芥川賞受賞者一覧」ページのURLからレスポンスコンテキストを作成します。

$ scrapy shell http://www.bunshun.co.jp/shinkoukai/award/akutagawa/list.html
2017-10-07 16:28:53 [scrapy.utils.log] INFO: Scrapy 1.4.0 started (bot: akutagawa_prize)
2017-10-07 16:28:53 [scrapy.utils.log] INFO: Overridden settings: {'BOT_NAME': 'akutagawa_prize', 'DUPEFILTER_CLASS': 'scrapy.dupefilters.BaseDupeFilter', 'LOGSTATS_INTERVAL': 0, 'NEWSPIDER_MODULE': 'akutagawa_prize.spiders', 'ROBOTSTXT_OBEY': True, 'SPIDER_MODULES': ['akutagawa_prize.spiders']}
2017-10-07 16:28:53 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.memusage.MemoryUsage']
2017-10-07 16:28:53 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware',
 'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
 'scrapy.downloadermiddlewares.retry.RetryMiddleware',
 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2017-10-07 16:28:53 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
 'scrapy.spidermiddlewares.referer.RefererMiddleware',
 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2017-10-07 16:28:53 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2017-10-07 16:28:53 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6024
2017-10-07 16:28:53 [scrapy.core.engine] INFO: Spider opened
2017-10-07 16:28:53 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://www.bunshun.co.jp/robots.txt> (referer: None)
2017-10-07 16:28:54 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://www.bunshun.co.jp/shinkoukai/award/akutagawa/list.html> (referer: None)
2017-10-07 16:28:54 [traitlets] DEBUG: Using default logger
2017-10-07 16:28:54 [traitlets] DEBUG: Using default logger
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x102eac320>
[s]   item       {}
[s]   request    <GET http://www.bunshun.co.jp/shinkoukai/award/akutagawa/list.html>
[s]   response   <200 http://www.bunshun.co.jp/shinkoukai/award/akutagawa/list.html>
[s]   settings   <scrapy.settings.Settings object at 0x103c8fac8>
[s]   spider     <DefaultSpider 'default' at 0x103f326a0>
[s] Useful shortcuts:
[s]   fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s]   fetch(req)                  Fetch a scrapy.Request and update local objects
[s]   shelp()           Shell help (print this help)
[s]   view(response)    View response in a browser

公式ドキュメントによると、IPythonがインストールされている場合、標準的なPythonコンソールの代わりにこちらが使用されるみたいです。
(コード補完やシンタックスハイライトが備わっているため、IPythonでの利用が推奨されています)

XPathでの対象指定

Scrapyのxpathクエリを使って、該当ページの全ての<h2>ヘッダを取得してみます。

ChromeのElementsタブを使って、ソースの上にマウスポインタを乗せて右クリックすると[Copy XPath]を選択できるので、xpath取得時などはこの機能を活用した方が楽かと思います。
スクリーンショット 2017-10-07 16.35.27.png

'''
該当ページの全ての<h2>ヘッダを取得する。
(h2sは、PythonのlistオブジェクトのSelectorListで取得される)
'''
In [1]: h2s = response.xpath('//h2')

In [2]: len(h2s)
Out[2]: 1

In [3]: h2s
Out[3]: [<Selector xpath='//h2' data='<h2>芥川賞受賞者一覧 <small>(2017年7月現在)</small><'>]

extractで生データを抽出

extractメソッドを使うことで、xpathセレクタの生の結果を取得することができます。

In [5]: h2s.extract()
Out[5]: ['<h2>芥川賞受賞者一覧 <small>(2017年7月現在)</small></h2>']

 
なお、xpathにtext()と指定することで、本文データを抽出することができます。

In [6]: h2s.xpath('text()').extract()
Out[6]: ['芥川賞受賞者一覧 ']

試しに140回以降のデータを抽出

「140〜最新」タブのデータを取得。

In [7]: content = response.xpath('//*[@id="myTabContent"]')

In [8]: no8 = content.xpath('div[@id="no8"]')

取得したデータから、「受賞者」列を抽出。

In [12]: no8.xpath('dl/*/span[@class="name"]/text()').extract()
Out[12]:
['受賞者',
 '沼田真佑',
 '山下澄人',
 '村田沙耶香',
 '本谷有希子',
 '滝口悠生',
...

同様に、取得したデータから、「受賞作」列を抽出。

In [14]: no8.xpath('dl/*/span[@class="title"]/text()').extract()
Out[14]:
['影裏',
 'しんせかい',
 'コンビニ人間',
 '異類婚姻譚',
 '死んでいない者',
 '火花',
...

受賞作はヘッダとデータ部のclass名が異なるため、受賞者と同様の方法では取得されませんでした。
スクレイピングの際は、DOMの構造により工夫する必要がありそうです。

In [15]: no8.xpath('dl/*/span[@class="title head"]/text()').extract()
Out[15]: ['受賞作']

Scapyスパイダー

Scapyスパイダーを作成して、WEBページのスクレイピングを行います。

Spiderクラス

SpiderクラスにWEBページをクロールして解析するための動作を定義します。
本クラスは、Scrapyプロジェクトのspidersディレクトリに配置します。

akutagawa_prize_spider.py
# -*- coding: utf-8 -*-
import scrapy
from akutagawa_prize.items import AkutagawaPrizeItem


# Scrapyスパイダー
class AkutagawaPrizeSpider(scrapy.Spider):
    name = 'akutagawa_prize_list'
    allowed_domains = ['www.bunshun.co.jp']
    start_urls = ["http://www.bunshun.co.jp/shinkoukai/award/akutagawa/list.html"]

    # HTTPレスポンスのパース
    def parse(self, response):
        contents = response.xpath('//*[@id="myTabContent"]')
        tabIds = contents.xpath('div/@id').extract()

        for tabId in tabIds:
            # データ抽出
            tab = contents.xpath('div[@id="' + tabId + '"]')
            nos = remove_header(tab.xpath('dl/*/span[@class="no"]/text()').extract())
            years = remove_header(tab.xpath('dl/*/span[@class="year"]/text()').extract())
            names = remove_header(tab.xpath('dl/*/span[@class="name"]/text()').extract())
            titles = tab.xpath('dl/*/span[@class="title"]/text()').extract()
            magazines = remove_header(tab.xpath('dl/*/span[@class="magazine "]/text()').extract())

            # 受賞者のない行を削除して行数を揃える
            na_indexes = [i for i, val in enumerate(names) if val == "なし"]
            remove_na_rows(nos, years, names, na_indexes)

            # データのセット
            for (_no, _year, _name, _title, _magazine) in zip(nos, years, names, titles, magazines):
                yield AkutagawaPrizeItem(no=_no, year=_year, name=_name, title=_title, magazine=_magazine)


# リストのヘッダ行削除
def remove_header(items):
    return items[1:]


# リストの指定行削除
def remove_na_rows(nos, years, names, indexes):
    for i in sorted(indexes, reverse=True):
        del nos[i], years[i], names[i]

Itemクラス

Itemクラスには、Spiderが抽出するデータの出力データフォーマットを定義します。

items.py
# -*- coding: utf-8 -*-
import scrapy


class AkutagawaPrizeItem(scrapy.Item):
    no = scrapy.Field()         # 開催数
    year = scrapy.Field()       # 受賞年
    name = scrapy.Field()       # 受賞者
    title = scrapy.Field()      # 受賞作
    magazine = scrapy.Field()   # 掲載誌

プロジェクト設定モジュール

Scrapyプロジェクトでは、プロジェクト用に作成された settings.py ファイルに、設定が記載されます。

デフォルトのsettings.pyのままで、日本語を含むページをクロールしてJSONファイルに出力した際、日本語の文字列が「\uXXXX」というUnicodeの文字列で出力されてしまいました。日本語で出力するためには、settings.pyに下記を追記します。

settings.py
FEED_EXPORT_ENCODING='utf-8'

Scapyスパイダーの実行

利用できるスクレイピングスパイダーの確認

$ scrapy list
akutagawa_prize_list

crawlの実行

$ scrapy crawl akutagawa_prize_list -o akutagawa_prize_list.json
2017-10-08 00:11:58 [scrapy.utils.log] INFO: Scrapy 1.4.0 started (bot: akutagawa_prize)
2017-10-08 00:11:58 [scrapy.utils.log] INFO: Overridden settings: {'BOT_NAME': 'akutagawa_prize', 'FEED_FORMAT': 'json', 'FEED_URI': 'akutagawa_prize_list.json', 'NEWSPIDER_MODULE': 'akutagawa_prize.spiders', 'ROBOTSTXT_OBEY': True, 'SPIDER_MODULES': ['akutagawa_prize.spiders']}
2017-10-08 00:11:58 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.memusage.MemoryUsage',
 'scrapy.extensions.feedexport.FeedExporter',
 'scrapy.extensions.logstats.LogStats']
2017-10-08 00:11:58 [scrapy.middleware] INFO: Enabled downloader middlewares:
...

出力されたJSONファイルの確認

[
    {
        "magazine": "文學界", 
        "name": "沼田真佑", 
        "no": "157", 
        "title": "影裏", 
        "year": "2017上"
    }, 
    {
        "magazine": "新潮", 
        "name": "山下澄人", 
        "no": "156", 
        "title": "しんせかい", 
        "year": "2016下"
    }, 
    {
        "magazine": "文學界", 
        "name": "村田沙耶香", 
        "no": "155", 
        "title": "コンビニ人間", 
        "year": "2016上"
    }, 
...

Scapyスパイダーにより、対象Webページから目的のデータを取得することができました。
今回紹介できていませんが、Scrapyは有用な機能が多く、色々なことに応用できそうです。

参考

Scrapy 1.4 documentation
https://doc.scrapy.org/en/latest/index.html

20
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
32