rohaniのブログ

ゆるっと自然言語処理奴。ときどき工作系バイト。

Scrapyでリンクを辿りながら欲しい情報をクロールする

Scrapy

Python製Crawlerライブラリのひとつで、ブログ記事をクロールするために現在利用を検討している。

初手で日本語の入門記事を参考にして何度か痛い目を見ているので、おとなしく本家のScrapy Tutorialを読んだ。この記事では、公式のDocumentを参考に勉強したことをまとめる。

Scrapyでできること

  • 基本:websiteの内容を抽出&保存する
  • HTML/XMLソースからCSSXPathを使って簡単にデータを選択抽出するための補完
  • インタラクティブなShellコンソールも利用可能(簡単にscrapingを試せる)
  • ローカルやFTP等の環境にJSON等の形で簡単にファイル生成するための補完
  • 文字コード関係の補完
  • signalsとAPIを使った独自機能のプラグインが可能??
  • 様々な拡張ビルドインとミドルウェア
    • cookiesとsessionの操作
    • cachingなどのHTTP機能
    • user-agentになりすます
    • robots.txtに関する機能(クローラに対する指令書)
    • クロール深度制限
  • scrapyプロセスの監視&操作のためのTelnet console
  • サイトマップなどからサイトをクロールするための再利用可能なスパイダー
  • 画像自動ダウンロード機能
  • caching DNS resolver

(webの知識が浅いので見当違いな訳をしているかも)

始め方

Scrapyのインストール

$ pip install scrapy

欲しいデータの手がかり表現を見つける

クロール対象のソースファイルを見て、HTMLタグなどの手がかり表現を見つける。

例えば、以下のような構造を持つファイルを対象とするとき、

<div class="quote">
    <span class="text">“The world as we have created it is a process of our
    thinking. It cannot be changed without changing our thinking.”</span>
    <span>
        by <small class="author">Albert Einstein</small>
        <a href="/author/Albert-Einstein">(about)</a>
    </span>
    <div class="tags">
        Tags:
        <a class="tag" href="/tag/change/page/1/">change</a>
        <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
        <a class="tag" href="/tag/thinking/page/1/">thinking</a>
        <a class="tag" href="/tag/world/page/1/">world</a>
    </div>
</div>

<ul class="pager">
    <li class="next">
        <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
    </li>
</ul>

上記の例からテキスト・著者名・タグ・次ページへのリンクを抽出したい場合なら、

  • 抽出範囲:< div class="" > と < /div > で囲まれた範囲
  • テキスト:< span class="text" > ココ < /span >
  • 著者:< small class="author" > ココ < /small >
  • タグ:< div class="tag" > < a class="tag" href="~~~" > ココ < /a > < /div >
  • リンク: < li class="next" > < a href="ココ" > Next < span aria-hidden="true" > → < /span > < /a >

に目星をつける。

対話形式でクロールしてみる

ちゃんと取り出せるかをSpiderのShellConsorlで確認する。

  • $ scrapy shell <クロール対象ドメイン url>
    SpiderShellConsorlを起動。
  • >>> response.css('<css/xpath表現>')
    SelectorListオブジェクトを返す。
    • 'div.quote'
      例) < div class="quote" > ... < /div > の範囲を抽出。
    • 'span.text::text'
      例) < span class="text" > ABCDE < /span > のABCDEを抽出。
  • >>> response.css('<css/xpath表現>').extract()
    dataが入ったlistオブジェクトを返す。
  • >>> response.re(r'<正規表現>')
    SelectorListオブジェクトを返す。

欲しい情報を取って来れそうであることが確認できたら、クロールプログラムを作る。

新規プロジェクトの作成

Scrapy projectの基本構造は以下の通り。

scrapy.cfg  #デプロイ設定ファイル
myproject/  #プロジェクトディレクトリ
    __init__.py
    items.py
    middlewares.py
    pipelines.py
    settings.py
    spiders/  #Spiderディレクトリ
        __init__.py
        spider1.py
        spider2.py
  • $ scrapy startproject <プロジェクト名>
    上記構造を持ったファイル群が作られる。

新規Spider(クローラ)の作成

  • $ scrapy genspider <spider名> <クロール対象ドメイン url>
    Spiderの雛形ソースファイルが生成される。構造図で言うとspider1.pyなど。-t <template>をspider名の前に入れることでテンプレートを指定できる。
  • $ scrapy genspider -l
    spiderファイルのテンプレート一覧を取得。

Itemファイルの編集

一般的にスクレイピングでは、web pagesなどのごちゃっとしたデータから構造化されたデータを抽出したい。SpidersはPythonのdict型でデータを抽出できるが、dict型の構造化はあまり厳密なものではない。そこで、データ出力フォーマットを定義するItem Classが提供されている。

# items.pyの例
import scrapy
class QuotesItem(scrapy.Item):
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

Spiderファイルの編集

import scrapy
from myproject.items import QuotesItem
class QuotesSpider(scrapy.Spider):
    name = 'author'  #<spider名>
    start_urls = ['<クロール対象ドメイン url>']  #AuthorSpiderクラスが呼ばれる時にまず呼ばれる。
    def parse(self, response):  #データ抽出
        for quote in response.css('div.quote'):
            qitems = QuotesItem()
            qitems['text'] = quote.css('span.text::text').extract_first()
            qitems['author'] = quote.css('span small::text').extract_first()
            qitems['tags'] = quote.css('div.tags a.tag::text').extract_first()
            yield qitems
        
        yield response.follow(response.css('li.next a::attr(href)')[0], callback=self.parse)

Spiderファイルの補足説明

start_urlsが呼ばれると、裏側で以下のような処理が実行される。

def start_requests(self):
        for url in start_urls:
            yield scrapy.Request(url=url, callback=self.parse)

従って、parse関数が自動的に呼び出される。

また、リンクを辿って次のページに行く以下の部分は、

yield response.follow(response.css('li.next a::attr(href)')[0], callback=self.parse)

これは複数の処理をまとめて行なっている。以下のように書き下すことができる。

next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
    next_page = response.urljoin(next_page)
    yield scrapy.Request(next_page, callback=self.parse)

従ってresponse.follow()response.css('li.next a::attr(href)')[0]が返すSelectorListオブジェクトの第一要素から

  1. dataを抜き出して、
  2. ドメインのurlと結合し、
  3. scrapy.Requestオブジェクトを返す

という3つの操作をしている。 (この場合要素は一つしかあり得ないが、要素数1であってもリスト型で返ってくるので第一要素の指定が必要。)

クロールする

  • $ scrapy crawl <spider名>
    クロール開始
  • $ scrapy list
    spider一覧を取得。

感想

まとめ終えてからQiitaでscrapyと検索したら、出るわ出るわ。 もっと詳しい記事がたくさん。。。 なんと言うことでしょう。。。

はい。今日も勉強おつかーれさんでした。