無駄と文化

実用的ブログ

Scrapy のクロール実行時 win32api の ImportError でコケる (Windows10, Python 3.5.2, Scrapy 1.5.0)

f:id:todays_mitsui:20160827190511p:plain


Scrapy でバグっぽい挙動にぶつかったので状況と解決策の記録です。

クローラーの実行に失敗する

いつものように Scrapy でクローラーを走らせようとしたらエラーでコケました。
エラーのログはこんな感じ。

2018-01-27 14:36:06 [scrapy.utils.log] INFO: Scrapy 1.5.0 started (bot: foo)
2018-01-27 14:36:06 [scrapy.utils.log] INFO: Versions: lxml 4.1.1.0, libxml2 2.9.5, cssselect 1.0.3, parsel 1.3.1, w3lib 1.19.0, Twisted 17.9.0, Python 3.5.2 (v3.5.2:4def2a2901a5, Jun 25 2016, 22:01:18) [MSC v.1900 32 bit (Intel)], pyOpenSSL 17.5.0 (OpenSSL 1.1.0g  2 Nov 2017), cryptography 2.1.4, Platform Windows-10-10.0.16299-SP0
2018-01-27 14:36:06 [scrapy.crawler] INFO: Overridden settings: {'BOT_NAME': 'foo', 'SPIDER_MODULES': ['foo.spiders'], 'ROBOTSTXT_OBEY': True, 'LOG_FILE': '143606.log', 'FEED_URI': 'result.csv', 'DOWNLOAD_DELAY': 1, 'FEED_FORMAT': 'csv', 'NEWSPIDER_MODULE': 'foo.spiders'}
2018-01-27 14:36:07 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.corestats.CoreStats',
 'scrapy.extensions.feedexport.FeedExporter',
 'scrapy.extensions.logstats.LogStats']
2018-01-27 14:36:07 [twisted] CRITICAL: Unhandled error in Deferred:
2018-01-27 14:36:07 [twisted] CRITICAL:
Traceback (most recent call last):
  File "c:\works\foo\env\lib\site-packages\twisted\internet\defer.py", line 1386, in _inlineCallbacks
    result = g.send(result)
  File "c:\works\foo\env\lib\site-packages\scrapy\crawler.py", line 80, in crawl
    self.engine = self._create_engine()
  File "c:\works\foo\env\lib\site-packages\scrapy\crawler.py", line 105, in _create_engine
    return ExecutionEngine(self, lambda _: self.stop())
  File "c:\works\foo\env\lib\site-packages\scrapy\core\engine.py", line 69, in __init__
    self.downloader = downloader_cls(crawler)
  File "c:\works\foo\env\lib\site-packages\scrapy\core\downloader\__init__.py", line 88, in __init__
    self.middleware = DownloaderMiddlewareManager.from_crawler(crawler)
  File "c:\works\foo\env\lib\site-packages\scrapy\middleware.py", line 58, in from_crawler
    return cls.from_settings(crawler.settings, crawler)
  File "c:\works\foo\env\lib\site-packages\scrapy\middleware.py", line 34, in from_settings
    mwcls = load_object(clspath)
  File "c:\works\foo\env\lib\site-packages\scrapy\utils\misc.py", line 44, in load_object
    mod = import_module(module)
  File "c:\works\foo\env\lib\importlib\__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 986, in _gcd_import
  File "<frozen importlib._bootstrap>", line 969, in _find_and_load
  File "<frozen importlib._bootstrap>", line 958, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 673, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 665, in exec_module
  File "<frozen importlib._bootstrap>", line 222, in _call_with_frames_removed
  File "c:\works\foo\env\lib\site-packages\scrapy\downloadermiddlewares\retry.py", line 20, in <module>
    from twisted.web.client import ResponseFailed
  File "c:\works\foo\env\lib\site-packages\twisted\web\client.py", line 42, in <module>
    from twisted.internet.endpoints import HostnameEndpoint, wrapClientTLS
  File "c:\works\foo\env\lib\site-packages\twisted\internet\endpoints.py", line 41, in <module>
    from twisted.internet.stdio import StandardIO, PipeAddress
  File "c:\works\foo\env\lib\site-packages\twisted\internet\stdio.py", line 30, in <module>
    from twisted.internet import _win32stdio
  File "c:\works\foo\env\lib\site-packages\twisted\internet\_win32stdio.py", line 9, in <module>
    import win32api
ImportError: DLL load failed: 指定されたモジュールが見つかりません。

win32api の import に失敗 しているようです。
もともと Scrapy は Windows で走らせる場合に限り win32api に依存します。そのために事前に pip install pypiwin32 して必要なライブラリをインストールしておいたのですが。
というか、数週間前にセットアップした Scrapy は同じ環境で普通に動いているので不思議なことです。


解決策を探る

結論から云うと原因は不明で、 pypiwin32 のバージョンを下げる ことで対処しました。

数週間前にセットアップした環境で pip freeze -l > requirements.txt してインストールされている pypiwin32 のバージョンを比べてみます。

差分だけ取り出すと、

cffi==1.11.2   => cffi==1.11.4
pypiwin32==219 => pypiwin32==222
               => pywin32==222
w3lib==1.18.0  => w3lib==1.19.0

これだけ見ると pypiwin32 のバージョン 220 から 222 の間で何かしらのバグが入った事が原因のように見えます。
または pywin32 に依存するようになったことが原因でしょうか?


今回は取り急ぎ pypiwin32 をアンインストールしてから pypiwin32==219 で古いバージョンの pypiwin32 を入れ直す ことで対応しました。


私からは以上です。

いろいろなパッケージから提供されている ListT モナド変換子の違いについて調べてみる

f:id:todays_mitsui:20180103233022p:plain


この記事は Haskell (その2) Advent Calendar 2017 の10日目の記事です。あけましておめでとうございます。 今回は ListT モナド変換子 について調べたことをまとめます。


hackage で検索をすると ListT と名の付くモナド変換子を提供しているパッケージは4つほど見つかります。

このうち mtl パッケージで提供している ListT は transformers パッケージからインポートされているだけなので実装としては同一のものです。
また、pipes パッケージで提供されているものは Pipes と組み合わせて使うもののようで、単独で List モナドの変換子版というわけではなさそうです。


さらにさらに、 transformers パッケージの Control.Monad.Trans.List のドキュメント を見にいくとページ先頭に赤字で不穏な注釈が書かれています。

Deprecated: This transformer is invalid on most monads
(非推奨:このモナド変換子はほとんどのモナドに対して無効です)

非常に不穏ですね。

ただ、これだけの情報ではなぜ transformers パッケージの実装ではダメなのか、代わりに何を使えばいいのか分かりません。


ListT done right を読む

transformers パッケージの ListT について、ListT done right に詳しく書かれています。
要点をかいつまむと、

  • 従来の ListT は過剰な正格性を課している
  • 従来の ListT はいわゆるモナド変換子ではない。 m がモナドであっても ListT m がモナドになるとは限らない
  • なので、上記の欠点を解消したバージョンの実装を提案する
  • 新しい実装をさらに洗練させてパッケージにまとめたものが list-t である

というわけで、
TL;DR な方のために結論を書いておくと、transformers パッケージの実装はいろいろと間違っているので list-t パッケージの方を使おう ということみたいです。


テストプログラムで ListT を比べる

ListT done right には transformers パッケージの実装が正しく動かないことを示すテストのプログラムが挙げられています。
引用して、実際に実行出来るかたちにまとめてみました。パッケージ毎に結果を見てみましょう。今回は transformers と list-t を比べてみます。

github.com

いつものとおり全てのコードは GitHub に置いておきます。


1. Sum of squares - 平方和

整数 n を与えると二平方の和で n に等しくなる組を探すプログラムです。 例えば、 5 を与えると (1, 4)(4, 1) が答えになります。

testSumOfSquares :: Int -> ListT IO (Int, Int)
testSumOfSquares n = do
    let squares = liftList . takeWhile (<= n) $ map (^ (2 :: Int)) [0 ..]
    x <- squares
    y <- squares
    lift $ print (x, y)
    guard $ x + y == n
    lift $ putStrLn "Sum of Squares."
    return (x, y)

runTestSumOfSquares :: Int -> IO (Int, Int)
runTestSumOfSquares = fmap fromJust . ListT.head . testSumOfSquares

これ自体、リスト内包表記などでシンプルに実装できそうな処理ですが、今回は ListT IO を使って平方和の探索の途中途中で標準出力を挟むようにしてあります。

さて、testSumOfSquares は全ての答えをリストで返してくれますが、答えが複数あるときに 最初の階だけ が必要なときにはどうでしょうか。
リストから最初の要素だけを取り出すことは簡単です。問題は 遅延評価のアイデアに従って最初の答えを得るために必要な IO アクションだけ実行されて欲しい という部分です。

では見てみましょう。


実行結果 : Control.Monad.Trans.List.ListT from transformers
ghci> FromTransformers.runTestSumOfSquares 5
(0,0)
(0,1)
(0,4)
(1,0)
(1,1)
(1,4)
Sum of Squares.
(4,0)
(4,1)
Sum of Squares.
(4,4)

(1,4)

候補となる全ての組 (0,0),(0,1),(0,4), ... ,(4,1),(4,4) をまとめて探索しつくしている様が見られます。
今回は候補となる組が有限なのでいいですが、候補となる組が無限リストで与えられた場合には上手く動作しないでしょう。


実行結果 : ListT.ListT from list-t
ghci> FromListT.runTestSumOfSquares 5
(0,0)
(0,1)
(0,4)
(1,0)
(1,1)
(1,4)
Sum of Squares.

(1,4)

答えとなる最初の組 (1,4) が見つかった時点で、残りの IO アクションは実行されずに捨てられています。
こちらが望ましい動作ですね。

これは list-t パッケージが uncons :: ListT m a -> m (Maybe (a, ListT m a)) という関数を提供しているおかげです。
uncons は先頭の要素を得るのに必要な分だけ内側のモナドを実行します。


2. Grouping effects - 結合効果

今度は モナド則のうち結合則が破れる という例です。
モナドの結合則とは、

(m >>= f) >>= g == m >>= (\x -> f x >>= g)

のことです。

testGroupingEffects1 :: ListT IO Int
testGroupingEffects1 = do
    r <- liftIO $ newIORef 0
    (next r `mplus` next r >> next r `mplus` next r) >> next r `mplus` next r

runTestGroupingEffects1 :: IO [Int]
runTestGroupingEffects1 = toList testGroupingEffects1


testGroupingEffects2 :: ListT IO Int
testGroupingEffects2 = do
    r <- liftIO $ newIORef 0
    next r `mplus` next r >> (next r `mplus` next r >> next r `mplus` next r)

runTestGroupingEffects2 :: IO [Int]
runTestGroupingEffects2 = toList testGroupingEffects2


next :: IORef Int -> ListT IO Int
next r = liftIO $ do
    x <- readIORef r
    writeIORef r (x + 1)
    return x

本来であれば runTestGroupingEffects1runTestGroupingEffects2 の実行結果は等しくなるはずです。


実行結果 : Control.Monad.Trans.List.ListT from transformers
ghci> FromTransformers.runTestGroupingEffects1
[6,7,8,9,10,11,12,13]

ghci> FromTransformers.runTestGroupingEffects2
[4,5,6,7,10,11,12,13]

transformers パッケージの実装ではモナドの結合則が破れていることが確認できました。
これは m がモナドであっても ListT m がモナドになるとは限らない ということを意味します。


実行結果 : ListT.ListT from list-t
ghci> FromListT.runTestGroupingEffects1
[2,3,5,6,9,10,12,13]

ghci> FromListT.runTestGroupingEffects2
[2,3,5,6,9,10,12,13]

list-t の方は正しく動作しています。


3. Order of printing - 印字順

リスト操作の中でも頻繁に使うものの中にリストの結合 (++) があります。
ListT の結合は MonadPlus クラスの mplus メソッドで代用できますが、transformers パッケージの実装では mplus が正しく動かないという例です。

a, b, c :: ListT IO ()
[a, b, c] = map (liftIO . putChar) ['a', 'b', 'c']


testOrderOfPrinting1 :: ListT IO ()
testOrderOfPrinting1 = ((a `mplus` a) >> b) >> c

runTestOrderOfPrinting1 :: IO (Maybe ((), ListT IO ()))
runTestOrderOfPrinting1 = runListT' testOrderOfPrinting1


testOrderOfPrinting2 :: ListT IO ()
testOrderOfPrinting2 = (a `mplus` a) >> (b >> c)

runTestOrderOfPrinting2 :: IO (Maybe ((), ListT IO ()))
runTestOrderOfPrinting2 = runListT' testOrderOfPrinting2

testOrderOfPrinting1, testOrderOfPrinting2 とも、正しく実装されていれば [putChar 'a' >> putChar 'b' >> putChar 'c', putChar 'a' >> putChar 'b' >> putChar 'c'] みたいな IO アクションを生成してくれるはずです。


実行結果 : Control.Monad.Trans.List.ListT from transformers
ghci> FromTransformers.runTestOrderOfPrinting1
aabbcc

ghci> FromTransformers.runTestOrderOfPrinting2
aabcbc

リストを構成する時点で IO アクションが実行されてしまうためにこのような印字になってしまうようです。


実行結果 : ListT.ListT from list-t
ghci> FromListT.runTestOrderOfPrinting1
abc

ghci> FromListT.runTestOrderOfPrinting1
abc

いい感じですね。


4. Order of ListT []

これまでの例は全て ListT IO に関するものでした。
IO モナドを含まないシンプルな例でもモナドの結合則が破れることを示すために ListT [] の例が示してあります。

v :: Int -> ListT [] Int
v 0 = ListT [0, 1]
v 1 = ListT [[0], [1]]


runTestOrderOfListTList1 :: IO ()
runTestOrderOfListTList1 = print $ toList $ ((v >=> v) >=> v) 0

runTestOrderOfListTList2 :: IO ()
runTestOrderOfListTList2 = print $ toList $ (v >=> (v >=> v)) 0

なんだかややこしそうですが、 ((v >=> v) >=> v) 0(v >=> (v >=> v)) 0 が等しくなるかどうかを確かめるだけのプログラムです。


実行結果 : Control.Monad.Trans.List.ListT from transformers
ghci> FromTransformers.runTestOrderOfListTList1
[[0,1,0,0,1],[0,1,1,0,1],[0,1,0,0],[0,1,0,1],[0,1,1,0],[0,1,1,1]]

ghci> FromTransformers.runTestOrderOfListTList2
[[0,1,0,0,1],[0,1,0,0],[0,1,0,1],[0,1,1,0,1],[0,1,1,0],[0,1,1,1]]

transformers パッケージの実装では結合則が破れていることを確かめられました。


実行結果 : ListT.ListT from list-t
ghci> FromListT.runTestOrderOfListTList1
[[1,0,0,1,0],[1,0,1,1,0],[0,0,1,0],[0,1,1,0],[1,0,1,0],[1,1,1,0]]

ghci> FromListT.runTestOrderOfListTList2
[[1,0,0,1,0],[1,0,1,1,0],[0,0,1,0],[0,1,1,0],[1,0,1,0],[1,1,1,0]]

バッチリですね。


まとめ

というわけで、 ListT done right に挙げられている例を実際のプログラムで確かめることができました。
また、 transformers と mtl の動作が全く同じになる事も確かめています。

さて、ここまできて確かめられていないこと、それは、

  • list-t パッケージの実装なら全てのモナド m に対して ListT m がモナドになるの?

ということですね。こちらは追試が必要そうです。
(>>=) の実装から運算によって確かめられないかと思って手を動かしてみたのですが、私の運算力では無理でした。

どなたかご存知の方がいれば情報ください。


私からは以上です。