無駄と文化

実用的ブログ

九九表のすべてのマスの和

takatoh 様 が同じ問題を Ruby と Scheme で解いてくださいました。許可を得て転記させていたいています
Ruby 版の簡素さはさすがという感じです。そして、Scheme 版の S式 は慣れないと読めないし分からないのが面白いですね。takatoh 様、本当にありがとうございます。


f:id:todays_mitsui:20161012233845p:plain


小ネタです。九九表の81マスに書かれている全ての数を足し合わせる計算をやってみましょう。
いろんな言語で書きます。

意味?特に無いですね。


Python2

import itertools
print(sum(map(lambda (x, y): x*y, itertools.product(range(1, 10), repeat=2))))
# => 2025

実行結果

途中で直積を求める必要があるんですが、itertools を使えば一撃です。さすが。


PHP

<?php
$sum = 0;

foreach (range(1, 9) as $i) {
    foreach (range(1, 9) as $j) {
        $sum += $i*$j;
    }
}

echo $sum;
// => 2025

実行結果

もっと関数型っぽくして行圧縮したかったんですけど、PHP なので潔く PHP らしく。

PHP で直積計算 も出来るようですが、直積だけで20行使ってるのにオエッとなりました。
3引数以上にも対応した一般的な書き方だからですかねぇ。


Haskell

import Control.Applicative
main = putStrLn . show . sum $ (*) <$> [1..9] <*> [1..9]
-- => 2025

実行結果

私が最も愛する言語 Haskell です。

Applicative Functor が便利ですね。
(,) <$> xs <*> ys で直積が取れるし (*) <$> xs <*> ys で直積の生成をすっ飛ばして九九表の全てのマスの生成ができます。


JavaScript (ES2015)

console.log([].concat(...[...Array(9).keys()].map(i=>i+1).map((i,_,ns)=>ns.map(j=>i*j))).reduce((i,j)=>i+j,0))
// => 2025

実行結果

たぶん初見では読めないので適度に分解すると、

// range(1, 9) の代わり
const ns = [...Array(9).keys()].map(i => i+1);

// flatten() の代わり
const flatten = arr => [].concat(...arr);

// sum() の代わり
const sum = ns => ns.reduce((i, j) => i+j, 0);

let result = sum(flatten(ns.map((i, _, ms) => ms.map(j => i*j))));
console.log(result);
// => 2025

こうやってみると JavaScript 貧弱ですねぇ。個人的には好きなんですけど。

Array.range()Array.flatten() も Set.product()Math.sum() も無いとかさすがにツラいです。
直積計算を Array.prototype.map() で無理やりやってるところがダサさポイントですね。


Lazy_K

`k`````s``s``s`ksk`k``sii``s``s`ksk`k``sii``s`k`s``s`ks``s`ki``s``si`k`k`k`k`ki`
kk``s`k`s``s`ks``s`kk``s`k``s``s`ks``s`kk``s`ks``s`k`sik`kk``si`kk``s``s`ksk`k``
si`k`ki```s`k````s``s``s`ksk`k``sii``s``s`ksk`k``sii``s`k`s``s`ks``s`k`s``s`ki``
s``si`k`k`k`k`ki`kkk``s``s`ks``s`k`s`ks``s``s`ks``s`kk``s`ksk`k``s`k`s``s`k``s``
s`ks``s`kk``s`ks``s`k`sik`kk``si`kkk`k`k``si`k`ki```sii```sii``s``s`kski```s``s`
`s`ksk`k``sii``s``s`ksk`k``sii``s`k`s``s`k``s``s`ks``s`kk``s`ks``s`k`sik`kk``s`k
```s`ks``s`k`s`ks`s`kk````s`ks``s`k`s`ks`s`kk````s`ksk```sii``s``s`kski````s`ksk
``s``s`kski``s``s`ksk``s``s`ksk``s``s`ksk``s``s`kski```s``s`ksk``s``s`kski``s``s
`kski``s```s``s``s`ksk`k``sii``s``s`ksk`k``sii``s`k`s``s`ks``s``s`ks``s`k`s`ki``
s`k`s`k``s``si`k`k`ki`kk``s`k`s``si`k``s``s`ks``s`k`s`ks``s``s`ks``s`k`s`ks``s`k
`s`kk``s``s`ksk`k``s`k`s`k`si``s`k`s`kk``s`k`sik`k`kk`k`k`kik``s``s`ks``s``s`ks`
`s`k`s`ki``s`k`s`k``s``si`k`k`ki`kk``s`k`s``s`k`s``si`k``s``s`ks``s`k`s`ks``s``s
`ks``s`k`s`ks``s`k`s`kk``s``s`ksk`k``s`k`s`k`si``s`k`s`kk``s`k`sik`k`kk`k`k`kikk
`k`k`kik``s``s`ks``s`k`s`ks``s``s`ks``s`kk``s`ksk`k``s`k`s``si`k``s``s`ks``s`k`s
`ks``s``s`ks``s`k`s`ks``s`k`s`kk``s``s`ksk`k``s`k`s`k`si``s`k`s`kk``s`k`sik`k`kk
`k`k`kik`k`ki`k````s`ksk``s``s`kski``s``s`ksk``s``s`ksk``s``s`ksk``s``s`kski``s`
k`s``s``s`ki``s``s`k`s`k``s``si`k`k`ki`kk``s`k`s``si`k``s``s`ks``s`k`s`ks``s``s`
ks``s`k`s`ks``s`k`s`kk``s``s`ksk`k``s`k`s`k`si``s`k`s`kk``s`k`sik`k`kk`k`k`kik`k
```s``s`kski``s``s`ksk``s``s`kski`k`ki``s``s`ksk`k``s```s``s``s`ksk`k``sii``s``s
`ksk`k``sii``s`k`s``s`ks``s``s`ks``s`k`s`ki``s`k`s`k``s``si`k`k`ki`kk``s`k`s``si
`k``s``s`ks``s`k`s`ks``s``s`ks``s`k`s`ks``s`k`s`kk``s``s`ksk`k``s`k`s`k`si``s`k`
s`kk``s`k`sik`k`kk`k`k`kik``s``s`ks``s``s`ks``s`k`s`ki``s`k`s`k``s``si`k`k`ki`kk
``s`k`s``s`k`s``si`k``s``s`ks``s`k`s`ks``s``s`ks``s`k`s`ks``s`k`s`kk``s``s`ksk`k
``s`k`s`k`si``s`k`s`kk``s`k`sik`k`kk`k`k`kikk`k`ki`k`k`ki``s`k`s`k`s`k`s``s`ksk`
`s``s`ks``s`k`s`ks``s``s`ks``s`kk``s`ksk`k``s`k`s``si`k``s``s`ks``s`k`s`ks``s``s
`ks``s`k`s`ks``s`k`s`kk``s``s`ksk`k``s`k`s`k`si``s`k`s`kk``s`k`sik`k`kk`k`k`kik`
k`ki`k````s`ksk``s``s`kski``s``s`ksk``s``s`ksk``s``s`ksk``s``s`kski````s``s``s`k
sk`k``sii``s``s`ksk`k``sii``s`k`s``s``s`ki``s``si`k`k`k`k`ki`kk`k`ki``s`k`s``s`k
``s`ks``s`k`s`ks`s`kk``si`kk``s``s`ksk`k``si`k`ki````s``s``s`ksk`k``sii``s``s`ks
k`k``sii``s`k`s``s``s`ki``s``si`k`k`k`k`ki`kk`k`ki``s`k`s``s`k```s``s``s`ksk`k``
sii``s``s`ksk`k``sii``s`k`s``s`ks``s`ki``s``si`k`k`k`k`ki`kk``s`k`s``s`ks``s`kk`
`s`k``s``s`ks``s`kk``s`ks``s`k`sik`kk``si`kk``s``s`ksk`k``si`k`ki``si`kk``s``s`k
sk`k``si`k`ki`````s``s``s`ksk`k``sii``s``s`ksk`k``sii``s`k`s`k`s``s``s`ki``s``si
`k`k`k`k`ki`kk`k`ki``s`k`s``s`ks``s`k`s`k``s``s`ks``s`kk``s`ks``s`k`sik`kk``s``s
`ksk`k``si`kk``s``s`ks``s`k`s`ks`s`kk`k`k``si`k`ki``s``s`k```s``s``s`ksk`k``sii`
`s``s`ksk`k``sii``s`k`s`k`s``s``s`ki``s``si`k`k`k`k`ki`kk`k`ki``s`k`s``s`ks``s`k
`s`k``s``s`ks``s`kk``s`ks``s`k`sik`kk``s``s`ksk`k``si`kk``s``s`ks``s`k`s`ks`s`kk
`k`k``si`k`ki``s`ksk`k`````s``s``s`ksk`k``sii``s``s`ksk`k``sii``s`k`s`k`s``s``s`
ki``s``si`k`k`k`k`ki`kk`k`ki``s`k`s``s`ks``s`k`s`k``s``s`ks``s`kk``s`ks``s`k`sik
`kk``s``s`ksk`k``si`kk``s``s`ks``s`k`s`ks`s`kk`k`k``si`k`ki`s``s`ksk````s``s``s`
ksk`k``sii``s``s`ksk`k``sii``s`k`s``s``s`ks``s`kk``s`ks``s`k`sik`kk``s`k`s``s``s
`ki``s``si`k`k`ki`kk`k`ki``s``s`ksk`k``s``s`ks``s`k`s`ks``s``s`ks``s`k`s`ks``s`k
`s`kk``s``s`ksk`k``s`k`s`k`si``s`k`s`kk``s`k`sik`k`kk`k`k`ki```s``s`ksk``s``s`ks
ki``s``s`kski`````s``s``s`ksk`k``sii``s``s`ksk`k``sii``s`k`s`k`s``s``s`ki``s``si
`k`k`k`k`ki`kk`k`ki``s`k`s``s`ks``s`k`s`k``s``s`ks``s`kk``s`ks``s`k`sik`kk``s``s
`ksk`k``si`kk``s``s`ks``s`k`s`ks`s`kk`k`k``si`k`ki`s``s`ksk````s``s``s`ksk`k``si
i``s``s`ksk`k``sii``s`k`s``s``s`ks``s`kk``s`ks``s`k`sik`kk``s`k`s``s``s`ki``s``s
i`k`k`ki`kk`k`ki``s``s`ksk`k``s``s`ks``s`k`s`ks``s``s`ks``s`k`s`ks``s`k`s`kk``s`
`s`ksk`k``s`k`s`k`si``s`k`s`kk``s`k`sik`k`kk`k`k`ki```s``s`ksk``s``s`kski``s``s`
kski```sii```sii``s``s`kski

書きました!
Lazy K Playground で走らせてみたら結果を得るまでに150秒以上かかりました...。


Ruby

九九表のすべてのマスの和 | blog.PanicBlanket.com より転記。

# encoding: utf-8


def sum_of_kuku
  a = (1..9).to_a
  a.product(a).map{|x,y| x * y}.inject(:+)
end


puts sum_of_kuku

なるほど .inject(:+) のようなメソッド呼び出しで sum() が実現できるんですね。
.product() が標準で提供されているのも流石 Ruby といった感じ。


Scheme

九九表のすべてのマスの和 | blog.PanicBlanket.com より転記。

(use srfi-1)

(define direct-product
  (lambda (lis1 lis2)
    (append-map
      (lambda (x) (map (lambda (y) (list x y)) lis2))
      lis1)))


(define sum-of-kuku
  (lambda ()
    (let ((l1 '(1 2 3 4 5 6 7 8 9))
          (l2 '(1 2 3 4 5 6 7 8 9)))
      (apply + (map (lambda (x) (apply * x)) (direct-product l1 l2))))))


(print (sum-of-kuku))

読めない...。
(apply + (map (lambda (x) (apply * x)) (direct-product l1 l2))) のあたりが本題なんだろうなという感じはしますが、式が評価される様を頭でイメージできなければ、コードの解読も厳しい感じがしますね。


私からは以上です。

Python+OpenCV で顔検出 - OpenCV に付属の評価器を試す

f:id:todays_mitsui:20161003015838j:plain


画像の中から人の顔が写っている場所を自動的に判定する 顔検出 ってやつをやってみようと思います。

そのために OpenCV という有名なライブラリを使用します。OpenCV 自体は様々な言語と組み合わせて使うことが出来るのですが、今回は自分が書き慣れている Python でいきます。
幸いに Python は OpenCV が公式でサポートしている言語(C++, Python, Java)の一つですしね。


評価器を用意する

顔検出をするためには「画像のこの部分は、人の顔である/人の顔ではない」という判定をする 評価器 という部品が必要です。
この評価器は通常、事前に用意した何千枚というテスト画像をプログラムに読ませ、学習させて(機械学習というやつですね)作らなければいけません。

この機械学習で評価器を用意するという工程にとてつもない労力が掛かります。
人の顔が写ったたくさんの画像を用意して、画像の中の顔の部分を指定するなどの下準備が必要になるからです。

ところが、ありがたいことに OpenCV には標準で多くの種類の評価器が付属しています。
今回は OpenCV 標準の評価器を GitHub から落としてきて使おうと思います。

github.com

顔以外の評価器もあるようですが、今回使うのは以下の7つです。

  • haarcascade_frontalcatface.xml
  • haarcascade_frontalcatface_extended.xml
  • haarcascade_frontalface_alt.xml
  • haarcascade_frontalface_alt_tree.xml
  • haarcascade_frontalface_alt2.xml
  • haarcascade_frontalface_default.xml
  • haarcascade_profileface.xml

6つは正面顔(front face)用、1つは横顔(profile face)用です。


基本的な顔検出

シンプルに顔検出して顔の部分を枠で囲むだけならば、非常に少ないコード量で可能です。

import cv2


# 画像の読み込み
image = cv2.imread("img/src/01.jpg")

# 処理速度を高めるために画像をグレースケールに変換
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 評価器を読み込み
cascade = cv2.CascadeClassifier("opencv/data/haarcascades/haarcascade_frontalcatface.xml")

# 顔検出
facerect = cascade.detectMultiScale(
    gray,
    scaleFactor=1.11,
    minNeighbors=3,
    minSize=(30, 30)
)

if 0 != len(facerect):
    BORDER_COLOR = (255, 255, 255) # 線色を白に
    for rect in facerect:
        # 顔検出した部分に枠を描画
        cv2.rectangle(
            image,
            tuple(rect[0:2]),
            tuple(rect[0:2] + rect[2:4]),
            BORDER_COLOR,
            thickness=2
        )

# 結果の画像を保存
cv2.imwrite("img/dest/detected.jpg", image)

import cv2 で読み込んでいるモジュールが OpenCV です。
簡単ですね。


精度を高める

顔検出が簡単に行えるのは分かりました。問題は精度です。
顔検出の間違いには2つのパターンがあります。

  • 第一の過誤 - 顔ではない対象を顔であると誤って検出する
  • 第二の過誤 - 顔である対象を顔であると正しく検出できない

今回は顔検出の際のパラメーターをいろいろに変えつつ、7つの評価器を比べてみます。


と言いつつ、比べてみるパラメーターは minNeighbors だけです。
minNeighbors は信頼度に関するパラメーターで、値を大きくすると第一の過誤のリスクを抑える代わりに第二の過誤のリスクが増します。つまり、何でもない部分を顔だと勘違いしづらくなる代わりに人の顔の部分も見落としやすくなるわけです。
今回は minNeighbors を 3, 10, 20 と変えてそれぞれの評価器の精度を比べてみます。


評価器毎の精度を比較する

信頼度=3 のとき

というわけでこちらをご覧ください。

minNeighbors=3 で各評価器を使って顔検出を走らせた結果です。
ものによっては枠が30個以上描かれて大変なことになってる画像もありますね。これは人の顔ではない対象を誤検出してしまっている状態です。


各評価器の精度を数字で比べましょう。
まずは第一の過誤の割合から。

f:id:todays_mitsui:20161003013209p:plain

f:id:todays_mitsui:20161003013225p:plain

全部で12枚の画像があり、1枚あたり2.4個の顔が写っています。精度の悪い評価器では1枚あたり19.6カ所もの誤検出があるようです。
その中で frontalface_alt2profileface の精度が良いですね。frontalface_alt2 は1枚あたりの誤検出が0.2カ所、非常に優秀です。


第二の過誤についても比べてみましょう。

f:id:todays_mitsui:20161003013846p:plain

f:id:todays_mitsui:20161003013249p:plain

はい、人の顔を検出できなかった割合は、全て横並びで7%という結果になりました。
とは言っても、さきほどの結果の画像を並べたページを見てもらえれば分かるとおり評価器によっては手当たり次第に枠がついている状態なので、まさに下手な鉄砲も数打ちゃ当たるの結果ですね。

信頼度のパラメーターをいじってもう少し比べてみましょう。


信頼度=10のとき

minNeighbors=10 で試して見ます。誤検出は減り、検出できない顔が増えるはずです。


第一の過誤

f:id:todays_mitsui:20161003013858p:plain

f:id:todays_mitsui:20161003013907p:plain

さすがに誤検出は1枚あたり7カ所ほどに減りましたね。
そして先ほど優秀だった frontalface_alt2, profileface がイマイチになり、今度は frontalface_default が優秀です。はて。


第二の過誤

f:id:todays_mitsui:20161003013917p:plain

f:id:todays_mitsui:20161003013932p:plain

続いて見落とし率ですが、信頼度を上げ慎重になった代わりに見落としが増えています。
軒並み4,5割は見落としてますね。これはいただけない。


信頼度=20のとき

もう少し試してみましょう minNeighbors=20 です。


第一の過誤

f:id:todays_mitsui:20161003013941p:plain

f:id:todays_mitsui:20161003013951p:plain

再び frontalface_alt2 が優秀です。驚異の誤検出0%。

第二の過誤

f:id:todays_mitsui:20161003013958p:plain

f:id:todays_mitsui:20161003014006p:plain

5割以上の顔を見落とすようになってしまいました。これ以上 信頼度を上げていっても期待はできないですね。


まとめ

というわけで比較の結果、評価器は frontalface_alt2 を使い minNeighbors=3 の設定で顔検出するのが最も精度を高めることができました。
この設定では人の顔が映った個所を見落とす確率は7%、そして1枚あたり約0.2カ所の誤検出があります。

今回特に検出しづらかったのは、この方のお顔です。

f:id:todays_mitsui:20161003014115j:plain

横顔ですし天地も逆になってますからねぇ。このお方の顔を検出するには画像を回転させながら評価器にかけるなどの工夫が必要そうです。


今回の比較のためのコード一式は下記のリポジトリに置いています。

github.com


また、その他のパラメーターによって顔検出の制度がどのように変わるかについても興味深いところですが、それについてはMakoto Koikeさんが書かれた下記の記事でよく研究されていましたので、興味のある方はそちらを参考にしてみてください。

http://workpiles.com/2015/04/opencv-detectmultiscale-scalefactor/

http://workpiles.com/2015/04/opencv-detectmultiscale-minneighbors/


私からは以上です。

Scrapy でエラーハンドリング for v1.1.3 (※一部未解決)

f:id:todays_mitsui:20160827190511p:plain


突然ですが Scrapy v1.1.0 から Python 3 に対応して嬉しいですね。これまで Scrapy のために 2.7 で通してきたんで。


さて、今回は Scrapy における エラーハンドリング(例外処理) についてまとめようと思います。

スクレイピングという行為は外部の構造化されていないデータを取ってくるものなので例外はつきものです。
例外が投げられたとき 何となく正常終了したように見せる ことは厳禁です。例外から正しく復帰させるか、または例外が投げられたならば正しく落とすことが重要です。
でないと、その後に例外に気づいて調節→リトライできませんからね。


Scrapy データフローに沿ったエラーハンドリング

スクレイピング中に起こる不測の例外をキャッチするために通常の try ... except 文を使う事はできません。
なぜなら、我々が記述した Spider を実際に起動するのは Scrapy エンジンだからです。

我々は parse() メソッドを実装することはあっても、parse() メソッドに response を食わせて呼び出すことはしません。parse() メソッドの呼び出しは Scrapy エンジンの管轄になります。

では parse() メソッドでの処理中に投げられた例外は誰が面倒を見るのでしょうか?
デフォルトでは Scrapy エンジンが控えめにログに書き出してくれます。が、その動作をカスタマイズすることは可能です。


Scrapy でエラーハンドリングするために (私の観測範囲では) 以下の3種類の方法があります。

  1. Request オブジェクトを生成するときに errback を渡す
  2. Spider の spider_error シグナルをキャッチする
  3. Spider Middleware の process_spider_exception で処理する

それぞれ解説していきます。


1. Request オブジェクトを生成するときに errback を渡す

スクレイピング中、parse() メソッドの中で scrapy.Request のインスタンスを返すと、スクレイピング対象 URL を追加できます。
そのとき errback という名前付き引数に関数を渡してあげると、レスポンスの取得に失敗したときにその関数が呼ばれます。

こんな風に、

# spiders/foo.py

import scrapy
from scrapy import Request


class FooSpider(scrapy.Spider):
    name = "foo"
    allowed_domains = ["httpbin.org"]
    start_urls = (
        "http://www.httpbin.org/",           # ステータスコード: 200
        "http://www.httpbin.org/status/500", # ステータスコード: 500
    )

    def start_requests(self):
        for url in self.start_urls:
            yield Request(
                url,
                callback=self.parse,
                errback=self._errback # 200 以外が返ったときに呼ばれる
            )

    def parse(self, response):
        return {
            "url": response.url,
            "status": response.status,
        }

    def _errback(self, failure):
        self.logger.error("### ERRBACK ###")
        self.logger.error(failure.type)
        self.logger.error(failure.getErrorMessage())

Request オブジェクトの errback にコールバック関数を渡しています。
コールバック関数の _errback() はリクエストに対して 200 以外のステータスコードが返ってきたときに呼ばれます。

上記のサンプルコードで start_urls に指定している httpbin(1) は色々なステータスコードで色々なレスポンスを返してくれる便利なサービスです。
1つめURLは ステータスコード200 が、2つめのURLは ステータスコード500 が返ります。なので2つめのリクエストは parse() ではなく _errback() で処理されることになります。

errback で指定したコールバック関数には failure オブジェクトを渡してもらえるので、エラーメッセージなどを取得することも可能です。
今回のログはこんな感じになりました

2016-09-24 22:35:08 [foo] ERROR: ### ERRBACK ###
2016-09-24 22:35:08 [foo] ERROR: <class 'scrapy.spidermiddlewares.httperror.HttpError'>
2016-09-24 22:35:08 [foo] ERROR: Ignoring non-200 response


ところが、この方法は私の求めている例外処理ではありません。
なぜなら parse() メソッドの実行中に何かしらの例外が投げられても、_errback() が呼ばれることは無いからです。

_errback() が呼ばれるのは

  • リクエストに対して 200 以外のステータスコードが返ったとき
  • そもそもDNSルックアップに失敗したとき
  • リクエストがタイムアウトしたとき

この場合に限ります。

ステータスコード200 が返って、そのあとに parse() で例外が投げられたとしても _errback() は呼ばれません。

この方法は「全部でnページ分のリクエストを投げてmページの取得に失敗した」みたいなログを残すのに使うようです。
あくまでもダウンロード時のエラーに対処する目的ですね。


2. Spider の spider_error シグナルをキャッチする

というわけで、本当の例外処理です。

パース中に例外が投げられると、spider_error というシグナルが発生します。
spider_error シグナルにコールバック関数を紐づけることでエラーハンドリングが可能です。

# spiders/bar.py

import scrapy
from scrapy import signals


class BarSpider(scrapy.Spider):
    name = "bar"
    allowed_domains = ["httpbin.org"]
    start_urls = (
        "http://www.httpbin.org/",
    )

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = super(BarSpider, cls).from_crawler(crawler, *args, **kwargs)

        # シグナルとコールバック関数を紐づける
        crawler.signals.connect(spider._handle_error, signal=signals.spider_error)

        return spider

    def parse(self, response):
        bar = 1 / 0 # ZeroDivisionError 例外が発生

        yield {
            "url": response.url,
            "status": response.status,
            "bar": bar,
        }

    def _handle_error(self, failure, response, spider):
        self.logger.error("### HANDLE_ERROR ###")
        self.logger.error(failure.type)
        self.logger.error(failure.getErrorMessage())

from_crawler() の中で signals.spider_error シグナルと _handle_error() メソッドを紐づけています。

parse() メソッドの中の bar = 1 / 0 の行で ZeroDivisionError 例外が投げられます。
それが _handle_error() でキャッチされ、ログにエラーの詳細が吐き出される流れです。


ログはこんな感じ、

2016-09-24 22:38:15 [bar] ERROR: ### HANDLE_ERROR ###
2016-09-24 22:38:15 [bar] ERROR: <class 'ZeroDivisionError'>
2016-09-24 22:38:15 [bar] ERROR: division by zero

例のごとく failure オブジェクトを渡してもらえるので、カスタムのエラーログを書き出したりエラー詳細をメールで通知することも可能でしょう。


ところが、この方法ではパース中の例外の通知を受けることしかできません。
そう、エラーからの復帰が出来ない のです。

というわけで続きます。


3. Spider Middleware の process_spider_exception で処理する

最後の方法です。例外をキャッチして、復帰するところまでをやります。
そのためには Spider Middleware を自作する必要があります。


f:id:todays_mitsui:20160924231058p:plain

Scrapy のアーキテクチャを紹介でよく見る図ですね。Spider と Scrapy エンジンの間にあるのが Spider Middleware です。

カスタム Middleware を定義することで、

  • Downloader から Spider に渡される直前のリクエストオブジェクト
  • Spider から Pipeline に渡される直前のアイテムオブジェクト
  • Spider から投げられた例外オブジェクト

この3つを加工できるようになります。

3つめのやつが本題ですね。
Spider で投げられた例外を Middleware の中で正常なアイテムにすり替えて Pipeline に流すことができるわけです。

これが try ... except で例外から復帰する処理に最も似ていると思いませんか?


カスタム Middleware 自体は普通の class として書きます。

# middlewares.py

class HandleErrorMiddleware(object):
    def process_spider_exception(self, response, exception, spider):
        spider.logger.error("### PROCESS_SPIDER_EXCEPTION ###")
        spider.logger.error(exception)

        return [{
            "url": response.url,
            "status": response.status,
            "error": str(exception),
        }]

このように、

そして、settings.py の中で有効化してあげます。

# Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
SPIDER_MIDDLEWARES = {
    'example.middlewares.HandleErrorMiddleware': 543,
}

そうして、先ほどと同じ Spider を走らせてみると、

2016-09-24 22:54:46 [bar] ERROR: ### PROCESS_SPIDER_EXCEPTION ###
2016-09-24 22:54:46 [bar] ERROR: division by zero
2016-09-24 22:54:46 [scrapy] DEBUG: Scraped from <200 http://www.httpbin.org/>
{'url': 'http://www.httpbin.org/', 'error': 'division by zero', 'status': 200}

このように、bar = 1 / 0 の時点で process_spider_exception() が呼び出されつつ、例外が無かったことにされてアイテムが流れています。

試しに、結果を JSON で保存してみると、

[
  {
    "url": "http://www.httpbin.org/",
    "error": "division by zero",
    "status": 200
  }
]

このようにエラーの詳細をアイテムとして流すことに成功しています。


ところが...


何度目の「ところが」でしょうか。
Spider Middleware の process_spider_exception には バグがあります


Spider Middleware のバグ

先ほどの process_spider_exception() ですが、通常のメソッドの中で投げられた例外は正しくキャッチしてくれますが、ジェネレーター関数の中で投げられた例外をキャッチすることができません 。(v1.1.3 時点)

Issue にも挙げられています。

どうやら Scrapy が内部的に依存している Twisted の挙動が関係しているらしく一筋縄では解消できないようです。
「help-wanted」というタグがつけられているのが絶望感を煽りますね。

Scrapy では複数のアイテムを抜き出して返すためにジェネレーター関数を多用しますからね。これは痛い。痛すぎる。


まとめ

というわけで Scrapy のエラーハンドリングの方法3種類をまとめてみました。

ちなみに、最後のバグについては、現時点でこれといった解決策を見つけられていません。 ひとまず今後の Isuue の流れを watch しておきます。あー、力が欲しい。


私からは以上です。