mirror of https://github.com/scrapy/scrapy.git
merge with ismael branch
This commit is contained in:
commit
aace51f336
|
|
@ -20,4 +20,4 @@ it's properly merged) . Use at your own risk.
|
|||
:maxdepth: 1
|
||||
|
||||
newitems
|
||||
loaders
|
||||
itemparser
|
||||
|
|
|
|||
|
|
@ -1,55 +1,55 @@
|
|||
.. _topics-loader:
|
||||
.. _topics-loaders:
|
||||
|
||||
============
|
||||
Item Loaders
|
||||
============
|
||||
|
||||
.. module:: scrapy.newitem.loader
|
||||
.. module:: scrapy.contrib.loader
|
||||
:synopsis: Item Loader class
|
||||
|
||||
Item Loaders (or Loaders, for short) provide a convenient mechanism for
|
||||
populating scraped :ref:`Items <topics-newitems>`. Even though Items can be
|
||||
populated using their own dictionary-like API, the Loaders provide a much more
|
||||
convenient API for populating them from a scraping process, by automating some
|
||||
common tasks like parsing the raw extracted data before assigning it.
|
||||
Item Loaders provide a convenient mechanism for populating scraped :ref:`Items
|
||||
<topics-newitems>`. Even though Items can be populated using their own
|
||||
dictionary-like API, the Item Loaders provide a much more convenient API for
|
||||
populating them from a scraping process, by automating some common tasks like
|
||||
parsing the raw extracted data before assigning it.
|
||||
|
||||
In other words, :ref:`Items <topics-newitems>` provide the *container* of
|
||||
scraped data, while Loaders provide the mechanism for *populating* that
|
||||
scraped data, while Item Loaders provide the mechanism for *populating* that
|
||||
container.
|
||||
|
||||
Loaders are designed to provide a flexible, efficient and easy mechanism for
|
||||
extending and overriding different field parsing rules (either by spider, or by
|
||||
source format) without becoming a nightmare to maintain
|
||||
Item Loaders are designed to provide a flexible, efficient and easy mechanism
|
||||
for extending and overriding different field parsing rules, either by spider,
|
||||
or by source format (HTML, XML, etc) without becoming a nightmare to maintain.
|
||||
|
||||
Using Loaders to populate items
|
||||
===============================
|
||||
Using Item Loaders to populate items
|
||||
====================================
|
||||
|
||||
To use a Loader, you must first instantiate it. You can either instantiate it
|
||||
with an Item object or without one, in which case an Item is automatically
|
||||
instantiated in the Loader constructor using the Item class specified in the
|
||||
:attr:`Loader.default_item_class` attribute.
|
||||
To use an Item Loader, you must first instantiate it. You can either
|
||||
instantiate it with an Item object or without one, in which case an Item is
|
||||
automatically instantiated in the Item Loader constructor using the Item class
|
||||
specified in the :attr:`ItemLoader.default_item_class` attribute.
|
||||
|
||||
Then, you start adding values to the Loader, typically collecting them using
|
||||
:ref:`Selectors <topics-selectors>`. You can add more than one value to the
|
||||
same item field, the Loader will know how to "join" those values later using a
|
||||
Reducer.
|
||||
Then, you start collecting values into the Item Loader, typically using using
|
||||
:ref:`XPath Selectors <topics-selectors>`. You can add more than one value to
|
||||
the same item field, the Item Loader will know how to "join" those values later
|
||||
using a proper processing function.
|
||||
|
||||
Here is a typical Loader usage in a :ref:`Spider <topics-spiders>`, using the
|
||||
:ref:`Product item <topics-newitems-declaring>` declared in the :ref:`Items
|
||||
section <topics-newitems>`::
|
||||
Here is a typical Item Loader usage in a :ref:`Spider <topics-spiders>`, using
|
||||
the :ref:`Product item <topics-newitems-declaring>` declared in the :ref:`Items
|
||||
chapter <topics-newitems>`::
|
||||
|
||||
from scrapy.item.loader import XPathLoader
|
||||
from scrapy.contrib.loader import XPathItemLoader
|
||||
from scrapy.xpath import HtmlXPathSelector
|
||||
from myproject.items import Product
|
||||
|
||||
def parse(self, response):
|
||||
l = XPathLoader(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_xpath('stock', '//p[@id="stock"]')
|
||||
l.add_value('last_updated', 'today') # you can also use literal values
|
||||
return l.get_item()
|
||||
p = XPathItemLoader(item=Product(), response=response)
|
||||
p.add_xpath('name', '//div[@class="product_name"]')
|
||||
p.add_xpath('name', '//div[@class="product_title"]')
|
||||
p.add_xpath('price', '//p[@id="price"]')
|
||||
p.add_xpath('stock', '//p[@id="stock"]')
|
||||
p.add_value('last_updated', 'today') # you can also use literal values
|
||||
return p.populate_item()
|
||||
|
||||
By quickly looking at that code we can see the ``name`` field is being
|
||||
extracted from two different XPath locations in the page:
|
||||
|
|
@ -58,178 +58,199 @@ extracted from two different XPath locations in the page:
|
|||
2. ``//div[@class="product_title"]``
|
||||
|
||||
In other words, data is being collected by extracting it from two XPath
|
||||
locations, using the :meth:`~XPathLoader.add_xpath` method. This is the data
|
||||
locations, using the :meth:`~XPathItemLoader.add_xpath` method. This is the data
|
||||
that will be assigned to the ``name`` field later.
|
||||
|
||||
Afterwards, similar calls are used for ``price`` and ``stock`` fields, and
|
||||
finally the ``last_update`` field is populated directly with a literal value
|
||||
(``today``) using a different method: :meth:`~Loader.add_value`.
|
||||
(``today``) using a different method: :meth:`~ItemLoader.add_value`.
|
||||
|
||||
Finally, when all data is collected, the :meth:`Loader.get_item` method is
|
||||
called which actually populates and returns the item populated with the data
|
||||
previously extracted and collected with the :meth:`~XPathLoader.add_xpath` and
|
||||
:meth:`~Loader.add_value` calls.
|
||||
Finally, when all data is collected, the :meth:`ItemLoader.populate_item`
|
||||
method is called which actually populates and returns the item populated with
|
||||
the data previously extracted and collected with the
|
||||
:meth:`~XPathItemLoader.add_xpath` and :meth:`~ItemLoader.add_value` calls.
|
||||
|
||||
.. _topics-loader-expred:
|
||||
.. _topics-loaders-processors:
|
||||
|
||||
Expanders and Reducers
|
||||
======================
|
||||
Input and Output processors
|
||||
===========================
|
||||
|
||||
A Loader is composed of one expander and one reducer for each (item) field. The
|
||||
Expander processes the extracted data as soon as it's received (through the
|
||||
:meth:`~XPathLoader.add_xpath` or :meth:`~Loader.add_value` methods) and the
|
||||
result of the expander is collected and kept inside the Loader. After
|
||||
collecting all data, the :meth:`Loader.get_item` method is called to actually
|
||||
populate and get the :class:`~scrapy.newitem.Item` object. That's when the
|
||||
Reducers are called with the data previously collected (using the Expanders)
|
||||
and the output of the Reducers are the actual values that get assigned to the
|
||||
item.
|
||||
An Item Loader contains one input processor and one output processor for each
|
||||
(item) field. The input processor processes the extracted data as soon as it's
|
||||
received (through the :meth:`~XPathItemLoader.add_xpath` or
|
||||
:meth:`~ItemLoader.add_value` methods) and the result of the input processor is
|
||||
collected and kept inside the ItemLoader. After collecting all data, the
|
||||
:meth:`ItemLoader.populate_item` method is called to populate and get the
|
||||
populated :class:`~scrapy.newitem.Item` object. That's when the output processor
|
||||
is called with the data previously collected (and processed using the input
|
||||
processor). The result of the output processor is the final value that gets assigned
|
||||
to the item.
|
||||
|
||||
Let's see an example to illustrate how Expanders and Reducers are called, for a
|
||||
particular field (the same applies for any other field)::
|
||||
Let's see an example to illustrate how this input and output processors are
|
||||
called for a particular field (the same applies for any other field)::
|
||||
|
||||
l = XPathLoader(Product(), some_selector)
|
||||
l.add_xpath('name', xpath1) # (1)
|
||||
l.add_xpath('name', xpath2) # (2)
|
||||
return l.get_item() # (3)
|
||||
p = XPathItemLoader(Product(), some_xpath_selector)
|
||||
p.add_xpath('name', xpath1) # (1)
|
||||
p.add_xpath('name', xpath2) # (2)
|
||||
return p.populate_item() # (3)
|
||||
|
||||
So what happens is:
|
||||
|
||||
1. Data from ``xpath1`` is extracted, and passed through the Expander of the
|
||||
``name`` field. The output of the expander is collected and kept in the
|
||||
loader (but not yet assigned to the item).
|
||||
1. Data from ``xpath1`` is extracted, and passed through the *input processor* of
|
||||
the ``name`` field. The result of the input processor is collected and kept in
|
||||
the Item Loader (but not yet assigned to the item).
|
||||
|
||||
2. Data from ``xpath2`` is extracted, and passed through the same Expander used
|
||||
in (1). The output of the expander is appended to the data collected in (1)
|
||||
(if any).
|
||||
2. Data from ``xpath2`` is extracted, and passed through the same *input
|
||||
processor* used in (1). The result of the input processor is appended to the
|
||||
data collected in (1) (if any).
|
||||
|
||||
3. The data collected in (1) and (2) is passed through the Reducer of the
|
||||
``name`` field. The output of the Reducer is the value assigned to the
|
||||
``name`` field in the item.
|
||||
3. The data collected in (1) and (2) is passed through the *output processor* of
|
||||
the ``name`` field. The result of the output processor is the value assigned to
|
||||
the ``name`` field in the item.
|
||||
|
||||
Scrapy comes with one major expander built-in, the :ref:`Tree Expander
|
||||
<topics-loader-tree-expander>`, and :ref:`a couple of commonly used reducers
|
||||
<topics-loader-reducers>`.
|
||||
It's worth noticing that processors are just callable objects, which are called
|
||||
with the data to be parsed, and return a parsed value. So you can use any
|
||||
function as input or output processor, provided they can receive only one
|
||||
positional (required) argument.
|
||||
|
||||
Declaring Loaders
|
||||
=================
|
||||
The other thing you need to keep in mind is that the values returned by input
|
||||
processors are collected internally (in lists) and then passed to output
|
||||
processors to populate the fields, so output processors should expect iterables as
|
||||
input.
|
||||
|
||||
Loaders are declared like Items, by using a class definition syntax. Here is an
|
||||
example::
|
||||
Last, but not least, Scrapy comes with some :ref:`commonly used processors
|
||||
<topics-loaders-available-processors>` built-in for convenience.
|
||||
|
||||
from scrapy.newitem.loader import Loader
|
||||
from scrapy.newitem.loader.expanders import TreeExpander
|
||||
from scrapy.newitem.loader.reducers import Join, TakeFirst
|
||||
|
||||
class ProductLoader(Loader):
|
||||
Declaring Item Loaders
|
||||
======================
|
||||
|
||||
default_expander = TakeFirst()
|
||||
Item Loaders are declared like Items, by using a class definition syntax. Here
|
||||
is an example::
|
||||
|
||||
name_exp = TreeExpander(unicode.title)
|
||||
name_red = Join()
|
||||
from scrapy.contrib.loader import ItemLoader
|
||||
from scrapy.contrib.loader.processor import TakeFirst, ApplyConcat, Join
|
||||
|
||||
price_exp = TreeExpander(unicode.strip)
|
||||
price_red = TakeFirst()
|
||||
class ProductLoader(ItemLoader):
|
||||
|
||||
...
|
||||
default_input_processor = TakeFirst()
|
||||
|
||||
As you can see, expanders are declared using the ``_exp`` suffix while reducers
|
||||
are declared using the ``_red`` suffix. And you can also declare a default
|
||||
expander using the :attr:`Loader.default_expander` attribute.
|
||||
name_in = ApplyConcat(unicode.title)
|
||||
name_out = Join()
|
||||
|
||||
.. _topics-loader-expred-declaring:
|
||||
price_in = ApplyConcat(unicode.strip)
|
||||
price_out = TakeFirst()
|
||||
|
||||
Declaring Extenders and Reducers
|
||||
================================
|
||||
# ...
|
||||
|
||||
As seen in the previous section, extenders and reducers can be declared in the
|
||||
Loader definition, and it's very common to declare expanders this way. However,
|
||||
there is one more place where you can specify the exanders and reducers to use:
|
||||
in the :ref:`Item Field <topics-newitems-fields>` metadata. Here is an example::
|
||||
As you can see, input processors are declared using the ``_in`` suffix while
|
||||
output processors are declared using the ``_out`` suffix. And you can also
|
||||
declare a default input/output processors using the
|
||||
:attr:`ItemLoader.default_input_processor` and
|
||||
:attr:`ItemLoader.default_output_processor` attributes.
|
||||
|
||||
.. _topics-loaders-processors-declaring:
|
||||
|
||||
Declaring Input and Output Processors
|
||||
=====================================
|
||||
|
||||
As seen in the previous section, input and output processors can be declared in
|
||||
the Item Loader definition, and it's very common to declare input processors
|
||||
this way. However, there is one more place where you can specify the input and
|
||||
output processors to use: in the :ref:`Item Field <topics-newitems-fields>`
|
||||
metadata. Here is an example::
|
||||
|
||||
from scrapy.newitem import Item, Field
|
||||
from scrapy.newitem.loader.expanders import TreeExpander
|
||||
from scrapy.newitem.loader.reducers import Join, TakeFirst
|
||||
from scrapy.contrib.loader.processor import ApplyConcat, Join, TakeFirst
|
||||
|
||||
from scrapy.utils.markup import remove_entities
|
||||
from myprojct.utils import filter_prices
|
||||
from myproject.utils import filter_prices
|
||||
|
||||
class Product(Item):
|
||||
name = Field(
|
||||
expander=TreeExpander(remove_entities),
|
||||
reducer=Join(),
|
||||
input_processor=ApplyConcat(remove_entities),
|
||||
output_processor=Join(),
|
||||
)
|
||||
price = Field(
|
||||
default=0,
|
||||
expander=TreeExpander(remove_entities, filter_prices),
|
||||
reducer=TakeFirst(),
|
||||
input_processor=ApplyConcat(remove_entities, filter_prices),
|
||||
output_processor=TakeFirst(),
|
||||
)
|
||||
|
||||
The precendece order, for both expander and reducer declarations, is as
|
||||
follows:
|
||||
The precedence order, for both input and output processors, is as follows:
|
||||
|
||||
1. Loader field-specific attributes: ``field_exp`` and ``field_red`` (more
|
||||
1. Item Loader field-specific attributes: ``field_in`` and ``field_out`` (most
|
||||
precedence)
|
||||
2. Field metadata (``expander`` and ``reducer`` key)
|
||||
3. Loader defaults: :meth:`Loader.default_expander` and
|
||||
:meth:`Loader.default_reducer` (less precedence)
|
||||
2. Field metadata (``input_processor`` and ``output_processor`` key)
|
||||
3. Item Loader defaults: :meth:`ItemLoader.default_input_processor` and
|
||||
:meth:`ItemLoader.default_output_processor` (least precedence)
|
||||
|
||||
See also: :ref:`topics-loader-extending`.
|
||||
See also: :ref:`topics-loaders-extending`.
|
||||
|
||||
.. _topics-loader-args:
|
||||
.. _topics-loaders-context:
|
||||
|
||||
Item Loader arguments
|
||||
=====================
|
||||
Item Loader Context
|
||||
===================
|
||||
|
||||
The Loader arguments is a dict of arbitrary key/values which can be passed when
|
||||
declaring, instantiating or using Loaders. They are used modify the behaviour
|
||||
of the expanders.
|
||||
The Item Loader Context is a dict of arbitrary key/values which is shared among
|
||||
all input and output processors in the Item Loader. It can be passed when
|
||||
declaring, instantiating or using Item Loader. They are used to modify the
|
||||
behaviour of the input/output processors.
|
||||
|
||||
For example, suppose you have a function ``parse_length`` which receives a text
|
||||
value and extracts a length from it::
|
||||
|
||||
def parse_length(text, loader_args):
|
||||
unit = loader_args('unit', 'm')
|
||||
def parse_length(text, loader_context):
|
||||
unit = loader_context.get('unit', 'm')
|
||||
# ... length parsing code goes here ...
|
||||
return parsed_length
|
||||
|
||||
Since it receives a ``loader_args`` the Expander will pass the currently active
|
||||
Loader arguments when calling it.
|
||||
By accepting a ``loader_context`` argument the function is explicitly telling
|
||||
the Item Loader that is able to receive an Item Loader context, so the Item
|
||||
Loader passes the currently active context when calling it, and the processor
|
||||
function (``parse_length`` in this case) can thus use them.
|
||||
|
||||
There are seveal ways to pass Loader arguments:
|
||||
There are several ways to modify Item Loader context values:
|
||||
|
||||
1. Passing arguments on Loader declaration::
|
||||
1. By modifying the currently active Item Loader context
|
||||
(:meth:`ItemLoader.context` attribute)::
|
||||
|
||||
class ProductLoader(Loader):
|
||||
length_exp = TreeExpander(parse_length, unit='cm')
|
||||
loader = ItemLoader(product, unit='cm')
|
||||
loader.context['unit'] = 'cm'
|
||||
|
||||
2. Passing arguments on Loader instantiation::
|
||||
2. On Item Loader instantiation (the keyword arguments of Item Loader
|
||||
constructor are stored in the Item Loader context)::
|
||||
|
||||
l = Loader(product, unit='cm')
|
||||
p = ItemLoader(product, unit='cm')
|
||||
|
||||
3. Passing arguments on Loader usage::
|
||||
2. On Item Loader declaration, for those input/output processors that support
|
||||
instatiating them with a Item Loader context. :class:`ApplyConcat` is one of
|
||||
them::
|
||||
|
||||
l.add_xpath('length', '//div', unit='cm')
|
||||
class ProductLoader(ItemLoader):
|
||||
length_out = ApplyConcat(parse_length, unit='cm')
|
||||
|
||||
Loader objects
|
||||
==============
|
||||
|
||||
.. class:: Loader([item], \**loader_args)
|
||||
ItemLoader objects
|
||||
==================
|
||||
|
||||
.. class:: ItemLoader([item], \**kwargs)
|
||||
|
||||
Return a new Item Loader for populating the given Item. If no item is
|
||||
given, one is instantiated using the class in :attr:`default_item_class`.
|
||||
given, one is instantiated automatically using the class in
|
||||
:attr:`default_item_class`.
|
||||
|
||||
.. method:: add_value(field_name, value, \**new_loader_args)
|
||||
The item and the remaining keyword arguments are assigned to the Loader
|
||||
context (accesible through the :attr:`context` attribute).
|
||||
|
||||
.. method:: add_value(field_name, value)
|
||||
|
||||
Add the given ``value`` for the given field.
|
||||
|
||||
The value is passed through the :ref:`field expander
|
||||
<topics-loader-expred>` and its output appened to the data collected
|
||||
for that field. If the field already contains collected data, the new
|
||||
data is added.
|
||||
|
||||
If any keyword arguments are passed, they're used as :ref:`Loader
|
||||
arguments <topics-loader-args>` when calling the expanders.
|
||||
The value is passed through the :ref:`field input processor
|
||||
<topics-loaders-processors>` and its result appened to the data
|
||||
collected for that field. If the field already contains collected data,
|
||||
the new data is added.
|
||||
|
||||
Examples::
|
||||
|
||||
|
|
@ -237,60 +258,68 @@ Loader objects
|
|||
loader.add_value('colours', [u'white', u'blue'])
|
||||
loader.add_value('length', u'100', default_unit='cm')
|
||||
|
||||
.. method:: replace_value(field_name, value, \**new_loader_args)
|
||||
.. method:: replace_value(field_name, value)
|
||||
|
||||
Similar to :meth:`add_value` but replaces collected data instead of
|
||||
adding it.
|
||||
Similar to :meth:`add_value` but replaces the collected data with the
|
||||
new value instead of adding it.
|
||||
|
||||
|
||||
.. method:: get_item()
|
||||
.. method:: populate_item()
|
||||
|
||||
Populate the item with the data collected so far, and return it. The
|
||||
data collected is first passed through the :ref:`field reducers
|
||||
<topics-loader-expred>` to get the final value to assign to each item
|
||||
field.
|
||||
data collected is first passed through the :ref:`field output processors
|
||||
<topics-loaders-processors>` to get the final value to assign to each
|
||||
item field.
|
||||
|
||||
.. method:: get_expanded_value(field_name)
|
||||
.. method:: get_collected_values(field_name)
|
||||
|
||||
Return the expanded data for the given field. In other words, return
|
||||
the dat collected so far for the given field, without reducing it.
|
||||
Return the collected values for the given field.
|
||||
|
||||
.. method:: get_reduced_value(field_name)
|
||||
.. method:: get_output_value(field_name)
|
||||
|
||||
Return the reduced value for the given field, without modifying the
|
||||
item.
|
||||
Return the collected values parsed using the output processor, for the
|
||||
given field. This method doesn't populate or modify the item at all.
|
||||
|
||||
.. method:: get_expander(field_name)
|
||||
.. method:: get_input_processor(field_name)
|
||||
|
||||
Return the expander for the given field.
|
||||
Return the input processor for the given field.
|
||||
|
||||
.. method:: get_reducer(field_name)
|
||||
.. method:: get_output_processor(field_name)
|
||||
|
||||
Return the reducer for the given field.
|
||||
Return the output processor for the given field.
|
||||
|
||||
.. attribute:: item
|
||||
|
||||
The :class:`~scrapy.newitem.Item` object being parsed by this Item
|
||||
Loader.
|
||||
|
||||
.. attribute:: context
|
||||
|
||||
The currently active :ref:`Context <topics-loaders-context>` of this
|
||||
Item Loader.
|
||||
|
||||
.. attribute:: default_item_class
|
||||
|
||||
An Item class (or factory), used to instantiate items when not given in
|
||||
the constructor.
|
||||
|
||||
.. attribute:: default_expander
|
||||
.. attribute:: default_input_processor
|
||||
|
||||
The default expander to use for those fields which don't define a
|
||||
specific expander
|
||||
The default input processor to use for those fields which don't specify
|
||||
one.
|
||||
|
||||
.. attribute:: default_reducer
|
||||
.. attribute:: default_output_processor
|
||||
|
||||
The default reducer to use for those fields which don't define a
|
||||
specific expander
|
||||
The default output processor to use for those fields which don't specify
|
||||
one.
|
||||
|
||||
.. class:: XPathLoader([item, selector, response], \**loader_args)
|
||||
.. class:: XPathItemLoader([item, selector, response], \**kwargs)
|
||||
|
||||
The :class:`XPathLoader` class extends the :class:`Loader` class providing
|
||||
more convenient mechanisms for extracting data from web pages using
|
||||
:ref:`XPath selectors <topics-selectors>`.
|
||||
The :class:`XPathItemLoader` class extends the :class:`ItemLoader` class
|
||||
providing more convenient mechanisms for extracting data from web pages
|
||||
using :ref:`XPath selectors <topics-selectors>`.
|
||||
|
||||
:class:`XPathLoader` objects accept two more additional parameters in their
|
||||
constructors:
|
||||
:class:`XPathItemLoader` objects accept two more additional parameters in
|
||||
their constructors:
|
||||
|
||||
:param selector: The selector to extract data from, when using the
|
||||
:meth:`add_xpath` or :meth:`replace_xpath` method.
|
||||
|
|
@ -301,11 +330,11 @@ Loader objects
|
|||
in which case this argument is ignored.
|
||||
:type response: :class:`~scrapy.http.Response` object
|
||||
|
||||
.. method:: add_xpath(field_name, xpath, re=None, \**new_loader_args)
|
||||
.. method:: add_xpath(field_name, xpath, re=None)
|
||||
|
||||
Similar to :meth:`Loader.add_value` but receives an XPath instead of a
|
||||
Similar to :meth:`ItemLoader.add_value` but receives an XPath instead of a
|
||||
value, which is used to extract a list of unicode strings from the
|
||||
selector associated with this :class:`XPathLoader`. If the ``re``
|
||||
selector associated with this :class:`XPathItemLoader`. If the ``re``
|
||||
argument is given, it's used for extrating data from the selector using
|
||||
the :meth:`~scrapy.xpath.XPathSelector.re` method.
|
||||
|
||||
|
|
@ -323,7 +352,7 @@ Loader objects
|
|||
# HTML snippet: <p id="price">the price is $1200</p>
|
||||
loader.add_xpath('price', '//p[@id="price"]', re='the price is (.*)')
|
||||
|
||||
.. method:: replace_xpath(field_name, xpath, re=None, \**new_loader_args)
|
||||
.. method:: replace_xpath(field_name, xpath, re=None)
|
||||
|
||||
Similar to :meth:`add_xpath` but replaces collected data instead of
|
||||
adding it.
|
||||
|
|
@ -331,7 +360,7 @@ Loader objects
|
|||
.. attribute:: default_selector_class
|
||||
|
||||
The class used to construct the :attr:`selector` of this
|
||||
:class:`XPathLoader`, if only a response is given in the constructor.
|
||||
:class:`XPathItemLoader`, if only a response is given in the constructor.
|
||||
If a selector is given in the constructor this attribute is ignored.
|
||||
This attribute is sometimes overridden in subclasses.
|
||||
|
||||
|
|
@ -343,159 +372,156 @@ Loader objects
|
|||
:attr:`default_selector_class`. This attribute is meant to be
|
||||
read-only.
|
||||
|
||||
.. _topics-loader-extending:
|
||||
.. _topics-loaders-extending:
|
||||
|
||||
Reusing and extending Loaders
|
||||
=============================
|
||||
Reusing and extending Item Loaders
|
||||
==================================
|
||||
|
||||
As your project grows bigger and acquires more and more spiders, maintenance
|
||||
becomes a fundamental problem, specially when you have to deal with many
|
||||
different parsing rules per spider, a lot of exceptions, but also want to reuse
|
||||
the common cases.
|
||||
different parsing rules for each spider, having a lot of exceptions, but also
|
||||
wanting to reuse the common processors.
|
||||
|
||||
Loaders are designed to ease the maintenance of parsing rules, without loosing
|
||||
flexibility and, at the same time, providing a convenient mechanism for
|
||||
extending and overriding them. For this reason Loaders support traditional
|
||||
class inheritance for for dealing with differences of specific spiders (or
|
||||
group of spiders).
|
||||
Item Loaders are designed to ease the maintenance burden of parsing rules,
|
||||
without loosing flexibility and, at the same time, providing a convenient
|
||||
mechanism for extending and overriding them. For this reason Item Loaders
|
||||
support traditional Python class inheritance for dealing with differences of
|
||||
specific spiders (or group of spiders).
|
||||
|
||||
Suppose, for example, that some particular site encloses their product names
|
||||
between three dashes (ie. ``---Plasma TV---``) and you don't want to end up
|
||||
scraping those dashes in the final product names.
|
||||
Suppose, for example, that some particular site encloses their product names in
|
||||
three dashes (ie. ``---Plasma TV---``) and you don't want to end up scraping
|
||||
those dashes in the final product names.
|
||||
|
||||
Here's how you can remove those dashes by reusing and extending the default
|
||||
Product Loader::
|
||||
Product Item Loader (``ProductLoader``)::
|
||||
|
||||
strip_dashes = lambda x: x.strip('-')
|
||||
from scrapy.contrib.loader.processor import ApplyConcat
|
||||
from myproject.ItemLoaders import ProductLoader
|
||||
|
||||
def strip_dashes(x):
|
||||
return x.strip('-')
|
||||
|
||||
class SiteSpecificLoader(ProductLoader):
|
||||
name_exp = TreeExpander(ProductLoader.name_exp, strip_dashes)
|
||||
name_in = ApplyConcat(ProductLoader.name_in, strip_dashes)
|
||||
|
||||
Another case where extending Loaders can be very helpful is when you have
|
||||
Another case where extending Item Loaders can be very helpful is when you have
|
||||
multiple source formats, for example XML and HTML. In the XML version you may
|
||||
want to remove ``CDATA`` occurrences. Here's an example of how to do it::
|
||||
|
||||
from scrapy.contrib.loader.processor import ApplyConcat
|
||||
from myproject.ItemLoaders import ProductLoader
|
||||
from myproject.utils.xml import remove_cdata
|
||||
|
||||
class XmlLoader(ProductLoader):
|
||||
name_exp = TreeExpander(remove_cdata, ProductLoader.name_exp)
|
||||
class XmlProductLoader(ProductLoader):
|
||||
name_in = ApplyConcat(remove_cdata, ProductLoader.name_in)
|
||||
|
||||
And that's how you typically extend expanders.
|
||||
And that's how you typically extend input processors.
|
||||
|
||||
As for reducers, it is more common to declare them in the field metadata, as
|
||||
they usually depend only on the field and not on each specific site parsing
|
||||
rule (as expanders do). See also: :ref:`topics-loader-expred-declaring`.
|
||||
As for output processors, it is more common to declare them in the field metadata,
|
||||
as they usually depend only on the field and not on each specific site parsing
|
||||
rule (as input processors do). See also:
|
||||
:ref:`topics-loaders-processors-declaring`.
|
||||
|
||||
There are many other possible ways to extend, inherit and override your
|
||||
Loaders, and different Loader hierarchies may fit better for different
|
||||
There are many other possible ways to extend, inherit and override your Item
|
||||
Loaders, and different Item Loaders hierarchies may fit better for different
|
||||
projects. Scrapy only provides the mechanism, it doesn't impose any specific
|
||||
organization of your Loaders collection - that's up to you and your project
|
||||
needs.
|
||||
|
||||
Available Expanders
|
||||
===================
|
||||
.. _topics-loaders-available-processors:
|
||||
|
||||
.. _topics-loader-tree-expander:
|
||||
Available built-in processors
|
||||
=============================
|
||||
|
||||
Tree Expander
|
||||
-------------
|
||||
Even though you can use any callable function as input and output processors,
|
||||
Scrapy provides some commonly used processors, which are described below. Some
|
||||
of them, like the :class:`ApplyConcat` (which is typically used as input
|
||||
processor) composes the output of several functions executed in order, to
|
||||
produce the final parsed value.
|
||||
|
||||
The Tree Expander is the recommended Expander to use and the only really useful
|
||||
one, as the other is just an identity expander.
|
||||
Here is a list of all built-in processors:
|
||||
|
||||
.. module:: scrapy.newitem.loader.expanders
|
||||
:synopsis: Expander classes to use with Item Loaders
|
||||
.. _topics-loaders-applyconcat:
|
||||
|
||||
.. class:: TreeExpander(\*functions, \**default_loader_arguments)
|
||||
ApplyConcat processor
|
||||
---------------------
|
||||
|
||||
An expander which applies the given functions consecutively, in order, to
|
||||
each value returned by the previous function.
|
||||
The ApplyConcat processor is the recommended processor to use if you want to
|
||||
concatenate the processing of several functions in a pipeline.
|
||||
|
||||
The algorithm consists in an ordered list of functions, each of which
|
||||
receives one value and can return zero, one or more values (as a list or
|
||||
iterable). If a function returns more than one value, the next function in
|
||||
the list will be called with each of those values, potentially returning
|
||||
more values and thus expanding the execution into different branches, which
|
||||
is why this expander is called Tree Expander.
|
||||
.. module:: scrapy.contrib.loader.processor
|
||||
:synopsis: A collection of processors to use with Item Loaders
|
||||
|
||||
Each expander function can optionally receive a ``loader_args`` argument,
|
||||
which will contain the currently active :ref:`Loader arguments
|
||||
<topics-loader-args>`.
|
||||
.. class:: ApplyConcat(\*functions, \**default_loader_context)
|
||||
|
||||
A processor which applies the given functions consecutively, in order,
|
||||
concatenating their results before next function call. So each function
|
||||
returns a list of values (though it could return ``None`` or a signle value
|
||||
too) and the next function is called once for each of those values,
|
||||
receiving one of those values as input each time. The output of each
|
||||
function call (for each input value) is concatenated and each values of the
|
||||
concatenation is used to call the next function, and the process repeats
|
||||
until there are no functions left.
|
||||
|
||||
Each function can optionally receive a ``loader_context`` parameter, which
|
||||
will contain the currently active :ref:`Item Loader context
|
||||
<topics-loaders-context>`.
|
||||
|
||||
The keyword arguments passed in the consturctor are used as the default
|
||||
Loader arguments passed to on each expander call. This arguments can be
|
||||
overriden with specific noader arguments passed on each expander call.
|
||||
Item Loader context values passed on each function call. However, the final
|
||||
Item Loader context values passed to funtions get overriden with the
|
||||
currently active Item Loader context accesible through the
|
||||
:meth:`ItemLoader.context` attribute.
|
||||
|
||||
Example::
|
||||
|
||||
>>> def filter_world(x):
|
||||
... return None if x == 'world' else x
|
||||
...
|
||||
>>> from scrapy.newitem.loader.expanders import TreeExpander
|
||||
>>> expander = TreeExpander(filter_world, str.upper)
|
||||
>>> expander(['hello', 'world', 'this', 'is', 'scrapy'])
|
||||
>>> from scrapy.contrib.loader.processor import ApplyConcat
|
||||
>>> proc = ApplyConcat(filter_world, str.upper)
|
||||
>>> proc(['hello', 'world', 'this', 'is', 'scrapy'])
|
||||
['HELLO, 'THIS', 'IS', 'SCRAPY']
|
||||
|
||||
|
||||
IdentityExpander
|
||||
----------------
|
||||
|
||||
.. class:: IdentityExpander
|
||||
|
||||
An expander which returns the original values unchanged. It doesn't support
|
||||
any constructor arguments.
|
||||
|
||||
.. _topics-loader-reducers:
|
||||
|
||||
Available Reducers
|
||||
==================
|
||||
|
||||
.. module:: scrapy.newitem.loader.reducers
|
||||
:synopsis: Reducer classes to use with Item Loaders
|
||||
|
||||
Reducers are callable objects which are called with a list of values (to be
|
||||
reduced) as their first and only argument. Scrapy provides some simple,
|
||||
commonly used reducers, which are described below. But you can use any function
|
||||
or callable as reducer.
|
||||
|
||||
.. class:: TakeFirst
|
||||
|
||||
Return the first non-null value from the values to reduce, so it's used for
|
||||
single-valued fields. It doesn't receive any constructor arguments.
|
||||
Return the first non null/empty value from the values to received, so it's
|
||||
typically used as output processor of single-valued fields. It doesn't
|
||||
receive any constructor arguments, nor accepts a Item Loader context.
|
||||
|
||||
Example::
|
||||
|
||||
>>> from scrapy.newitem.loader.reducers import TakeFirst
|
||||
>>> reducer = TakeFirst()
|
||||
>>> reducer(['', 'one', 'two', 'three'])
|
||||
>>> from scrapy.contrib.loader.processor import TakeFirst
|
||||
>>> proc = TakeFirst()
|
||||
>>> proc(['', 'one', 'two', 'three'])
|
||||
'one'
|
||||
|
||||
.. class:: Identity
|
||||
|
||||
Return the values to reduce unchanged, so it's used for multi-valued
|
||||
fields. It doesn't receive any constructor arguments.
|
||||
Return the original values unchanged. It doesn't receive any constructor
|
||||
arguments nor accepts a Item Loader context.
|
||||
|
||||
Example::
|
||||
|
||||
>>> from scrapy.newitem.loader.reducers import Identity
|
||||
>>> reducer = Identity()
|
||||
>>> reducer(['one', 'two', 'three'])
|
||||
>>> from scrapy.contrib.loader.processor import Identity
|
||||
>>> proc = Identity()
|
||||
>>> proc(['one', 'two', 'three'])
|
||||
['one', 'two', 'three']
|
||||
|
||||
.. class:: Join(separator=u' ')
|
||||
|
||||
Return a the values to reduce joined with the separator given in the
|
||||
constructor, which defaults to ``u' '``.
|
||||
Return the values joined with the separator given in the constructor, which
|
||||
defaults to ``u' '``. It doesn't accept a Item Loader context.
|
||||
|
||||
When using the default separator, this reducer is equivalent to the
|
||||
When using the default separator, this processor is equivalent to the
|
||||
function: ``u' '.join``
|
||||
|
||||
Examples::
|
||||
|
||||
>>> from scrapy.newitem.loader.reducers import Join
|
||||
>>> reducer = Join()
|
||||
>>> reducer(['one', 'two', 'three'])
|
||||
>>> from scrapy.contrib.loader.processor import Join
|
||||
>>> proc = Join()
|
||||
>>> proc(['one', 'two', 'three'])
|
||||
u'one two three'
|
||||
>>> reducer = Join('<br>')
|
||||
>>> reducer(['one', 'two', 'three'])
|
||||
>>> proc = Join('<br>')
|
||||
>>> proc(['one', 'two', 'three'])
|
||||
u'one<br>two<br>three'
|
||||
|
|
|
|||
|
|
@ -1,205 +0,0 @@
|
|||
.. _topics-adaptors:
|
||||
|
||||
==============================
|
||||
Old Item Adaptors (deprecated)
|
||||
==============================
|
||||
|
||||
.. warning::
|
||||
|
||||
This documentation is deprecated, and will be superseeded by the new item
|
||||
adaptors (not yet documented).
|
||||
|
||||
Quick overview
|
||||
==============
|
||||
|
||||
Scrapy's adaptors are a nice feature attached to :class:`RobustScrapedItem`
|
||||
that allow you to easily modify (adapt to your needs) any kind of information
|
||||
you want to put in your items at assignation time.
|
||||
|
||||
The following diagram shows the data flow from the moment you call the
|
||||
``attribute`` method until the attribute is actually set.
|
||||
|
||||
.. image:: _images/adaptors_diagram.png
|
||||
|
||||
As you can see, adaptor pipelines are executed in tree form; which means that,
|
||||
for each of the values you pass to the ``attribute`` method, the first adaptor
|
||||
will be applied. Then, for each of the resulting values of the first adaptor,
|
||||
the second adaptor will be called, and so on. This process will end up with a
|
||||
list of adapted values, which may contain zero, one, or many values.
|
||||
|
||||
In case the attribute is a single-valued (this is defined in the item's
|
||||
``ATTRIBUTES`` dictionary), the first element of this list will be set, unless
|
||||
you call the ``attribute`` method with the add parameter as True, in which case
|
||||
the item's method ``_add_single_attributes`` will be called with the
|
||||
attribute's name, type, and the list of attributes to join as parameters. By
|
||||
default, this method raises NotImplementedError, so you should override it in
|
||||
your items in order to join any kind of objects.
|
||||
|
||||
If the attribute is a multivalued, the resulting list will be set to the item
|
||||
as is, unless you use -again- add=True, in which case the list of
|
||||
already-existing values (if any) will be extended with the new one.pgq
|
||||
|
||||
Adaptor Pipelines
|
||||
=================
|
||||
|
||||
.. class:: AdaptorPipe(adaptors=None)
|
||||
|
||||
An instance of this class represents an adaptor pipeline to be set for
|
||||
adapting a certain item's attribute. It provides some useful methods for
|
||||
adding/removing adaptors, and takes care of executing them properly.
|
||||
Usually this class is not used directly, since the items already provide
|
||||
ways to manage adaptors without having to handle AdaptorPipes.
|
||||
|
||||
:param adaptors: A list of callables to be added as adaptors at
|
||||
instancing time.
|
||||
|
||||
Methods:
|
||||
|
||||
.. method:: add_adaptor(adaptor, position=None)
|
||||
|
||||
This method is used for adding adaptors to the pipeline given
|
||||
a certain position.
|
||||
|
||||
:param adaptor: Any callable that works as an adaptor
|
||||
:param position: An integer meaning the position in which the adaptor
|
||||
will be inserted. If it's None the adaptor will be appended at
|
||||
the end of the pipeline.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
As it was previously said, in order to use adaptor pipelines you must inherit
|
||||
your items from the :class:`RobustScrapedItem` class. If you don't know
|
||||
anything about these items, read the :ref:`topics-items` reference first.
|
||||
|
||||
Once you've created your own item class (inherited from
|
||||
:class:`RobustScrapedItem`) with the attributes you're going to use, you have
|
||||
to add adaptor pipelines to each attribute you'd like to adapt data for. For
|
||||
doing so, RobustScrapedItems provide some useful methods like ``set_adaptors``,
|
||||
``set_attrib_adaptors``, and more (which are also described in its reference)
|
||||
so that you don't need to work with :class:`AdaptorPipe` objects directly.
|
||||
|
||||
Adaptors
|
||||
--------
|
||||
|
||||
Let's now talk a bit about adaptors (singularly), what are them, and how
|
||||
should they be implemented?
|
||||
|
||||
Adaptors are basically, any callable that receives
|
||||
a value, modifies it, and returns a new value (or more) so that the next
|
||||
adaptor goes on with another adapting task (or not). This is done this way to
|
||||
make the process of modifying information very customizable, and also to make
|
||||
adaptors reusable, since they are intended to be small functions designed for
|
||||
simple purposes that can be applied in many different cases. For example, you
|
||||
could make an adaptor for removing any <b> tags in a text, like this::
|
||||
|
||||
>>> B_TAG_RE = re.compile(r'</?b\s*>')
|
||||
>>> def remove_b_tags(text):
|
||||
>>> return B_TAG_RE.sub('', text)
|
||||
|
||||
Then you could easily add this adaptor to a certain attribute's pipeline like
|
||||
this::
|
||||
|
||||
>>> item = MyItem()
|
||||
>>> item.add_adaptor('text', remove_b_tags)
|
||||
>>> item.attribute('text', u'<b>some random text in bold</b> and some random text in normal font')
|
||||
>>> item.text
|
||||
u'some random text in bold and some random text in normal font'
|
||||
|
||||
As you can see, this would make any value that you set to the item through the
|
||||
``attribute`` method first pass through the ``remove_b_tags`` adaptor, which
|
||||
would also replace any matching tag with an empty string.
|
||||
|
||||
----
|
||||
|
||||
But anyway, let's now think of a bit more complicated (and useless) example:
|
||||
let's say you want to scrape a text, split it into single letters, strip the
|
||||
vowels, turn the rest to capital letters, and join them again. In this case,
|
||||
we could use three simple adaptors to process our data, plus a customized
|
||||
:class:`RobustScrapedItem` for joining single text attributes; let's see an
|
||||
example::
|
||||
|
||||
>>> # First of all, we define the item class we're going to use
|
||||
>>> from string import ascii_letters
|
||||
>>> from scrapy.contrib.item import RobustScrapedItem
|
||||
>>> class MyItem(RobustScrapedItem):
|
||||
>>> ATTRIBUTES = {
|
||||
>>> 'text': basestring,
|
||||
>>> }
|
||||
|
||||
>>> def _add_single_attributes(self, attrname, attrtype, attributes):
|
||||
>>> return ''.join(attributes)
|
||||
|
||||
>>> # Now we'll write the needed adaptors
|
||||
>>> def to_letters(text):
|
||||
>>> return tuple(letter for letter in text)
|
||||
|
||||
>>> def is_vowel(letter):
|
||||
>>> if letter in ascii_letters and letter.lower() not in ('a', 'e', 'i', 'o', 'u'):
|
||||
>>> return letter
|
||||
|
||||
>>> def to_upper(letter):
|
||||
>>> return letter.upper()
|
||||
|
||||
>>> # Finally, we'll join all the pieces and see how it works
|
||||
>>> item = MyItem()
|
||||
>>> item.set_attrib_adaptors('text', [
|
||||
>>> to_letters,
|
||||
>>> is_vowel,
|
||||
>>> to_upper,
|
||||
>>> ])
|
||||
|
||||
Let's now try with an example text to see what happens::
|
||||
|
||||
>>> item.attribute('text', 'pi', 'wind', add=True)
|
||||
>>> item.text
|
||||
'PWND'
|
||||
|
||||
More complex adaptors
|
||||
---------------------
|
||||
|
||||
Now, after using adaptors a bit, you may find yourself in situations where you need
|
||||
to use adaptors that receive other parameters from the ``attribute`` method
|
||||
apart from the value to adapt.
|
||||
|
||||
For example, imagine you have an adaptor that removes certain characters from strings
|
||||
you provide. Would you make an adaptor for each combination of characters you'd like
|
||||
to strip? Of course not!
|
||||
|
||||
The way to handle this cases, is to make an adaptor that apart from receiving a value,
|
||||
as any other adaptor, receives a parameter called ``adaptor_args``.
|
||||
It's important that the parameter is called this way, since Scrapy finds out whether
|
||||
an adaptor is able to receive extra parameters or not by making instrospection
|
||||
and looking for a parameter called this way in the adaptor's parameters list.
|
||||
|
||||
The information this parameter will receive won't be anything else but the same dictionary
|
||||
of keyword arguments that you pass to the ``attribute`` method when calling it.
|
||||
|
||||
But let's get back to the characters example, how would we implement this?
|
||||
Quite simmilar to any other adaptor, let's see::
|
||||
|
||||
def strip_chars(value, adaptor_args):
|
||||
chars = adaptor_args.get('strip_chars', [])
|
||||
for char in chars:
|
||||
value = value.replace(char, '')
|
||||
return value
|
||||
|
||||
Then, after creating an item and adding the adaptor to one of its pipelines, we could do::
|
||||
|
||||
>>> item.attribute('text', 'Hi, my name is John', strip_chars=['a', 'i', 'm'])
|
||||
>>> item.text
|
||||
'H, y ne s John'
|
||||
|
||||
Debugging
|
||||
=========
|
||||
|
||||
While you're coding spiders and adaptors, you usually need to know exactly what
|
||||
does Scrapy do under the hood with the values you provide. There's a setting
|
||||
called :setting:``ADAPTORS_DEBUG`` for this purpose that makes Scrapy print
|
||||
debugging messages each time an adaptors pipeline is run, specifying which
|
||||
attribute is being adapted data for, the input/output values of each adaptor in
|
||||
the pipeline, and the input/output of ``_add_single_attributes`` (in some
|
||||
cases).
|
||||
|
||||
You can enable this setting as any other, either by adding it to your settings
|
||||
file, or by enabling the environment variable ``SCRAPY_ADAPTORS_DEBUG``.
|
||||
|
|
@ -51,144 +51,3 @@ Creating an item and setting its attributes inline::
|
|||
>>> person
|
||||
ScrapedItem({'age': 23, 'last_name': 'Smith', 'name': 'John'})
|
||||
|
||||
RobustScrapedItems
|
||||
==================
|
||||
|
||||
.. warning::
|
||||
|
||||
RobustScapedItems are deprecated and will be replaced by the :ref:`New item
|
||||
API <topics-newitems>` (still in development).
|
||||
|
||||
.. module:: scrapy.contrib.item
|
||||
:synopsis: Objects for storing scraped data
|
||||
|
||||
.. class:: RobustScrapedItem
|
||||
|
||||
RobustScrapedItems are more complex items (compared to
|
||||
:class:`ScrapedItem`) and have a few more features available, which
|
||||
include:
|
||||
|
||||
* Attributes dictionary: items that inherit from RobustScrapedItem are
|
||||
defined with a dictionary of attributes in the class. This allows the
|
||||
item to have more logic at the moment of handling and setting attributes
|
||||
than the :class:`ScrapedItem`.
|
||||
|
||||
* Adaptors: perhaps the most important of the features these items provide.
|
||||
The adaptors are a system designed for filtering/modifying data before
|
||||
setting it to the item, that makes cleansing tasks a lot easier.
|
||||
|
||||
* Type checking: RobustScrapedItems come with a built-in type checking
|
||||
which assures you that no data of the wrong type will get into the items
|
||||
without raising a warning.
|
||||
|
||||
* Versioning: These items also provide versioning by making a unique hash
|
||||
for each item based on its attributes values.
|
||||
|
||||
* ItemDeltas: You can subtract two RobustScrapedItems, which allows you to
|
||||
know the difference between a pair of items. This difference is
|
||||
represented by a RobustItemDelta object.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
||||
.. attribute:: RobustScrapedItem.ATTRIBUTES
|
||||
|
||||
This attribute **must** be specified when writing your items, and it's a
|
||||
dictionary in which the keys are the names of the attributes your item will
|
||||
have, and their values are the type of those attributes. For multivalued
|
||||
attributes, you should write the type of the values inside a list, e.g:
|
||||
``'numbers': [int]``
|
||||
|
||||
Methods
|
||||
-------
|
||||
|
||||
.. method:: RobustScrapedItem.__init__(data=None, adaptor_args=None)
|
||||
|
||||
:param data: Idem as for ScrapedItems
|
||||
:param adaptor_args: A dictionary of the kind
|
||||
``'attribute': [list_of_adaptors]``" for defining adaptors automatically
|
||||
after instancing the item.
|
||||
|
||||
Constructor of RobustScrapedItem objects.
|
||||
|
||||
.. method:: RobustScrapedItem.attribute(attrname, value, override=False, add=False, ***kwargs)
|
||||
|
||||
Sets the item's ``attrname`` attribute with the given ``value`` filtering
|
||||
it through the given attribute's adaptor pipeline (if any).
|
||||
|
||||
:param attrname: a string containing the name of the attribute you want
|
||||
to set.
|
||||
|
||||
:param value: the value you want to assign, which will be adapted by
|
||||
the corresponding adaptors for the given attribute (if any).
|
||||
|
||||
:param override: if True, makes this method avoid checking if there
|
||||
was a previous value and sets ``value`` no matter what.
|
||||
|
||||
:param add: if True, tries to concatenate the given ``value`` with the one
|
||||
already set in the item. For multivalued attributes, this will extend
|
||||
the list of already-set values, with the new ones.
|
||||
For single valued attributes, the method _add_single_attributes (which
|
||||
is explained below) will be called.
|
||||
|
||||
:param kwargs: any extra parameters will be passed in a dictionary to any
|
||||
adaptor that receives a parameter called ``adaptor_args``.
|
||||
Check the :ref:`topics-adaptors` topic for more information.
|
||||
|
||||
.. method:: RobustScrapedItem.set_adaptors(adaptors_dict)
|
||||
|
||||
Receives a dict containing a list of adaptors for each desired attribute
|
||||
(key) and sets each of them as their adaptor pipeline.
|
||||
|
||||
.. method:: RobustScrapedItem.set_attrib_adaptors(attrib, pipe)
|
||||
|
||||
Sets the provided iterable (``pipe``) as the adaptor pipeline for the
|
||||
given attribute (``attrib``)
|
||||
|
||||
.. method:: RobustScrapedItem.add_adaptor(attrib, adaptor, position=None)
|
||||
|
||||
Adds an adaptor to an already existing (or not) pipeline.
|
||||
|
||||
:param attr: the name of the attribute you're adding adaptors to.
|
||||
|
||||
:param adaptor: a callable to be added to the pipeline.
|
||||
|
||||
:param position: an integer representing the place where to add the adaptor.
|
||||
If it's ``None``, the adaptor will be appended at the end of the pipeline.
|
||||
|
||||
.. method:: RobustScrapedItem._add_single_attributes(attrname, attrtype, attributes)
|
||||
|
||||
This method is the one to be called whenever a single attribute has to be
|
||||
joined before storing into an item. That is,
|
||||
every time you have multiple results at the end of your adaptors pipeline,
|
||||
and you called the ``attribute`` method with the parameter `add=True`.
|
||||
|
||||
This method is intended to be overriden by you, since by default it
|
||||
raises an exception.
|
||||
|
||||
:param attrname: the name of the attribute you're setting
|
||||
:param attrtype: the type of the attribute you're setting
|
||||
:param attributes: the list of resulting values after the adaptors pipeline
|
||||
(the one you have to join somehow)
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Creating a pretty basic item with a few attributes::
|
||||
|
||||
from scrapy.contrib.item import RobustScrapedItem
|
||||
|
||||
class MyItem(RobustScrapedItem):
|
||||
ATTRIBUTES = {
|
||||
'name': basestring,
|
||||
'size': basestring,
|
||||
'colours': [basestring],
|
||||
}
|
||||
|
||||
Setting some adaptors::
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
More RobustScrapedItem examples are about to come. In the meantime, check the :ref:`topics-adaptors` topic to see a few of them.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
"""
|
||||
Item Loader
|
||||
|
||||
See documentation in docs/topics/loaders.rst
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from scrapy.newitem import Item
|
||||
from scrapy.xpath import HtmlXPathSelector
|
||||
from scrapy.utils.misc import arg_to_iter
|
||||
from .common import wrap_loader_context
|
||||
from .processor import Identity
|
||||
|
||||
class ItemLoader(object):
|
||||
|
||||
default_item_class = Item
|
||||
default_input_processor = Identity()
|
||||
default_output_processor = Identity()
|
||||
|
||||
def __init__(self, item=None, **context):
|
||||
if item is None:
|
||||
item = self.default_item_class()
|
||||
self.item = context['item'] = item
|
||||
self.context = context
|
||||
self._values = defaultdict(list)
|
||||
|
||||
def add_value(self, field_name, value):
|
||||
parsed_value = self._parse_input_value(field_name, value)
|
||||
self._values[field_name] += arg_to_iter(parsed_value)
|
||||
|
||||
def replace_value(self, field_name, value):
|
||||
parsed_value = self._parse_input_value(field_name, value)
|
||||
self._values[field_name] = arg_to_iter(parsed_value)
|
||||
|
||||
def populate_item(self):
|
||||
item = self.item
|
||||
for field_name in self._values:
|
||||
item[field_name] = self.get_output_value(field_name)
|
||||
return item
|
||||
|
||||
def get_output_value(self, field_name):
|
||||
proc = self.get_output_processor(field_name)
|
||||
proc = wrap_loader_context(proc, self.context)
|
||||
return proc(self._values[field_name])
|
||||
|
||||
def get_collected_values(self, field_name):
|
||||
return self._values[field_name]
|
||||
|
||||
def get_input_processor(self, field_name):
|
||||
proc = getattr(self, '%s_in' % field_name, None)
|
||||
if not proc:
|
||||
proc = self.item.fields[field_name].get('input_processor', \
|
||||
self.default_input_processor)
|
||||
return proc
|
||||
|
||||
def get_output_processor(self, field_name):
|
||||
proc = getattr(self, '%s_out' % field_name, None)
|
||||
if not proc:
|
||||
proc = self.item.fields[field_name].get('output_processor', \
|
||||
self.default_output_processor)
|
||||
return proc
|
||||
|
||||
def _parse_input_value(self, field_name, value):
|
||||
proc = self.get_input_processor(field_name)
|
||||
proc = wrap_loader_context(proc, self.context)
|
||||
return proc(value)
|
||||
|
||||
|
||||
class XPathItemLoader(ItemLoader):
|
||||
|
||||
default_selector_class = HtmlXPathSelector
|
||||
|
||||
def __init__(self, item=None, selector=None, response=None, **context):
|
||||
if selector is None and response is None:
|
||||
raise RuntimeError("%s must be instantiated with a selector" \
|
||||
"or response" % self.__class__.__name__)
|
||||
if selector is None:
|
||||
selector = self.default_selector_class(response)
|
||||
self.selector = selector
|
||||
context.update(selector=selector, response=response)
|
||||
super(XPathItemLoader, self).__init__(item, **context)
|
||||
|
||||
def add_xpath(self, field_name, xpath, re=None):
|
||||
self.add_value(field_name, self._get_values(field_name, xpath, re))
|
||||
|
||||
def replace_xpath(self, field_name, xpath, re=None):
|
||||
self.replace_value(field_name, self._get_values(field_name, xpath, re))
|
||||
|
||||
def _get_values(self, field_name, xpath, re):
|
||||
x = self.selector.x(xpath)
|
||||
return x.re(re) if re else x.extract()
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
"""Common functions used in Item Loaders code"""
|
||||
|
||||
from functools import partial
|
||||
from scrapy.utils.python import get_func_args
|
||||
|
||||
def wrap_loader_context(function, context):
|
||||
"""Wrap functions that receive loader_context to contain the context
|
||||
"pre-loaded" and expose a interface that receives only one argument
|
||||
"""
|
||||
if 'loader_context' in get_func_args(function):
|
||||
return partial(function, loader_context=context)
|
||||
else:
|
||||
return function
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
"""
|
||||
This module provides some commonly used processors for Item Loaders.
|
||||
|
||||
See documentation in docs/topics/loaders.rst
|
||||
"""
|
||||
|
||||
from scrapy.utils.misc import arg_to_iter
|
||||
from scrapy.utils.datatypes import MergeDict
|
||||
from .common import wrap_loader_context
|
||||
|
||||
class ApplyConcat(object):
|
||||
|
||||
def __init__(self, *functions, **default_loader_context):
|
||||
self.functions = functions
|
||||
self.default_loader_context = default_loader_context
|
||||
|
||||
def __call__(self, value, loader_context=None):
|
||||
values = arg_to_iter(value)
|
||||
if loader_context:
|
||||
context = MergeDict(loader_context, self.default_loader_context)
|
||||
else:
|
||||
context = self.default_loader_context
|
||||
wrapped_funcs = [wrap_loader_context(f, context) for f in self.functions]
|
||||
for func in wrapped_funcs:
|
||||
next_values = []
|
||||
for v in values:
|
||||
next_values += arg_to_iter(func(v))
|
||||
values = next_values
|
||||
return list(values)
|
||||
|
||||
|
||||
class TakeFirst(object):
|
||||
|
||||
def __call__(self, values):
|
||||
for value in values:
|
||||
if value:
|
||||
return value
|
||||
|
||||
|
||||
class Identity(object):
|
||||
|
||||
def __call__(self, values):
|
||||
return values
|
||||
|
||||
|
||||
class Join(object):
|
||||
|
||||
def __init__(self, separator=u' '):
|
||||
self.separator = separator
|
||||
|
||||
def __call__(self, values):
|
||||
return self.separator.join(values)
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from scrapy.utils.datatypes import MergeDict
|
||||
from scrapy.newitem import Item
|
||||
from scrapy.newitem.loader.reducers import TakeFirst
|
||||
from scrapy.newitem.loader.expanders import IdentityExpander
|
||||
from scrapy.xpath import HtmlXPathSelector
|
||||
|
||||
class Loader(object):
|
||||
|
||||
default_item_class = Item
|
||||
default_expander = IdentityExpander()
|
||||
default_reducer = TakeFirst()
|
||||
|
||||
def __init__(self, item=None, **loader_args):
|
||||
if item is None:
|
||||
item = self.default_item_class()
|
||||
self._item = loader_args['item'] = item
|
||||
self._loader_args = loader_args
|
||||
self._values = defaultdict(list)
|
||||
|
||||
def add_value(self, field_name, value, **new_loader_args):
|
||||
self._values[field_name] += self._expand_value(field_name, value, \
|
||||
new_loader_args)
|
||||
|
||||
def replace_value(self, field_name, value, **new_loader_args):
|
||||
self._values[field_name] = self._expand_value(field_name, value, \
|
||||
new_loader_args)
|
||||
|
||||
def get_item(self):
|
||||
item = self._item
|
||||
for field_name in self._values:
|
||||
item[field_name] = self.get_reduced_value(field_name)
|
||||
return item
|
||||
|
||||
def get_expanded_value(self, field_name):
|
||||
return self._values[field_name]
|
||||
|
||||
def get_reduced_value(self, field_name):
|
||||
reducer = self.get_reducer(field_name)
|
||||
return reducer(self._values[field_name])
|
||||
|
||||
def get_expander(self, field_name):
|
||||
expander = getattr(self, '%s_exp' % field_name, None)
|
||||
if not expander:
|
||||
expander = self._item.fields[field_name].get('expander', \
|
||||
self.default_expander)
|
||||
return expander
|
||||
|
||||
def get_reducer(self, field_name):
|
||||
reducer = getattr(self, '%s_red' % field_name, None)
|
||||
if not reducer:
|
||||
reducer = self._item.fields[field_name].get('reducer', \
|
||||
self.default_reducer)
|
||||
return reducer
|
||||
|
||||
def _expand_value(self, field_name, value, new_loader_args):
|
||||
loader_args = self._loader_args
|
||||
if new_loader_args:
|
||||
loader_args = MergeDict(new_loader_args, self._loader_args)
|
||||
expander = self.get_expander(field_name)
|
||||
return expander(value, loader_args=loader_args)
|
||||
|
||||
|
||||
class XPathLoader(Loader):
|
||||
|
||||
default_selector_class = HtmlXPathSelector
|
||||
|
||||
def __init__(self, item=None, selector=None, response=None, **loader_args):
|
||||
if selector is None and response is None:
|
||||
raise RuntimeError("%s must be instantiated with a selector" \
|
||||
"or response" % self.__class__.__name__)
|
||||
if selector is None:
|
||||
selector = self.default_selector_class(response)
|
||||
self.selector = selector
|
||||
loader_args.update(selector=selector, response=response)
|
||||
super(XPathLoader, self).__init__(item, **loader_args)
|
||||
|
||||
def add_xpath(self, field_name, xpath, re=None, **new_loader_args):
|
||||
self.add_value(field_name, self._get_values(field_name, xpath, re),
|
||||
**new_loader_args)
|
||||
|
||||
def replace_xpath(self, field_name, xpath, re=None, **new_loader_args):
|
||||
self.replace_value(field_name, self._get_values(field_name, xpath, re), \
|
||||
**new_loader_args)
|
||||
|
||||
def _get_values(self, field_name, xpath, re):
|
||||
x = self.selector.x(xpath)
|
||||
return x.re(re) if re else x.extract()
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"""
|
||||
This module provides some commonly used Expanders.
|
||||
|
||||
See documentation in docs/topics/newitem-loader.rst
|
||||
"""
|
||||
|
||||
from scrapy.utils.misc import arg_to_iter
|
||||
from scrapy.utils.python import get_func_args
|
||||
from scrapy.utils.datatypes import MergeDict
|
||||
|
||||
class TreeExpander(object):
|
||||
|
||||
def __init__(self, *functions, **default_loader_args):
|
||||
self.default_loader_args = default_loader_args
|
||||
self.wrapped_funcs = []
|
||||
for func in functions:
|
||||
if 'loader_args' in get_func_args(func):
|
||||
wfunc = self.wrap_with_args(func)
|
||||
else:
|
||||
wfunc = self.wrap_no_args(func)
|
||||
self.wrapped_funcs.append(wfunc)
|
||||
|
||||
def wrap_no_args(self, f):
|
||||
return lambda x, y: f(x)
|
||||
|
||||
def wrap_with_args(self, f):
|
||||
return lambda x, y: f(x, loader_args=y)
|
||||
|
||||
def __call__(self, value, loader_args=None):
|
||||
values = arg_to_iter(value)
|
||||
largs = self.default_loader_args
|
||||
if loader_args:
|
||||
largs = MergeDict(loader_args, self.default_loader_args)
|
||||
for func in self.wrapped_funcs:
|
||||
next_values = []
|
||||
for v in values:
|
||||
next_values += arg_to_iter(func(v, largs))
|
||||
values = next_values
|
||||
return list(values)
|
||||
|
||||
|
||||
class IdentityExpander(object):
|
||||
|
||||
def __call__(self, values, loader_args):
|
||||
return arg_to_iter(values)
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
"""
|
||||
This module provides some commonly used Reducers.
|
||||
|
||||
See documentation in docs/topics/newitem-loader.rst
|
||||
"""
|
||||
|
||||
class TakeFirst(object):
|
||||
|
||||
def __call__(self, values):
|
||||
for value in values:
|
||||
if value:
|
||||
return value
|
||||
|
||||
|
||||
class Identity(object):
|
||||
|
||||
def __call__(self, values):
|
||||
return values
|
||||
|
||||
|
||||
class Join(object):
|
||||
|
||||
def __init__(self, separator=u' '):
|
||||
self.separator = separator
|
||||
|
||||
def __call__(self, values):
|
||||
return self.separator.join(values)
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
import unittest
|
||||
|
||||
from scrapy.contrib.loader import ItemLoader, XPathItemLoader
|
||||
from scrapy.contrib.loader.processor import ApplyConcat, Join, Identity
|
||||
from scrapy.newitem import Item, Field
|
||||
from scrapy.xpath import HtmlXPathSelector
|
||||
from scrapy.http import HtmlResponse
|
||||
|
||||
# test items
|
||||
|
||||
class NameItem(Item):
|
||||
name = Field()
|
||||
|
||||
class TestItem(NameItem):
|
||||
url = Field()
|
||||
summary = Field()
|
||||
|
||||
# test item loaders
|
||||
|
||||
class NameItemLoader(ItemLoader):
|
||||
default_item_class = TestItem
|
||||
|
||||
class TestItemLoader(NameItemLoader):
|
||||
name_in = ApplyConcat(lambda v: v.title())
|
||||
|
||||
class DefaultedItemLoader(NameItemLoader):
|
||||
default_input_processor = ApplyConcat(lambda v: v[:-1])
|
||||
|
||||
# test processors
|
||||
|
||||
def processor_with_args(value, other=None, loader_context=None):
|
||||
if 'key' in loader_context:
|
||||
return loader_context['key']
|
||||
return value
|
||||
|
||||
class ItemLoaderTest(unittest.TestCase):
|
||||
|
||||
def test_populate_item_using_default_loader(self):
|
||||
i = TestItem()
|
||||
i['summary'] = u'lala'
|
||||
ip = ItemLoader(item=i)
|
||||
ip.add_value('name', u'marta')
|
||||
item = ip.populate_item()
|
||||
assert item is i
|
||||
self.assertEqual(item['summary'], u'lala')
|
||||
self.assertEqual(item['name'], [u'marta'])
|
||||
|
||||
def test_populate_item_using_custom_loader(self):
|
||||
ip = TestItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
item = ip.populate_item()
|
||||
self.assertEqual(item['name'], [u'Marta'])
|
||||
|
||||
def test_add_value(self):
|
||||
ip = TestItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_collected_values('name'), [u'Marta'])
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Marta'])
|
||||
ip.add_value('name', u'pepe')
|
||||
self.assertEqual(ip.get_collected_values('name'), [u'Marta', u'Pepe'])
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Marta', u'Pepe'])
|
||||
|
||||
def test_replace_value(self):
|
||||
ip = TestItemLoader()
|
||||
ip.replace_value('name', u'marta')
|
||||
self.assertEqual(ip.get_collected_values('name'), [u'Marta'])
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Marta'])
|
||||
ip.replace_value('name', u'pepe')
|
||||
self.assertEqual(ip.get_collected_values('name'), [u'Pepe'])
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Pepe'])
|
||||
|
||||
def test_apply_concat_filter(self):
|
||||
def filter_world(x):
|
||||
return None if x == 'world' else x
|
||||
|
||||
proc = ApplyConcat(filter_world, str.upper)
|
||||
self.assertEqual(proc(['hello', 'world', 'this', 'is', 'scrapy']),
|
||||
['HELLO', 'THIS', 'IS', 'SCRAPY'])
|
||||
|
||||
def test_map_concat_filter_multiple_functions(self):
|
||||
class TestItemLoader(NameItemLoader):
|
||||
name_in = ApplyConcat(lambda v: v.title(), lambda v: v[:-1])
|
||||
|
||||
ip = TestItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Mart'])
|
||||
item = ip.populate_item()
|
||||
self.assertEqual(item['name'], [u'Mart'])
|
||||
|
||||
def test_default_input_processor(self):
|
||||
ip = DefaultedItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'mart'])
|
||||
|
||||
def test_inherited_default_input_processor(self):
|
||||
class InheritDefaultedItemLoader(DefaultedItemLoader):
|
||||
pass
|
||||
|
||||
ip = InheritDefaultedItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'mart'])
|
||||
|
||||
def test_input_processor_inheritance(self):
|
||||
class ChildItemLoader(TestItemLoader):
|
||||
url_in = ApplyConcat(lambda v: v.lower())
|
||||
|
||||
ip = ChildItemLoader()
|
||||
ip.add_value('url', u'HTTP://scrapy.ORG')
|
||||
self.assertEqual(ip.get_output_value('url'), [u'http://scrapy.org'])
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Marta'])
|
||||
|
||||
class ChildChildItemLoader(ChildItemLoader):
|
||||
url_in = ApplyConcat(lambda v: v.upper())
|
||||
summary_in = ApplyConcat(lambda v: v)
|
||||
|
||||
ip = ChildChildItemLoader()
|
||||
ip.add_value('url', u'http://scrapy.org')
|
||||
self.assertEqual(ip.get_output_value('url'), [u'HTTP://SCRAPY.ORG'])
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Marta'])
|
||||
|
||||
def test_empty_map_concat(self):
|
||||
class IdentityDefaultedItemLoader(DefaultedItemLoader):
|
||||
name_in = ApplyConcat()
|
||||
|
||||
ip = IdentityDefaultedItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'marta'])
|
||||
|
||||
def test_identity_input_processor(self):
|
||||
class IdentityDefaultedItemLoader(DefaultedItemLoader):
|
||||
name_in = Identity()
|
||||
|
||||
ip = IdentityDefaultedItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'marta'])
|
||||
|
||||
def test_extend_custom_input_processors(self):
|
||||
class ChildItemLoader(TestItemLoader):
|
||||
name_in = ApplyConcat(TestItemLoader.name_in, unicode.swapcase)
|
||||
|
||||
ip = ChildItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'mARTA'])
|
||||
|
||||
def test_extend_default_input_processors(self):
|
||||
class ChildDefaultedItemLoader(DefaultedItemLoader):
|
||||
name_in = ApplyConcat(DefaultedItemLoader.default_input_processor, unicode.swapcase)
|
||||
|
||||
ip = ChildDefaultedItemLoader()
|
||||
ip.add_value('name', u'marta')
|
||||
self.assertEqual(ip.get_output_value('name'), [u'MART'])
|
||||
|
||||
def test_output_processor_using_function(self):
|
||||
ip = TestItemLoader()
|
||||
ip.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Mar', u'Ta'])
|
||||
|
||||
class TakeFirstItemLoader(TestItemLoader):
|
||||
name_out = u" ".join
|
||||
|
||||
ip = TakeFirstItemLoader()
|
||||
ip.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(ip.get_output_value('name'), u'Mar Ta')
|
||||
|
||||
def test_output_processor_using_classes(self):
|
||||
ip = TestItemLoader()
|
||||
ip.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Mar', u'Ta'])
|
||||
|
||||
class TakeFirstItemLoader(TestItemLoader):
|
||||
name_out = Join()
|
||||
|
||||
ip = TakeFirstItemLoader()
|
||||
ip.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(ip.get_output_value('name'), u'Mar Ta')
|
||||
|
||||
class TakeFirstItemLoader(TestItemLoader):
|
||||
name_out = Join("<br>")
|
||||
|
||||
ip = TakeFirstItemLoader()
|
||||
ip.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(ip.get_output_value('name'), u'Mar<br>Ta')
|
||||
|
||||
def test_default_output_processor(self):
|
||||
ip = TestItemLoader()
|
||||
ip.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Mar', u'Ta'])
|
||||
|
||||
class LalaItemLoader(TestItemLoader):
|
||||
default_output_processor = Identity()
|
||||
|
||||
ip = LalaItemLoader()
|
||||
ip.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(ip.get_output_value('name'), [u'Mar', u'Ta'])
|
||||
|
||||
def test_loader_context_on_declaration(self):
|
||||
class ChildItemLoader(TestItemLoader):
|
||||
url_in = ApplyConcat(processor_with_args, key=u'val')
|
||||
|
||||
ip = ChildItemLoader()
|
||||
ip.add_value('url', u'text')
|
||||
self.assertEqual(ip.get_output_value('url'), ['val'])
|
||||
ip.replace_value('url', u'text2')
|
||||
self.assertEqual(ip.get_output_value('url'), ['val'])
|
||||
|
||||
def test_loader_context_on_instantiation(self):
|
||||
class ChildItemLoader(TestItemLoader):
|
||||
url_in = ApplyConcat(processor_with_args)
|
||||
|
||||
ip = ChildItemLoader(key=u'val')
|
||||
ip.add_value('url', u'text')
|
||||
self.assertEqual(ip.get_output_value('url'), ['val'])
|
||||
ip.replace_value('url', u'text2')
|
||||
self.assertEqual(ip.get_output_value('url'), ['val'])
|
||||
|
||||
def test_loader_context_on_assign(self):
|
||||
class ChildItemLoader(TestItemLoader):
|
||||
url_in = ApplyConcat(processor_with_args)
|
||||
|
||||
ip = ChildItemLoader()
|
||||
ip.context['key'] = u'val'
|
||||
ip.add_value('url', u'text')
|
||||
self.assertEqual(ip.get_output_value('url'), ['val'])
|
||||
ip.replace_value('url', u'text2')
|
||||
self.assertEqual(ip.get_output_value('url'), ['val'])
|
||||
|
||||
def test_item_passed_to_input_processor_functions(self):
|
||||
def processor(value, loader_context):
|
||||
return loader_context['item']['name']
|
||||
|
||||
class ChildItemLoader(TestItemLoader):
|
||||
url_in = ApplyConcat(processor)
|
||||
|
||||
it = TestItem(name='marta')
|
||||
ip = ChildItemLoader(item=it)
|
||||
ip.add_value('url', u'text')
|
||||
self.assertEqual(ip.get_output_value('url'), ['marta'])
|
||||
ip.replace_value('url', u'text2')
|
||||
self.assertEqual(ip.get_output_value('url'), ['marta'])
|
||||
|
||||
def test_add_value_on_unknown_field(self):
|
||||
ip = TestItemLoader()
|
||||
self.assertRaises(KeyError, ip.add_value, 'wrong_field', [u'lala', u'lolo'])
|
||||
|
||||
|
||||
class TestXPathItemLoader(XPathItemLoader):
|
||||
default_item_class = TestItem
|
||||
name_in = ApplyConcat(lambda v: v.title())
|
||||
|
||||
class XPathItemLoaderTest(unittest.TestCase):
|
||||
|
||||
def test_constructor_errors(self):
|
||||
self.assertRaises(RuntimeError, XPathItemLoader)
|
||||
|
||||
def test_constructor_with_selector(self):
|
||||
sel = HtmlXPathSelector(text=u"<html><body><div>marta</div></body></html>")
|
||||
l = TestXPathItemLoader(selector=sel)
|
||||
self.assert_(l.selector is sel)
|
||||
l.add_xpath('name', '//div/text()')
|
||||
self.assertEqual(l.get_output_value('name'), [u'Marta'])
|
||||
|
||||
def test_constructor_with_response(self):
|
||||
response = HtmlResponse(url="", body="<html><body><div>marta</div></body></html>")
|
||||
l = TestXPathItemLoader(response=response)
|
||||
self.assert_(l.selector)
|
||||
l.add_xpath('name', '//div/text()')
|
||||
self.assertEqual(l.get_output_value('name'), [u'Marta'])
|
||||
|
||||
def test_add_xpath_re(self):
|
||||
response = HtmlResponse(url="", body="<html><body><div>marta</div></body></html>")
|
||||
l = TestXPathItemLoader(response=response)
|
||||
l.add_xpath('name', '//div/text()', re='ma')
|
||||
self.assertEqual(l.get_output_value('name'), [u'Ma'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
|
@ -1,272 +0,0 @@
|
|||
import unittest
|
||||
|
||||
from scrapy.newitem.loader import Loader, XPathLoader
|
||||
from scrapy.newitem.loader.expanders import TreeExpander, IdentityExpander
|
||||
from scrapy.newitem.loader.reducers import Join, Identity
|
||||
from scrapy.newitem import Item, Field
|
||||
from scrapy.xpath import HtmlXPathSelector
|
||||
from scrapy.http import HtmlResponse
|
||||
|
||||
# test items
|
||||
|
||||
class NameItem(Item):
|
||||
name = Field()
|
||||
|
||||
class TestItem(NameItem):
|
||||
url = Field()
|
||||
summary = Field()
|
||||
|
||||
# test loaders
|
||||
|
||||
class NameLoader(Loader):
|
||||
default_item_class = TestItem
|
||||
|
||||
class TestLoader(NameLoader):
|
||||
name_exp = TreeExpander(lambda v: v.title())
|
||||
|
||||
class DefaultedLoader(NameLoader):
|
||||
default_expander = TreeExpander(lambda v: v[:-1])
|
||||
|
||||
# test expanders
|
||||
|
||||
def expander_func_with_args(value, other=None, loader_args=None):
|
||||
if 'key' in loader_args:
|
||||
return loader_args['key']
|
||||
return value
|
||||
|
||||
class LoaderTest(unittest.TestCase):
|
||||
|
||||
def test_get_item_using_default_loader(self):
|
||||
i = TestItem()
|
||||
i['summary'] = u'lala'
|
||||
il = Loader(item=i)
|
||||
il.add_value('name', u'marta')
|
||||
item = il.get_item()
|
||||
assert item is i
|
||||
self.assertEqual(item['summary'], u'lala')
|
||||
self.assertEqual(item['name'], u'marta')
|
||||
|
||||
def test_get_item_using_custom_loader(self):
|
||||
il = TestLoader()
|
||||
il.add_value('name', u'marta')
|
||||
item = il.get_item()
|
||||
self.assertEqual(item['name'], u'Marta')
|
||||
|
||||
def test_add_value(self):
|
||||
il = TestLoader()
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_expanded_value('name'), [u'Marta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Marta')
|
||||
il.add_value('name', u'pepe')
|
||||
self.assertEqual(il.get_expanded_value('name'), [u'Marta', u'Pepe'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Marta')
|
||||
|
||||
def test_replace_value(self):
|
||||
il = TestLoader()
|
||||
il.replace_value('name', u'marta')
|
||||
self.assertEqual(il.get_expanded_value('name'), [u'Marta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Marta')
|
||||
il.replace_value('name', u'pepe')
|
||||
self.assertEqual(il.get_expanded_value('name'), [u'Pepe'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Pepe')
|
||||
|
||||
def test_tree_expander_filter(self):
|
||||
def filter_world(x):
|
||||
return None if x == 'world' else x
|
||||
|
||||
expander = TreeExpander(filter_world, str.upper)
|
||||
self.assertEqual(expander(['hello', 'world', 'this', 'is', 'scrapy']),
|
||||
['HELLO', 'THIS', 'IS', 'SCRAPY'])
|
||||
|
||||
def test_tree_expander_multiple_functions(self):
|
||||
class TestLoader(NameLoader):
|
||||
name_exp = TreeExpander(lambda v: v.title(), lambda v: v[:-1])
|
||||
|
||||
il = TestLoader()
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Mart')
|
||||
item = il.get_item()
|
||||
self.assertEqual(item['name'], u'Mart')
|
||||
|
||||
def test_default_expander(self):
|
||||
il = DefaultedLoader()
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'mart')
|
||||
|
||||
def test_inherited_default_expander(self):
|
||||
class InheritDefaultedLoader(DefaultedLoader):
|
||||
pass
|
||||
|
||||
il = InheritDefaultedLoader()
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'mart')
|
||||
|
||||
def test_expander_inheritance(self):
|
||||
class ChildLoader(TestLoader):
|
||||
url_exp = TreeExpander(lambda v: v.lower())
|
||||
|
||||
il = ChildLoader()
|
||||
il.add_value('url', u'HTTP://scrapy.ORG')
|
||||
self.assertEqual(il.get_reduced_value('url'), u'http://scrapy.org')
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Marta')
|
||||
|
||||
class ChildChildLoader(ChildLoader):
|
||||
url_exp = TreeExpander(lambda v: v.upper())
|
||||
summary_exp = TreeExpander(lambda v: v)
|
||||
|
||||
il = ChildChildLoader()
|
||||
il.add_value('url', u'http://scrapy.org')
|
||||
self.assertEqual(il.get_reduced_value('url'), u'HTTP://SCRAPY.ORG')
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Marta')
|
||||
|
||||
def test_empty_tree_expander(self):
|
||||
class IdentityDefaultedLoader(DefaultedLoader):
|
||||
name_exp = TreeExpander()
|
||||
|
||||
il = IdentityDefaultedLoader()
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'marta')
|
||||
|
||||
def test_identity_expander(self):
|
||||
class IdentityDefaultedLoader(DefaultedLoader):
|
||||
name_exp = IdentityExpander()
|
||||
|
||||
il = IdentityDefaultedLoader()
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'marta')
|
||||
|
||||
def test_extend_custom_expanders(self):
|
||||
class ChildLoader(TestLoader):
|
||||
name_exp = TreeExpander(TestLoader.name_exp, unicode.swapcase)
|
||||
|
||||
il = ChildLoader()
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'mARTA')
|
||||
|
||||
def test_extend_default_expanders(self):
|
||||
class ChildDefaultedLoader(DefaultedLoader):
|
||||
name_exp = TreeExpander(DefaultedLoader.default_expander, unicode.swapcase)
|
||||
|
||||
il = ChildDefaultedLoader()
|
||||
il.add_value('name', u'marta')
|
||||
self.assertEqual(il.get_reduced_value('name'), u'MART')
|
||||
|
||||
def test_reducer_using_function(self):
|
||||
il = TestLoader()
|
||||
il.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Mar')
|
||||
|
||||
class TakeFirstLoader(TestLoader):
|
||||
name_red = u" ".join
|
||||
|
||||
il = TakeFirstLoader()
|
||||
il.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Mar Ta')
|
||||
|
||||
def test_reducer_using_classes(self):
|
||||
il = TestLoader()
|
||||
il.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Mar')
|
||||
|
||||
class TakeFirstLoader(TestLoader):
|
||||
name_red = Join()
|
||||
|
||||
il = TakeFirstLoader()
|
||||
il.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Mar Ta')
|
||||
|
||||
class TakeFirstLoader(TestLoader):
|
||||
name_red = Join("<br>")
|
||||
|
||||
il = TakeFirstLoader()
|
||||
il.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Mar<br>Ta')
|
||||
|
||||
def test_default_reducer(self):
|
||||
il = TestLoader()
|
||||
il.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), u'Mar')
|
||||
|
||||
class LalaLoader(TestLoader):
|
||||
default_reducer = Identity()
|
||||
|
||||
il = LalaLoader()
|
||||
il.add_value('name', [u'mar', u'ta'])
|
||||
self.assertEqual(il.get_reduced_value('name'), [u'Mar', u'Ta'])
|
||||
|
||||
def test_expander_args_on_declaration(self):
|
||||
class ChildLoader(TestLoader):
|
||||
url_exp = TreeExpander(expander_func_with_args, key=u'val')
|
||||
|
||||
il = ChildLoader()
|
||||
il.add_value('url', u'text', key=u'val')
|
||||
self.assertEqual(il.get_reduced_value('url'), 'val')
|
||||
|
||||
def test_expander_args_on_instantiation(self):
|
||||
class ChildLoader(TestLoader):
|
||||
url_exp = TreeExpander(expander_func_with_args)
|
||||
|
||||
il = ChildLoader(key=u'val')
|
||||
il.add_value('url', u'text')
|
||||
self.assertEqual(il.get_reduced_value('url'), 'val')
|
||||
|
||||
def test_expander_args_on_assign(self):
|
||||
class ChildLoader(TestLoader):
|
||||
url_exp = TreeExpander(expander_func_with_args)
|
||||
|
||||
il = ChildLoader()
|
||||
il.add_value('url', u'text', key=u'val')
|
||||
self.assertEqual(il.get_reduced_value('url'), 'val')
|
||||
|
||||
def test_item_passed_to_expander_functions(self):
|
||||
def exp_func(value, loader_args):
|
||||
return loader_args['item']['name']
|
||||
|
||||
class ChildLoader(TestLoader):
|
||||
url_exp = TreeExpander(exp_func)
|
||||
|
||||
it = TestItem(name='marta')
|
||||
il = ChildLoader(item=it)
|
||||
il.add_value('url', u'text', key=u'val')
|
||||
self.assertEqual(il.get_reduced_value('url'), 'marta')
|
||||
|
||||
def test_add_value_on_unknown_field(self):
|
||||
il = TestLoader()
|
||||
self.assertRaises(KeyError, il.add_value, 'wrong_field', [u'lala', u'lolo'])
|
||||
|
||||
|
||||
class TestXPathLoader(XPathLoader):
|
||||
default_item_class = TestItem
|
||||
name_exp = TreeExpander(lambda v: v.title())
|
||||
|
||||
class XPathLoaderTest(unittest.TestCase):
|
||||
|
||||
def test_constructor_errors(self):
|
||||
self.assertRaises(RuntimeError, XPathLoader)
|
||||
|
||||
def test_constructor_with_selector(self):
|
||||
sel = HtmlXPathSelector(text=u"<html><body><div>marta</div></body></html>")
|
||||
l = TestXPathLoader(selector=sel)
|
||||
self.assert_(l.selector is sel)
|
||||
l.add_xpath('name', '//div/text()')
|
||||
self.assertEqual(l.get_reduced_value('name'), u'Marta')
|
||||
|
||||
def test_constructor_with_response(self):
|
||||
response = HtmlResponse(url="", body="<html><body><div>marta</div></body></html>")
|
||||
l = TestXPathLoader(response=response)
|
||||
self.assert_(l.selector)
|
||||
l.add_xpath('name', '//div/text()')
|
||||
self.assertEqual(l.get_reduced_value('name'), u'Marta')
|
||||
|
||||
def test_add_xpath_re(self):
|
||||
response = HtmlResponse(url="", body="<html><body><div>marta</div></body></html>")
|
||||
l = TestXPathLoader(response=response)
|
||||
l.add_xpath('name', '//div/text()', re='ma')
|
||||
self.assertEqual(l.get_reduced_value('name'), u'Ma')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
|
@ -54,6 +54,14 @@ class ResponseUtilsTest(unittest.TestCase):
|
|||
response = Response(url='http://example.org', body=body)
|
||||
self.assertEqual(get_meta_refresh(response), ('5', 'http://example.org/newpage'))
|
||||
|
||||
# meta refresh in multiple lines
|
||||
body = """<html><head>
|
||||
<META
|
||||
HTTP-EQUIV="Refresh"
|
||||
CONTENT="1; URL=http://example.org/newpage">"""
|
||||
response = Response(url='http://example.org', body=body)
|
||||
self.assertEqual(get_meta_refresh(response), ('1', 'http://example.org/newpage'))
|
||||
|
||||
def test_response_httprepr(self):
|
||||
r1 = Response("http://www.example.com")
|
||||
self.assertEqual(response_httprepr(r1), 'HTTP/1.1 200 OK\r\n\r\n')
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ def get_base_url(response):
|
|||
response.cache['base_url'] = match.group(1) if match else response.url
|
||||
return response.cache['base_url']
|
||||
|
||||
META_REFRESH_RE = re.compile(r'<meta[^>]*http-equiv[^>]*refresh[^>].*?(\d+);\s*url=([^"\']+)', re.IGNORECASE)
|
||||
META_REFRESH_RE = re.compile(r'<meta[^>]*http-equiv[^>]*refresh[^>].*?(\d+);\s*url=([^"\']+)', re.DOTALL | re.IGNORECASE)
|
||||
def get_meta_refresh(response):
|
||||
""" Return a tuple of two strings containing the interval and url included
|
||||
in the http-equiv parameter of the HTML meta element. If no url is included
|
||||
|
|
|
|||
Loading…
Reference in New Issue