forked from scrapinghub/scrapy-poet
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpage_input_providers.py
More file actions
210 lines (168 loc) · 7.68 KB
/
Copy pathpage_input_providers.py
File metadata and controls
210 lines (168 loc) · 7.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""The Injection Middleware needs a standard way to build the Page Inputs dependencies
that the Page Objects uses to get external data (e.g. the HTML). That's why we
have created a repository of ``PageObjectInputProviders``.
The current module implements a ``PageObjectInputProviders`` for
:class:`web_poet.page_inputs.HttpResponse`, which is in charge of providing the response
HTML from Scrapy. You could also implement different providers in order to
acquire data from multiple external sources, for example,
Splash or Auto Extract API.
"""
import abc
import json
from typing import Set, Union, Callable, ClassVar, List, Any, Sequence
import attr
from scrapy import Request
from scrapy.http import Response
from scrapy.crawler import Crawler
from scrapy.utils.request import request_fingerprint
from scrapy_poet.injection_errors import MalformedProvidedClassesError
from web_poet import HttpResponse, HttpResponseHeaders, RequestUrl
class PageObjectInputProvider:
"""
This is the base class for creating Page Object Input Providers.
A Page Object Input Provider (POIP) takes responsibility for providing
instances of some types to Scrapy callbacks. The types a POIP provides must
be declared in the class attribute ``provided_classes``.
POIPs are initialized when the spider starts by invoking the ``__init__`` method,
which receives the crawler instance as argument.
The ``__call__`` method must be overridden, and it is inside this method
where the actual instances must be build. The default ``__call__`` signature
is as follows:
.. code-block:: python
def __call__(self, to_provide: Set[Callable]) -> Sequence[Any]:
Therefore, it receives a list of types to be provided and return a list
with the instances created (don't get confused by the
``Callable`` annotation. Think on it as a synonym of ``Type``).
Additional dependencies can be declared in the ``__call__`` signature
that will be automatically injected. Currently, scrapy-poet is able
to inject instances of the following classes:
- :class:`~scrapy.http.Request`
- :class:`~scrapy.http.Response`
- :class:`~scrapy.crawler.Crawler`
- :class:`~scrapy.settings.Settings`
- :class:`~scrapy.statscollectors.StatsCollector`
Finally, ``__call__`` function can execute asynchronous code. Just
either prepend the declaration with ``async`` to use futures or annotate it with
``@inlineCallbacks`` for deferred execution. Additionally, you
might want to configure Scrapy ``TWISTED_REACTOR`` to support ``asyncio``
libraries.
The available POIPs should be declared in the spider setting using the key
``SCRAPY_POET_PROVIDERS``. It must be a dictionary that follows same
structure than the
:ref:`Scrapy Middlewares <scrapy:topics-downloader-middleware-ref>`
configuration dictionaries.
A simple example of a provider:
.. code-block:: python
class BodyHtml(str): pass
class BodyHtmlProvider(PageObjectInputProvider):
provided_classes = {BodyHtml}
def __call__(self, to_provide, response: Response):
return [BodyHtml(response.css("html body").get())]
The **provided_classes** class attribute is the ``set`` of classes
that this provider provides.
Alternatively, it can be a function with type ``Callable[[Callable], bool]`` that
returns ``True`` if and only if the given type, which must be callable,
is provided by this provider.
"""
provided_classes: ClassVar[Union[Set[Callable], Callable[[Callable], bool]]]
name: ClassVar[str] = "" # It must be a unique name. Used by the cache mechanism
@classmethod
def is_provided(cls, type_: Callable):
"""
Return ``True`` if the given type is provided by this provider based
on the value of the attribute ``provided_classes``
"""
if isinstance(cls.provided_classes, Set):
return type_ in cls.provided_classes
elif callable(cls.provided_classes):
return cls.provided_classes(type_)
else:
raise MalformedProvidedClassesError(
f"Unexpected type '{type_}' for 'provided_classes' attribute of"
f"'{cls}.'. Expected either 'set' or 'callable'")
def __init__(self, crawler: Crawler):
"""Initializes the provider. Invoked only at spider start up."""
pass
# Remember that is expected for all children to implement the ``__call__``
# method. The simplest signature for it is:
#
# def __call__(self, to_provide: Set[Callable]) -> Sequence[Any]:
#
# But some adding some other injectable attributes are possible
# (see the class docstring)
#
# The technical reason why this method was not declared abstract is that
# injection breaks the method overriding rules and mypy then complains.
class CacheDataProviderMixin(abc.ABC):
"""Providers that intend to support the ``SCRAPY_POET_CACHE`` should inherit
from this mixin class.
"""
@abc.abstractmethod
def fingerprint(self, to_provide: Set[Callable], request: Request) -> str:
"""
Return a fingerprint that identifies this particular request. It will be used to implement
the cache and record/replay mechanism
"""
pass
@abc.abstractmethod
def serialize(self, result: Sequence[Any]) -> Any:
"""
Serializes the results of this provider. The data returned will be pickled.
"""
pass
@abc.abstractmethod
def deserialize(self, data: Any) -> Sequence[Any]:
"""
Deserialize some results of the provider that were previously serialized using the method
:meth:`serialize`.
"""
pass
@property
def has_cache_support(self):
return True
class HttpResponseProvider(PageObjectInputProvider, CacheDataProviderMixin):
"""This class provides ``web_poet.page_inputs.HttpResponse`` instances."""
provided_classes = {HttpResponse}
name = "response_data"
def __call__(self, to_provide: Set[Callable], response: Response):
"""Builds a ``HttpResponse`` instance using a Scrapy ``Response``"""
return [
HttpResponse(
url=response.url,
body=response.body,
status=response.status,
headers=HttpResponseHeaders.from_bytes_dict(response.headers),
)
]
def fingerprint(self, to_provide: Set[Callable], request: Request) -> str:
request_keys = {"url", "method", "body"}
request_data = {
k: str(v)
for k, v in request.to_dict().items()
if k in request_keys
}
fp_data = {
"SCRAPY_FINGERPRINT": request_fingerprint(request),
**request_data,
}
return json.dumps(fp_data, ensure_ascii=False, sort_keys=True)
def serialize(self, result: Sequence[Any]) -> Any:
return [attr.asdict(response_data) for response_data in result]
def deserialize(self, data: Any) -> Sequence[Any]:
return [
HttpResponse(
response_data["url"],
response_data["body"],
status=response_data["status"],
headers=response_data["headers"],
encoding=response_data["_encoding"],
)
for response_data in data
]
class RequestUrlProvider(PageObjectInputProvider):
"""This class provides ``web_poet.page_inputs.RequestUrl`` instances."""
provided_classes = {RequestUrl}
name = "request_url"
def __call__(self, to_provide: Set[Callable], request: Request):
"""Builds a ``RequestUrl`` instance using a Scrapy ``Request``"""
return [RequestUrl(url=request.url)]