※ 2020/02/21 色々説明不足な点を追記
昔血迷ってただけの文章なので優しい目で読んで下さい。
きっかけは。
まだそんなに開発が進んでいないが、とっても気になるリポジトリを見つけた。
scrapy-selenium。要するに、Scrapy上でSeleniumを使用できるらしい。
これのイイところは、Downloader MiddlewareでSeleniumの設定ができるようにしようとしてるところ。
「できるようにしている」と表現したのは、まだwait_time
、screen_shot
くらいしか実装されていないから。設計自体はなかなか良さそうなので、これを試してみようとした。
scrapyのRequest
モジュールと同じ要領でhttpRequestを送ろうとしたところ、、
エラッた。
from scrapy_selenium import SeleniumRequest yield SeleniumRequest("https://www.kimoton.com/")
↓
Traceback (most recent call last): File "/home/kimoton/.pyenv/versions/3.6.5/lib/python3.6/site-packages/scrapy/core/engine.py", line 127, in _next_request request = next(slot.start_requests) File "/home/kimoton/project/private/FlightScraper/FlightScraper/FlightScraper/spiders/toscrape_xpath.py", line 14, in start_requests self.parse_result, File "/home/kimoton/.pyenv/versions/3.6.5/lib/python3.6/site-packages/scrapy_selenium/http.py", line 29, in __init__ super().__init__(*args, **kwargs) TypeError: __init__() missing 1 required positional argument: 'url'
このエラーが、なかなか面白かった。
エラーが出たクラス
エラーの根源は、以下のSeleniumRequest
クラスの初期化関数であった。
class SeleniumRequest(Request): def __init__(self, wait_time=None, wait_until=None, screenshot=False, *args, **kwargs): self.wait_time = wait_time self.wait_until = wait_until self.screenshot = screenshot super().__init__(*args, **kwargs)
ここでは、可変長引数を使って、親クラスであるRequest
クラスの初期化関数(__init__()
)で使用されている引数を受けられるようにしていた。
つまり開発者は、Request
クラスと同じように扱えるようにしようと、そう思っていたはず。。
しかし、ここには決定的なミスがあった(追記:敢えてそうしたのかも)。
親クラスのscrapy.http.Request
クラスを見てみよう。
親クラスのRequest
クラス
class Request(object_ref): def __init__(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, dont_filter=False, errback=None, flags=None): self._encoding = encoding # this one has to be set first self.method = str(method).upper() self._set_url(url) self._set_body(body) assert isinstance(priority, int), "Request priority not an integer: %r" % priority self.priority = priority if callback is not None and not callable(callback): raise TypeError('callback must be a callable, got %s' % type(callback).__name__) if errback is not None and not callable(errback): raise TypeError('errback must be a callable, got %s' % type(errback).__name__) assert callback or not errback, "Cannot use errback without a callback" self.callback = callback self.errback = errback self.cookies = cookies or {} self.headers = Headers(headers or {}, encoding=encoding) self.dont_filter = dont_filter self._meta = dict(meta) if meta else None self.flags = [] if flags is None else list(flags)
着目してほしいのは、url
引数が、位置引数(positional argument)として渡されている点だ。
これを子クラスで使用したい場合、子クラスのキーワード引数の後で*args
として与えると位置引数として与えられなくなる。
class SeleniumRequest(Request): def __init__(self, wait_time=None, wait_until=None, screenshot=False, *args, **kwargs): self.wait_time = wait_time self.wait_until = wait_until self.screenshot = screenshot super().__init__(*args, **kwargs)
追記:敢えてこうすることによって明示的に引数定義するようにユーザーに仕向けられるというメリットがある。
簡単なクラスでテスト
挙動を再確認するため、簡単なクラスを作成する。
親クラス(parent
)
class parent(): def __init__(self, one, two=None, three=None): self.one = one self.two = two self.three = three
子クラス(child
)
class child(parent): def __init__(self, child_one="child_one", child_two="child_two", *args, **kwargs): self.child_one = child_one self.child_two = child_two super().__init__(*args, **kwargs)
として、
>>> c = child("one") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in __init__ TypeError: __init__() missing 1 required positional argument: 'one'
親クラスの位置引数(one
)が与えられてないよって怒られます。
与えているのに。
>>> c = child(one="one") >>> c.one 'one'
キーワード引数として与えてあげると問題なく読み込めます。
結局どうすればよかったのか
親クラスと同じような引数指定にしたかったら、これでいいんでないかな。。
class SeleniumRequest(Request): def __init__(self, url, wait_time=None, wait_until=None, screenshot=False, **kwargs): self.wait_time = wait_time self.wait_until = wait_until self.screenshot = screenshot super().__init__(url, **kwargs)