Merge branch 'master' into telnet-auth

This commit is contained in:
Mikhail Korobov 2018-12-26 01:04:34 +05:00
commit dbfabf02e8
38 changed files with 1053 additions and 568 deletions

2
.gitignore vendored
View File

@ -12,8 +12,10 @@ dist
.idea .idea
htmlcov/ htmlcov/
.coverage .coverage
.pytest_cache/
.coverage.* .coverage.*
.cache/ .cache/
.pytest_cache/
# Windows # Windows
Thumbs.db Thumbs.db

View File

@ -1,5 +1,4 @@
language: python language: python
sudo: false
branches: branches:
only: only:
- master - master

View File

@ -3,13 +3,24 @@ include AUTHORS
include INSTALL include INSTALL
include LICENSE include LICENSE
include MANIFEST.in include MANIFEST.in
include NEWS
include scrapy/VERSION include scrapy/VERSION
include scrapy/mime.types include scrapy/mime.types
include codecov.yml
include conftest.py
include pytest.ini
include requirements-*.txt
include tox.ini
recursive-include scrapy/templates * recursive-include scrapy/templates *
recursive-include scrapy license.txt recursive-include scrapy license.txt
recursive-include docs * recursive-include docs *
prune docs/build prune docs/build
recursive-include extras * recursive-include extras *
recursive-include bin * recursive-include bin *
recursive-include tests * recursive-include tests *
global-exclude __pycache__ *.py[cod] global-exclude __pycache__ *.py[cod]

View File

@ -30,7 +30,8 @@ dependencies depending on your operating system, so be sure to check the
We strongly recommend that you install Scrapy in :ref:`a dedicated virtualenv <intro-using-virtualenv>`, We strongly recommend that you install Scrapy in :ref:`a dedicated virtualenv <intro-using-virtualenv>`,
to avoid conflicting with your system packages. to avoid conflicting with your system packages.
For more detailed and platform specifics instructions, read on. For more detailed and platform specifics instructions, as well as
troubleshooting information, read on.
Things that are good to know Things that are good to know
@ -247,6 +248,34 @@ that setuptools was unable to pick up one PyPy-specific dependency.
To fix this issue, run ``pip install 'PyPyDispatcher>=2.1.0'``. To fix this issue, run ``pip install 'PyPyDispatcher>=2.1.0'``.
.. _intro-install-troubleshooting:
Troubleshooting
===============
AttributeError: 'module' object has no attribute 'OP_NO_TLSv1_1'
----------------------------------------------------------------
After you install or upgrade Scrapy, Twisted or pyOpenSSL, you may get an
exception with the following traceback::
[…]
File "[…]/site-packages/twisted/protocols/tls.py", line 63, in <module>
from twisted.internet._sslverify import _setAcceptableProtocols
File "[…]/site-packages/twisted/internet/_sslverify.py", line 38, in <module>
TLSVersion.TLSv1_1: SSL.OP_NO_TLSv1_1,
AttributeError: 'module' object has no attribute 'OP_NO_TLSv1_1'
The reason you get this exception is that your system or virtual environment
has a version of pyOpenSSL that your version of Twisted does not support.
To install a version of pyOpenSSL that your version of Twisted supports,
reinstall Twisted with the :code:`tls` extra option::
pip install twisted[tls]
For details, see `Issue #2473 <https://github.com/scrapy/scrapy/issues/2473>`_.
.. _Python: https://www.python.org/ .. _Python: https://www.python.org/
.. _pip: https://pip.pypa.io/en/latest/installing/ .. _pip: https://pip.pypa.io/en/latest/installing/
.. _lxml: http://lxml.de/ .. _lxml: http://lxml.de/

View File

@ -34,11 +34,11 @@ http://quotes.toscrape.com, following the pagination::
def parse(self, response): def parse(self, response):
for quote in response.css('div.quote'): for quote in response.css('div.quote'):
yield { yield {
'text': quote.css('span.text::text').extract_first(), 'text': quote.css('span.text::text').get(),
'author': quote.xpath('span/small/text()').extract_first(), 'author': quote.xpath('span/small/text()').get(),
} }
next_page = response.css('li.next a::attr("href")').extract_first() next_page = response.css('li.next a::attr("href")').get()
if next_page is not None: if next_page is not None:
yield response.follow(next_page, self.parse) yield response.follow(next_page, self.parse)

View File

@ -22,9 +22,7 @@ Scrapy is written in Python_. If you're new to the language you might want to
start by getting an idea of what the language is like, to get the most out of start by getting an idea of what the language is like, to get the most out of
Scrapy. Scrapy.
If you're already familiar with other languages, and want to learn Python If you're already familiar with other languages, and want to learn Python quickly, the `Python Tutorial`_ is a good resource.
quickly, we recommend reading through `Dive Into Python 3`_. Alternatively,
you can follow the `Python Tutorial`_.
If you're new to programming and want to start with Python, the following books If you're new to programming and want to start with Python, the following books
may be useful to you: may be useful to you:
@ -40,7 +38,6 @@ as well as the `suggested resources in the learnpython-subreddit`_.
.. _Python: https://www.python.org/ .. _Python: https://www.python.org/
.. _this list of Python resources for non-programmers: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers .. _this list of Python resources for non-programmers: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers
.. _Dive Into Python 3: http://www.diveintopython3.net
.. _Python Tutorial: https://docs.python.org/3/tutorial .. _Python Tutorial: https://docs.python.org/3/tutorial
.. _Automate the Boring Stuff With Python: https://automatetheboringstuff.com/ .. _Automate the Boring Stuff With Python: https://automatetheboringstuff.com/
.. _How To Think Like a Computer Scientist: http://openbookproject.net/thinkcs/python/english3e/ .. _How To Think Like a Computer Scientist: http://openbookproject.net/thinkcs/python/english3e/
@ -254,7 +251,7 @@ data.
To extract the text from the title above, you can do:: To extract the text from the title above, you can do::
>>> response.css('title::text').extract() >>> response.css('title::text').getall()
['Quotes to Scrape'] ['Quotes to Scrape']
There are two things to note here: one is that we've added ``::text`` to the There are two things to note here: one is that we've added ``::text`` to the
@ -262,32 +259,33 @@ CSS query, to mean we want to select only the text elements directly inside
``<title>`` element. If we don't specify ``::text``, we'd get the full title ``<title>`` element. If we don't specify ``::text``, we'd get the full title
element, including its tags:: element, including its tags::
>>> response.css('title').extract() >>> response.css('title').getall()
['<title>Quotes to Scrape</title>'] ['<title>Quotes to Scrape</title>']
The other thing is that the result of calling ``.extract()`` is a list, because The other thing is that the result of calling ``.getall()`` is a list: it is
we're dealing with an instance of :class:`~scrapy.selector.SelectorList`. When possible that a selector returns more than one result, so we extract them all.
you know you just want the first result, as in this case, you can do:: When you know you just want the first result, as in this case, you can do::
>>> response.css('title::text').extract_first() >>> response.css('title::text').get()
'Quotes to Scrape' 'Quotes to Scrape'
As an alternative, you could've written:: As an alternative, you could've written::
>>> response.css('title::text')[0].extract() >>> response.css('title::text')[0].get()
'Quotes to Scrape' 'Quotes to Scrape'
However, using ``.extract_first()`` avoids an ``IndexError`` and returns However, using ``.get()`` directly on a :class:`~scrapy.selector.SelectorList`
``None`` when it doesn't find any element matching the selection. instance avoids an ``IndexError`` and returns ``None`` when it doesn't
find any element matching the selection.
There's a lesson here: for most scraping code, you want it to be resilient to There's a lesson here: for most scraping code, you want it to be resilient to
errors due to things not being found on a page, so that even if some parts fail errors due to things not being found on a page, so that even if some parts fail
to be scraped, you can at least get **some** data. to be scraped, you can at least get **some** data.
Besides the :meth:`~scrapy.selector.Selector.extract` and Besides the :meth:`~scrapy.selector.SelectorList.getall` and
:meth:`~scrapy.selector.SelectorList.extract_first` methods, you can also use :meth:`~scrapy.selector.SelectorList.get` methods, you can also use
the :meth:`~scrapy.selector.Selector.re` method to extract using `regular the :meth:`~scrapy.selector.SelectorList.re` method to extract using `regular
expressions`:: expressions`_::
>>> response.css('title::text').re(r'Quotes.*') >>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape'] ['Quotes to Scrape']
@ -298,7 +296,8 @@ expressions`::
In order to find the proper CSS selectors to use, you might find useful opening In order to find the proper CSS selectors to use, you might find useful opening
the response page from the shell in your web browser using ``view(response)``. the response page from the shell in your web browser using ``view(response)``.
You can use your browser developer tools (see section about :ref:`topics-developer-tools`). You can use your browser developer tools to inspect the HTML and come up
with a selector (see section about :ref:`topics-developer-tools`).
`Selector Gadget`_ is also a nice tool to quickly find CSS selector for `Selector Gadget`_ is also a nice tool to quickly find CSS selector for
visually selected elements, which works in many browsers. visually selected elements, which works in many browsers.
@ -314,7 +313,7 @@ Besides `CSS`_, Scrapy selectors also support using `XPath`_ expressions::
>>> response.xpath('//title') >>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>] [<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').extract_first() >>> response.xpath('//title/text()').get()
'Quotes to Scrape' 'Quotes to Scrape'
XPath expressions are very powerful, and are the foundation of Scrapy XPath expressions are very powerful, and are the foundation of Scrapy
@ -383,17 +382,17 @@ variable, so that we can run our CSS selectors directly on a particular quote::
Now, let's extract ``title``, ``author`` and the ``tags`` from that quote Now, let's extract ``title``, ``author`` and the ``tags`` from that quote
using the ``quote`` object we just created:: using the ``quote`` object we just created::
>>> title = quote.css("span.text::text").extract_first() >>> title = quote.css("span.text::text").get()
>>> title >>> title
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”' '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").extract_first() >>> author = quote.css("small.author::text").get()
>>> author >>> author
'Albert Einstein' 'Albert Einstein'
Given that the tags are a list of strings, we can use the ``.extract()`` method Given that the tags are a list of strings, we can use the ``.getall()`` method
to get all of them:: to get all of them::
>>> tags = quote.css("div.tags a.tag::text").extract() >>> tags = quote.css("div.tags a.tag::text").getall()
>>> tags >>> tags
['change', 'deep-thoughts', 'thinking', 'world'] ['change', 'deep-thoughts', 'thinking', 'world']
@ -401,9 +400,9 @@ Having figured out how to extract each bit, we can now iterate over all the
quotes elements and put them together into a Python dictionary:: quotes elements and put them together into a Python dictionary::
>>> for quote in response.css("div.quote"): >>> for quote in response.css("div.quote"):
... text = quote.css("span.text::text").extract_first() ... text = quote.css("span.text::text").get()
... author = quote.css("small.author::text").extract_first() ... author = quote.css("small.author::text").get()
... tags = quote.css("div.tags a.tag::text").extract() ... tags = quote.css("div.tags a.tag::text").getall()
... print(dict(text=text, author=author, tags=tags)) ... print(dict(text=text, author=author, tags=tags))
{'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'} {'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'}
{'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'} {'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'}
@ -434,9 +433,9 @@ in the callback, as you can see below::
def parse(self, response): def parse(self, response):
for quote in response.css('div.quote'): for quote in response.css('div.quote'):
yield { yield {
'text': quote.css('span.text::text').extract_first(), 'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').extract_first(), 'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').extract(), 'tags': quote.css('div.tags a.tag::text').getall(),
} }
If you run this spider, it will output the extracted data with the log:: If you run this spider, it will output the extracted data with the log::
@ -508,16 +507,22 @@ markup:
We can try extracting it in the shell:: We can try extracting it in the shell::
>>> response.css('li.next a').extract_first() >>> response.css('li.next a').get()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>' '<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'
This gets the anchor element, but we want the attribute ``href``. For that, This gets the anchor element, but we want the attribute ``href``. For that,
Scrapy supports a CSS extension that let's you select the attribute contents, Scrapy supports a CSS extension that let's you select the attribute contents,
like this:: like this::
>>> response.css('li.next a::attr(href)').extract_first() >>> response.css('li.next a::attr(href)').get()
'/page/2/' '/page/2/'
There is also an ``attrib`` property available
(see :ref:`selecting-attributes` for more)::
>>> response.css('li.next a').attrib['href']
'/page/2'
Let's see now our spider modified to recursively follow the link to the next Let's see now our spider modified to recursively follow the link to the next
page, extracting data from it:: page, extracting data from it::
@ -533,12 +538,12 @@ page, extracting data from it::
def parse(self, response): def parse(self, response):
for quote in response.css('div.quote'): for quote in response.css('div.quote'):
yield { yield {
'text': quote.css('span.text::text').extract_first(), 'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').extract_first(), 'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').extract(), 'tags': quote.css('div.tags a.tag::text').getall(),
} }
next_page = response.css('li.next a::attr(href)').extract_first() next_page = response.css('li.next a::attr(href)').get()
if next_page is not None: if next_page is not None:
next_page = response.urljoin(next_page) next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse) yield scrapy.Request(next_page, callback=self.parse)
@ -584,12 +589,12 @@ As a shortcut for creating Request objects you can use
def parse(self, response): def parse(self, response):
for quote in response.css('div.quote'): for quote in response.css('div.quote'):
yield { yield {
'text': quote.css('span.text::text').extract_first(), 'text': quote.css('span.text::text').get(),
'author': quote.css('span small::text').extract_first(), 'author': quote.css('span small::text').get(),
'tags': quote.css('div.tags a.tag::text').extract(), 'tags': quote.css('div.tags a.tag::text').getall(),
} }
next_page = response.css('li.next a::attr(href)').extract_first() next_page = response.css('li.next a::attr(href)').get()
if next_page is not None: if next_page is not None:
yield response.follow(next_page, callback=self.parse) yield response.follow(next_page, callback=self.parse)
@ -641,7 +646,7 @@ this time for scraping author information::
def parse_author(self, response): def parse_author(self, response):
def extract_with_css(query): def extract_with_css(query):
return response.css(query).extract_first().strip() return response.css(query).get(default='').strip()
yield { yield {
'name': extract_with_css('h3.author-title::text'), 'name': extract_with_css('h3.author-title::text'),
@ -710,11 +715,11 @@ with a specific tag, building the URL based on the argument::
def parse(self, response): def parse(self, response):
for quote in response.css('div.quote'): for quote in response.css('div.quote'):
yield { yield {
'text': quote.css('span.text::text').extract_first(), 'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').extract_first(), 'author': quote.css('small.author::text').get(),
} }
next_page = response.css('li.next a::attr(href)').extract_first() next_page = response.css('li.next a::attr(href)').get()
if next_page is not None: if next_page is not None:
yield response.follow(next_page, self.parse) yield response.follow(next_page, self.parse)
@ -738,4 +743,3 @@ modeling the scraped data. If you prefer to play with an example project, check
the :ref:`intro-examples` section. the :ref:`intro-examples` section.
.. _JSON: https://en.wikipedia.org/wiki/JSON .. _JSON: https://en.wikipedia.org/wiki/JSON
.. _dirbot: https://github.com/scrapy/dirbot

View File

@ -37,7 +37,7 @@ Scrapy also understands, and can be configured through, a number of environment
variables. Currently these are: variables. Currently these are:
* ``SCRAPY_SETTINGS_MODULE`` (see :ref:`topics-settings-module-envvar`) * ``SCRAPY_SETTINGS_MODULE`` (see :ref:`topics-settings-module-envvar`)
* ``SCRAPY_PROJECT`` * ``SCRAPY_PROJECT`` (see :ref:`topics-project-envvar`)
* ``SCRAPY_PYTHON_SHELL`` (see :ref:`topics-shell`) * ``SCRAPY_PYTHON_SHELL`` (see :ref:`topics-shell`)
.. _topics-project-structure: .. _topics-project-structure:
@ -71,6 +71,33 @@ the project settings. Here is an example::
[settings] [settings]
default = myproject.settings default = myproject.settings
.. _topics-project-envvar:
Sharing the root directory between projects
===========================================
A project root directory, the one that contains the ``scrapy.cfg``, may be
shared by multiple Scrapy projects, each with its own settings module.
In that case, you must define one or more aliases for those settings modules
under ``[settings]`` in your ``scrapy.cfg`` file::
[settings]
default = myproject1.settings
project1 = myproject1.settings
project2 = myproject2.settings
By default, the ``scrapy`` command-line tool will use the ``default`` settings.
Use the ``SCRAPY_PROJECT`` environment variable to specify a different project
for ``scrapy`` to use::
$ scrapy settings --get BOT_NAME
Project 1 Bot
$ export SCRAPY_PROJECT=project2
$ scrapy settings --get BOT_NAME
Project 2 Bot
Using the ``scrapy`` tool Using the ``scrapy`` tool
========================= =========================
@ -458,9 +485,9 @@ Usage example::
>>> STATUS DEPTH LEVEL 1 <<< >>> STATUS DEPTH LEVEL 1 <<<
# Scraped Items ------------------------------------------------------------ # Scraped Items ------------------------------------------------------------
[{'name': u'Example item', [{'name': 'Example item',
'category': u'Furniture', 'category': 'Furniture',
'length': u'12 cm'}] 'length': '12 cm'}]
# Requests ----------------------------------------------------------------- # Requests -----------------------------------------------------------------
[] []

View File

@ -86,8 +86,11 @@ override three methods:
.. method:: Contract.adjust_request_args(args) .. method:: Contract.adjust_request_args(args)
This receives a ``dict`` as an argument containing default arguments This receives a ``dict`` as an argument containing default arguments
for :class:`~scrapy.http.Request` object. Must return the same or a for request object. :class:`~scrapy.http.Request` is used by default,
modified version of it. but this can be changed with the ``request_cls`` attribute.
If multiple contracts in chain have this attribute defined, the last one is used.
Must return the same or a modified version of it.
.. method:: Contract.pre_process(response) .. method:: Contract.pre_process(response)

View File

@ -18,11 +18,13 @@ Consider the following scrapy spider below::
) )
def parse(self, response): def parse(self, response):
# collect `item_urls` # <processing code not shown>
# collect `item_urls`
for item_url in item_urls: for item_url in item_urls:
yield scrapy.Request(item_url, self.parse_item) yield scrapy.Request(item_url, self.parse_item)
def parse_item(self, response): def parse_item(self, response):
# <processing code not shown>
item = MyItem() item = MyItem()
# populate `item` fields # populate `item` fields
# and extract item_details_url # and extract item_details_url

View File

@ -86,7 +86,7 @@ Creating items
:: ::
>>> product = Product(name='Desktop PC', price=1000) >>> product = Product(name='Desktop PC', price=1000)
>>> print product >>> print(product)
Product(name='Desktop PC', price=1000) Product(name='Desktop PC', price=1000)
Getting field values Getting field values
@ -161,11 +161,11 @@ Other common tasks
Copying items:: Copying items::
>>> product2 = Product(product) >>> product2 = Product(product)
>>> print product2 >>> print(product2)
Product(name='Desktop PC', price=1000) Product(name='Desktop PC', price=1000)
>>> product3 = product2.copy() >>> product3 = product2.copy()
>>> print product3 >>> print(product3)
Product(name='Desktop PC', price=1000) Product(name='Desktop PC', price=1000)
Creating dicts from items:: Creating dicts from items::

View File

@ -84,7 +84,7 @@ So, for example, this won't work::
return scrapy.Request('http://www.example.com', callback=lambda r: self.other_callback(r, somearg)) return scrapy.Request('http://www.example.com', callback=lambda r: self.other_callback(r, somearg))
def other_callback(self, response, somearg): def other_callback(self, response, somearg):
print "the argument passed is:", somearg print("the argument passed is: %s" % somearg)
But this will:: But this will::
@ -94,7 +94,7 @@ But this will::
def other_callback(self, response): def other_callback(self, response):
somearg = response.meta['somearg'] somearg = response.meta['somearg']
print "the argument passed is:", somearg print("the argument passed is: %s" % somearg)
If you wish to log the requests that couldn't be serialized, you can set the If you wish to log the requests that couldn't be serialized, you can set the
:setting:`SCHEDULER_DEBUG` setting to ``True`` in the project's settings page. :setting:`SCHEDULER_DEBUG` setting to ``True`` in the project's settings page.

View File

@ -678,10 +678,10 @@ Here is a list of all built-in processors:
>>> from scrapy.loader.processors import Join >>> from scrapy.loader.processors import Join
>>> proc = Join() >>> proc = Join()
>>> proc(['one', 'two', 'three']) >>> proc(['one', 'two', 'three'])
u'one two three' 'one two three'
>>> proc = Join('<br>') >>> proc = Join('<br>')
>>> proc(['one', 'two', 'three']) >>> proc(['one', 'two', 'three'])
u'one<br>two<br>three' 'one<br>two<br>three'
.. class:: Compose(\*functions, \**default_loader_context) .. class:: Compose(\*functions, \**default_loader_context)
@ -744,9 +744,9 @@ Here is a list of all built-in processors:
... return None if x == 'world' else x ... return None if x == 'world' else x
... ...
>>> from scrapy.loader.processors import MapCompose >>> from scrapy.loader.processors import MapCompose
>>> proc = MapCompose(filter_world, unicode.upper) >>> proc = MapCompose(filter_world, str.upper)
>>> proc([u'hello', u'world', u'this', u'is', u'scrapy']) >>> proc(['hello', 'world', 'this', 'is', 'scrapy'])
[u'HELLO, u'THIS', u'IS', u'SCRAPY'] ['HELLO, 'THIS', 'IS', 'SCRAPY']
As with the Compose processor, functions can receive Loader contexts, and As with the Compose processor, functions can receive Loader contexts, and
constructor keyword arguments are used as default context values. See constructor keyword arguments are used as default context values. See
@ -772,7 +772,7 @@ Here is a list of all built-in processors:
>>> import json >>> import json
>>> proc_single_json_str = Compose(json.loads, SelectJmes("foo")) >>> proc_single_json_str = Compose(json.loads, SelectJmes("foo"))
>>> proc_single_json_str('{"foo": "bar"}') >>> proc_single_json_str('{"foo": "bar"}')
u'bar' 'bar'
>>> proc_json_list = Compose(json.loads, MapCompose(SelectJmes('foo'))) >>> proc_json_list = Compose(json.loads, MapCompose(SelectJmes('foo')))
>>> proc_json_list('[{"foo":"bar"}, {"baz":"tar"}]') >>> proc_json_list('[{"foo":"bar"}, {"baz":"tar"}]')
[u'bar'] ['bar']

File diff suppressed because it is too large Load Diff

View File

@ -871,7 +871,7 @@ LOG_STDOUT
Default: ``False`` Default: ``False``
If ``True``, all standard output (and error) of your process will be redirected If ``True``, all standard output (and error) of your process will be redirected
to the log. For example if you ``print 'hello'`` it will appear in the Scrapy to the log. For example if you ``print('hello')`` it will appear in the Scrapy
log. log.
.. setting:: LOG_SHORT_NAMES .. setting:: LOG_SHORT_NAMES

View File

@ -179,13 +179,13 @@ all start with the ``[s]`` prefix)::
After that, we can start playing with the objects:: After that, we can start playing with the objects::
>>> response.xpath('//title/text()').extract_first() >>> response.xpath('//title/text()').get()
'Scrapy | A Fast and Powerful Scraping and Web Crawling Framework' 'Scrapy | A Fast and Powerful Scraping and Web Crawling Framework'
>>> fetch("https://reddit.com") >>> fetch("https://reddit.com")
>>> response.xpath('//title/text()').extract() >>> response.xpath('//title/text()').get()
['reddit: the front page of the internet'] 'reddit: the front page of the internet'
>>> request = request.replace(method="POST") >>> request = request.replace(method="POST")

View File

@ -279,6 +279,22 @@ request_dropped
:param spider: the spider that yielded the request :param spider: the spider that yielded the request
:type spider: :class:`~scrapy.spiders.Spider` object :type spider: :class:`~scrapy.spiders.Spider` object
request_reached_downloader
---------------------------
.. signal:: request_reached_downloader
.. function:: request_reached_downloader(request, spider)
Sent when a :class:`~scrapy.http.Request` reached downloader.
The signal does not support returning deferreds from their handlers.
:param request: the request that reached downloader
:type request: :class:`~scrapy.http.Request` object
:param spider: the spider that yielded the request
:type spider: :class:`~scrapy.spiders.Spider` object
response_received response_received
----------------- -----------------

View File

@ -229,11 +229,11 @@ Return multiple Requests and items from a single callback::
] ]
def parse(self, response): def parse(self, response):
for h3 in response.xpath('//h3').extract(): for h3 in response.xpath('//h3').getall():
yield {"title": h3} yield {"title": h3}
for url in response.xpath('//a/@href').extract(): for href in response.xpath('//a/@href').getall():
yield scrapy.Request(url, callback=self.parse) yield scrapy.Request(response.urljoin(href), self.parse)
Instead of :attr:`~.start_urls` you can use :meth:`~.start_requests` directly; Instead of :attr:`~.start_urls` you can use :meth:`~.start_requests` directly;
to give data more structure you can use :ref:`topics-items`:: to give data more structure you can use :ref:`topics-items`::
@ -251,11 +251,11 @@ to give data more structure you can use :ref:`topics-items`::
yield scrapy.Request('http://www.example.com/3.html', self.parse) yield scrapy.Request('http://www.example.com/3.html', self.parse)
def parse(self, response): def parse(self, response):
for h3 in response.xpath('//h3').extract(): for h3 in response.xpath('//h3').getall():
yield MyItem(title=h3) yield MyItem(title=h3)
for url in response.xpath('//a/@href').extract(): for href in response.xpath('//a/@href').getall():
yield scrapy.Request(url, callback=self.parse) yield scrapy.Request(response.urljoin(href), self.parse)
.. _spiderargs: .. _spiderargs:
@ -434,8 +434,8 @@ Let's now take a look at an example CrawlSpider with rules::
self.logger.info('Hi, this is an item page! %s', response.url) self.logger.info('Hi, this is an item page! %s', response.url)
item = scrapy.Item() item = scrapy.Item()
item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)') item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
item['name'] = response.xpath('//td[@id="item_name"]/text()').extract() item['name'] = response.xpath('//td[@id="item_name"]/text()').get()
item['description'] = response.xpath('//td[@id="item_description"]/text()').extract() item['description'] = response.xpath('//td[@id="item_description"]/text()').get()
return item return item
@ -545,12 +545,12 @@ These spiders are pretty easy to use, let's have a look at one example::
itertag = 'item' itertag = 'item'
def parse_node(self, response, node): def parse_node(self, response, node):
self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.extract())) self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.getall()))
item = TestItem() item = TestItem()
item['id'] = node.xpath('@id').extract() item['id'] = node.xpath('@id').get()
item['name'] = node.xpath('name').extract() item['name'] = node.xpath('name').get()
item['description'] = node.xpath('description').extract() item['description'] = node.xpath('description').get()
return item return item
Basically what we did up there was to create a spider that downloads a feed from Basically what we did up there was to create a spider that downloads a feed from

View File

@ -6,5 +6,5 @@ queuelib
w3lib>=1.17.0 w3lib>=1.17.0
six>=1.5.2 six>=1.5.2
PyDispatcher>=2.0.5 PyDispatcher>=2.0.5
parsel>=1.4 parsel>=1.5
service_identity service_identity

View File

@ -6,5 +6,5 @@ queuelib>=1.1.1
w3lib>=1.17.0 w3lib>=1.17.0
six>=1.5.2 six>=1.5.2
PyDispatcher>=2.0.5 PyDispatcher>=2.0.5
parsel>=1.4 parsel>=1.5
service_identity service_identity

View File

@ -42,23 +42,38 @@ class ContractsManager(object):
requests = [] requests = []
for method in self.tested_methods_from_spidercls(type(spider)): for method in self.tested_methods_from_spidercls(type(spider)):
bound_method = spider.__getattribute__(method) bound_method = spider.__getattribute__(method)
requests.append(self.from_method(bound_method, results)) try:
requests.append(self.from_method(bound_method, results))
except Exception:
case = _create_testcase(bound_method, 'contract')
results.addError(case, sys.exc_info())
return requests return requests
def from_method(self, method, results): def from_method(self, method, results):
contracts = self.extract_contracts(method) contracts = self.extract_contracts(method)
if contracts: if contracts:
request_cls = Request
for contract in contracts:
if contract.request_cls is not None:
request_cls = contract.request_cls
# calculate request args # calculate request args
args, kwargs = get_spec(Request.__init__) args, kwargs = get_spec(request_cls.__init__)
# Don't filter requests to allow
# testing different callbacks on the same URL.
kwargs['dont_filter'] = True
kwargs['callback'] = method kwargs['callback'] = method
for contract in contracts: for contract in contracts:
kwargs = contract.adjust_request_args(kwargs) kwargs = contract.adjust_request_args(kwargs)
# create and prepare request
args.remove('self') args.remove('self')
# check if all positional arguments are defined in kwargs
if set(args).issubset(set(kwargs)): if set(args).issubset(set(kwargs)):
request = Request(**kwargs) request = request_cls(**kwargs)
# execute pre and post hooks in order # execute pre and post hooks in order
for contract in reversed(contracts): for contract in reversed(contracts):
@ -94,6 +109,7 @@ class ContractsManager(object):
class Contract(object): class Contract(object):
""" Abstract class for contracts """ """ Abstract class for contracts """
request_cls = None
def __init__(self, method, *args): def __init__(self, method, *args):
self.testcase_pre = _create_testcase(method, '@%s pre-hook' % self.name) self.testcase_pre = _create_testcase(method, '@%s pre-hook' % self.name)

View File

@ -129,6 +129,9 @@ class Downloader(object):
return response return response
slot.active.add(request) slot.active.add(request)
self.signals.send_catch_log(signal=signals.request_reached_downloader,
request=request,
spider=spider)
deferred = defer.Deferred().addBoth(_deactivate) deferred = defer.Deferred().addBoth(_deactivate)
slot.queue.append((request, deferred)) slot.queue.append((request, deferred))
self._process_queue(spider, slot) self._process_queue(spider, slot)

View File

@ -141,7 +141,7 @@ class SgmlLinkExtractor(FilteringLinkExtractor):
base_url = get_base_url(response) base_url = get_base_url(response)
body = u''.join(f body = u''.join(f
for x in self.restrict_xpaths for x in self.restrict_xpaths
for f in response.xpath(x).extract() for f in response.xpath(x).getall()
).encode(response.encoding, errors='xmlcharrefreplace') ).encode(response.encoding, errors='xmlcharrefreplace')
else: else:
body = response.body body = response.body

View File

@ -181,7 +181,7 @@ class ItemLoader(object):
def _get_xpathvalues(self, xpaths, **kw): def _get_xpathvalues(self, xpaths, **kw):
self._check_selector_method() self._check_selector_method()
xpaths = arg_to_iter(xpaths) xpaths = arg_to_iter(xpaths)
return flatten(self.selector.xpath(xpath).extract() for xpath in xpaths) return flatten(self.selector.xpath(xpath).getall() for xpath in xpaths)
def add_css(self, field_name, css, *processors, **kw): def add_css(self, field_name, css, *processors, **kw):
values = self._get_cssvalues(css, **kw) values = self._get_cssvalues(css, **kw)
@ -198,6 +198,6 @@ class ItemLoader(object):
def _get_cssvalues(self, csss, **kw): def _get_cssvalues(self, csss, **kw):
self._check_selector_method() self._check_selector_method()
csss = arg_to_iter(csss) csss = arg_to_iter(csss)
return flatten(self.selector.css(css).extract() for css in csss) return flatten(self.selector.css(css).getall() for css in csss)
XPathItemLoader = create_deprecated_class('XPathItemLoader', ItemLoader) XPathItemLoader = create_deprecated_class('XPathItemLoader', ItemLoader)

View File

@ -27,6 +27,10 @@ def _response_from_text(text, st):
class SelectorList(_ParselSelector.selectorlist_cls, object_ref): class SelectorList(_ParselSelector.selectorlist_cls, object_ref):
"""
The :class:`SelectorList` class is a subclass of the builtin ``list``
class, which provides a few additional methods.
"""
@deprecated(use_instead='.extract()') @deprecated(use_instead='.extract()')
def extract_unquoted(self): def extract_unquoted(self):
return [x.extract_unquoted() for x in self] return [x.extract_unquoted() for x in self]
@ -41,6 +45,35 @@ class SelectorList(_ParselSelector.selectorlist_cls, object_ref):
class Selector(_ParselSelector, object_ref): class Selector(_ParselSelector, object_ref):
"""
An instance of :class:`Selector` is a wrapper over response to select
certain parts of its content.
``response`` is an :class:`~scrapy.http.HtmlResponse` or an
:class:`~scrapy.http.XmlResponse` object that will be used for selecting
and extracting data.
``text`` is a unicode string or utf-8 encoded text for cases when a
``response`` isn't available. Using ``text`` and ``response`` together is
undefined behavior.
``type`` defines the selector type, it can be ``"html"``, ``"xml"``
or ``None`` (default).
If ``type`` is ``None``, the selector automatically chooses the best type
based on ``response`` type (see below), or defaults to ``"html"`` in case it
is used together with ``text``.
If ``type`` is ``None`` and a ``response`` is passed, the selector type is
inferred from the response type as follows:
* ``"html"`` for :class:`~scrapy.http.HtmlResponse` type
* ``"xml"`` for :class:`~scrapy.http.XmlResponse` type
* ``"html"`` for anything else
Otherwise, if ``type`` is set, the selector type will be forced and no
detection will occur.
"""
__slots__ = ['response'] __slots__ = ['response']
selectorlist_cls = SelectorList selectorlist_cls = SelectorList

View File

@ -13,6 +13,7 @@ spider_closed = object()
spider_error = object() spider_error = object()
request_scheduled = object() request_scheduled = object()
request_dropped = object() request_dropped = object()
request_reached_downloader = object()
response_received = object() response_received = object()
response_downloaded = object() response_downloaded = object()
item_scraped = object() item_scraped = object()

View File

@ -14,8 +14,8 @@ class $classname(CrawlSpider):
) )
def parse_item(self, response): def parse_item(self, response):
i = {} item = {}
#i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract() #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#i['name'] = response.xpath('//div[@id="name"]').extract() #item['name'] = response.xpath('//div[@id="name"]').get()
#i['description'] = response.xpath('//div[@id="description"]').extract() #item['description'] = response.xpath('//div[@id="description"]').get()
return i return item

View File

@ -10,8 +10,8 @@ class $classname(XMLFeedSpider):
itertag = 'item' # change it accordingly itertag = 'item' # change it accordingly
def parse_node(self, response, selector): def parse_node(self, response, selector):
i = {} item = {}
#i['url'] = selector.select('url').extract() #item['url'] = selector.select('url').get()
#i['name'] = selector.select('name').extract() #item['name'] = selector.select('name').get()
#i['description'] = selector.select('description').extract() #item['description'] = selector.select('description').get()
return i return item

View File

@ -71,7 +71,7 @@ setup(
'pyOpenSSL', 'pyOpenSSL',
'cssselect>=0.9', 'cssselect>=0.9',
'six>=1.5.2', 'six>=1.5.2',
'parsel>=1.4', 'parsel>=1.5',
'PyDispatcher>=2.0.5', 'PyDispatcher>=2.0.5',
'service_identity', 'service_identity',
], ],

View File

@ -35,7 +35,7 @@ class ShellTest(ProcessTest, SiteTest, unittest.TestCase):
@defer.inlineCallbacks @defer.inlineCallbacks
def test_response_selector_html(self): def test_response_selector_html(self):
xpath = 'response.xpath("//p[@class=\'one\']/text()").extract()[0]' xpath = 'response.xpath("//p[@class=\'one\']/text()").get()'
_, out, _ = yield self.execute([self.url('/html'), '-c', xpath]) _, out, _ = yield self.execute([self.url('/html'), '-c', xpath])
self.assertEqual(out.strip(), b'Works') self.assertEqual(out.strip(), b'Works')

View File

@ -1,18 +1,23 @@
from unittest import TextTestResult from unittest import TextTestResult
from six import get_unbound_function
from twisted.internet import defer
from twisted.python import failure from twisted.python import failure
from twisted.trial import unittest from twisted.trial import unittest
from scrapy import FormRequest
from scrapy.crawler import CrawlerRunner
from scrapy.spidermiddlewares.httperror import HttpError from scrapy.spidermiddlewares.httperror import HttpError
from scrapy.spiders import Spider from scrapy.spiders import Spider
from scrapy.http import Request from scrapy.http import Request
from scrapy.item import Item, Field from scrapy.item import Item, Field
from scrapy.contracts import ContractsManager from scrapy.contracts import ContractsManager, Contract
from scrapy.contracts.default import ( from scrapy.contracts.default import (
UrlContract, UrlContract,
ReturnsContract, ReturnsContract,
ScrapesContract, ScrapesContract,
) )
from tests.mockserver import MockServer
class TestItem(Item): class TestItem(Item):
@ -24,6 +29,30 @@ class ResponseMock(object):
url = 'http://scrapy.org' url = 'http://scrapy.org'
class CustomSuccessContract(Contract):
name = 'custom_success_contract'
def adjust_request_args(self, args):
args['url'] = 'http://scrapy.org'
return args
class CustomFailContract(Contract):
name = 'custom_fail_contract'
def adjust_request_args(self, args):
raise TypeError('Error in adjust_request_args')
class CustomFormContract(Contract):
name = 'custom_form'
request_cls = FormRequest
def adjust_request_args(self, args):
args['formdata'] = {'name': 'scrapy'}
return args
class TestSpider(Spider): class TestSpider(Spider):
name = 'demo_spider' name = 'demo_spider'
@ -100,13 +129,47 @@ class TestSpider(Spider):
""" """
pass pass
def custom_form(self, response):
"""
@url http://scrapy.org
@custom_form
"""
pass
class CustomContractSuccessSpider(Spider):
name = 'custom_contract_success_spider'
def parse(self, response):
"""
@custom_success_contract
"""
pass
class CustomContractFailSpider(Spider):
name = 'custom_contract_fail_spider'
def parse(self, response):
"""
@custom_fail_contract
"""
pass
class InheritsTestSpider(TestSpider): class InheritsTestSpider(TestSpider):
name = 'inherits_demo_spider' name = 'inherits_demo_spider'
class ContractsManagerTest(unittest.TestCase): class ContractsManagerTest(unittest.TestCase):
contracts = [UrlContract, ReturnsContract, ScrapesContract] contracts = [
UrlContract,
ReturnsContract,
ScrapesContract,
CustomFormContract,
CustomSuccessContract,
CustomFailContract,
]
def setUp(self): def setUp(self):
self.conman = ContractsManager(self.contracts) self.conman = ContractsManager(self.contracts)
@ -120,6 +183,9 @@ class ContractsManagerTest(unittest.TestCase):
self.assertTrue(self.results.failures) self.assertTrue(self.results.failures)
self.assertFalse(self.results.errors) self.assertFalse(self.results.errors)
def should_error(self):
self.assertTrue(self.results.errors)
def test_contracts(self): def test_contracts(self):
spider = TestSpider() spider = TestSpider()
@ -181,17 +247,22 @@ class ContractsManagerTest(unittest.TestCase):
self.should_succeed() self.should_succeed()
# scrapes_item_fail # scrapes_item_fail
request = self.conman.from_method(spider.scrapes_item_fail, request = self.conman.from_method(spider.scrapes_item_fail, self.results)
self.results)
request.callback(response) request.callback(response)
self.should_fail() self.should_fail()
# scrapes_dict_item_fail # scrapes_dict_item_fail
request = self.conman.from_method(spider.scrapes_dict_item_fail, request = self.conman.from_method(spider.scrapes_dict_item_fail, self.results)
self.results)
request.callback(response) request.callback(response)
self.should_fail() self.should_fail()
def test_custom_contracts(self):
self.conman.from_spider(CustomContractSuccessSpider(), self.results)
self.should_succeed()
self.conman.from_spider(CustomContractFailSpider(), self.results)
self.should_error()
def test_errback(self): def test_errback(self):
spider = TestSpider() spider = TestSpider()
response = ResponseMock() response = ResponseMock()
@ -207,6 +278,44 @@ class ContractsManagerTest(unittest.TestCase):
self.assertFalse(self.results.failures) self.assertFalse(self.results.failures)
self.assertTrue(self.results.errors) self.assertTrue(self.results.errors)
@defer.inlineCallbacks
def test_same_url(self):
class TestSameUrlSpider(Spider):
name = 'test_same_url'
def __init__(self, *args, **kwargs):
super(TestSameUrlSpider, self).__init__(*args, **kwargs)
self.visited = 0
def start_requests(s):
return self.conman.from_spider(s, self.results)
def parse_first(self, response):
self.visited += 1
return TestItem()
def parse_second(self, response):
self.visited += 1
return TestItem()
with MockServer() as mockserver:
contract_doc = '@url {}'.format(mockserver.url('/status?n=200'))
get_unbound_function(TestSameUrlSpider.parse_first).__doc__ = contract_doc
get_unbound_function(TestSameUrlSpider.parse_second).__doc__ = contract_doc
crawler = CrawlerRunner().create_crawler(TestSameUrlSpider)
yield crawler.crawl()
self.assertEqual(crawler.spider.visited, 2)
def test_form_contract(self):
spider = TestSpider()
request = self.conman.from_method(spider.custom_form, self.results)
self.assertEqual(request.method, 'POST')
self.assertIsInstance(request, FormRequest)
def test_inherited_contracts(self): def test_inherited_contracts(self):
spider = InheritsTestSpider() spider = InheritsTestSpider()

View File

@ -103,6 +103,7 @@ class CrawlerRun(object):
self.respplug = [] self.respplug = []
self.reqplug = [] self.reqplug = []
self.reqdropped = [] self.reqdropped = []
self.reqreached = []
self.itemerror = [] self.itemerror = []
self.itemresp = [] self.itemresp = []
self.signals_catched = {} self.signals_catched = {}
@ -124,6 +125,7 @@ class CrawlerRun(object):
self.crawler.signals.connect(self.item_error, signals.item_error) self.crawler.signals.connect(self.item_error, signals.item_error)
self.crawler.signals.connect(self.request_scheduled, signals.request_scheduled) self.crawler.signals.connect(self.request_scheduled, signals.request_scheduled)
self.crawler.signals.connect(self.request_dropped, signals.request_dropped) self.crawler.signals.connect(self.request_dropped, signals.request_dropped)
self.crawler.signals.connect(self.request_reached, signals.request_reached_downloader)
self.crawler.signals.connect(self.response_downloaded, signals.response_downloaded) self.crawler.signals.connect(self.response_downloaded, signals.response_downloaded)
self.crawler.crawl(start_urls=start_urls) self.crawler.crawl(start_urls=start_urls)
self.spider = self.crawler.spider self.spider = self.crawler.spider
@ -155,6 +157,9 @@ class CrawlerRun(object):
def request_scheduled(self, request, spider): def request_scheduled(self, request, spider):
self.reqplug.append((request, spider)) self.reqplug.append((request, spider))
def request_reached(self, request, spider):
self.reqreached.append((request, spider))
def request_dropped(self, request, spider): def request_dropped(self, request, spider):
self.reqdropped.append((request, spider)) self.reqdropped.append((request, spider))
@ -212,6 +217,8 @@ class EngineTest(unittest.TestCase):
responses_count = len(self.run.respplug) responses_count = len(self.run.respplug)
self.assertEqual(scheduled_requests_count, self.assertEqual(scheduled_requests_count,
dropped_requests_count + responses_count) dropped_requests_count + responses_count)
self.assertEqual(len(self.run.reqreached),
responses_count)
def _assert_dropped_requests(self): def _assert_dropped_requests(self):
self.assertEqual(len(self.run.reqdropped), 1) self.assertEqual(len(self.run.reqdropped), 1)
@ -219,6 +226,7 @@ class EngineTest(unittest.TestCase):
def _assert_downloaded_responses(self): def _assert_downloaded_responses(self):
# response tests # response tests
self.assertEqual(8, len(self.run.respplug)) self.assertEqual(8, len(self.run.respplug))
self.assertEqual(8, len(self.run.reqreached))
for response, _ in self.run.respplug: for response, _ in self.run.respplug:
if self.run.getpath(response.url) == '/item999.html': if self.run.getpath(response.url) == '/item999.html':

View File

@ -336,11 +336,11 @@ class TextResponseTest(BaseResponseTest):
self.assertIs(response.selector.response, response) self.assertIs(response.selector.response, response)
self.assertEqual( self.assertEqual(
response.selector.xpath("//title/text()").extract(), response.selector.xpath("//title/text()").getall(),
[u'Some page'] [u'Some page']
) )
self.assertEqual( self.assertEqual(
response.selector.css("title::text").extract(), response.selector.css("title::text").getall(),
[u'Some page'] [u'Some page']
) )
self.assertEqual( self.assertEqual(
@ -353,12 +353,12 @@ class TextResponseTest(BaseResponseTest):
response = self.response_class("http://www.example.com", body=body) response = self.response_class("http://www.example.com", body=body)
self.assertEqual( self.assertEqual(
response.xpath("//title/text()").extract(), response.xpath("//title/text()").getall(),
response.selector.xpath("//title/text()").extract(), response.selector.xpath("//title/text()").getall(),
) )
self.assertEqual( self.assertEqual(
response.css("title::text").extract(), response.css("title::text").getall(),
response.selector.css("title::text").extract(), response.selector.css("title::text").getall(),
) )
def test_selector_shortcuts_kwargs(self): def test_selector_shortcuts_kwargs(self):
@ -366,13 +366,13 @@ class TextResponseTest(BaseResponseTest):
response = self.response_class("http://www.example.com", body=body) response = self.response_class("http://www.example.com", body=body)
self.assertEqual( self.assertEqual(
response.xpath("normalize-space(//p[@class=$pclass])", pclass="content").extract(), response.xpath("normalize-space(//p[@class=$pclass])", pclass="content").getall(),
response.xpath("normalize-space(//p[@class=\"content\"])").extract(), response.xpath("normalize-space(//p[@class=\"content\"])").getall(),
) )
self.assertEqual( self.assertEqual(
response.xpath("//title[count(following::p[@class=$pclass])=$pcount]/text()", response.xpath("//title[count(following::p[@class=$pclass])=$pcount]/text()",
pclass="content", pcount=1).extract(), pclass="content", pcount=1).getall(),
response.xpath("//title[count(following::p[@class=\"content\"])=1]/text()").extract(), response.xpath("//title[count(following::p[@class=\"content\"])=1]/text()").getall(),
) )
def test_urljoin_with_base_url(self): def test_urljoin_with_base_url(self):
@ -562,7 +562,7 @@ class XmlResponseTest(TextResponseTest):
self.assertIs(response.selector.response, response) self.assertIs(response.selector.response, response)
self.assertEqual( self.assertEqual(
response.selector.xpath("//elem/text()").extract(), response.selector.xpath("//elem/text()").getall(),
[u'value'] [u'value']
) )
@ -571,8 +571,8 @@ class XmlResponseTest(TextResponseTest):
response = self.response_class("http://www.example.com", body=body) response = self.response_class("http://www.example.com", body=body)
self.assertEqual( self.assertEqual(
response.xpath("//elem/text()").extract(), response.xpath("//elem/text()").getall(),
response.selector.xpath("//elem/text()").extract(), response.selector.xpath("//elem/text()").getall(),
) )
def test_selector_shortcuts_kwargs(self): def test_selector_shortcuts_kwargs(self):
@ -583,12 +583,12 @@ class XmlResponseTest(TextResponseTest):
response = self.response_class("http://www.example.com", body=body) response = self.response_class("http://www.example.com", body=body)
self.assertEqual( self.assertEqual(
response.xpath("//s:elem/text()", namespaces={'s': 'http://scrapy.org'}).extract(), response.xpath("//s:elem/text()", namespaces={'s': 'http://scrapy.org'}).getall(),
response.selector.xpath("//s:elem/text()", namespaces={'s': 'http://scrapy.org'}).extract(), response.selector.xpath("//s:elem/text()", namespaces={'s': 'http://scrapy.org'}).getall(),
) )
response.selector.register_namespace('s2', 'http://scrapy.org') response.selector.register_namespace('s2', 'http://scrapy.org')
self.assertEqual( self.assertEqual(
response.xpath("//s1:elem/text()", namespaces={'s1': 'http://scrapy.org'}).extract(), response.xpath("//s1:elem/text()", namespaces={'s1': 'http://scrapy.org'}).getall(),
response.selector.xpath("//s2:elem/text()").extract(), response.selector.xpath("//s2:elem/text()").getall(),
) )

View File

@ -634,7 +634,7 @@ class SubselectorLoaderTest(unittest.TestCase):
nl = l.nested_xpath("//header") nl = l.nested_xpath("//header")
nl.add_xpath('name', 'div/text()') nl.add_xpath('name', 'div/text()')
nl.add_css('name_div', '#id') nl.add_css('name_div', '#id')
nl.add_value('name_value', nl.selector.xpath('div[@id = "id"]/text()').extract()) nl.add_value('name_value', nl.selector.xpath('div[@id = "id"]/text()').getall())
self.assertEqual(l.get_output_value('name'), [u'marta']) self.assertEqual(l.get_output_value('name'), [u'marta'])
self.assertEqual(l.get_output_value('name_div'), [u'<div id="id">marta</div>']) self.assertEqual(l.get_output_value('name_div'), [u'<div id="id">marta</div>'])
@ -649,7 +649,7 @@ class SubselectorLoaderTest(unittest.TestCase):
nl = l.nested_css("header") nl = l.nested_css("header")
nl.add_xpath('name', 'div/text()') nl.add_xpath('name', 'div/text()')
nl.add_css('name_div', '#id') nl.add_css('name_div', '#id')
nl.add_value('name_value', nl.selector.xpath('div[@id = "id"]/text()').extract()) nl.add_value('name_value', nl.selector.xpath('div[@id = "id"]/text()').getall())
self.assertEqual(l.get_output_value('name'), [u'marta']) self.assertEqual(l.get_output_value('name'), [u'marta'])
self.assertEqual(l.get_output_value('name_div'), [u'<div id="id">marta</div>']) self.assertEqual(l.get_output_value('name_div'), [u'<div id="id">marta</div>'])

View File

@ -29,7 +29,7 @@ class MediaDownloadSpider(SimpleSpider):
for href in response.xpath(''' for href in response.xpath('''
//table[thead/tr/th="Filename"] //table[thead/tr/th="Filename"]
/tbody//a/@href /tbody//a/@href
''').extract()], ''').getall()],
} }
yield item yield item

View File

@ -20,17 +20,17 @@ class SelectorTestCase(unittest.TestCase):
for x in xl: for x in xl:
assert isinstance(x, Selector) assert isinstance(x, Selector)
self.assertEqual(sel.xpath('//input').extract(), self.assertEqual(sel.xpath('//input').getall(),
[x.extract() for x in sel.xpath('//input')]) [x.get() for x in sel.xpath('//input')])
self.assertEqual([x.extract() for x in sel.xpath("//input[@name='a']/@name")], self.assertEqual([x.get() for x in sel.xpath("//input[@name='a']/@name")],
[u'a']) [u'a'])
self.assertEqual([x.extract() for x in sel.xpath("number(concat(//input[@name='a']/@value, //input[@name='b']/@value))")], self.assertEqual([x.get() for x in sel.xpath("number(concat(//input[@name='a']/@value, //input[@name='b']/@value))")],
[u'12.0']) [u'12.0'])
self.assertEqual(sel.xpath("concat('xpath', 'rules')").extract(), self.assertEqual(sel.xpath("concat('xpath', 'rules')").getall(),
[u'xpathrules']) [u'xpathrules'])
self.assertEqual([x.extract() for x in sel.xpath("concat(//input[@name='a']/@value, //input[@name='b']/@value)")], self.assertEqual([x.get() for x in sel.xpath("concat(//input[@name='a']/@value, //input[@name='b']/@value)")],
[u'12']) [u'12'])
def test_root_base_url(self): def test_root_base_url(self):
@ -60,12 +60,12 @@ class SelectorTestCase(unittest.TestCase):
text = b'<div><img src="a.jpg"><p>Hello</div>' text = b'<div><img src="a.jpg"><p>Hello</div>'
sel = Selector(XmlResponse('http://example.com', body=text, encoding='utf-8')) sel = Selector(XmlResponse('http://example.com', body=text, encoding='utf-8'))
self.assertEqual(sel.type, 'xml') self.assertEqual(sel.type, 'xml')
self.assertEqual(sel.xpath("//div").extract(), self.assertEqual(sel.xpath("//div").getall(),
[u'<div><img src="a.jpg"><p>Hello</p></img></div>']) [u'<div><img src="a.jpg"><p>Hello</p></img></div>'])
sel = Selector(HtmlResponse('http://example.com', body=text, encoding='utf-8')) sel = Selector(HtmlResponse('http://example.com', body=text, encoding='utf-8'))
self.assertEqual(sel.type, 'html') self.assertEqual(sel.type, 'html')
self.assertEqual(sel.xpath("//div").extract(), self.assertEqual(sel.xpath("//div").getall(),
[u'<div><img src="a.jpg"><p>Hello</p></div>']) [u'<div><img src="a.jpg"><p>Hello</p></div>'])
def test_http_header_encoding_precedence(self): def test_http_header_encoding_precedence(self):
@ -84,15 +84,15 @@ class SelectorTestCase(unittest.TestCase):
headers = {'Content-Type': ['text/html; charset=utf-8']} headers = {'Content-Type': ['text/html; charset=utf-8']}
response = HtmlResponse(url="http://example.com", headers=headers, body=html_utf8) response = HtmlResponse(url="http://example.com", headers=headers, body=html_utf8)
x = Selector(response) x = Selector(response)
self.assertEqual(x.xpath("//span[@id='blank']/text()").extract(), self.assertEqual(x.xpath("//span[@id='blank']/text()").getall(),
[u'\xa3']) [u'\xa3'])
def test_badly_encoded_body(self): def test_badly_encoded_body(self):
# \xe9 alone isn't valid utf8 sequence # \xe9 alone isn't valid utf8 sequence
r1 = TextResponse('http://www.example.com', \ r1 = TextResponse('http://www.example.com',
body=b'<html><p>an Jos\xe9 de</p><html>', \ body=b'<html><p>an Jos\xe9 de</p><html>',
encoding='utf-8') encoding='utf-8')
Selector(r1).xpath('//text()').extract() Selector(r1).xpath('//text()').getall()
def test_weakref_slots(self): def test_weakref_slots(self):
"""Check that classes are using slots and are weak-referenceable""" """Check that classes are using slots and are weak-referenceable"""

View File

@ -147,10 +147,10 @@ class XMLFeedSpiderTest(SpiderTest):
def parse_node(self, response, selector): def parse_node(self, response, selector):
yield { yield {
'loc': selector.xpath('a:loc/text()').extract(), 'loc': selector.xpath('a:loc/text()').getall(),
'updated': selector.xpath('b:updated/text()').extract(), 'updated': selector.xpath('b:updated/text()').getall(),
'other': selector.xpath('other/@value').extract(), 'other': selector.xpath('other/@value').getall(),
'custom': selector.xpath('other/@b:custom').extract(), 'custom': selector.xpath('other/@b:custom').getall(),
} }
for iterator in ('iternodes', 'xml'): for iterator in ('iternodes', 'xml'):

View File

@ -30,10 +30,13 @@ class XmliterTestCase(unittest.TestCase):
response = XmlResponse(url="http://example.com", body=body) response = XmlResponse(url="http://example.com", body=body)
attrs = [] attrs = []
for x in self.xmliter(response, 'product'): for x in self.xmliter(response, 'product'):
attrs.append((x.xpath("@id").extract(), x.xpath("name/text()").extract(), x.xpath("./type/text()").extract())) attrs.append((
x.attrib['id'],
x.xpath("name/text()").getall(),
x.xpath("./type/text()").getall()))
self.assertEqual(attrs, self.assertEqual(attrs,
[(['001'], ['Name 1'], ['Type 1']), (['002'], ['Name 2'], ['Type 2'])]) [('001', ['Name 1'], ['Type 1']), ('002', ['Name 2'], ['Type 2'])])
def test_xmliter_unusual_node(self): def test_xmliter_unusual_node(self):
body = b"""<?xml version="1.0" encoding="UTF-8"?> body = b"""<?xml version="1.0" encoding="UTF-8"?>
@ -43,7 +46,7 @@ class XmliterTestCase(unittest.TestCase):
</root> </root>
""" """
response = XmlResponse(url="http://example.com", body=body) response = XmlResponse(url="http://example.com", body=body)
nodenames = [e.xpath('name()').extract() nodenames = [e.xpath('name()').getall()
for e in self.xmliter(response, 'matchme...')] for e in self.xmliter(response, 'matchme...')]
self.assertEqual(nodenames, [['matchme...']]) self.assertEqual(nodenames, [['matchme...']])
@ -93,19 +96,19 @@ class XmliterTestCase(unittest.TestCase):
attrs = [] attrs = []
for x in self.xmliter(r, u'þingflokkur'): for x in self.xmliter(r, u'þingflokkur'):
attrs.append((x.xpath('@id').extract(), attrs.append((x.attrib['id'],
x.xpath(u'./skammstafanir/stuttskammstöfun/text()').extract(), x.xpath(u'./skammstafanir/stuttskammstöfun/text()').getall(),
x.xpath(u'./tímabil/fyrstaþing/text()').extract())) x.xpath(u'./tímabil/fyrstaþing/text()').getall()))
self.assertEqual(attrs, self.assertEqual(attrs,
[([u'26'], [u'-'], [u'80']), [(u'26', [u'-'], [u'80']),
([u'21'], [u'Ab'], [u'76']), (u'21', [u'Ab'], [u'76']),
([u'27'], [u'A'], [u'27'])]) (u'27', [u'A'], [u'27'])])
def test_xmliter_text(self): def test_xmliter_text(self):
body = u"""<?xml version="1.0" encoding="UTF-8"?><products><product>one</product><product>two</product></products>""" body = u"""<?xml version="1.0" encoding="UTF-8"?><products><product>one</product><product>two</product></products>"""
self.assertEqual([x.xpath("text()").extract() for x in self.xmliter(body, 'product')], self.assertEqual([x.xpath("text()").getall() for x in self.xmliter(body, 'product')],
[[u'one'], [u'two']]) [[u'one'], [u'two']])
def test_xmliter_namespaces(self): def test_xmliter_namespaces(self):
@ -132,15 +135,15 @@ class XmliterTestCase(unittest.TestCase):
node = next(my_iter) node = next(my_iter)
node.register_namespace('g', 'http://base.google.com/ns/1.0') node.register_namespace('g', 'http://base.google.com/ns/1.0')
self.assertEqual(node.xpath('title/text()').extract(), ['Item 1']) self.assertEqual(node.xpath('title/text()').getall(), ['Item 1'])
self.assertEqual(node.xpath('description/text()').extract(), ['This is item 1']) self.assertEqual(node.xpath('description/text()').getall(), ['This is item 1'])
self.assertEqual(node.xpath('link/text()').extract(), ['http://www.mydummycompany.com/items/1']) self.assertEqual(node.xpath('link/text()').getall(), ['http://www.mydummycompany.com/items/1'])
self.assertEqual(node.xpath('g:image_link/text()').extract(), ['http://www.mydummycompany.com/images/item1.jpg']) self.assertEqual(node.xpath('g:image_link/text()').getall(), ['http://www.mydummycompany.com/images/item1.jpg'])
self.assertEqual(node.xpath('g:id/text()').extract(), ['ITEM_1']) self.assertEqual(node.xpath('g:id/text()').getall(), ['ITEM_1'])
self.assertEqual(node.xpath('g:price/text()').extract(), ['400']) self.assertEqual(node.xpath('g:price/text()').getall(), ['400'])
self.assertEqual(node.xpath('image_link/text()').extract(), []) self.assertEqual(node.xpath('image_link/text()').getall(), [])
self.assertEqual(node.xpath('id/text()').extract(), []) self.assertEqual(node.xpath('id/text()').getall(), [])
self.assertEqual(node.xpath('price/text()').extract(), []) self.assertEqual(node.xpath('price/text()').getall(), [])
def test_xmliter_exception(self): def test_xmliter_exception(self):
body = u"""<?xml version="1.0" encoding="UTF-8"?><products><product>one</product><product>two</product></products>""" body = u"""<?xml version="1.0" encoding="UTF-8"?><products><product>one</product><product>two</product></products>"""
@ -159,7 +162,7 @@ class XmliterTestCase(unittest.TestCase):
body = b'<?xml version="1.0" encoding="ISO-8859-9"?>\n<xml>\n <item>Some Turkish Characters \xd6\xc7\xde\xdd\xd0\xdc \xfc\xf0\xfd\xfe\xe7\xf6</item>\n</xml>\n\n' body = b'<?xml version="1.0" encoding="ISO-8859-9"?>\n<xml>\n <item>Some Turkish Characters \xd6\xc7\xde\xdd\xd0\xdc \xfc\xf0\xfd\xfe\xe7\xf6</item>\n</xml>\n\n'
response = XmlResponse('http://www.example.com', body=body) response = XmlResponse('http://www.example.com', body=body)
self.assertEqual( self.assertEqual(
next(self.xmliter(response, 'item')).extract(), next(self.xmliter(response, 'item')).get(),
u'<item>Some Turkish Characters \xd6\xc7\u015e\u0130\u011e\xdc \xfc\u011f\u0131\u015f\xe7\xf6</item>' u'<item>Some Turkish Characters \xd6\xc7\u015e\u0130\u011e\xdc \xfc\u011f\u0131\u015f\xe7\xf6</item>'
) )
@ -192,9 +195,9 @@ class LxmlXmliterTestCase(XmliterTestCase):
namespace_iter = self.xmliter(response, 'image_link', 'http://base.google.com/ns/1.0') namespace_iter = self.xmliter(response, 'image_link', 'http://base.google.com/ns/1.0')
node = next(namespace_iter) node = next(namespace_iter)
self.assertEqual(node.xpath('text()').extract(), ['http://www.mydummycompany.com/images/item1.jpg']) self.assertEqual(node.xpath('text()').getall(), ['http://www.mydummycompany.com/images/item1.jpg'])
node = next(namespace_iter) node = next(namespace_iter)
self.assertEqual(node.xpath('text()').extract(), ['http://www.mydummycompany.com/images/item2.jpg']) self.assertEqual(node.xpath('text()').getall(), ['http://www.mydummycompany.com/images/item2.jpg'])
def test_xmliter_namespaces_prefix(self): def test_xmliter_namespaces_prefix(self):
body = b"""\ body = b"""\
@ -219,14 +222,14 @@ class LxmlXmliterTestCase(XmliterTestCase):
my_iter = self.xmliter(response, 'table', 'http://www.w3.org/TR/html4/', 'h') my_iter = self.xmliter(response, 'table', 'http://www.w3.org/TR/html4/', 'h')
node = next(my_iter) node = next(my_iter)
self.assertEqual(len(node.xpath('h:tr/h:td').extract()), 2) self.assertEqual(len(node.xpath('h:tr/h:td').getall()), 2)
self.assertEqual(node.xpath('h:tr/h:td[1]/text()').extract(), ['Apples']) self.assertEqual(node.xpath('h:tr/h:td[1]/text()').getall(), ['Apples'])
self.assertEqual(node.xpath('h:tr/h:td[2]/text()').extract(), ['Bananas']) self.assertEqual(node.xpath('h:tr/h:td[2]/text()').getall(), ['Bananas'])
my_iter = self.xmliter(response, 'table', 'http://www.w3schools.com/furniture', 'f') my_iter = self.xmliter(response, 'table', 'http://www.w3schools.com/furniture', 'f')
node = next(my_iter) node = next(my_iter)
self.assertEqual(node.xpath('f:name/text()').extract(), ['African Coffee Table']) self.assertEqual(node.xpath('f:name/text()').getall(), ['African Coffee Table'])
def test_xmliter_objtype_exception(self): def test_xmliter_objtype_exception(self):
i = self.xmliter(42, 'product') i = self.xmliter(42, 'product')

View File

@ -51,6 +51,9 @@ deps =
cssselect==0.9.1 cssselect==0.9.1
zope.interface==4.1.1 zope.interface==4.1.1
-rtests/requirements-py2.txt -rtests/requirements-py2.txt
# Not used directly but allows boto GCE plugins to load.
# https://github.com/GoogleCloudPlatform/compute-image-packages/issues/262
google-compute-engine==2.8.12
[testenv:trunk] [testenv:trunk]
basepython = python2.7 basepython = python2.7