1
/
5

データ収集の効率を圧倒的に高めるスクレイピングFW【Scrapy】

はじめまして、R&Dに所属している @shimada です。6月より業務委託として、データ分析・収集やマーケティング自動化などをやらせていただいています。

R&Dチームと何をやっているかについては、CTO id:kotamat のエントリーを参照していただければと思います。

R&Dと開発でチームを分けた理由とOKR

一部抜粋

SCOUTER社はSCOUTER、SARDINE共に人材紹介の事業を行っており、「転職者に寄り添う支援活動」を後押しするような機能開発を行っております。 この文脈において、すぐには結果に結びつかないかもしれないが、将来的にはやるべきな機能の検討から調査・開発までをR&Dチームは行っております。具体的には、上記リリースの通り、転職者属性の解析や求人のリコメンド、獲得をAIを用いて解決していくことを行っています。

上記エントリーにあるように、R&Dでは転職者・求人情報の解析やリコメンド・検索エンジンアルゴリズムの改良に取り組み始めているのですが、それらの機能を開発する前の段階として、SCOUTERが所有しているデータ以外にもWeb上から転職者や求人情報を解析するために必要な大量のデータを収集する必要が出てきました。またR&Dでは、データ分析・解析にはPHPではなく主にPythonを使用しています。このような背景から、今回はPython製の強固なスクレイピングフレームワークであるScrapyを採用するに至りました。

Scrapyって?

ScrapyはPython製のフルスタックなスクレイピングフレームワークです。スクレイピングに最低限必要な機能に加えて、スロットリングや非同期実行、リクエスト/レスポンスのフックなど、だいたいなんでもできちゃいます。

Scrapy | A Fast and Powerful Scraping and Web Crawling Framework

また、Djangoにインスパイアされた作りになっており、各コンポーネントの概念さえ把握してしまえば学習コストも高くはありません。

下の図がScrapyのアーキテクチャです。

Architecture overview — Scrapy 1.5.1 documentation

  • Engine:各コンポネーントを制御
  • Scheduler:キューイングとスケジューリング
  • Downloader:リクエストとレスポンスオブジェクトの生成
  • Spider:実際のデータ抽出処理を記述する部分、データをItemオブジェクトに詰めてPipelineに渡す
  • Pipeline:CSVやデータベースへの保存など、データの加工・出力を担当

一見複雑そうですが、SchedulerやDownloaderの部分はScrapyがやってくれるので、実際にコードを書くところはSpiderとPipelineぐらいです。

このエントリーでは、SARDINE人材紹介マガジンを例に、Scrapyの基本的な使い方について紹介していきたいと思います。

Scrapyのインストール

Python2.7にも対応していますが、スクレイピングでは特に文字コード問題に悩まされることになるので、3系を使いましょう。最新の3.7でも問題ありません。

$ pip3 install scrapy

今回は、tutorialという名前でプロジェクトを作成します。

$ scrapy startproject tutorial

下記のようなディレクトリが作成されたと思います。 コマンドもディレクトリ構成もDjangoそっくりですね。

tutorial/
scrapy.cfg
tutorial/
__init__.py
items.py
middlewares.py
pipelines.py
settings.py
spiders/
__init__.py

Spider

サンプルとして人材紹介マガジンのタイトルとURLを取得するSpiderを作成してみます。SARDINE人材紹介マガジンはNuxt.jsで作成されたプロジェクトですが、サーバーサイドでレンダリングされているため通常のサイトと同じようにスクレイピング可能です。

最初に tutorial/spiders/ の下に magazine.pyというファイル名でSpiderを作成します。

import scrapy

class MagazineSpider(scrapy.Spider):
name = 'magazine'

def start_requests(self):
url = 'https://sardine-system.com/media/'
yield scrapy.Request(url, callback=self.parse)

def parse(self, response):
for selector in response.css('.main-wrapper .container a'):
yield {
'title': selector.css('.main-text::text').extract_first(),
'url': response.urljoin(selector.css('::attr(href)').extract_first())
}

上から順番に見ていくと、スクレイピング開始直後に start_requests が呼ばれ、リクエストを作成し yield すると、レスポンス取得後のコールバックとして parse メソッドが呼ばれます。

parse メソッドでは、https://sardine-system.com/media/ からのレスポンスを引数として受け取り、.css() メソッドでHTMLをパースする処理を記述しています。

response.css('.main-wrapper .container a') では、下の画像の部分のHTMLを抽出しています。

Devツールではこんな感じ


.css() メソッドでリターンされるのは、 Selector オブジェクト(のリスト)です。Selector
オブジェクトから、さらに .css() メソッドをチェーンして、最終的に .extract() でテキストとして抽出するのが基本的な流れです。

Selectorの詳しい使い方については公式ドキュメントを参照してください。

Selectors — Scrapy 1.5.1 documentation

HTMLからのデータ抽出は、XPathで記述する方法もありますが、CSSの方が簡単です。普段CSSを書くのと同じようにクラス名やタグを指定すればOKです。

プロジェクトルートで下記のコマンドを入力すると、out.csv
にタイトルとURLの一覧が出力されていると思います。

$ scrapy crawl magazine -o out.csv

簡単ですね!

Scrapy Shell

Python系のライブラリの強みは、iPythonやJupyter notebookでコードをテスト出来るところだと思います。いちいち確認のためにテストやプログラムを実行する必要はありません。

ScrapyにもDjango Shell(Laravelで言うところのtinkerみたいなやつ)のようなシェルが組み込まれており、iPython環境で気軽にターゲットのXPathやCSSのテストを行うことができます。

下記のようにコマンドを入力すると、シェルが立ち上がり、https://sardine-system.com/media/
からのレスポンスが response オブジェクトに自動で格納されます。

$ scrapy shell 'https://sardine-system.com/media/'

試しに、最新記事の作成日を取得してみましょう。

In [x]: pub_date = response.css('.main-wrapper .container a time::text').extract_first()
In [x]: pub_date
Out [x]: '2018/08/09'

また、新しいリクエストオブジェクトを作成し、fetch することでshell上で新しいページに遷移することができます。

In [x]: latest_page = response.css('.main-wrapper .container a::attr(href)').extract_first()
In [x]: r = scrapy.Request(response.urljoin(latest_page))
In [x]: fetch(r)
In [x]: response.url
Out [x]: 'https://sardine-system.com/media/posts/p180809'

取得したCookieも自動で送信してれるので、ログイン後に保護されたページをテストしたい場合でも問題ありません。

Pipeline

スクレイピングしたデータは、CSVやJSONよりデータベースに保存したい場合の方が多いと思います。Scrapyでは、Pipelineを作成し settings.py に追加することで複数のデータソースに出力することが可能です。

例として、MySQL保存用のPipelineを作成してみたいと思います。 最初にテスト用のテーブルを作成してください。

sql
create database scrapy;
create table scrapy.magazine (
`guid` varchar(32) not null primary key,
`title` text null,
`url` text null,
`created` datetime default CURRENT_TIMESTAMP not null,
`updated` datetime default CURRENT_TIMESTAMP not null
);

pipelines.pyMySQLPipeline という名前の新しいPipelineを作成します。

from datetime import datetime
import hashlib
import logging
from twisted.enterprise import adbapi

class MySQLPipeline(object):
def __init__(self, db_pool):
self.db_pool = db_pool

@classmethod
def from_settings(cls, settings):
db_args = {
'host': settings['DB_HOST'],
'db': settings['DB_NAME'],
'user': settings['DB_USER'],
'passwd': settings['DB_PASSWORD'],
'charset': 'utf8',
'use_unicode': True
}
db_pool = adbapi.ConnectionPool('MySQLdb', **db_args)
return cls(db_pool)

def process_item(self, item, spider):
d = self.db_pool.runInteraction(self._upsert, item, spider)
d.addErrback(self._handle_error, item, spider)
d.addBoth(lambda _: item)
return d

def _upsert(self, conn, item, spider):
guid = self.get_guid(item['url'])
now = datetime.utcnow().replace(microsecond=0).isoformat(' ')

conn.execute("""
SELECT EXISTS(
SELECT 1 FROM magazine WHERE guid = %s
)
""", (guid,))
ret = conn.fetchone()[0]

if ret:
conn.execute("""
UPDATE magazine
SET title=%s, updated=%s
WHERE guid=%s
""", (item['title'], now, guid))
else:
conn.execute("""
INSERT INTO magazine (
guid, title, url, created, updated
) VALUES (
%s, %s, %s, %s, %s
)
""", (guid, item['title'], item['url'], now, now))

spider.log('Saved!')

def _handle_error(self, failure, item, spider):
spider.log(failure, logging.ERROR)

def get_guid(self, url):
return hashlib.md5(url.encode('utf-8')).hexdigest()

Scrapyでは twisted という非同期ライブラリを使用しているため、ここではDBへの保存もノンブロッキングにしています。twistedのadbapi というモジュールを使っています。

Twisted RDBMS support — Twisted 15.3.0 documentation

settings.py に作成したPipelineを追加して、もう一度Crawlしてみましょう。

out.csvmagazine テーブルにデータが出力されていれば完成です!

SPAサイトをスクレイピング

最後にScrapyでSPAサイトをスクレイピングする方法を簡単に紹介したいと思います。

前提として、SPAサイトに限らずJavascriptで動的にデータを取得している場合などは、seleniumを使ってブラウザ経由の(Javascript実行後の)HTMLを解析することになります。しかし、seleniumは本来Webアプリケーションのテスト自動化のためのツールであり、スクレイピングをする上で使い勝手がいい訳ではありません。

動的なサイトのスクレイピングを行う場合、Scrapyと同じscrapinghub社によって開発されたSplashというヘッドレスブラウザがおすすめです。

Splash

Splashは、高速なレンダリング以外にも、複数ページの非同期取得・カスタムJavasriptの実行など、スクレイピングに便利な多くの機能を備えています。

Splash - A javascript rendering service

また、"Javascript rendering service" と記述されるように、Javascriptレンダリング後のHTMLを返すAPIインターフェースを備えた "サービス" であるため、単なるブラウザと表現するのは正しくないのかもしれません。

インストールは、公式から提供されているdockerfileでコンテナを作成する方法が簡単です。

$ docker pull scrapinghub/splash
$ docker run -p 8050:8050 scrapinghub/splash

これで render.html エンドポイントに下のようなリクエストを投げることで、Javascriptレンダリング後のHTMLを取得することができるようになりました。

ScrapyからSplashのAPIを使ってスクレイピング

Scrapyから直接APIを叩いてもいいのですが、いくつか問題があるようなので、scrapy-splash
というプラグインが推奨されています。

pipでインストール

$ pip3 install scrapy-splash

scrapy-splash settings.py にミドルウェアとして追加します。

DOWNLOADER_MIDDLEWARES = {
'scrapy_splash.SplashCookiesMiddleware': 723,
'scrapy_splash.SplashMiddleware': 725,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}

SPLASH_URL = 'http://localhost:8050/'

HttpProxyMiddleware の優先度が750に設定されているため、scrapy_splashの優先度は750以下になるよう注意してください。

後はspiderをちょっと変更するだけで動的ページに対応です!

import scrapy
from scrapy_splash import SplashRequest

class MagazineSpider(scrapy.Spider):
name = 'magazine'

def start_requests(self):
url = 'https://sardine-system.com/media/'
yield SplashRequest(url,
callback=self.parse,
endpoint='render.html',
args={'wait': 1.0})

def parse(self, response):
for selector in response.css('.main-wrapper .container a'):
yield {
'title': selector.css('.main-text::text').extract_first(),
'url': response.urljoin(selector.css('::attr(href)').extract_first())
}

終わりに

今回はScrapyの紹介的な内容で終わってしまいましたが、機会があればスクレイピングをする上で実際の業務でつまずいた点やインフラを交えた話もできればなと思います。

Scrapyは、Pythonを技術スタックとして置いていない企業でも採用する価値のあるフレームワークです。もし業務で(趣味でも)クローリング・スクレイピングを行う機会が出てきたら是非Scrapyを検討してみてください。

エンジニアの皆さん!弊社は積極採用中です!
どういったエンジニアチームなのか?、ぜひ一度オフィスに遊びに来ませんか?

フロントエンドエンジニア
最新技術で成長業界の波に乗りたいVue.jsフロントエンドエンジニア募集!
ROXXは「時代の転換点を創る」をビジョンに、2013年に設立。この先何十年も使い続けられるような社会的意義のあるサービスを目指し、現在はHR Techサービスを展開しています。 ■月額制リファレンスチェックサービス『back check』( https://backcheck.jp ) 書類選考や面接だけでは分からない採用候補者の経歴や実績に関する情報を、候補者の上司や同僚といった一緒に働いた経験のある第三者から取得することができる、オンライン完結型リファレンスチェックサービスです。back checkでは、採用予定の職種やポジションに合わせて数十問の質問を自動生成し、オンライン上で簡単にリファレンスチェックを実施できるだけでなく、低単価(※1)での実施が可能であることから、スタートアップから大手企業まで、採用人数やポジションに関わらず、幅広い企業に導入いただいています。2019年10月、正式リリース。2021年7月、累計リファレンスチェック実施人数1万人を突破。オンラインリファレンスサービスを利用した年間リファレンス実施人数No.1獲得(※2)。 ※1…従来のリファレンスチェックサービスと比べて1/10程度の価格。 ※2…商工リサーチ調べ(期間:2020年4月~2021年3月​) ■ 採用企業と人材紹介会社を繋ぐ、求人プラットフォーム『agent bank』(https://agent-bank.com/) 転職決定人数 No.1、掲載求人数 No.1、推薦人数 No.1(※1)のクラウド求人データベースです。「人材紹介会社」は、月額利用料のみで、サービス上に掲載されている約15,000件(※2)の求人に対して、自社で抱える転職希望者を掲載企業に紹介することが可能です。「求人企業」は、完全成功報酬型で募集求人を何件でも無料で掲載。『agent bank』導入中の人材紹介会社から、掲載求人に対して紹介が集まります。また、最大の特徴として、成功報酬を求人ごとに自由に設定いただけるため、従来の人材紹介よりも圧倒的に低コストで採用することが可能です。大手企業からベンチャー/スタートアップ企業まで、幅広い年収・業界・業種の求人が掲載されていることから、転職希望者に紹介できる案件を最大化できるだけでなく、過去の選考結果や業務内容に関する詳細情報が全て蓄積されており、効率だけでなく、紹介の質を大幅に向上することが可能です。 ※1…転職決定者数・推薦人数…東京商工リサーチ調べ(調査期間:2021年1月~12月) ※2…掲載求人数…調査対象に『agent bank + パーソルキャリア』掲載の求人数を含む。東京商工リサーチ調べ(調査期間:2022年4月11日〜15日)
株式会社ROXX
サーバーサイドエンジニア
Laravelでマーケット成長の波に乗りたいエンジニアを募集!
ROXXは「時代の転換点を創る」をビジョンに、2013年に設立。この先何十年も使い続けられるような社会的意義のあるサービスを目指し、現在はHR Techサービスを展開しています。 ■月額制リファレンスチェックサービス『back check』( https://backcheck.jp ) 書類選考や面接だけでは分からない採用候補者の経歴や実績に関する情報を、候補者の上司や同僚といった一緒に働いた経験のある第三者から取得することができる、オンライン完結型リファレンスチェックサービスです。back checkでは、採用予定の職種やポジションに合わせて数十問の質問を自動生成し、オンライン上で簡単にリファレンスチェックを実施できるだけでなく、低単価(※1)での実施が可能であることから、スタートアップから大手企業まで、採用人数やポジションに関わらず、幅広い企業に導入いただいています。2019年10月、正式リリース。2021年7月、累計リファレンスチェック実施人数1万人を突破。オンラインリファレンスサービスを利用した年間リファレンス実施人数No.1獲得(※2)。 ※1…従来のリファレンスチェックサービスと比べて1/10程度の価格。 ※2…商工リサーチ調べ(期間:2020年4月~2021年3月​) ■ 採用企業と人材紹介会社を繋ぐ、求人プラットフォーム『agent bank』(https://agent-bank.com/) 転職決定人数 No.1、掲載求人数 No.1、推薦人数 No.1(※1)のクラウド求人データベースです。「人材紹介会社」は、月額利用料のみで、サービス上に掲載されている約15,000件(※2)の求人に対して、自社で抱える転職希望者を掲載企業に紹介することが可能です。「求人企業」は、完全成功報酬型で募集求人を何件でも無料で掲載。『agent bank』導入中の人材紹介会社から、掲載求人に対して紹介が集まります。また、最大の特徴として、成功報酬を求人ごとに自由に設定いただけるため、従来の人材紹介よりも圧倒的に低コストで採用することが可能です。大手企業からベンチャー/スタートアップ企業まで、幅広い年収・業界・業種の求人が掲載されていることから、転職希望者に紹介できる案件を最大化できるだけでなく、過去の選考結果や業務内容に関する詳細情報が全て蓄積されており、効率だけでなく、紹介の質を大幅に向上することが可能です。 ※1…転職決定者数・推薦人数…東京商工リサーチ調べ(調査期間:2021年1月~12月) ※2…掲載求人数…調査対象に『agent bank + パーソルキャリア』掲載の求人数を含む。東京商工リサーチ調べ(調査期間:2022年4月11日〜15日)
株式会社ROXX
株式会社ROXXでは一緒に働く仲間を募集しています
6 いいね!
6 いいね!
同じタグの記事
今週のランキング
株式会社ROXXからお誘い
この話題に共感したら、メンバーと話してみませんか?