読者です 読者をやめる 読者になる 読者になる

ボールを蹴りたいシステムエンジニア

ボール蹴りが大好きなシステムエンジニア、ボールを蹴る時間確保の為に時間がある時には勉強する。

Python製のクローラー「scrapy」の利用方法や初期設定など纏め

Python製のクローラーフレームワークscrapyを使用してクローラーを構築する。

環境

Windows10 64bit
Python for Windows(3.5.1)
Eclipse4.6
PyDev(Eclipseプラグイン)
Cygwin

やりたい事

どこかのサイトのクローラーを1時間で実装
今回は参考サイトと同じグノシーを対象とする。

手順

クローラプロジェクトの雛形作成

$ scrapy startproject gunosy_crawl

スパイダーの作成

scrapy genspiderの第1引数はPythonファイル名、第2引数はクロール対象サイトのドメイン

$ cd gunosy_crawl/
$ scrapy genspider gunosy gunosy.com

起点URLを定義

/gunosy_crawl/gunosy_crawl/spiders/gunosy.py

  • start_urls

クロール起点URLを定義、タプルで複数指定も可能。

  • allowed_domains

クロール許可ドメイン、配列で複数指定可能

class GunosySpider(scrapy.Spider):
    name = "gunosy"
    allowed_domains = ["gunosy.com"]
    start_urls = (
    'https://gunosy.com/'
    )

アイテムクラスを実装

クロール結果を保存するアイテムクラスを実装します。

gunosy_crawl/gunosy_crawl/items.py

class GunosyCrawlItem(scrapy.Item):
    title = scrapy.Field()
    url = scrapy.Field()
    text = scrapy.Field()

今回はタイトルとURLと本文を設定する。

パース処理実装

今回はグノシーのニュース記事ページから記事本文を取得対象とする。
例としてこのようなページ
https://gunosy.com/articles/Rol5u

このページのソースをブラウザで確認して記事本文エリアのタグを取得する。
見た感じこれ

<div class="article gtm-click" data-gtm="article_article">
Scrapy Shellによるデバッグ

scrapyのデバッグ機能を使って当該ページのパース処理を確認

$ scrapy shell https://gunosy.com/articles/Rol5u  

実行するとiPythonが起動する。

タグに対応したパース処理実行

response.xpath('//div[@class="article gtm-click"]/*/text()').extract()

テキスト取得された。
当方はcygwinで実行している為、日本語が表示されていないが恐らく問題無し。
配列データとなってるのはdivタグ内のpタグ分が存在している為と思われる。

In [1]: response.xpath('//div[@class="article gtm-click"]/*/text()').extract()
Out[1]: 

['929',
 '',
 '4',
 '4',
 '4,000iOSAndroidMacWindows342',
 '',
 '3',
 '13,5003',
 '22006Discover WeeklyRelease Reader',
 '3Spotify Free',
 '3',
 '']
パース処理の実装

/gunosy_crawl/gunosy_crawl/spiders/gunosy.py

    def parse(self, response):
        
        article_text = response.xpath('//div[@class="article gtm-click"]/*/text()').extract()
        #取得対象のページのみヒットするはず
        if article_text is not None :
            article = GunosyCrawlItem()
            article['title'] = response.selector.xpath('//title/text()')
            article['url'] = response.urljoin
            article['text'] = article_text
            yield article
        else:
            print("not get text data : {0}".format(response.urljoin))
        
        linklist = response.xpath('//a/@href').extract()
        for link in linklist:
            yield scrapy.Request(link, callback=self.parse)
Parse コマンドによるデバッグ
scrapy parse --spider=gunosy https://gunosy.com/articles/Rol5u

以下のように、Scraped ItemsとRequestsの結果を見て、スクレイピング結果とクロールURLの取得が出来ている事を確認します。

# Scraped Items  ------------------------------------------------------------
[{'text': '929 '
          ' '
          '4 '
          '4 '
          '4,000iOSAndroidMacWindows342 '
          ' 3 '
          '13,5003 '
          '22006Discover '
          'WeeklyRelease '
          'Reader '
          '3Spotify '
          'Free '
          '3 '
          '',
  'title': '... - ',
  'url': 'https://gunosy.com/articles/Rol5u'}]
# Requests  -----------------------------------------------------------------
[<GET https://gunosy.com/>,
 <GET https://gunosy.com/users/sign_in>,
 <GET https://gunosy.com/sitemap>,

以下のようなエラーが出た場合、yield scrapy.Requestで返してるURLが不正という事らしいです。

ValueError: Missing scheme in request url: //gunosy.co.jp/

対策として以下のようにurljoinメソッドを使って整形しましょう

            yield scrapy.Request(response.urljoin(link), callback=self.parse)

保存先指定

gunosy_crawl/gunosy_crawl/settings.py

# シードURLのファイル形式
FEED_FORMAT = 'csv'

# シードURLの保存先
FEED_URI = 'file:///tmp/export.csv'

2016/9/5時点では、jsonとjsonlinesは日本語ユニコードデコードに未対応なので使いたい場合はカスタマイズが必要らしい。
Python: Scrapy と BeautifulSoup4 を使った快適 Web スクレイピング | CUBE SUGAR STORAGE

その他設定

gunosy_crawl/gunosy_crawl/settings.py

#ユーザーエージェント
USER_AGENT = 'gunosy_crawl (+http://www.yourdomain.com)'

#True = Webサイトのrobots.txtに従う
ROBOTSTXT_OBEY = True

#Webページのダウンロード間隔
DOWNLOAD_DELAY = 2

#ログ出力先
LOG_FILE = '/tmp/scrapy.log'

色々やりたい時

スクレイピングURLルールを指定したい場合

まず、スーパークラスを変更

class GunosySpider(scrapy.Spider):

↓変更↓

class GunosySpider(CrawlSpider):

スクレイピング対象URLとか指定する

    # スクレイピングを開始するURL、複数指定可
    start_urls = ['https://gunosy.com/']
    
    # スクレイピング対象のパスパターン、ドメイン以下のURLに関して正規表現で指定します
    allow_list = ['/articles/']
    
    # スクレイピング対象外パスパターン、ドメイン以下のURLに関して正規表現で指定します
    deny_list = ['/categories/', '/ranking/']

    rules = (
            # スクレイピングするURLのルールを指定
            Rule(LinkExtractor( allow=allow_list, deny=deny_list), callback='parse_item'),
            # spiderがたどるURLを指定
            Rule(LinkExtractor(), follow=True),
        )

上記callbackで指定した関数を実装。
元々あったparse関数は削除。
※parse関数がある場合はparse関数が優先して実行されるので注意

    def parse_item(self, response):
        
        article_text = response.xpath('//div[@class="article gtm-click"]/*/text()').extract()
        article = GunosyCrawlItem()
        article['url'] = response.urljoin('')
        article['title'] = response.selector.xpath('//title/text()').extract()[0].replace('\n','')
        article['text'] = ' '.join(article_text).replace('\n','')
        print("article_text2 : {0}".format(article))
        #クロール結果を出力
        yield article 
            
        #ページ内の全てのaタグリンクをクロール
        linklist = response.xpath('//a/@href').extract()
        for link in linklist:
            # 次のクロール対象を渡す
            yield scrapy.Request(response.urljoin(link), callback=self.parse)
日本語Unicodeデコードした文字列でjson形式で出力

scrapyでは出力フォーマットの形式をexporterで拡張できるらしい。
独自モジュールを実装して日本語エスケープ文字列を出力前にデコードする。
gunosy_crawl/gunosy_crawl/exporters.py
を新規作成

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from scrapy.contrib.exporter import JsonLinesItemExporter


class NonEscapeJsonLinesItemExporter(JsonLinesItemExporter):

    def __init__(self, filepath, **kwargs):
        super(NonEscapeJsonLinesItemExporter, self).__init__(
            filepath,
            ensure_ascii=False
        )

設定モジュールも編集
gunosy_crawl/gunosy_crawl/settings.py

FEED_FORMAT = 'jsonlines'

FEED_URI = 'file:///tmp/export.json'

FEED_EXPORTERS = {          
    'jsonlines': 'gunosy_crawl.exporters.NonEscapeJsonLinesItemExporter',  
}

jsonlinesというのはJSONを行形式にしたものらしく、その形式を指定。
FEED_EXPORTERSという変数で拡張したexporterを指定する。
ここでjsonlinesを指定して、jsonlinesの時はこのexporterを使用するという意味らしい。

ログ出力

バージョン1.0より前ではscrapy.log.msgメソッドを使用していたようだけど、現在はビルトインのloggingを利用推奨みたい。

import logging
logging.warning("This is a warning")

実行

$ scrapy crawl gunosy

取得結果を確認

less /cygdrive/d/tmp/export.csv 

おわりに

  • scrapy1.0でpython3に対応してからは日本語の解説ページも増えてきてるみたい。
  • 公式のドキュメントを読み漁ればもっと色々できそう。
  • 学習コストもそこまで高くない気がするので、汎用的な処理を実装しておきサイト個別の処理を設定ファイルとかに纏めておけば新たなサイトのクローラーを構築するのも直ぐにできそう。
  • DBとの連携も出来るらしいので今後調査
  • プロセスが落ちた後、新たにプロセスを立ち上げると一度クロール済みのページも再度取得済みなるらしいので対策が無いか調査する
  • Scrapy Cloudというサービスがあるらしいので要調査