diff --git a/scrapy/trunk/scrapy/contrib/adaptors/__init__.py b/scrapy/trunk/scrapy/contrib/adaptors/__init__.py
new file mode 100644
index 000000000..97e76e48b
--- /dev/null
+++ b/scrapy/trunk/scrapy/contrib/adaptors/__init__.py
@@ -0,0 +1,4 @@
+from scrapy.contrib.adaptors.extraction import extract, extract_unquoted, ExtractImages
+from scrapy.contrib.adaptors.markup import remove_tags, remove_root, Unquote
+from scrapy.contrib.adaptors.misc import to_unicode, clean_spaces, strip_list, drop_empty, Delist, Regex
+from scrapy.utils.python import unique, flatten
diff --git a/scrapy/trunk/scrapy/contrib/adaptors/extraction.py b/scrapy/trunk/scrapy/contrib/adaptors/extraction.py
new file mode 100644
index 000000000..c059bc350
--- /dev/null
+++ b/scrapy/trunk/scrapy/contrib/adaptors/extraction.py
@@ -0,0 +1,95 @@
+"""
+Adaptors related with extraction of data
+"""
+
+import urlparse
+from scrapy.utils.python import flatten
+from scrapy.xpath.selector import XPathSelector, XPathSelectorList
+
+def _extract(locations, extractor='extract'):
+ if isinstance(locations, (XPathSelector, XPathSelectorList)):
+ return flatten(getattr(locations, extractor)())
+ elif hasattr(locations, '__iter__'):
+ return flatten([getattr(x, extractor)() if isinstance(x, (XPathSelector, XPathSelectorList)) else x for x in flatten(locations)])
+ elif isinstance(locations, basestring):
+ return [locations]
+ else:
+ return []
+
+def extract(locations):
+ """
+ This adaptor extracts a list of strings
+ from 'locations', which can be either an iterable,
+ or an XPathSelector/XPathSelectorList.
+
+ Input: XPathSelector, XPathSelectorList, iterable, basestring
+ Output: list of unicodes
+ """
+ return _extract(locations)
+
+def extract_unquoted(locations):
+ """
+ This adaptor extracts a list of unquoted strings
+ from 'locations', which can be either an iterable,
+ or an XPathSelector/XPathSelectorList.
+ The difference between this and the extract adaptor is
+ that this adaptor will only extract text nodes and unquote them.
+
+ Input: XPathSelector, XPathSelectorList, iterable, basestring
+ Output: list of unicodes
+ """
+ return _extract(locations, 'extract_unquoted')
+
+class ExtractImages(object):
+ """
+ This adaptor receives either an XPathSelector containing
+ the desired locations for finding urls, or a list of relative
+ links to be resolved.
+
+ Input: XPathSelector, XPathSelectorList, iterable
+ Output: list of unicodes
+ """
+ def __init__(self, response=None, base_url=None):
+ self.response = response
+ self.base_url = base_url
+
+ def extract_from_xpath(self, selector):
+ ret = []
+ if selector.xmlNode.type == 'element':
+ if selector.xmlNode.name == 'a':
+ children = selector.x('child::*')
+ if len(children) > 1:
+ ret.extend(selector.x('.//@href'))
+ ret.extend(selector.x('.//@src'))
+ elif len(children) == 1 and children[0].xmlNode.name == 'img':
+ ret.extend(children.x('@src'))
+ else:
+ ret.extend(selector.x('@href'))
+ elif selector.xmlNode.name == 'img':
+ ret.extend(selector.x('@src'))
+ else:
+ ret.extend(selector.x('.//@href'))
+ ret.extend(selector.x('.//@src'))
+ elif selector.xmlNode.type == 'attribute' and selector.xmlNode.name in ['href', 'src']:
+ ret.append(selector)
+
+ return ret
+
+ def __call__(self, locations):
+ if isinstance(locations, tuple) and len(locations) > 1:
+ self.base_url = locations[1]
+ locations = locations[0]
+
+ if not self.response and not self.base_url:
+ raise AttributeError('You must specify either a response or a base_url to the ExtractImages adaptor.')
+
+ rel_links = []
+ for location in flatten(locations):
+ if isinstance(location, (XPathSelector, XPathSelectorList)):
+ rel_links.extend(self.extract_from_xpath(location))
+ else:
+ rel_links.append(location)
+ rel_links = extract(rel_links)
+
+ base_url = self.base_url or self.response.url
+ return [urlparse.urljoin(base_url, link) for link in rel_links]
diff --git a/scrapy/trunk/scrapy/contrib/adaptors/markup.py b/scrapy/trunk/scrapy/contrib/adaptors/markup.py
new file mode 100644
index 000000000..b912f73e1
--- /dev/null
+++ b/scrapy/trunk/scrapy/contrib/adaptors/markup.py
@@ -0,0 +1,39 @@
+import re
+from scrapy.utils.markup import replace_tags, remove_entities
+
+def remove_tags(value):
+ """
+ Input: iterable with strings
+ Output: list of strings
+ """
+ return [ replace_tags(v) for v in value ]
+
+def remove_root(value):
+ """
+ Input: iterable with strings
+ Output: list of strings
+ """
+ def _remove_root(value):
+ _remove_root_re = re.compile(r'^\s*<.*?>(.*)\s*$', re.DOTALL)
+ m = _remove_root_re.search(value)
+ if m:
+ value = m.group(1)
+ return value
+ return [ _remove_root(v) for v in value ]
+
+class Unquote(object):
+ """
+ Receives a list of strings, removes all of the
+ entities the strings may have, and returns
+ a new list
+
+ Input: iterable with strings
+ Output: list of strings
+ """
+ def __init__(self, keep=['lt', 'amp']):
+ self.keep = keep
+
+ def __call__(self, value):
+ return [ remove_entities(v, keep=self.keep) for v in value ]
+
+
diff --git a/scrapy/trunk/scrapy/contrib/adaptors/misc.py b/scrapy/trunk/scrapy/contrib/adaptors/misc.py
new file mode 100644
index 000000000..a93ad9f87
--- /dev/null
+++ b/scrapy/trunk/scrapy/contrib/adaptors/misc.py
@@ -0,0 +1,59 @@
+import re
+from scrapy.xpath.selector import XPathSelector, XPathSelectorList
+from scrapy.utils.misc import extract_regex
+from scrapy.utils.python import flatten
+
+def to_unicode(value):
+ """
+ Receives a list of strings, converts
+ it to unicode, and returns a new list.
+
+ Input: iterable with strings
+ Output: list of unicodes
+ """
+ if hasattr(value, '__iter__'):
+ return [ unicode(v) for v in value ]
+ else:
+ raise TypeError('to_unicode must receive an iterable.')
+
+def clean_spaces(value):
+ _clean_spaces_re = re.compile("\s+", re.U)
+ return [ _clean_spaces_re.sub(' ', v) for v in value ]
+
+def strip_list(value):
+ return [ v.strip() for v in value ]
+
+def drop_empty(value):
+ return [ v for v in value if v ]
+
+class Delist(object):
+ """
+ Input: iterable with strings
+ Output: unicode
+ """
+ def __init__(self, delimiter=' '):
+ self.delimiter = delimiter
+
+ def __call__(self, value):
+ return self.delimiter.join(value)
+
+class Regex(object):
+ """
+ This adaptor must receive either a list of strings or an XPathSelector
+ and return a new list with the matches of the given strings with the given regular
+ expression (which is passed by a keyword argument, and is mandatory for this adaptor).
+
+ Input: XPathSelector, XPathSelectorList, iterable
+ Output: list of unicodes
+ """
+ def __init__(self, regex=r''):
+ self.regex = regex
+
+ def __call__(self, value):
+ if self.regex:
+ if isinstance(value, (XPathSelector, XPathSelectorList)):
+ return value.re(self.regex)
+ elif hasattr(value, '__iter__'):
+ return flatten([extract_regex(self.regex, string, 'utf-8') for string in value])
+ return value
+
diff --git a/scrapy/trunk/scrapy/contrib/item/models.py b/scrapy/trunk/scrapy/contrib/item/models.py
index 0f8679c47..2530d6814 100644
--- a/scrapy/trunk/scrapy/contrib/item/models.py
+++ b/scrapy/trunk/scrapy/contrib/item/models.py
@@ -37,12 +37,11 @@ class RobustScrapedItem(ScrapedItem):
'url': basestring, # the main URL where this item was scraped from
}
- def __init__(self, data=None, adaptors_pipe={}):
+ def __init__(self, data=None):
"""
A scraped item can be initialised with a dictionary that will be
squirted directly into the object.
"""
- super(RobustScrapedItem, self).__init__(adaptors_pipe)
if isinstance(data, dict):
for attr, value in data.iteritems():
setattr(self, attr, value)
@@ -71,8 +70,8 @@ class RobustScrapedItem(ScrapedItem):
self.__dict__.pop(attr, None)
return
- if attr == '_adaptors_pipe':
- return object.__setattr__(self, '_adaptors_pipe', value)
+ if attr == '_adaptors_dict':
+ return object.__setattr__(self, '_adaptors_dict', value)
type1 = self.ATTRIBUTES[attr]
if hasattr(type1, '__iter__'):
diff --git a/scrapy/trunk/scrapy/item/adaptors.py b/scrapy/trunk/scrapy/item/adaptors.py
index 67e3243e4..2580c955b 100644
--- a/scrapy/trunk/scrapy/item/adaptors.py
+++ b/scrapy/trunk/scrapy/item/adaptors.py
@@ -1,17 +1,7 @@
-import re
-import cPickle as pickle
-
from traceback import format_exc
-from urlparse import urlparse
-
-from scrapy.xpath.selector import XPathSelector, XPathSelectorList
-from scrapy.utils.python import unique, flatten
-from scrapy.utils.markup import replace_tags, remove_entities
-from scrapy.utils.misc import extract_regex
from scrapy.conf import settings
-from scrapy import log
-class AdaptorPipe(object):
+class AdaptorDict(dict):
"""
Class that represents an item's attribute pipeline.
@@ -19,290 +9,25 @@ class AdaptorPipe(object):
of adaptors to be run for filtering the input before storing.
"""
- def __init__(self, adaptors_pipe=None):
- """
- Receives a dictionary that maps attribute_name to a list of adaptor functions
- """
- self.pipes = adaptors_pipe or {}
-
- def set_adaptors(self, attr, adaptors):
- """
- Set the adaptor pipeline that will be used for the specified attribute
- """
- self.pipes[attr] = adaptors
-
- def execute(self, attrname, value, kwargs):
+ def execute(self, attrname, value, debug=False):
"""
Execute pipeline for attribute name "attrname" and value "value".
"""
- debug = kwargs.get('debug') or all([settings.getbool('LOG_ENABLED'), settings.get('LOGLEVEL') == 'TRACE'])
+ debug = debug or all([settings.getbool('LOG_ENABLED'), settings.get('LOGLEVEL') == 'TRACE'])
- for adaptor in self.pipes.get(attrname, []):
+ for adaptor in self.get(attrname, []):
+ name = adaptor.__class__.__name__ if hasattr(adaptor, '__class__') else adaptor.__name__
try:
if debug:
- print " %07s | input >" % adaptor.__name__, repr(value)
- value = adaptor(kwargs)(value)
+ print " %07s | input >" % name, repr(value)
+ value = adaptor(value)
if debug:
- print " %07s | output >" % adaptor.__name__, repr(value)
+ print " %07s | output >" % name, repr(value)
- except Exception, e:
- print "Error in '%s' adaptor. Traceback text:" % adaptor.__name__
+ except Exception:
+ print "Error in '%s' adaptor. Traceback text:" % name
print format_exc()
return
return value
-class AdaptorFunc(object):
- """
- This is the base class for adaptors.
-
- An adaptor is just an object subclassed from this class
- which defines the __call__ method, and receives/returns only
- one value.
-
- You can send the adaptor some extra options while creating it (just before running it)
- through **kwargs, and managing them by overriding the __init__ method, as shown on
- the UnquoteAdaptor, for example.
- """
- def __init__(self, kwargs={}):
- pass
-
- def __call__(self):
- raise NotImplementedError('You must define the __call__ method to create and use an adaptor')
-
-############
-# Adaptors #
-############
-class ExtractAdaptor(AdaptorFunc):
- """
- This adaptor extracts a list of strings
- from 'location', which can be either a list (or tuple),
- or an XPathSelector.
-
- This adaptor *always* returns a list.
- """
-
- def __call__(self, location):
- if not location:
- return []
- elif isinstance(location, (XPathSelector, XPathSelectorList)):
- return flatten(location.extract())
- elif isinstance(location, (list, tuple)):
- return flatten(map(lambda x: x.extract() if isinstance(x, (XPathSelector, XPathSelectorList)) else x, flatten(location)))
- elif isinstance(location, basestring):
- return [location]
-
-class ExtractImagesAdaptor(AdaptorFunc):
- """
- This adaptor receives either an XPathSelector containing
- the desired locations for finding urls, or a tuple like (xpath, regexp)
- containing the xpath locations to look in, and a regular expression
- to parse those locations.
-
- In any case, this adaptor returns a list containing the absolute urls extracted.
- """
-
- def __init__(self, kwargs):
- self.base_url = kwargs.get('base_url')
- self.response = kwargs.get('response')
- if not self.response and not self.base_url:
- raise AttributeError('You must specify either a response or a base_url to the ExtractImages adaptor.')
-
- def extract_from_xpath(self, selector):
- ret = []
-
- if selector.xmlNode.type == 'element':
- if selector.xmlNode.name == 'a':
- children = selector.x('child::*')
- if len(children) > 1:
- ret.extend(selector.x('.//@href'))
- ret.extend(selector.x('.//@src'))
- elif len(children) == 1 and children[0].xmlNode.name == 'img':
- ret.extend(children.x('@src'))
- else:
- ret.extend(selector.x('@href'))
- elif selector.xmlNode.name == 'img':
- ret.extend(selector.x('@src'))
- else:
- ret.extend(selector.x('.//@href'))
- ret.extend(selector.x('.//@src'))
- elif selector.xmlNode.type == 'attribute' and selector.xmlNode.name in ['href', 'src']:
- ret.append(selector)
-
- return ret
-
- def absolutize_link(self, base_url, link):
- base_url = urlparse(base_url)
- ret = []
-
- if link.startswith('/'):
- ret.append('http://%s%s' % (base_url.hostname, link))
- elif link.startswith('http://'):
- ret.append(link)
- else:
- ret.append('http://%s%s/%s' % (base_url.hostname, base_url.path, link))
-
- return ret
-
- def __call__(self, locations):
- rel_links = []
- for location in flatten(locations):
- if isinstance(location, (XPathSelector, XPathSelectorList)):
- rel_links.extend(self.extract_from_xpath(location))
- else:
- rel_links.append(location)
- rel_links = ExtractAdaptor()(rel_links)
-
- if self.response:
- return flatten([self.absolutize_link(self.response.url, link) for link in rel_links])
- elif self.base_url:
- return flatten([self.absolutize_link(self.base_url, link) for link in rel_links])
- else:
- abs_links = []
- for link in rel_links:
- if link.startswith('http://'):
- abs_links.append(link)
- else:
- log.msg('Couldnt get the absolute url for "%s". Ignoring link...' % link, 'WARNING')
- return abs_links
-
-class BoolAdaptor(AdaptorFunc):
- def __call__(self, value):
- return bool(value)
-
-class ToUnicodeAdaptor(AdaptorFunc):
- """
- Receives a list of strings, converts
- it to unicode, and returns a new list.
- """
-
- def __call__(self, value):
- if isinstance(value, (list, tuple)):
- return [ unicode(v) for v in value ]
- else:
- raise TypeError('ToUnicodeAdaptor must receive either a list or a tuple.')
-
-class RegexAdaptor(AdaptorFunc):
- """
- This adaptor must receive either a list of strings or an XPathSelector
- and return a new list with the matches of the given strings with the given regular
- expression (which is passed by a keyword argument, and is mandatory for this adaptor).
- """
-
- def __init__(self, kwargs):
- self.regex = kwargs.get('regex')
-
- def __call__(self, value):
- if self.regex:
- if isinstance(value, (XPathSelector, XPathSelectorList)):
- return value.re(self.regex)
- elif isinstance(value, list) and value:
- return flatten([extract_regex(self.regex, string, 'utf-8') for string in value])
- return value
-
-class UnquoteAdaptor(AdaptorFunc):
- """
- Receives a list of strings, removes all of the
- entities the strings may have, and returns
- a new list
- """
-
- def __init__(self, kwargs={}):
- self.keep = kwargs.get('keep', ['lt', 'amp'])
-
- def __call__(self, value):
- return [ remove_entities(v, keep=self.keep) for v in value ]
-
-class RemoveTagsAdaptor(AdaptorFunc):
- def __call__(self, value):
- return [ replace_tags(v) for v in value ]
-
-class RemoveRootAdaptor(AdaptorFunc):
- _remove_root_re = re.compile(r'^\s*<.*?>(.*)\s*$', re.DOTALL)
- def _remove_root(self, value):
- m = self._remove_root_re.search(value)
- if m:
- value = m.group(1)
- return value
-
- def __call__(self, value):
- return [ self._remove_root(v) for v in value ]
-
-class CleanSpacesAdaptor(AdaptorFunc):
- _clean_spaces_re = re.compile("\s+", re.U)
- def __call__(self, value):
- return [ self._clean_spaces_re.sub(' ', v) for v in value ]
-
-class StripAdaptor(AdaptorFunc):
- def __call__(self, value):
- return [ v.strip() for v in value ]
-
-class DropEmptyAdaptor(AdaptorFunc):
- def __call__(self, value):
- return [ v for v in value if v ]
-
-class DelistAdaptor(AdaptorFunc):
- def __init__(self, kwargs={}):
- self.delimiter = kwargs.get('join_delimiter', ' ')
-
- def __call__(self, value):
- return self.delimiter.join(value)
-
-class PickleAdaptor(AdaptorFunc):
- def __call__(self, value):
- return pickle.dumps(value)
-
-class DePickleAdaptor(AdaptorFunc):
- def __call__(self, value):
- return pickle.loads(value)
-
-class UniqueAdaptor(AdaptorFunc):
- def __call__(self, value):
- return unique(value)
-
-class FlattenAdaptor(AdaptorFunc):
- def __call__(self, value):
- return flatten(value)
-
-#############
-# Pipelines #
-#############
-"""
-The following methods automatically generate adaptor pipelines
-for some basic datatypes, according to the parameters you pass them.
-"""
-def single_pipeline(do_remove_root=True, do_remove_tags=True, do_unquote=True):
- pipe = [ ExtractAdaptor,
- UniqueAdaptor,
- ToUnicodeAdaptor,
- DropEmptyAdaptor,
- CleanSpacesAdaptor,
- StripAdaptor,
- ]
-
- if do_remove_root:
- pipe.insert(4, RemoveRootAdaptor)
- if do_remove_tags:
- pipe.insert(5, RemoveTagsAdaptor)
- if do_unquote:
- pipe.append(UnquoteAdaptor)
- return pipe + [DelistAdaptor]
-
-def url_pipeline():
- return [ ExtractImagesAdaptor,
- UniqueAdaptor,
- ToUnicodeAdaptor,
- DropEmptyAdaptor,
- ]
-
-list_pipeline = [ ExtractAdaptor,
- UniqueAdaptor,
- ToUnicodeAdaptor,
- DropEmptyAdaptor,
- UnquoteAdaptor,
- RemoveTagsAdaptor,
- RemoveRootAdaptor,
- StripAdaptor,
- ]
-
-list_join_pipeline = list_pipeline + [DelistAdaptor]
diff --git a/scrapy/trunk/scrapy/item/models.py b/scrapy/trunk/scrapy/item/models.py
index 771d3a9b4..8a5bfe2c9 100644
--- a/scrapy/trunk/scrapy/item/models.py
+++ b/scrapy/trunk/scrapy/item/models.py
@@ -1,4 +1,4 @@
-from scrapy.item.adaptors import AdaptorPipe
+from scrapy.item.adaptors import AdaptorDict
from scrapy.conf import settings
class ScrapedItem(object):
@@ -8,23 +8,23 @@ class ScrapedItem(object):
that identifies uniquely the given scraped item.
"""
- def set_adaptors(self, adaptors_pipe):
+ def set_adaptors(self, adaptors_dict, **kwargs):
"""
Set the adaptors to use for this item. Receives a dict of the adaptors
desired for each attribute and returns the item itself.
"""
- setattr(self, '_adaptors_pipe', AdaptorPipe(adaptors_pipe))
+ setattr(self, '_adaptors_dict', AdaptorDict(adaptors_dict))
return self
- def set_attrib_adaptors(self, attrib, adaptors):
+ def set_attrib_adaptors(self, attrib, adaptors, **kwargs):
"""
Set the adaptors (from a list or tuple) to be used for a specific attribute.
"""
- self._adaptors_pipe.set_adaptors(attrib, adaptors)
+ self._adaptors_dict[attrib] = adaptors
- def attribute(self, attrname, value, **kwargs):
- val = self._adaptors_pipe.execute(attrname, value, kwargs)
- if not getattr(self, attrname, None) or kwargs.get('override'):
+ def attribute(self, attrname, value, debug=False, override=False):
+ val = self._adaptors_dict.execute(attrname, value, debug)
+ if not getattr(self, attrname, None) or override:
setattr(self, attrname, val)
def __sub__(self, other):
diff --git a/scrapy/trunk/scrapy/tests/test_adaptors.py b/scrapy/trunk/scrapy/tests/test_adaptors.py
index c8e1db286..63b4d3a1f 100644
--- a/scrapy/trunk/scrapy/tests/test_adaptors.py
+++ b/scrapy/trunk/scrapy/tests/test_adaptors.py
@@ -1,86 +1,84 @@
# -*- coding: utf8 -*-
import unittest
-from scrapy.item import adaptors
-from scrapy.xpath.selector import XmlXPathSelector
+from scrapy.contrib import adaptors
from scrapy.http import Response, ResponseBody
+from scrapy.xpath.selector import XmlXPathSelector
class AdaptorsTestCase(unittest.TestCase):
def test_extract(self):
sample_xsel = XmlXPathSelector(text='