Scrapy 利用ノート¶
Scrapy の利用に関するいろいろなことを記す。公式ドキュメントの注釈という形式にしたかったが、分量が多いのでやめて、前半部分を見ていくだけにする。
Note
本稿執筆時の動作環境は次のとおり。
bash$ scrapy version -v
Scrapy       : 2.4.1
lxml         : 4.6.2.0
libxml2      : 2.9.10
cssselect    : 1.1.0
parsel       : 1.6.0
w3lib        : 1.22.0
Twisted      : 20.3.0
Python       : 3.9.2 (default, Mar  3 2021, 20:02:32) - [GCC 7.3.0]
pyOpenSSL    : 20.0.1 (OpenSSL 1.1.1j  16 Feb 2021)
cryptography : 3.4.6
Platform     : Linux-4.19.128-microsoft-standard-x86_64-with-glibc2.31
インストール¶
私の Python 環境は WSL 2 上に Miniconda を用いた仮想環境だ。したがってインストール手段はほぼ自明なものになる。
Scrapy は Python のサードパーティー製パッケージであるので、コンソールからの次のコマンド実行のどちらかでインストールしたと記憶している。
- conda install -c conda-forge scrapy 
- pip install scrapy 
チュートリアル¶
チュートリアルとしては、公式ドキュメントのチュートリアル Scrapy Tutorial を一通り実施するのが自然だ。 Scrapy はフレームワーク指向のパッケージなので、それを利用者に理解させることを優先した指導内容だ。むしろシェルを先にいじるのがスクレイピングの本質に近いところから始めることになっていいと思うのだが、どうだろう。
Scrapy Tutorial を読み、それを実施したときの私の感想を羅列して示す。
概要¶
このチュートリアルを簡単に説明すると、quotes.toscrape.com という有名な名言集のウェブサイトを scrape するというものだ。 Scrapy の標準的フレームワークに従うプロジェクトを作成し、コードを書き、テストし、データを集めるという構造のチュートリアルだ。したがって、読了後は利用者は次のことを習得していることになる:
- プロジェクトの作成方法 
- スパイダーの実装方法 
- ウェブサイトを crawl してデータを収集する方法 
- コンソールでかき集めたデータをエクスポートする方法 
- リンクを再帰的にたどるようにスパイダーを変更する方法 
- スパイダーに引数を扱わせる方法 
プロジェクトを作成する¶
Scrapy ではスパイダーというクラスを実装することでスクレイピングの一連の処理を表現するのだが、その枠組に乗せるためにプロジェクトと呼ばれる定型的なファイルディレクトリー構造を作成する。このためには次のコマンドを適当なディレクトリーからコンソールで実行する。
bash$ scrapy startproject PROJECT_NAME [PROJECT_DIR]
bash$ cd PROJECT_DIR
チュートリアルでは PROJECT_NAME を tutorial と入力する。
PROJECT_DIR は省略するとプロジェクト名がディレクトリー名として採用される。
コマンドが成功すると、ドキュメントにあるようなディレクトリー構造が生じる。作業ディレクトリーをそこへ cd する。
スパイダーを実装する¶
チュートリアルではファイル tutorial/spiders/quotes_spider.py を新しく編集するが、次のように Scrapy のコマンドを利用したものを編集する習慣をつけるとよい:
bash$ scrapy genspider quotes quotes.toscrape.com
bash$ mv quotes.py spiders
コードとドキュメントを読んで次のクラス構成要素を習得すること:
- name: スパイダーの名前はコマンドラインツールやログを扱うときに重要だ。
- start_requests(): このチュートリアルのように自分で実装する場合には- Requestオブジェクトを return するか yield するということを憶えておくこと。
- parse(): 本来ならばデータを収集して処理するメソッドになる。
このようにして作成したスパイダーを実行するのに、Scrapy のコマンドラインツールを起動する:
bash$ scrapy crawl quotes
実行直後から Scrapy のフレームワークからのログが出力され続けるようならばひとまずよしとする。 Scrapy のスケジューラーが Request オブジェクトを適宜インターネットに飛ばして、得られた応答を基に Response オブジェクトを生成して指定されたコールバック関数、この場合には parse() をその引数として呼び出すというような仕組みだ。
もう一度書くと、スパイダークラスをテンプレートから生成すると
start_requests() を書かずに済む。代わりに、フレームワークがクラス属性
start_urls の URL を順次リクエストするので、こちらのリストの中身を指定することになる。
ログインリクエストなど、特別な Request を要するときに start_requests()
を書くと憶えておく。
データを収集する¶
Scrapy Tutorial ではこのタイミングでシェルの説明が始まるが、別に分けるほうがいいと思う。応答オブジェクトからメソッド呼び出しで DOM ノードを再帰的に参照できるという話をしたいだけだろう。
メソッド parse() に与えられた Response オブジェクトは、要求したページの
DOM オブジェクトを含んでいると思って構わない。このオブジェクトのメンバーデータ
selector から次の問い合わせメソッドを好きに呼び出すことで欲しいデータを収集する:
- .css(): CSS セレクター式を指定してマッチするノードを選択する
- .xpath(): XPath 式を指定してマッチするノードを選択する
どちらの方式でも戻り値のオブジェクトは list の特別なサブクラスだ。特に
.get() や .getall() で文字列や文字列のリストが得られることを理解すること。
本チュートリアルでは parse() でページ内にあるデータから dict オブジェクトをいくつか構築して yield していることが確認できる。生成するオブジェクトの型については後で詳述がある。
ログを見ると、そのようにして生成した辞書オブジェクトがダンプされていることが確認できる。
データをエクスポートする¶
コマンドラインからスパイダーを実行するときに、ふつうは出力先を指定する。ファイルに出力する場合、Scrapy はその拡張子で出力フォーマットを決定する。
bash$ scrapy crawl quotes -O quotes.json
bash$ scrapy crawl quotes -o quotes.jl
bash$ scrapy crawl quotes -o quotes.csv
bash$ scrapy crawl quotes -o quotes.xml
- オプション - -oと- -Oの違いはヘルプを参照。
- JSON の巨大なファイルを処理したいのならば JQ などのツールを導入するといいらしい。 
大規模なクローラーを実装するのであればアイテムパイプラインの導入を検討することとある。
リンクをたどる¶
検索サイトやブログのように、ある程度の分量のページを crawl する方法について説明がある。しかし、あとで Crawler を習得するのでここに書いてあることを習得する必要は実はない。
- スパイダーのメソッドから対象となるリンクを収集する。 - 例えば - response.css(...)などの選択メソッドで- a要素にマッチさせる。戻り値の- href値から- yield response.follow(url, callback=self.parse)などのようにする。
- URL が複数ある場合には一括して - yield from response.follow_all(urls, callback=self.parse)とできる。
 
引数を扱う¶
コマンドラインから -a オプションで引数をスパイダーに引き渡すことができる:
bash$ scrapy crawl quotes -a KEY1=VALUE1 -a KEY2=VALUE2 -a ...
スパイダー内部から参照するには方法が複数あるようだが、self.KEY1 などのようにするやり方もある。
- 安全のため - getattr(self, KEY1)でチェックしてからアクセスするようにすること。
- このような引数はスーパークラスの - __init__()が終了してから有効となる。
基本¶
ここからはドキュメントの BASIC CONCEPTS 内の節のうち、有用なものを拾って読んでいく。
コマンドラインツール¶
構成ファイル scrapy.cfg と環境変数¶
コマンドラインツール scrapy は構成ファイル scrapy.cfg が下記のパスにあるときにロードしに行く。
- /etc/scrapy.cfgや- c:\scrapy\scrapy.cfg
- ~/.config/scrapy.cfgと- ~/.scrapy.cfg
- プロジェクトディレクトリのルートにある - scrapy.cfg
構成ファイルの書式はいわゆる INI ファイルのそれと同じようなものだ。
[settings]
default = myproject.settings
さらに、以下の環境変数を考慮する:
- SCRAPY_SETTINGS_MODULE
- SCRAPY_PROJECT
- SCRAPY_PYTHON_SHELL
ツールコマンド一覧¶
コンソールから scrapy COMMAND options args のようにして起動するコマンドの一覧。プロジェクトが必要なものとそうでないものとに分類できる(ここには記さない)。
| Command | Description | Comment | 
|---|---|---|
| bench | Run quick benchmark test | 今は詳しく知らなくてよい | 
| check | Check spider contracts commands | 自分の書いたスパイダーを調べる | 
| crawl | Run a spider | スパイダーをふつうに実行 | 
| edit | Edit spider | テキストエディターでモジュールを開く | 
| fetch | Fetch a URL using the Scrapy downloader | ふつうは応答の本体を見るがヘッダーを見ることもできる | 
| genspider | Generate new spider using pre-defined templates | テンプレートからモジュールを生成 | 
| list | List available spiders | ここにスパイダークラスの  | 
| parse | Parse URL (using its spider) and print the results | 
 | 
| runspider | Run a self-contained spider (without creating a project) | スパイダークラスを定義したモジュールだけで実行できる | 
| settings | Get settings values | 構成ファイルで設定されている内容を確認する | 
| shell | Interactive scraping console | IPython があればそれが走る | 
| startproject | Create new project | フレームワークに乗るディレクトリー構造を生成する | 
| version | Print Scrapy version | 付随するライブラリーのバージョンも出力できる | 
| view | Open URL in browser, as seen by Scrapy | 
- scrapy crawl SPIDER がふつうの使い方だ。 - SPIDERはプロジェクトにあるスパイダークラスの- nameを指定する。これは- listで確認することもできる。
 
- scrapy edit SPIDER は対応するファイルを既定のエディターで開くだけ。自分でエディターをコンソールから入力するのと手間はほとんど変わらないだろう。 
- scrapy fetch --nolog --headers URL でヘッダーだけを得られる。 
- scrapy genspider SPIDER_NAME ALLOWED_DOMAIN の形式で実行するとスパイダークラスのための Python ファイルを生成する。 - --list,- -lでスパイダーのテンプレの名前を一覧する。- basic
- crawl
- csvfeed
- xmlfeed
 
- --dump=TEMPLATE,- -d TEMPLATEでテンプレの定義を標準出力に表示できる。
- --template=TEMPLATE,- -t TEMPLATEでスパイダークラスのテンプレを指定する。
- --edit,- -eでファイル生成後にそれをエディターを開く。
 
- scrapy parse [options] <url> はプロジェクトにあるスパイダーを使って文字列に関する分析をする。オプションを指定して特別なことをするのに利用する。 - -a NAME=VALUEでスパイダーに引数を渡す。これは複数あってよい。コードではスパイダークラスのメソッドで- self.NAMEの形式で- VALUEを参照する。
- --output=FILE,- -o FILE: スクレイプしたデータを出力するファイルを指定する。- これらは append モードで動く。 
- 標準出力は - FILEとして- -を指定する。
 
- --overwrite-output=FILE,- -O FILE: 上記オプションの write 版。
- --output-format=FORMAT,- -t FORMAT: 出力書式を指定する。- ただし出力先が標準出力の場合には妙な例外が送出されてダメ。 
- 有効な FORMAT は後述。 
 
- --spider=SPIDER: スパイダー- SPIDERを用いるようにする(クラスの定義から自動検出されるものではなく)。
- --callback CALLBACK,- -c CALLBACK: 応答を解釈するコールバックメソッドを指定する。
- --nolinksで抽出したリンクを出力しないようにする。
- --noitemsで抽出したものを出力しないようにする。
 
- scrapy runspider myspider.py とすると、プロジェクトを作る必要がないスパイダーを実行できる。 
- scrapy shell [URL|FILE] が基本形。 - scrapy shell -c CODE でコードを実行。例えば: - bash$ scrapy shell --nolog http://www.example.com/ -c '(response.status, response.url)' 
 
- scrapy startproject <project_name> [project_dir] を実行すると Scrapy が扱えるディレクトリー構造を生成する。 
- scrapy view <url> でブラウザーが開くことになっている。 
スパイダー¶
Scrapy ではスパイダーをクラスで表す。特定のウェブサイトを這い回っていろいろなページから欲しいデータをかき集める方法を指定するものだ。
スパイダーには反復手順とでもいうようなものがあり、だいたい次のようになる:
- 最初の URL を這いずり回るべく、リクエストを生成することから始める。そのリクエストからダウンロードされた応答を処理する関数を指定する。 - これはメソッド - start_requests()の呼び出しでなされる。
- URL を - start_urlsに指定する。形式はテンプレコードを参照。
 
- コールバック関数では応答すなわちウェブページを分析して、文字列分析したアイテムオブジェクトを返したり、 - Requestオブジェクトを返したり、そういうオブジェクトの iterable を返したりする。 ここで返した- Requestオブジェクトがまた(それらが指定する)コールバックに応答が到着する。- コールバック関数ではふつうは - Selectorを利用してページの内容を分析する。それから加工したデータをアイテムとして- yieldする。
- 最後に、スパイダーから返されるアイテムを、ふつうはデータベースに保存したり、ファイルに出力したりする。 
私が Scrapy を使い始めた当初のハードルは上記の 1. と 2. だ。リクエストと文字列処理の連携が非同期的だというのがわかっていなくて、MJ.NETのページ遷移で失敗しまくっていた。
クラス Spider¶
クラス Spider がいちばん単純なスパイダーだ。 上述した start_urls と
start_requests() の連携する既定の実装しか与えない。
主要なプロパティーを表にする:
| Name | Description | Comment | 
|---|---|---|
| 
 | スパイダーの名前 | 
 | 
| 
 | 這いずり回ることを認めるドメイン | リストで指定 | 
| 
 | 這いずり回る URL の始点 | リストで指定 | 
| 
 | Python 標準のログ機能 | 
 | 
主要なメソッドを表にする:
| Name | Description | Comment | 
|---|---|---|
| 
 | スパイダーが這い回るための  | ジェネレーターとして書くのが無難 | 
| 
 | 応答を処理する既定のコールバック | 応答を処理してデータか URL を返す | 
- start_urlsを明示的に設定してある場合、- start_requests()を実装せずに済ませることができる。 反対に、- start_requests()を実装して- start_urlsを無視するということもできる。
スパイダーに引数を渡す¶
コンソールからコマンド crawl や runspider を実行するときにオプション
-a KEY=VALUE でスパイダーに引数を渡せる。
bash$ scrapy crawl MYSPIDER -a KEY1=VALUE1 -a KEY2=VALUE2 ...
スパイダークラスで def __init__(self, KEY=None, *args, **kwargs) のように書くか、メソッド内で self.KEY の形式で参照する。ただしコマンドラインで指定されていない場合には例外が送出する。
Todo
次のオプションは別途処理される?
- http_user
- http_pass
- user_agent
一般的なスパイダー¶
- CrawlSpider: これがふつうのウェブサイドをクロールするのに用いられるスパイダー。- プロパティー - rulesに基づいてクロールするページが決まる。これは- Ruleオブジェクトのリスト。
- メソッド - parse_start_url()をオーバーライドすることがあるかもしれない。
 
- XMLFeedSpider: その名の示すとおりのものをクロールする。クロールというのか?- itertagを指定。その上でメソッド- parse_note()をオーバーライドする。
- 引数 - responseの次の引数が XML のノードを表す。これは- Itemオブジェクトを生成して返す。
 
- CSVFeedSpider: 上記スパイダーの CSV 版。- delimiter,- quotechar,- headersなどを指定。- メソッド - parse_row()をオーバーライドする。引数- rowは辞書オブジェクト。
 
- SitemapSpider:- sitemap.xmlや- robots.txtをクロールするためのスパイダー。
重要なのはクラス CrawlSpider だ。
スパイダークラス CrawlSpider の仕組みを理解するのにクラス Rule を理解する。これはコンストラクターの引数リストから察せられるように、ページ内の URL と処理規則とを結合する役を果たす。一部を示す。
| Parameter | Description | Comment | 
|---|---|---|
| 
 | クロール対象である URL を抽出する  | 後述 | 
| 
 | 抽出されたリンクを処理する callable | そのような callable は  | 
| 
 | 上記 callable のキーワード引数となる  | |
| 
 | 抽出されたリンク先にジャンプするか否かを表す  | |
| 
 | 抽出されたリンクのリストをフィルターするための callable | |
| 
 | 抽出された  | これもフィルターのように実装する | 
セレクター¶
セレクターの使い方¶
Response オブジェクトの .selector を経由してメソッド .css() や
.xpath() で CSS セレクターや XPath を指定することでノードを得るというのが基本形となる。これらを選択メソッドと呼ぶことにする。
Response オブジェクトに対して同名の選択メソッドを呼び出すこともできる。これらは本来のメソッドへの単なるショートカットだ。
Selector オブジェクトを直接生成することもできる。セレクターの練習のときにそうするかもしれない。
- コンストラクターの引数は HTML テキストを表す - strオブジェクトか- HtmlResponseオブジェクトとなる。
選択メソッドの戻り値は SelectorList オブジェクトだ。これに対する次の操作をしっかりと習得すること:
- .get()
- .getall()
- .attrib
CSS セレクターには Scrapy による次の拡張仕様が付与されている。
- ::textはテキストノードを選択する。
- ::attr(name)は属性ノードを選択する。
選択メソッドの戻り値はセレクターのリストであるので、その要素に対しても選択メソッドを呼び出せることに注意すること。
要素ノードの属性を選択する手段が複数あることに注意すること。
メソッド .re() を利用することで Selector オブジェクトに対して正規表現でフィルターすることができる。選択メソッドで抽出し切れないときにこれを併用するのだろう。
XPath の使い方¶
Scrapy に限らず有用なので XPath の基本は別途学習しておくこと。Scrapy をいじるついでに習得してもいい。注意点:
- 絶対パスと相対パスの区別に気をつける(ファイルパスのそれ以上に)。 
- 場合によっては - .css()を併用することになる。CSSクラスが複数ある要素ノードが絡むなど。
- これは Scrapy とは関係なく成り立つのだが、 - //node[1]と- (//node)[1]は異なる。
- XPath 関数 - text()を用いると選択メソッドに対する- .getall()の戻り値が- strのリストになる。
- XPath 関数 - string()を入れ子要素に対して使うと文字列解析が楽になる場合がある。
- XPath 関数 - contains()も使いやすい。
XPath 式で変数を埋め込むことができる。次のコード片はドキュメントより引用した:
response.xpath('//div[@id=$val]/a/text()', val='images')
response.xpath('//div[count(a)=$cnt]/@id', cnt=5)
RSS など、構文解析する対象によっては名前空間外しを必要とする。アクティブなセレクターに対して .remove_namespaces() を呼び出してから
.xpath() を呼ばないとまともに値を返さない。
その他発展的なトピックは省略。まずは基本を習得するのだ。
See also
組み込みセレクター¶
- Selector: 応答の内容の特定の部分を選択するための機能- .attribはノードの属性を表す- dictオブジェクト。
- .xpath(),- .css()は- SelectorListを返す。
- .get()はノードを- strで返す。
- .getall()はノードを- strで表したものからなるリストを返す。
- .re()は正規表現を適用して- strのリストを返す。
- .remove_namespaces()はあまり使いたくないが存在は憶えておくこと。
 
- SelectorList: 組み込み- listのサブクラスに- Selectorで見てきたメソッドのほとんどを加えたもの
アイテム¶
ウェブページにあるデータを構造化することがいちおうスクレイピングの目的だ。 Scrapy はクロール機能だけでなく、そのようなデータを取り扱うための機能も備えている。
- スパイダーは抽出データとして key-value の対を定義する Python オブジェクトを返して構わない。 
- Scrapy はアイテムの種類を複数サポートする。処理コードを書くならどんなタイプのアイテムを使っても構わない。 
Scrapy がサポートするアイテムの種類¶
- Python 組み込みの - dict
- Scrapy が提供するクラス - Item
- デコレーター - dataclassに修飾されるクラス
- デコレーター - attr.sに修飾されるクラス- サードパーティー製ライブラリー 
 
アイテムオブジェクトを使う¶
- クラス - Itemのサブクラスの定義方法を理解する
- クラス - Fieldの性質を理解する
- アイテムオブジェクトの生成方法を習得する - namedtupleに似ている
 
- アイテムオブジェクトのフィールドを参照する方法を習得する - item.get(field_name)または- item[field_name]
- in itemと- in item.fieldsの違いを理解する
 
- アイテムオブジェクトのフィールドに値を代入する方法を習得する - ここが - dictとの大きな違い
 
- Itemのサブクラスをさらに派生させることもできる
アイテムについてはこのへんでいいと思う。
アイテムローダー¶
アイテムローダーにはスクレイプしたアイテムを収容していくことに対する便利な仕組みが備わっている。アイテムが直接的に増やせるものであっても、スクレイプの工程からそれらを増やすためのもっと便利な API がアイテムローダーには備わっている。
アイテムはスクレイプしたデータの入れ物であり、アイテムローダーは入れ物の中身を増やす仕組みと考えていい。
アイテムローダーを使ってアイテムを収容する¶
アイテムローダーの基本的な利用法を記す。まず ItemLoader のオブジェクトを生成する。このときに何かアイテムを与える場合と与えない場合がある。後者の場合にはコンストラクター内部で属性 ItemLoader.default_item_class が指定するアイテムクラスを使う。
アイテムローダーを生成したら、アイテムを構成する値を集めて突っ込む。ふつうはセレクターによる。同じアイテムフィールドに対して一つ以上の値を追加できる。というのも、アイテムローダーは後で適切な加工関数を使って、その値の集まりを join する方法を知ることになる。
スパイダークラスでのよくあるアイテムローダーの利用例コードを見ていく。これは Scrapy のドキュメントから引用したものだ:
import scrapy
from scrapy.loader import ItemLoader
from myproject.items import Product
class MySpider(scrapy.Spider):
    def parse(self, response):
        l = ItemLoader(item=Product(), response=response)
        l.add_xpath('name', '//div[@class="product_name"]')
        l.add_xpath('name', '//div[@class="product_title"]')
        l.add_xpath('price', '//p[@id="price"]')
        l.add_css('stock', 'p#stock')
        l.add_value('last_updated', 'today')
        return l.load_item()
- クラス - Productは自作クラスである。ドキュメントを参照(このデモコードからフィールド定義を想像できると思う)。
- クラス - ItemLoaderのオブジェクトを生成。- ここではアイテムのテンプレを明示的に与えている。 
- キーワード引数 - responseや- selectorを指定して、スクレイプ対象を与える。
 
- ローダーのメソッド - .add_xpath()や- .add_css()により、スクレイプアイテムのフィールドの抽出仕様をローダーに収容していく。先述のように、単一のフィールドに対してロード方法が複数あっても構わない。
- ローダーのメソッド - .add_value()ではスクレイプ対象から値を取らせず、フィールドに好きな値を直接指定する。
- ローダーのメソッド - .load_item()でクラス- Productのオブジェクトを生成することが容易に想像できる。
dataclass アイテムを使う場合¶
サードパーティー製パッケージ dataclasses の @dataclass を利用してアイテムクラスを定義している場合には、フィールドを定義する際に関数 field() をデフォルト引数を指定する手間を要する(ドキュメントを参照)。
入力処理器と出力処理器¶
アイテムローダーには、アイテムフィールドそれぞれに対して入力処理器と出力処理器がそれぞれ一つずつある。先程のデモコードで解説する。
- 最初の - add_xpath()がデータを抽出し、フィールド- nameの入力処理器にそれを渡す。この入力処理結果は収集されてローダー内部のどこかにいったん保持される。アイテムに対しては結果をまだ割り当てない。
- 次の - add_xpath()がデータを抽出し、同じ入力処理器にそれを渡す。この入力処理結果は、ローダー内部のどこかに保持されている収集リストに追加する。
- このような処理がフィールドごと - load_item()呼び出しまでに行われる。
- 最終的収集データは - nameなどのフィールドごとに、対応する出力処理器に渡される。この出力処理結果は、アイテム内の- nameなどのフィールドそれぞれに代入される値だ。
プロセッサーは単に callable オブジェクトであるということを憶えておくといい。型さえ合えばどんな関数でも入力処理器や出力処理器として使えるのだ。
アイテムローダーを実装する¶
アイテムローダーを定義するにはクラス ItemLoader のサブクラスという形式で定義する。ドキュメントに一例がある。
- サードパーティー製パッケージのサブモジュール - itemloaders.processorsが提供する- TakeFirst,- MapCompose,- Joinといった構成要素(おそらくクラス)を利用する。
- ローダーには - _in,- _outで終わる名前のフィールド対を定義する。アンダースコアの前の部分はアイテムフィールドの名前とする。
- オプショナルに、既定の入力・出力処理器を与えることもできる。それぞれ - default_input_processor,- default_out_processorとする。
入出力処理器を定義する¶
入力処理器または出力処理器を宣言するのはローダークラスでもできるし、入力処理器を宣言するのはそのやり方で行うのが普通だ。しかし、アイテムフィールドのメタデータでプロセッサーを指定することもできる。ドキュメントのサンプルコードを一部抜粋するとこうだ:
class Product(scrapy.Item):
    name = scrapy.Field(
        input_processor=MapCompose(remove_tags),
        output_processor=Join())
    price = scrapy.Field(
        input_processor=MapCompose(remove_tags, filter_price),
        output_processor=TakeFirst())
プロセッサーの優先度は次の通り:
- ローダーフィールド固有の属性、つまり - xxxx_in,- xxxx_outのようなフィールド
- フィールドメタデータ、つまりキー - input_processorと- output_processorに対応する値
- ローダーの既定値、つまり - .default_input_processor()と- .default_output_processor()
アイテムローダーの入力処理器と出力処理器のすべて間で共有されるような辞書をアイテムローダーコンテキストという。ローダーコンテキストはローダーを宣言、生成、使用する際に処理関数に渡すことができる。ローダーコンテキストは入力処理器と出力処理器の挙動を変化するのに用いるものだ。
例えば、関数 parse_length() という処理関数があって、それはテキスト値を受け取って、そこから長さを抽出する関数だとする。
def parse_length(text, loader_context):
    unit = loader_context.get('unit', 'm')
    # ... length parsing code goes here ...
    return parsed_length
引数 loader_context を受け取るということで、当処理関数はローダーコンテキストを受け取ることができることをローダーに明示的に教えている。
ローダーコンテキストの値を変える方法はいくつかある。
- ローダーコンテキスト - .contextを直接アクセスする。
- ローダーコンテキスト生成のときに、つまりクラスの - __init__()に対してキーワード引数を与える。こうすることで- .contextにそのキーワードをキー、実引数を値とするエントリーが含まれる。
- ローダー定義における入力・出力処理器の生成時にキーワード引数を与える。 - MapComposeはそれをするものの一つだ。
# 1.
loader = ItemLoader(product)
loader.context['unit'] = 'cm'
# 2.
loader = ItemLoader(product, unit='cm')
# 3.
class ProductLoader(ItemLoader):
    length_out = MapCompose(parse_length, unit='cm')
ItemLoader¶
Todo
クラス ItemLoader のインターフェイスを簡単に述べる(今 Markdown で書くと Pandoc で reST にコンバートするときにムチャクチャなテーブルが生成されるから後回し)。
入れ子ローダー¶
部分文字列や部分集合がそれぞれ文字列、集合であるように、HTML や XML の「部分」にも同じ性質がある。文書の部分から値を文字列解析するときに、部分ローダーを定義することが便利であることがある。
次のような HTML を文字列解析することを考える:
<footer>
    <a class="social" href="https://facebook.com/whatever">Like Us</a>
    <a class="social" href="https://twitter.com/whatever">Follow Us</a>
    <a class="email" href="mailto:whatever@example.com">Email Us</a>
</footer>
入れ子なしローダーと入れ子ありローダーのコードを記す。実際には item=Item()
の式の右辺のオブジェクトは、フィールド social と email があるアイテムでなければならない。
# 1. load stuff not in the footer
loader = ItemLoader(item=Item())
loader.add_xpath('social', '//footer/a[@class = "social"]/@href')
loader.add_xpath('email', '//footer/a[@class = "email"]/@href')
loader.load_item()
# 2. load stuff not in the footer
loader = ItemLoader(item=Item())
footer_loader = loader.nested_xpath('//footer')
footer_loader.add_xpath('social', 'a[@class = "social"]/@href')
footer_loader.add_xpath('email', 'a[@class = "email"]/@href')
# no need to call footer_loader.load_item()
loader.load_item()
後半のコードでは footer_loader のカレントノードが文書中のすべての
<footer> であると考えられることに注意。
入れ子ローダーはコードを単純にすることもあるが、flat is better then nested の精神を忘れてはならない。
アイテムローダーを再利用したり拡張したりする¶
この節にはローダーの設計意図と応用方針が述べられている。今は精読する必要がない。
Scrapy シェル¶
Scrapy シェルは UNIX や Python の IDLE のそれと同様に対話的シェルだ。スパイダーを走らせることなくスクレイピングコードを素早く試すことができる。このシェルはデータを抽出するコードをテストするのに使うことを目的としているが、ふつうの Python シェルとしても利用できる。 XPath 式や CSS セレクター式を対話的にテストするのに使うといい。
シェルを構成する¶
私の環境ではコマンド scrapy shell で IPython が起動する。この振る舞いは構成ファイル scrapy.cfg や環境変数 SCRAPY_PYTHON_SHELL を設定することで変えられる。とはいっても私は IPython ユーザーであるので、以下、IPython がインストールされていることを前提として記す。
シェルを走らせる¶
コマンド scrapy shell の引数に URL やローカルファイルパスを与えるのがふつうだ。ただし、カレントディレクトリーにあるファイルを指定するときには ./
を明示しないと文字列は URL を表すものとして Scrapy に解釈されて失敗する。
bash$ scrapy shell https://www.example.com/
bash$ scrapy shell ./path/to/file.html
bash$ scrapy shell ../other/path/to/file.html
bash$ scrapy shell /absolute/path/to/file.html
bash$ scrapy shell file:///absolute/path/to/file.html
シェルを使う¶
常に利用可能な関数一覧:
| Function | Description | Comment | 
|---|---|---|
| 
 | Scrapy 固有のシェル関数・変数を出力する | その他のオブジェクトも一覧に現れるようだ | 
| 
 | 新しい応答を取得して関連オブジェクトすべてを更新する | |
| 
 | 上記とだいたい同じ | こちらのほうが簡単 | 
| 
 | 応答をブラウザーで表示する | 自動的に削除されない一時ファイルを生成する | 
常に利用可能なオブジェクト一覧:
| Object | Description | Comment | 
|---|---|---|
| 
 | 現在の  | 未習 | 
| 
 | 与えた URL を処理することができる  | 場合によっては  | 
| 
 | 最後に取得したページの  | 
 | 
| 
 | 最後に取得したページを含む  | |
| 
 | 現在の Scrapy 設定 | 
 | 
セッションの例¶
ドキュメントにある例を再現するなり、好きなページでアレンジして試すなりすること。
応答を検査するのにスパイダー内からシェルを起動する¶
関数 scrapy.shell.inspect_response(response, spider) を parse() 内から呼び出すなどする。この機能を breakpoint() 感覚で利用する。
ただしこのセッションでは fetch() を利用してはならない。スパイダーが壊れる。
アイテムパイプライン¶
スパイダーがアイテムを一つスクレイプすると、逐次的に実行される構成要素を通じてそれを加工するためのオブジェクトがある。それをパイプラインと呼ぶ。そのような構成要素のそれぞれは Python のクラスであって、簡単なメソッド一つを少なくとも実装するものだ。
パイプラインはアイテムを一つ受け取り、それに対して一つのアクションを取り、さらにパイプライン処理を後続のパイプラインに継続するか打ち切るかを決定する。
アイテムパイプラインの用途は次のようなものが普通だ:
- データの浄書 
- データを検証(例えばアイテムが特定のフィールドを含むか、など) 
- データの重複チェック(場合によっては一つだけに限定する) 
- データをデータベースなどに格納 
独自のパイプラインを書く¶
この節ではアイテムパイプラインを定義する方法を述べている。
先述したようにアイテムパイプラインはクラスであるので、サブクラスの形式で定義する。そのクラスでは少なくとも次のメソッドを実装する必要がある:
- process_item(self, item, spider):
- このメソッドが各アイテムパイプライン構成要素に対して呼び出される。このメソッドは 
 - アイテムオブジェクトを返すか、 
- Defferedオブジェクトを返すか、
- DropItem例外を送出するか(この場合はパイプライン処理はここで終わる)のいずれかでなければならない。
 
加えて、次に挙げるメソッドを実装しても構わない:
- open_spider(self, spider)
- close_spider(self, spider)
- from_crawler(cls, crawler):- Crawlerはこのクラスメソッドを呼び出してパイプラインオブジェクトを生成する。ドキュメントによると、パイプラインは Scrapy の中核構成要素すべてにアクセスするのに- Crawlerを経由すると読める。
アイテムパイプラインの例¶
サンプルコードの要所だけ説明する。
メソッド process_item() の基本形¶
メソッド process_item() では引数の item の属性を直接「編集」したい。しかし、アイテムが「良くない」場合には DropItem を送出してこのアイテムをないものとする。
def process_item(self, item, spider):
    adapter = ItemAdapter(item)
    if adapter.get('price'):
        if adapter.get('price_excludes_vat'):
            adapter['price'] = adapter['price'] * self.vat_factor
        return item
    else:
        raise DropItem(f"Missing price in {item}")
単一の JSON ファイルへ出力する¶
この例ではスクレイプしたアイテムすべてを単一の jl ファイルに格納するというものだ。
メソッド open_spider() と close_spider() では出力ファイルを決め打ちして開いたり閉じたりする。したがって、プログラマーはパイプラインとスパイダーの対応関係を理解しておかないとまずい。
ちなみに、このクラスを書かなくてもコマンドラインオプション -o items.jl でこれと同じことを実現できる。
データベースへ格納する¶
MongoDB なるものをよく知らないのだが、サンプルコードを見ると普通に SQLデータベースのようなインターフェイスを有するデータストレージだろう。
このコードの急所は from_crawler() の書き方と、open_spider(),
close_spider() でデータベースへの接続を管理するところだ。この例は前述のものと比べると意味がある。
アイテムのスクリーンショットをとる¶
Todo
明らかに、明らかに有用なトピックではあるのだが、準備が要るので後回しとする。
重複を除去するフィルター¶
Todo
ItemAdapter を習得してからでないと、断言できないことが含まれている。例自身は易しい。
自作アイテムパイプラインを有効にする¶
自作アイテムパイプラインを有効にするには設定 ITEM_PIPELINES に当該クラスを追加する必要がある。
ITEM_PIPELINES = {
    'myproject.pipelines.PricePipeline': 300,
    'myproject.pipelines.JsonWriterPipeline': 800,
}
ここで、辞書の各エントリーの値はパイプラインの優先度を表している。アイテムは値の小さいものから大きいものを持つクラスへと流れていく。優先度の値は 0 から 1000 までとする。
Feed exports¶
サードパーティー製ライブラリーの導入など、特別なことをしなくてもエクスポートできるデータ形式:
- JSON 
- JSON line 
- CSV 
- XML 
コマンドラインで -o output.json だの -o output.jl だのと出力先をファイルで指定するとき、Scrapy はこの拡張子からデータ形式を決定する。
Scrapy がサポートする出力先はファイルだけではない。設定次第では FTP や Google Cloud Storage なども使える。
エクスポートに使われる項目は次の通り。
| Key | Description | Comment | 
|---|---|---|
| 
 | 設定全般を包含する  | 指定必須 | 
| 
 | エンコーディング | 既定では UTF-8 | 
| 
 | 空のアイテムをエクスポートするかどうか | 既定では  | 
| 
 | エクスポートしたいフィールドの  | 指定なしの場合は存在するフィールドすべて | 
| 
 | 一階層あたりにのインデント量は空白文字いくつぶんか | JSON や XML で意味がある | 
| 
 | 追加的な格納先を指定する  | 説明略 | 
| 
 | (説明略) | FTP など使わない | 
| 
 | (説明略) | S3 は知らない | 
| 
 | 追加的なエクスポート器を指定する  | 使い途があるのか | 
| 
 | 出力先が複数ファイルにわたるときの、チャンクあたりのアイテム数 | 値を指定するときに初めてそのように動作する | 
このトピックもあまり興味がないので深く立ち入らないで次に行く。
要求と応答¶
クラス Request と Response を見ていく。このフレームワークでは重要な要素だ。後者に対するメソッド呼び出しのほうが圧倒的に多い。
Request のオブジェクトはスパイダー内部で yield されて Scrapy のフレームワークがそれを実行する。そして Response オブジェクトをそのスパイダーに返すというような仕組みだ。
これらのサブクラスの特性を理解しておくことが重要だろう。
クラス Request¶
コンストラクターの使い方を先に習得する。スパイダーの適当なメソッドから次のような感じで生成する:
Request(sample_url, callback=self.parse_sample)
Request(sample_url, callback=self.parse_sample, cb_kwargs=dict(main_url=response.url)
このようなオブジェクトを return または yield すると、新たに Response
オブジェクトを伴って指定したコールバックが呼び出される。
def parse_sample(self, response, main_url):
    # ...
クラス Request の主要フィールド一覧:
| Name | Description | Comment | 
|---|---|---|
| 
 | この要求のエスケープ済み URL を表す文字列 | read-only | 
| 
 | HTTP メソッドを表す文字列 | 大文字 | 
| 
 | HTTP ヘッダーを保持する辞書 | |
| 
 | HTTP ボデーそのもの  | read-only | 
Todo
エラー処理
エラー処理 (errback) については、自作するよりもフレームワークの既定の動作で当分は間に合う。ログ出力で十分わかりやすい。
メタ¶
属性 Request.meta は辞書オブジェクトであってどんなデータでも入れておけるが、
Scrapy が特別扱いするキーもある。例えばキー download_timeout の値はダウンローダーがタイムアウトまで待機する時間を秒単位で指定されているものとして参照される(この項目は設定の DOWNLOAD_TIMEOUT の影響も受ける)。
応答のダウンロードを中止する¶
スパイダークラスで次のようにすると、与えられた応答のダウンロードを中止することができる。つまり bytes_received シグナルのハンドラーを次のように書いて、そこで例外 StopDownload を送出する。
def on_bytes_received(self, data, request, spider):
    raise scrapy.exceptions.StopDownload(fail=False)
Todo
fail=False の指定により、応答に元々の固有の errback が呼び出されるのではなく、コールバックが呼び出されるようになる?
Request のサブクラス¶
- FormRequest: 何かを POST するフォームの対応する- Requestと考えてよい。例えば、よくあるログインページを通過するにはこれを利用することができる。- def parse(self, response, **kwargs): if (uid := getattr(self, 'uid', None)) is None: raise KeyError('missing -a uid=your-user-name') if (password := getattr(self, 'password', None)) is None: raise KeyError('missing -a password=your-password') return FormRequest.from_response( response, formdata={'uid': uid, 'password': password}, callback=self._after_login) 
- JsonRequest: JSON リクエストを処理できるクラス。コンストラクターの- dataに JSON シリアライズ可能なオブジェクトを渡せるということだ。- payload = dict(name1=value1, name2=value2) yield JsonRequest(url, data=payload) 
クラス Response¶
クラス Response はオブジェクトを直接生成するのではなく、フレームワークから受け取るのが基本的だ。
主要フィールド:
| Name | Description | Comment | 
|---|---|---|
| 
 | 応答の URL を表す文字列 | read-only | 
| 
 | HTTP コードを表す数 | 200 とか 404 とか | 
| 
 | HTTP ヘッダーを表す辞書風オブジェクト | これに対して  | 
| 
 | 応答ボデーを bytes で保持する | read-only | 
| 
 | self を生み出した Request オブジェクト | 
 | 
主要メソッド:
| Name | Description | Comment | 
|---|---|---|
| 
 | URL への  | |
| 
 | 複数 URL それぞれへの  | 
 | 
Response の派生クラス¶
Scrapy による派生クラスを記す。Response については派生クラスを利用者が書くこともできる。
TextResponse: クラス Response にエンコーディングの考え方を導入したものと考えてよい。したがって次の主要フィールドが有用だ:
| Name | Description | Comment | 
|---|---|---|
| 
 | 応答本体を  | すなわち  | 
| 
 | 応答のエンコーディングを表す文字列 | Scrapy が適宜解決、決定する | 
| 
 | 応答本文を対象とする  | これにより  | 
主要メソッドは本質的には追加されていない。
- .css()や- .xpath()は- .selectorの同名メソッドへのショートカットに過ぎない。
- .json()なるメソッドが提供されているらしいが、詳細不明。
TextResponse にはさらにサブクラスがある。HtmlResponse と XmlResponse だ。しかし、これらに固有の性質、機能を利用者が用いることはほぼない。
リンク抽出器¶
クラス LinkExtractor 周辺に関するあれこれを記す。
- scrapy.linkextractors.lxmlhtml.LxmlLinkExtractorが真のクラス名だが、- from scrapy.linkextractors import LinkExtractorで使える。
- チュートリアルでは crawler クラスの - rulesに組み込んでいるが、- Reponseオブジェクトさえ手許にあればこれ単体で利用できる。
- コンストラクターとメソッド - .extract_links()だけ理解すれば十分だ。
- コンストラクター - 引数のすべてがキーワード引数。デフォルトでもまともに機能する。その場合はページ中にある - <a>,- <area>の集合を抽出するように振る舞うようだ。 引数- allow,- denyを必要に応じて指定すれば事足りそうだ。
- 引数 - restrict_xpaths,- restrict_cssを利用すれば、あるノード範囲にあるリンクを選択できるだろう。
- 引数 - restrict_textはリンクテキストを制限する。私の場合は- "東風戦"と指定するのだろう。
- 引数 - tagsを使うと- a以外にもリンクを拾える。すぐに思いつくのは- imgだ。しかしこれには- hrefはない。
- そこで引数 - attrsを指定すればいい。- attrs=('src',)とすればいいだろう。
 
- メソッド - .extract_links()- Responseオブジェクトを受け取り、- scrapy.link.Linkオブジェクトのリストを返す。 ここで、クラス- Linkはドキュメント中のリンクを表す要素を表現するものだ。ドキュメントがないので IPython などでインターフェイスを調べる。
 
- クラス - Link- 基本的には構造体のようなものと思っていい。憶えておけばいいのは次のものだけ: - .url: もちろんリンクの URL を表す文字列だ。コメントでは絶対 URL だと言っている。
- .text: リンク要素の開始終了タグに囲まれているリンクテキスト文字列。
- .fragment: URL の- #から後の文字列を保持する。
 
 
設定¶
Scrapy の動作をカスタマイズするための設定方法について記す。
設定モジュールを指定する¶
環境変数 SCRAPY_SETTINGS_MODULE に myproject.settings のような Python モジュールのパス書式に従う文字列を指定すると、どの設定を使うのかを Scrapy に教えることができる。そして、この指定モジュールは Python 標準の検索パスに存在する必要がある。
設定を仕込む¶
色々な仕組みを使って設定を仕込むことができる。それぞれ異なる手続きをとる。優先度の高い順に次のようになる:
- コマンドラインオプション 
- スパイダー別の設定 
- プロジェクト設定モジュール 
- コマンド別の既定の設定 
- 既定のグローバル設定 
コマンドラインオプションが最も優先度が高い。その他の手段による設定内容を上書きする。これには -s KEY=VALUE, --set KEY=VALUE の書式により指定する。
スパイダー別の設定とは、次のように自作スパイダークラスのフィールドに
custom_settings を辞書で与えることで指定するものをいう。
class MySpider(scrapy.Spider):
    name = 'myspider'
    custom_settings = {
        KEY: VALUE,
    }
プロジェクト設定モジュールとは、Scrapy プロジェクト内にあるモジュール
settings.py を指す。なければそれを作成して編集することができる。
前述の Scrapy ツールコマンドそれぞれには固有の既定の設定があり、グローバル設定を上書きする。そのカスタムコマンドの設定をコマンドクラスの属性 default_settings
に指定する。
グローバル既定設定はモジュール scrapy.settings.default_settings にある。
設定にアクセスする方法¶
スパイダークラス内からは self.settings を通じて設定を利用することができる。これは __init__() が終了してから有効になることに注意すること。
組み込み設定項目¶
面白そうな項目をいくつかピックアップしておく。
- DOWNLOAD_DELAY
- DOWNLOAD_MAXSIZE
- LOG_ENABLED,- LOG_LEVELなどログ関連
- RANDOMIZE_DOWNLOAD_DELAY
- USER_AGENTの既定値は Scrapy であることが明白な文字列だ。
応用¶
実践例を記したい。
単体のスクリプトとして実装する¶
文書化されていないが、関数 scrapy.cmdline.execute() というのがある。ここからコマンド scrapy runspider を実行するなどの方法が考えられる:
#!/usr/bin/env python
import getpass
import sys
import scrapy
import scrapy.cmdline
# other import statements...
class MySpider(scrapy.Spider):
    # definition of spider...
if __name__ == '__main__':
    user_id = input('Enter your ID: ')
    password = getpass.getpass('Enter password: ')
    cmdline.execute(f"scrapy runspider {sys.argv[0]} -a uid={user_id} -a password={password}".split())
関連リンク¶
- Scrapy
- 公式サイト。