Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Taito Horiuchi 2017-02-27 09:36:03 +02:00
commit af23c920bf
85 changed files with 1217 additions and 352 deletions

View File

@ -1,29 +1,7 @@
[bumpversion]
current_version = 1.3.0
current_version = 1.3.2
commit = True
tag = True
tag_name = {new_version}
parse = ^
(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
(?:(?P<prerel>[abc]|rc|dev)(?P<prerelversion>\d+))?
serialize =
{major}.{minor}.{patch}{prerel}{prerelversion}
{major}.{minor}.{patch}
[bumpversion:file:scrapy/VERSION]
[bumpversion:part:prerel]
optional_value = gamma
values =
dev
rc
gamma
[bumpversion:part:prerelversion]
values =
1
2
3
4
5

2
.gitignore vendored
View File

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

View File

@ -1,19 +1,46 @@
language: python
python: 3.5
sudo: false
branches:
only:
- master
- /^\d\.\d+$/
- /^\d\.\d+\.\d+(rc\d+|dev\d+)?$/
env:
- TOXENV=py27
- TOXENV=jessie
- TOXENV=py33
- TOXENV=py35
- TOXENV=docs
- /^\d\.\d+\.\d+(rc\d+|\.dev\d+)?$/
matrix:
include:
- python: 2.7
env: TOXENV=py27
- python: 2.7
env: TOXENV=jessie
- python: 3.3
env: TOXENV=py33
- python: 3.5
env: TOXENV=py35
- python: 3.6
env: TOXENV=py36
- python: 2.7
env: TOXENV=pypy
- python: 3.6
env: TOXENV=docs
allow_failures:
- python: 2.7
env: TOXENV=pypy
install:
- pip install -U tox twine wheel codecov
- |
if [ "$TOXENV" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
pushd "$PYENV_ROOT" && git pull && popd
else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi
# get latest PyPy from pyenv directly (thanks to natural version sort option -V)
export PYPY_VERSION=`"$PYENV_ROOT/bin/pyenv" install --list |grep -o -E 'pypy-[0-9][\.0-9]*$' |sort -V |tail -1`
"$PYENV_ROOT/bin/pyenv" install --skip-existing "$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/$PYPY_VERSION/bin/python" "$HOME/virtualenvs/$PYPY_VERSION"
source "$HOME/virtualenvs/$PYPY_VERSION/bin/activate"
fi
- pip install -U tox twine wheel codecov
script: tox
after_success:
- codecov
@ -35,4 +62,4 @@ deploy:
on:
tags: true
repo: scrapy/scrapy
condition: "$TOXENV == py27 && $TRAVIS_TAG =~ ^[0-9]+[.][0-9]+[.][0-9]+(rc[0-9]+|dev[0-9]+)?$"
condition: "$TOXENV == py27 && $TRAVIS_TAG =~ ^[0-9]+[.][0-9]+[.][0-9]+(rc[0-9]+|[.]dev[0-9]+)?$"

View File

@ -13,10 +13,6 @@ Scrapy
.. image:: https://img.shields.io/badge/wheel-yes-brightgreen.svg
:target: https://pypi.python.org/pypi/Scrapy
:alt: Wheel Status
.. image:: http://static.scrapy.org/py3progress/badge.svg
:target: https://github.com/scrapy/scrapy/wiki/Python-3-Porting
:alt: Python 3 Porting Status
.. image:: https://img.shields.io/codecov/c/github/scrapy/scrapy/master.svg
:target: http://codecov.io/github/scrapy/scrapy?branch=master

View File

@ -1,3 +1,5 @@
:orphan:
Scrapy artwork
==============

View File

@ -1,3 +1,5 @@
:orphan:
======================================
Scrapy documentation quick start guide
======================================

View File

@ -40,8 +40,7 @@ http://quotes.toscrape.com, following the pagination::
next_page = response.css('li.next a::attr("href")').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
yield response.follow(next_page, self.parse)
Put this in a text file, name it to something like ``quotes_spider.py``

View File

@ -225,7 +225,7 @@ You will see something like::
[s] shelp() Shell help (print this help)
[s] fetch(req_or_url) Fetch request (or URL) and update local objects
[s] view(response) View response in a browser
>>>
>>>
Using the shell, you can try selecting elements using `CSS`_ with the response
object::
@ -399,7 +399,7 @@ quotes elements and put them together into a Python dictionary::
>>>
Extracting data in our spider
------------------------------
-----------------------------
Let's get back to our spider. Until now, it doesn't extract any data in
particular, just saves the whole HTML page to a local file. Let's integrate the
@ -423,7 +423,7 @@ in the callback, as you can see below::
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').extract_first(),
'author': quote.css('span small::text').extract_first(),
'author': quote.css('small.author::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
@ -522,7 +522,7 @@ page, extracting data from it::
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').extract_first(),
'author': quote.css('span small::text').extract_first(),
'author': quote.css('small.author::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
@ -551,13 +551,65 @@ In our example, it creates a sort of loop, following all the links to the next p
until it doesn't find one -- handy for crawling blogs, forums and other sites with
pagination.
.. _response-follow-example:
A shortcut for creating Requests
--------------------------------
As a shortcut for creating Request objects you can use
:meth:`response.follow <scrapy.http.TextResponse.follow>`::
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').extract_first(),
'author': quote.css('span small::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
Unlike scrapy.Request, ``response.follow`` supports relative URLs directly - no
need to call urljoin. Note that ``response.follow`` just returns a Request
instance; you still have to yield this Request.
You can also pass a selector to ``response.follow`` instead of a string;
this selector should extract necessary attributes::
for href in response.css('li.next a::attr(href)'):
yield response.follow(href, callback=self.parse)
For ``<a>`` elements there is a shortcut: ``response.follow`` uses their href
attribute automatically. So the code can be shortened further::
for a in response.css('li.next a'):
yield response.follow(a, callback=self.parse)
.. note::
``response.follow(response.css('li.next a'))`` is not valid because
``response.css`` returns a list-like object with selectors for all results,
not a single selector. A ``for`` loop like in the example above, or
``response.follow(response.css('li.next a')[0])`` is fine.
More examples and patterns
--------------------------
Here is another spider that illustrates callbacks and following links,
this time for scraping author information::
import scrapy
@ -568,15 +620,12 @@ this time for scraping author information::
def parse(self, response):
# follow links to author pages
for href in response.css('.author+a::attr(href)').extract():
yield scrapy.Request(response.urljoin(href),
callback=self.parse_author)
for href in response.css('.author + a::attr(href)'):
yield response.follow(href, self.parse_author)
# follow pagination links
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
for href in response.css('li.next a::attr(href)'):
yield response.follow(href, self.parse)
def parse_author(self, response):
def extract_with_css(query):
@ -592,6 +641,9 @@ This spider will start from the main page, it will follow all the links to the
authors pages calling the ``parse_author`` callback for each of them, and also
the pagination links with the ``parse`` callback as we saw before.
Here we're passing callbacks to ``response.follow`` as positional arguments
to make the code shorter; it also works for ``scrapy.Request``.
The ``parse_author`` callback defines a helper function to extract and cleanup the
data from a CSS query and yields the Python dict with the author data.
@ -624,7 +676,7 @@ option when running them::
scrapy crawl quotes -o quotes-humor.json -a tag=humor
These arguments are passed to the Spider's ``__init__`` method and become
spider attributes by default.
spider attributes by default.
In this example, the value provided for the ``tag`` argument will be available
via ``self.tag``. You can use this to make your spider fetch only quotes
@ -647,13 +699,12 @@ with a specific tag, building the URL based on the argument::
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').extract_first(),
'author': quote.css('span small a::text').extract_first(),
'author': quote.css('small.author::text').extract_first(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, self.parse)
yield response.follow(next_page, self.parse)
If you pass the ``tag=humor`` argument to this spider, you'll notice that it

View File

@ -3,6 +3,64 @@
Release notes
=============
Scrapy 1.3.2 (2017-02-13)
-------------------------
Bug fixes
~~~~~~~~~
- Preserve crequest class when converting to/from dicts (utils.reqser) (:issue:`2510`).
- Use consistent selectors for author field in tutorial (:issue:`2551`).
- Fix TLS compatibility in Twisted 17+ (:issue:`2558`)
Scrapy 1.3.1 (2017-02-08)
-------------------------
New features
~~~~~~~~~~~~
- Support ``'True'`` and ``'False'`` string values for boolean settings (:issue:`2519`);
you can now do something like ``scrapy crawl myspider -s REDIRECT_ENABLED=False``.
- Support kwargs with ``response.xpath()`` to use :ref:`XPath variables <topics-selectors-xpath-variables>`
and ad-hoc namespaces declarations ;
this requires at least Parsel v1.1 (:issue:`2457`).
- Add support for Python 3.6 (:issue:`2485`).
- Run tests on PyPy (warning: some tests still fail, so PyPy is not supported yet).
Bug fixes
~~~~~~~~~
- Enforce ``DNS_TIMEOUT`` setting (:issue:`2496`).
- Fix :command:`view` command ; it was a regression in v1.3.0 (:issue:`2503`).
- Fix tests regarding ``*_EXPIRES settings`` with Files/Images pipelines (:issue:`2460`).
- Fix name of generated pipeline class when using basic project template (:issue:`2466`).
- Fix compatiblity with Twisted 17+ (:issue:`2496`, :issue:`2528`).
- Fix ``scrapy.Item`` inheritance on Python 3.6 (:issue:`2511`).
- Enforce numeric values for components order in ``SPIDER_MIDDLEWARES``,
``DOWNLOADER_MIDDLEWARES``, ``EXTENIONS`` and ``SPIDER_CONTRACTS`` (:issue:`2420`).
Documentation
~~~~~~~~~~~~~
- Reword Code of Coduct section and upgrade to Contributor Covenant v1.4
(:issue:`2469`).
- Clarify that passing spider arguments converts them to spider attributes
(:issue:`2483`).
- Document ``formid`` argument on ``FormRequest.from_response()`` (:issue:`2497`).
- Add .rst extension to README files (:issue:`2507`).
- Mention LevelDB cache storage backend (:issue:`2525`).
- Use ``yield`` in sample callback code (:issue:`2533`).
- Add note about HTML entities decoding with ``.re()/.re_first()`` (:issue:`1704`).
- Typos (:issue:`2512`, :issue:`2534`, :issue:`2531`).
Cleanups
~~~~~~~~
- Remove reduntant check in ``MetaRefreshMiddleware`` (:issue:`2542`).
- Faster checks in ``LinkExtractor`` for allow/deny patterns (:issue:`2538`).
- Remove dead code supporting old Twisted versions (:issue:`2544`).
Scrapy 1.3.0 (2016-12-21)
-------------------------

View File

@ -358,6 +358,12 @@ Opens the given URL in a browser, as your Scrapy spider would "see" it.
Sometimes spiders see pages differently from regular users, so this can be used
to check what the spider "sees" and confirm it's what you expect.
Supported options:
* ``--spider=SPIDER``: bypass spider autodetection and force use of specific spider
* ``--no-redirect``: do not follow HTTP 3xx redirects (default is to follow them)
Usage example::
$ scrapy view http://www.example.com/some/page.html

View File

@ -318,10 +318,11 @@ HttpCacheMiddleware
This middleware provides low-level cache to all HTTP requests and responses.
It has to be combined with a cache storage backend as well as a cache policy.
Scrapy ships with two HTTP cache storage backends:
Scrapy ships with three HTTP cache storage backends:
* :ref:`httpcache-storage-fs`
* :ref:`httpcache-storage-dbm`
* :ref:`httpcache-storage-leveldb`
You can change the HTTP cache storage backend with the :setting:`HTTPCACHE_STORAGE`
setting. Or you can also implement your own storage backend.
@ -680,7 +681,9 @@ HttpProxyMiddleware
* ``no_proxy``
You can also set the meta key ``proxy`` per-request, to a value like
``http://some_proxy_server:port``.
``http://some_proxy_server:port`` or ``http://username:password@some_proxy_server:port``.
Keep in mind this value will take precedence over ``http_proxy``/``https_proxy``
environment variables, and it will also ignore ``no_proxy`` environment variable.
.. _urllib: https://docs.python.org/2/library/urllib.html
.. _urllib2: https://docs.python.org/2/library/urllib2.html
@ -748,7 +751,7 @@ REDIRECT_MAX_TIMES
Default: ``20``
The maximum number of redirections that will be follow for a single request.
The maximum number of redirections that will be followed for a single request.
MetaRefreshMiddleware
---------------------
@ -948,8 +951,16 @@ enable it for :ref:`broad crawls <topics-broad-crawls>`.
HttpProxyMiddleware settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. setting:: HTTPPROXY_ENABLED
.. setting:: HTTPPROXY_AUTH_ENCODING
HTTPPROXY_ENABLED
^^^^^^^^^^^^^^^^^
Default: ``True``
Whether or not to enable the :class:`HttpProxyMiddleware`.
HTTPPROXY_AUTH_ENCODING
^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -225,7 +225,8 @@ XmlItemExporter
Exports Items in XML format to the specified file object.
:param file: the file-like object to use for exporting the data.
:param file: the file-like object to use for exporting the data. Its ``write`` method should
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
:param root_element: The name of root element in the exported XML.
:type root_element: str
@ -281,7 +282,8 @@ CsvItemExporter
CSV columns and their order. The :attr:`export_empty_fields` attribute has
no effect on this exporter.
:param file: the file-like object to use for exporting the data.
:param file: the file-like object to use for exporting the data. Its ``write`` method should
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
:param include_headers_line: If enabled, makes the exporter output a header
line with the field names taken from
@ -312,7 +314,8 @@ PickleItemExporter
Exports Items in pickle format to the given file-like object.
:param file: the file-like object to use for exporting the data.
:param file: the file-like object to use for exporting the data. Its ``write`` method should
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
:param protocol: The pickle protocol to use.
:type protocol: int
@ -333,7 +336,8 @@ PprintItemExporter
Exports Items in pretty print format to the specified file object.
:param file: the file-like object to use for exporting the data.
:param file: the file-like object to use for exporting the data. Its ``write`` method should
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
The additional keyword arguments of this constructor are passed to the
:class:`BaseItemExporter` constructor.
@ -356,7 +360,8 @@ JsonItemExporter
arguments to the `JSONEncoder`_ constructor, so you can use any
`JSONEncoder`_ constructor argument to customize this exporter.
:param file: the file-like object to use for exporting the data.
:param file: the file-like object to use for exporting the data. Its ``write`` method should
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
A typical output of this exporter would be::
@ -386,7 +391,8 @@ JsonLinesItemExporter
the `JSONEncoder`_ constructor, so you can use any `JSONEncoder`_
constructor argument to customize this exporter.
:param file: the file-like object to use for exporting the data.
:param file: the file-like object to use for exporting the data. Its ``write`` method should
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
A typical output of this exporter would be::

View File

@ -51,7 +51,7 @@ LxmlLinkExtractor
:synopsis: lxml's HTMLParser-based link extractors
.. class:: LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, process_value=None)
.. class:: LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True, process_value=None, strip=True)
LxmlLinkExtractor is the recommended link extractor with handy filtering
options. It is implemented using lxml's robust HTMLParser.
@ -132,4 +132,13 @@ LxmlLinkExtractor
:type process_value: callable
:param strip: whether to strip whitespaces from extracted attributes.
According to HTML5 standard, leading and trailing whitespaces
must be stripped from ``href`` attributes of ``<a>``, ``<area>``
and many other elements, ``src`` attribute of ``<img>``, ``<iframe>``
elements, etc., so LinkExtractor strips space chars by default.
Set ``strip=False`` to turn it off (e.g. if you're extracting urls
from elements or attributes which allow leading/trailing whitespaces).
:type strip: boolean
.. _scrapy.linkextractors: https://github.com/scrapy/scrapy/blob/master/scrapy/linkextractors/__init__.py

View File

@ -121,6 +121,9 @@ Request objects
see :ref:`topics-request-response-ref-errbacks` below.
:type errback: callable
:param flags: Flags sent to the request, can be used for logging or similar purposes.
:type flags: list
.. attribute:: Request.url
A string containing the URL of this request. Keep in mind that this
@ -207,12 +210,12 @@ different fields from different pages::
request = scrapy.Request("http://www.example.com/some_page.html",
callback=self.parse_page2)
request.meta['item'] = item
return request
yield request
def parse_page2(self, response):
item = response.meta['item']
item['other_url'] = response.url
return item
yield item
.. _topics-request-response-ref-errbacks:
@ -358,7 +361,7 @@ fields with form data from :class:`Response` objects.
The :class:`FormRequest` objects support the following class method in
addition to the standard :class:`Request` methods:
.. classmethod:: FormRequest.from_response(response, [formname=None, formnumber=0, formdata=None, formxpath=None, formcss=None, clickdata=None, dont_click=False, ...])
.. classmethod:: FormRequest.from_response(response, [formname=None, formid=None, formnumber=0, formdata=None, formxpath=None, formcss=None, clickdata=None, dont_click=False, ...])
Returns a new :class:`FormRequest` object with its form field values
pre-populated with those found in the HTML ``<form>`` element contained
@ -376,6 +379,10 @@ fields with form data from :class:`Response` objects.
control clicked (instead of disabling it) you can also use the
``clickdata`` argument.
.. caution:: Using this method with select elements which have leading
or trailing whitespace in the option values will not work due to a
`bug in lxml`_, which should be fixed in lxml 3.8 and above.
:param response: the response containing a HTML form which will be used
to pre-populate the form fields
:type response: :class:`Response` object
@ -383,6 +390,9 @@ fields with form data from :class:`Response` objects.
:param formname: if given, the form with name attribute set to this value will be used.
:type formname: string
:param formid: if given, the form with id attribute set to this value will be used.
:type formid: string
:param formxpath: if given, the first form that matches the xpath will be used.
:type formxpath: string
@ -421,6 +431,9 @@ fields with form data from :class:`Response` objects.
.. versionadded:: 1.1.0
The ``formcss`` parameter.
.. versionadded:: 1.1.0
The ``formid`` parameter.
Request usage examples
----------------------
@ -591,6 +604,9 @@ Response objects
urlparse.urljoin(response.url, url)
.. automethod:: Response.follow
.. _urlparse.urljoin: https://docs.python.org/2/library/urlparse.html#urlparse.urljoin
.. _topics-request-response-ref-response-subclasses:
@ -677,6 +693,8 @@ TextResponse objects
response.css('p')
.. automethod:: TextResponse.follow
.. method:: TextResponse.body_as_unicode()
The same as :attr:`text`, but available as a method. This method is
@ -704,3 +722,4 @@ XmlResponse objects
line. See :attr:`TextResponse.encoding`.
.. _Twisted Failure: https://twistedmatrix.com/documents/current/api/twisted.python.failure.Failure.html
.. _bug in lxml: https://bugs.launchpad.net/lxml/+bug/1665241

View File

@ -283,6 +283,40 @@ XPath specification.
.. _Location Paths: https://www.w3.org/TR/xpath#location-paths
.. _topics-selectors-xpath-variables:
Variables in XPath expressions
------------------------------
XPath allows you to reference variables in your XPath expressions, using
the ``$somevariable`` syntax. This is somewhat similar to parameterized
queries or prepared statements in the SQL world where you replace
some arguments in your queries with placeholders like ``?``,
which are then substituted with values passed with the query.
Here's an example to match an element based on its "id" attribute value,
without hard-coding it (that was shown previously)::
>>> # `$val` used in the expression, a `val` argument needs to be passed
>>> response.xpath('//div[@id=$val]/a/text()', val='images').extract_first()
u'Name: My image 1 '
Here's another example, to find the "id" attribute of a ``<div>`` tag containing
five ``<a>`` children (here we pass the value ``5`` as an integer)::
>>> response.xpath('//div[count(a)=$cnt]/@id', cnt=5).extract_first()
u'images'
All variable references must have a binding value when calling ``.xpath()``
(otherwise you'll get a ``ValueError: XPath error:`` exception).
This is done by passing as many named arguments as necessary.
`parsel`_, the library powering Scrapy selectors, has more details and examples
on `XPath variables`_.
.. _parsel: https://parsel.readthedocs.io/
.. _XPath variables: https://parsel.readthedocs.io/en/latest/usage.html#variables-in-xpath-expressions
Using EXSLT extensions
----------------------
@ -626,6 +660,10 @@ Built-in Selectors reference
``regex`` can be either a compiled regular expression or a string which
will be compiled to a regular expression using ``re.compile(regex)``
.. note::
Note that ``re()`` and ``re_first()`` both decode HTML entities (except ``&lt;`` and ``&amp;``).
.. method:: register_namespace(prefix, uri)
Register the given namespace to be used in this :class:`Selector`.

View File

@ -686,6 +686,42 @@ The Feed Temp dir allows you to set a custom folder to save crawler
temporary files before uploading with :ref:`FTP feed storage <topics-feed-storage-ftp>` and
:ref:`Amazon S3 <topics-feed-storage-s3>`.
.. setting:: FTP_PASSIVE_MODE
FTP_PASSIVE_MODE
----------------
Default: ``True``
Whether or not to use passive mode when initiating FTP transfers.
.. setting:: FTP_PASSWORD
FTP_PASSWORD
------------
Default: ``"guest"``
The password to use for FTP connections when there is no ``"ftp_password"``
in ``Request`` meta.
.. note::
Paraphrasing `RFC 1635`_, although it is common to use either the password
"guest" or one's e-mail address for anonymous FTP,
some FTP servers explicitly ask for the user's e-mail address
and will not allow login with the "guest" password.
.. _RFC 1635: https://tools.ietf.org/html/rfc1635
.. setting:: FTP_USER
FTP_USER
--------
Default: ``"anonymous"``
The username to use for FTP connections when there is no ``"ftp_user"``
in ``Request`` meta.
.. setting:: ITEM_PIPELINES
@ -827,13 +863,15 @@ Example::
MEMUSAGE_ENABLED
----------------
Default: ``False``
Default: ``True``
Scope: ``scrapy.extensions.memusage``
Whether to enable the memory usage extension that will shutdown the Scrapy
process when it exceeds a memory limit, and also notify by email when that
happened.
Whether to enable the memory usage extension. This extension keeps track of
a peak memory used by the process (it writes it to stats). It can also
optionally shutdown the Scrapy process when it exceeds a memory limit
(see :setting:`MEMUSAGE_LIMIT_MB`), and notify by email when that happened
(see :setting:`MEMUSAGE_NOTIFY_MAIL`).
See :ref:`topics-extensions-ref-memusage`.

View File

@ -112,7 +112,7 @@ following methods:
.. method:: process_spider_exception(response, exception, spider)
This method is called when when a spider or :meth:`process_spider_input`
This method is called when a spider or :meth:`process_spider_input`
method (from other spider middleware) raises an exception.
:meth:`process_spider_exception` should return either ``None`` or an

View File

@ -144,16 +144,12 @@ scrapy.Spider
.. method:: start_requests()
This method must return an iterable with the first Requests to crawl for
this spider.
this spider. It is called by Scrapy when the spider is opened for
scraping. Scrapy calls it only once, so it is safe to implement
:meth:`start_requests` as a generator.
This is the method called by Scrapy when the spider is opened for
scraping when no particular URLs are specified. If particular URLs are
specified, the :meth:`make_requests_from_url` is used instead to create
the Requests. This method is also called only once from Scrapy, so it's
safe to implement it as a generator.
The default implementation uses :meth:`make_requests_from_url` to
generate Requests for each url in :attr:`start_urls`.
The default implementation generates ``Request(url, dont_filter=True)``
for each url in :attr:`start_urls`.
If you want to change the Requests used to start scraping a domain, this is
the method to override. For example, if you need to start by logging in using
@ -172,18 +168,6 @@ scrapy.Spider
# each of them, with another callback
pass
.. method:: make_requests_from_url(url)
A method that receives a URL and returns a :class:`~scrapy.http.Request`
object (or a list of :class:`~scrapy.http.Request` objects) to scrape. This
method is used to construct the initial requests in the
:meth:`start_requests` method, and is typically used to convert urls to
requests.
Unless overridden, this method returns Requests with the :meth:`parse`
method as their callback function, and with dont_filter parameter enabled
(see :class:`~scrapy.http.Request` class for more info).
.. method:: parse(response)
This is the default callback used by Scrapy to process downloaded

View File

@ -28,16 +28,16 @@ Query Scrapy settings
Print raw setting value
.TP
.I --getbool=SETTING
Print setting value, intepreted as a boolean
Print setting value, interpreted as a boolean
.TP
.I --getint=SETTING
Print setting value, intepreted as an integer
Print setting value, interpreted as an integer
.TP
.I --getfloat=SETTING
Print setting value, intepreted as an float
Print setting value, interpreted as a float
.TP
.I --getlist=SETTING
Print setting value, intepreted as an float
Print setting value, interpreted as a float
.TP
.I --init
Print initial setting value (before loading extensions and spiders)

View File

@ -3,5 +3,5 @@ lxml>=3.2.4
pyOpenSSL>=0.13.1
cssselect>=0.9
queuelib>=1.1.1
w3lib>=1.14.2
w3lib>=1.17.0
service_identity

View File

@ -2,9 +2,9 @@ Twisted>=13.1.0
lxml
pyOpenSSL
cssselect>=0.9
w3lib>=1.15.0
w3lib>=1.17.0
queuelib
six>=1.5.2
PyDispatcher>=2.0.5
service_identity
parsel>=0.9.5
parsel>=1.1

View File

@ -1 +1 @@
1.3.0
1.3.2

View File

@ -11,9 +11,8 @@ class Command(fetch.Command):
"contents in a browser"
def add_options(self, parser):
ScrapyCommand.add_options(self, parser)
parser.add_option("--spider", dest="spider",
help="use this spider")
super(Command, self).add_options(parser)
parser.remove_option("--headers")
def _print_response(self, response, opts):
open_in_browser(response)

View File

@ -1,15 +1,15 @@
from OpenSSL import SSL
from twisted.internet.ssl import ClientContextFactory
try:
from scrapy import twisted_version
if twisted_version >= (14, 0, 0):
from zope.interface.declarations import implementer
# the following should be available from Twisted 14.0.0
from twisted.internet.ssl import (optionsForClientTLS,
CertificateOptions,
platformTrust)
from twisted.web.client import BrowserLikePolicyForHTTPS
from twisted.web.iweb import IPolicyForHTTPS
@ -86,7 +86,7 @@ try:
'method': self._ssl_method,
})
except ImportError:
else:
class ScrapyClientContextFactory(ClientContextFactory):
"A SSL context factory which is more permissive against SSL bugs."

View File

@ -30,7 +30,7 @@ In case of status 200 request, response.headers will come with two keys:
import re
from io import BytesIO
from six.moves.urllib.parse import urlparse, unquote
from six.moves.urllib.parse import unquote
from twisted.internet import reactor
from twisted.protocols.ftp import FTPClient, CommandFailed
@ -38,6 +38,8 @@ from twisted.internet.protocol import Protocol, ClientCreator
from scrapy.http import Response
from scrapy.responsetypes import responsetypes
from scrapy.utils.httpobj import urlparse_cached
class ReceivedDataProtocol(Protocol):
def __init__(self, filename=None):
@ -64,14 +66,19 @@ class FTPDownloadHandler(object):
"default": 503,
}
def __init__(self, setting):
pass
def __init__(self, settings):
self.default_user = settings['FTP_USER']
self.default_password = settings['FTP_PASSWORD']
self.passive_mode = settings['FTP_PASSIVE_MODE']
def download_request(self, request, spider):
parsed_url = urlparse(request.url)
creator = ClientCreator(reactor, FTPClient, request.meta["ftp_user"],
request.meta["ftp_password"],
passive=request.meta.get("ftp_passive", 1))
parsed_url = urlparse_cached(request)
user = request.meta.get("ftp_user", self.default_user)
password = request.meta.get("ftp_password", self.default_password)
passive_mode = 1 if bool(request.meta.get("ftp_passive",
self.passive_mode)) else 0
creator = ClientCreator(reactor, FTPClient, user, password,
passive=passive_mode)
return creator.connectTCP(parsed_url.hostname, parsed_url.port or 21).addCallback(self.gotClient,
request, unquote(parsed_url.path))

View File

@ -1,10 +1,6 @@
from scrapy import twisted_version
from __future__ import absolute_import
from .http10 import HTTP10DownloadHandler
if twisted_version >= (11, 1, 0):
from .http11 import HTTP11DownloadHandler as HTTPDownloadHandler
else:
HTTPDownloadHandler = HTTP10DownloadHandler
from .http11 import HTTP11DownloadHandler as HTTPDownloadHandler
# backwards compatibility

View File

@ -105,6 +105,7 @@ class TunnelingTCP4ClientEndpoint(TCP4ClientEndpoint):
self._tunneledHost = host
self._tunneledPort = port
self._contextFactory = contextFactory
self._connectBuffer = bytearray()
def requestTunnel(self, protocol):
"""Asks the proxy to open a tunnel."""
@ -121,8 +122,16 @@ class TunnelingTCP4ClientEndpoint(TCP4ClientEndpoint):
created, notifies the client that we are ready to send requests. If not
raises a TunnelError.
"""
self._connectBuffer += rcvd_bytes
# make sure that enough (all) bytes are consumed
# and that we've got all HTTP headers (ending with a blank line)
# from the proxy so that we don't send those bytes to the TLS layer
#
# see https://github.com/scrapy/scrapy/issues/2491
if b'\r\n\r\n' not in self._connectBuffer:
return
self._protocol.dataReceived = self._protocolDataReceived
respm = TunnelingTCP4ClientEndpoint._responseMatcher.match(rcvd_bytes)
respm = TunnelingTCP4ClientEndpoint._responseMatcher.match(self._connectBuffer)
if respm and int(respm.group('status')) == 200:
try:
# this sets proper Server Name Indication extension

View File

@ -1,6 +1,8 @@
import logging
from OpenSSL import SSL
from scrapy import twisted_version
logger = logging.getLogger(__name__)
@ -18,11 +20,17 @@ openssl_methods = {
METHOD_TLSv12: getattr(SSL, 'TLSv1_2_METHOD', 6), # TLS 1.2 only
}
# ClientTLSOptions requires a recent-enough version of Twisted
try:
if twisted_version >= (14, 0, 0):
# ClientTLSOptions requires a recent-enough version of Twisted.
# Not having ScrapyClientTLSOptions should not matter for older
# Twisted versions because it is not used in the fallback
# ScrapyClientContextFactory.
# taken from twisted/twisted/internet/_sslverify.py
try:
# XXX: this try-except is not needed in Twisted 17.0.0+ because
# it requires pyOpenSSL 0.16+.
from OpenSSL.SSL import SSL_CB_HANDSHAKE_DONE, SSL_CB_HANDSHAKE_START
except ImportError:
SSL_CB_HANDSHAKE_START = 0x10
@ -30,10 +38,17 @@ try:
from twisted.internet.ssl import AcceptableCiphers
from twisted.internet._sslverify import (ClientTLSOptions,
_maybeSetHostNameIndication,
verifyHostname,
VerificationError)
if twisted_version < (17, 0, 0):
from twisted.internet._sslverify import _maybeSetHostNameIndication
set_tlsext_host_name = _maybeSetHostNameIndication
else:
def set_tlsext_host_name(connection, hostNameBytes):
connection.set_tlsext_host_name(hostNameBytes)
class ScrapyClientTLSOptions(ClientTLSOptions):
"""
SSL Client connection creator ignoring certificate verification errors
@ -46,7 +61,7 @@ try:
def _identityVerifyingInfoCallback(self, connection, where, ret):
if where & SSL_CB_HANDSHAKE_START:
_maybeSetHostNameIndication(connection, self._hostnameBytes)
set_tlsext_host_name(connection, self._hostnameBytes)
elif where & SSL_CB_HANDSHAKE_DONE:
try:
verifyHostname(connection, self._hostnameASCII)
@ -62,8 +77,3 @@ try:
self._hostnameASCII, repr(e)))
DEFAULT_CIPHERS = AcceptableCiphers.fromOpenSSLCipherString('DEFAULT')
except ImportError:
# ImportError should not matter for older Twisted versions
# as the above is not used in the fallback ScrapyClientContextFactory
pass

View File

@ -6,10 +6,18 @@ from scrapy.responsetypes import responsetypes
from scrapy.exceptions import NotConfigured
ACCEPTED_ENCODINGS = [b'gzip', b'deflate']
try:
import brotli
ACCEPTED_ENCODINGS.append(b'br')
except ImportError:
pass
class HttpCompressionMiddleware(object):
"""This middleware allows compressed (gzip, deflate) traffic to be
sent/received from web sites"""
@classmethod
def from_crawler(cls, crawler):
if not crawler.settings.getbool('COMPRESSION_ENABLED'):
@ -17,7 +25,8 @@ class HttpCompressionMiddleware(object):
return cls()
def process_request(self, request, spider):
request.headers.setdefault('Accept-Encoding', 'gzip,deflate')
request.headers.setdefault('Accept-Encoding',
b",".join(ACCEPTED_ENCODINGS))
def process_response(self, request, response, spider):
@ -55,5 +64,6 @@ class HttpCompressionMiddleware(object):
# http://www.port80software.com/200ok/archive/2005/10/31/868.aspx
# http://www.gzip.org/zlib/zlib_faq.html#faq38
body = zlib.decompress(body, -15)
if encoding == b'br' and b'br' in ACCEPTED_ENCODINGS:
body = brotli.decompress(body)
return body

View File

@ -20,23 +20,25 @@ class HttpProxyMiddleware(object):
for type, url in getproxies().items():
self.proxies[type] = self._get_proxy(url, type)
if not self.proxies:
raise NotConfigured
@classmethod
def from_crawler(cls, crawler):
if not crawler.settings.getbool('HTTPPROXY_ENABLED'):
raise NotConfigured
auth_encoding = crawler.settings.get('HTTPPROXY_AUTH_ENCODING')
return cls(auth_encoding)
def _basic_auth_header(self, username, password):
user_pass = to_bytes(
'%s:%s' % (unquote(username), unquote(password)),
encoding=self.auth_encoding)
return base64.b64encode(user_pass).strip()
def _get_proxy(self, url, orig_type):
proxy_type, user, password, hostport = _parse_proxy(url)
proxy_url = urlunparse((proxy_type or orig_type, hostport, '', '', '', ''))
if user:
user_pass = to_bytes(
'%s:%s' % (unquote(user), unquote(password)),
encoding=self.auth_encoding)
creds = base64.b64encode(user_pass).strip()
creds = self._basic_auth_header(user, password)
else:
creds = None
@ -45,6 +47,15 @@ class HttpProxyMiddleware(object):
def process_request(self, request, spider):
# ignore if proxy is already set
if 'proxy' in request.meta:
if request.meta['proxy'] is None:
return
# extract credentials if present
creds, proxy_url = self._get_proxy(request.meta['proxy'], '')
request.meta['proxy'] = proxy_url
if creds and not request.headers.get('Proxy-Authorization'):
request.headers['Proxy-Authorization'] = b'Basic ' + creds
return
elif not self.proxies:
return
parsed = urlparse_cached(request)

View File

@ -53,8 +53,10 @@ class BaseRedirectMiddleware(object):
class RedirectMiddleware(BaseRedirectMiddleware):
"""Handle redirection of requests based on response status and meta-refresh html tag"""
"""
Handle redirection of requests based on response status
and meta-refresh html tag.
"""
def process_response(self, request, response, spider):
if (request.meta.get('dont_redirect', False) or
response.status in getattr(spider, 'handle_httpstatus_list', []) or
@ -92,10 +94,9 @@ class MetaRefreshMiddleware(BaseRedirectMiddleware):
not isinstance(response, HtmlResponse):
return response
if isinstance(response, HtmlResponse):
interval, url = get_meta_refresh(response)
if url and interval < self._maxdelay:
redirected = self._redirect_request_using_get(request, url)
return self._redirect(redirected, request, spider, 'meta refresh')
interval, url = get_meta_refresh(response)
if url and interval < self._maxdelay:
redirected = self._redirect_request_using_get(request, url)
return self._redirect(redirected, request, spider, 'meta refresh')
return response

View File

@ -18,7 +18,7 @@ class Request(object_ref):
def __init__(self, url, callback=None, method='GET', headers=None, body=None,
cookies=None, meta=None, encoding='utf-8', priority=0,
dont_filter=False, errback=None):
dont_filter=False, errback=None, flags=None):
self._encoding = encoding # this one has to be set first
self.method = str(method).upper()
@ -36,6 +36,7 @@ class Request(object_ref):
self.dont_filter = dont_filter
self._meta = dict(meta) if meta else None
self.flags = [] if flags is None else list(flags)
@property
def meta(self):

View File

@ -5,10 +5,13 @@ This module implements the FormRequest class which is a more convenient class
See documentation in docs/topics/request-response.rst
"""
import six
from six.moves.urllib.parse import urljoin, urlencode
import lxml.html
from parsel.selector import create_root_node
import six
from w3lib.html import strip_html5_whitespace
from scrapy.http.request import Request
from scrapy.utils.python import to_bytes, is_listlike
from scrapy.utils.response import get_base_url
@ -51,7 +54,10 @@ class FormRequest(Request):
def _get_form_url(form, url):
if url is None:
return urljoin(form.base_url, form.action)
action = form.get('action')
if action is None:
return form.base_url
return urljoin(form.base_url, strip_html5_whitespace(action))
return urljoin(form.base_url, url)

View File

@ -6,7 +6,9 @@ See documentation in docs/topics/request-response.rst
"""
from six.moves.urllib.parse import urljoin
from scrapy.http.request import Request
from scrapy.http.headers import Headers
from scrapy.link import Link
from scrapy.utils.trackref import object_ref
from scrapy.http.common import obsolete_setter
from scrapy.exceptions import NotSupported
@ -101,3 +103,31 @@ class Response(object_ref):
is text (subclasses of TextResponse).
"""
raise NotSupported("Response content isn't text")
def follow(self, url, callback=None, method='GET', headers=None, body=None,
cookies=None, meta=None, encoding='utf-8', priority=0,
dont_filter=False, errback=None):
# type: (...) -> Request
"""
Return a :class:`~.Request` instance to follow a link ``url``.
It accepts the same arguments as ``Request.__init__`` method,
but ``url`` can be a relative URL or a ``scrapy.link.Link`` object,
not only an absolute URL.
:class:`~.TextResponse` provides a :meth:`~.TextResponse.follow`
method which supports selectors in addition to absolute/relative URLs
and Link objects.
"""
if isinstance(url, Link):
url = url.url
url = self.urljoin(url)
return Request(url, callback,
method=method,
headers=headers,
body=body,
cookies=cookies,
meta=meta,
encoding=encoding,
priority=priority,
dont_filter=dont_filter,
errback=errback)

View File

@ -8,8 +8,12 @@ See documentation in docs/topics/request-response.rst
import six
from six.moves.urllib.parse import urljoin
import parsel
from w3lib.encoding import html_to_unicode, resolve_encoding, \
html_body_declared_encoding, http_content_type_encoding
from w3lib.html import strip_html5_whitespace
from scrapy.http.request import Request
from scrapy.http.response import Response
from scrapy.utils.response import get_base_url
from scrapy.utils.python import memoizemethod_noargs, to_native_str
@ -111,8 +115,60 @@ class TextResponse(Response):
self._cached_selector = Selector(self)
return self._cached_selector
def xpath(self, query):
return self.selector.xpath(query)
def xpath(self, query, **kwargs):
return self.selector.xpath(query, **kwargs)
def css(self, query):
return self.selector.css(query)
def follow(self, url, callback=None, method='GET', headers=None, body=None,
cookies=None, meta=None, encoding=None, priority=0,
dont_filter=False, errback=None):
# type: (...) -> Request
"""
Return a :class:`~.Request` instance to follow a link ``url``.
It accepts the same arguments as ``Request.__init__`` method,
but ``url`` can be not only an absolute URL, but also
* a relative URL;
* a scrapy.link.Link object (e.g. a link extractor result);
* an attribute Selector (not SelectorList) - e.g.
``response.css('a::attr(href)')[0]`` or
``response.xpath('//img/@src')[0]``.
* a Selector for ``<a>`` element, e.g.
``response.css('a.my_link')[0]``.
See :ref:`response-follow-example` for usage examples.
"""
if isinstance(url, parsel.Selector):
url = _url_from_selector(url)
elif isinstance(url, parsel.SelectorList):
raise ValueError("SelectorList is not supported")
encoding = self.encoding if encoding is None else encoding
return super(TextResponse, self).follow(url, callback,
method=method,
headers=headers,
body=body,
cookies=cookies,
meta=meta,
encoding=encoding,
priority=priority,
dont_filter=dont_filter,
errback=errback
)
def _url_from_selector(sel):
# type: (parsel.Selector) -> str
if isinstance(sel.root, six.string_types):
# e.g. ::attr(href) result
return strip_html5_whitespace(sel.root)
if not hasattr(sel.root, 'tag'):
raise ValueError("Unsupported selector: %s" % sel)
if sel.root.tag != 'a':
raise ValueError("Only <a> elements are supported; got <%s>" %
sel.root.tag)
href = sel.root.get('href')
if href is None:
raise ValueError("<a> element has no href attribute: %s" % sel)
return strip_html5_whitespace(href)

View File

@ -25,6 +25,7 @@ class Field(dict):
class ItemMeta(ABCMeta):
def __new__(mcs, class_name, bases, attrs):
classcell = attrs.pop('__classcell__', None)
new_bases = tuple(base._class for base in bases if hasattr(base, '_class'))
_class = super(ItemMeta, mcs).__new__(mcs, 'x_' + class_name, new_bases, attrs)
@ -39,6 +40,8 @@ class ItemMeta(ABCMeta):
new_attrs['fields'] = fields
new_attrs['_class'] = _class
if classcell is not None:
new_attrs['__classcell__'] = classcell
return super(ItemMeta, mcs).__new__(mcs, class_name, bases, new_attrs)

View File

@ -40,7 +40,7 @@ IGNORED_EXTENSIONS = [
_re_type = type(re.compile("", 0))
_matches = lambda url, regexs: any((r.search(url) for r in regexs))
_matches = lambda url, regexs: any(r.search(url) for r in regexs)
_is_valid_url = lambda url: url.split('://', 1)[0] in {'http', 'https', 'file'}
@ -93,8 +93,8 @@ class FilteringLinkExtractor(object):
if self.deny_domains and url_is_from_any_domain(url, self.deny_domains):
return False
allowed = [regex.search(url) for regex in self.allow_res] if self.allow_res else [True]
denied = [regex.search(url) for regex in self.deny_res] if self.deny_res else []
allowed = (regex.search(url) for regex in self.allow_res) if self.allow_res else [True]
denied = (regex.search(url) for regex in self.deny_res) if self.deny_res else []
return any(allowed) and not any(denied)
def _process_links(self, links):

View File

@ -1,13 +1,13 @@
"""
HTMLParser-based link extractor
"""
import warnings
import six
from six.moves.html_parser import HTMLParser
from six.moves.urllib.parse import urljoin
from w3lib.url import safe_url_string
from w3lib.html import strip_html5_whitespace
from scrapy.link import Link
from scrapy.utils.python import unique as unique_list
@ -16,7 +16,8 @@ from scrapy.exceptions import ScrapyDeprecationWarning
class HtmlParserLinkExtractor(HTMLParser):
def __init__(self, tag="a", attr="href", process=None, unique=False):
def __init__(self, tag="a", attr="href", process=None, unique=False,
strip=True):
HTMLParser.__init__(self)
warnings.warn(
@ -29,6 +30,7 @@ class HtmlParserLinkExtractor(HTMLParser):
self.scan_attr = attr if callable(attr) else lambda a: a == attr
self.process_attr = process if callable(process) else lambda v: v
self.unique = unique
self.strip = strip
def _extract_links(self, response_text, response_url, response_encoding):
self.reset()
@ -69,6 +71,8 @@ class HtmlParserLinkExtractor(HTMLParser):
if self.scan_tag(tag):
for attr, value in attrs:
if self.scan_attr(attr):
if self.strip:
value = strip_html5_whitespace(value)
url = self.process_attr(value)
link = Link(url=url)
self.links.append(link)

View File

@ -2,15 +2,16 @@
Link extractor based on lxml.html
"""
import six
from six.moves.urllib.parse import urlparse, urljoin
from six.moves.urllib.parse import urljoin
import lxml.etree as etree
from w3lib.html import strip_html5_whitespace
from scrapy.link import Link
from scrapy.utils.misc import arg_to_iter, rel_has_nofollow
from scrapy.utils.python import unique as unique_list, to_native_str
from scrapy.linkextractors import FilteringLinkExtractor
from scrapy.utils.response import get_base_url
from scrapy.linkextractors import FilteringLinkExtractor
# from lxml/src/lxml/html/__init__.py
@ -27,11 +28,13 @@ def _nons(tag):
class LxmlParserLinkExtractor(object):
def __init__(self, tag="a", attr="href", process=None, unique=False):
def __init__(self, tag="a", attr="href", process=None, unique=False,
strip=True):
self.scan_tag = tag if callable(tag) else lambda t: t == tag
self.scan_attr = attr if callable(attr) else lambda a: a == attr
self.process_attr = process if callable(process) else lambda v: v
self.unique = unique
self.strip = strip
def _iter_links(self, document):
for el in document.iter(etree.Element):
@ -49,9 +52,11 @@ class LxmlParserLinkExtractor(object):
for el, attr, attr_val in self._iter_links(selector.root):
# pseudo lxml.html.HtmlElement.make_links_absolute(base_url)
try:
if self.strip:
attr_val = strip_html5_whitespace(attr_val)
attr_val = urljoin(base_url, attr_val)
except ValueError:
continue # skipping bogus links
continue # skipping bogus links
else:
url = self.process_attr(attr_val)
if url is None:
@ -85,12 +90,13 @@ class LxmlLinkExtractor(FilteringLinkExtractor):
def __init__(self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(),
tags=('a', 'area'), attrs=('href',), canonicalize=True,
unique=True, process_value=None, deny_extensions=None, restrict_css=()):
unique=True, process_value=None, deny_extensions=None, restrict_css=(),
strip=True):
tags, attrs = set(arg_to_iter(tags)), set(arg_to_iter(attrs))
tag_func = lambda x: x in tags
attr_func = lambda x: x in attrs
lx = LxmlParserLinkExtractor(tag=tag_func, attr=attr_func,
unique=unique, process=process_value)
unique=unique, process=process_value, strip=strip)
super(LxmlLinkExtractor, self).__init__(lx, allow=allow, deny=deny,
allow_domains=allow_domains, deny_domains=deny_domains,

View File

@ -10,9 +10,10 @@ linkre = re.compile(
"<a\s.*?href=(\"[.#]+?\"|\'[.#]+?\'|[^\s]+?)(>|\s.*?>)(.*?)<[/ ]?a>",
re.DOTALL | re.IGNORECASE)
def clean_link(link_text):
"""Remove leading and trailing whitespace and punctuation"""
return link_text.strip("\t\r\n '\"")
return link_text.strip("\t\r\n '\"\x0c")
class RegexLinkExtractor(SgmlLinkExtractor):

View File

@ -7,7 +7,8 @@ import warnings
from sgmllib import SGMLParser
from w3lib.url import safe_url_string
from scrapy.selector import Selector
from w3lib.html import strip_html5_whitespace
from scrapy.link import Link
from scrapy.linkextractors import FilteringLinkExtractor
from scrapy.utils.misc import arg_to_iter, rel_has_nofollow
@ -18,7 +19,8 @@ from scrapy.exceptions import ScrapyDeprecationWarning
class BaseSgmlLinkExtractor(SGMLParser):
def __init__(self, tag="a", attr="href", unique=False, process_value=None):
def __init__(self, tag="a", attr="href", unique=False, process_value=None,
strip=True):
warnings.warn(
"BaseSgmlLinkExtractor is deprecated and will be removed in future releases. "
"Please use scrapy.linkextractors.LinkExtractor",
@ -30,6 +32,7 @@ class BaseSgmlLinkExtractor(SGMLParser):
self.process_value = (lambda v: v) if process_value is None else process_value
self.current_link = None
self.unique = unique
self.strip = strip
def _extract_links(self, response_text, response_url, response_encoding, base_url=None):
""" Do the real extraction work """
@ -79,6 +82,8 @@ class BaseSgmlLinkExtractor(SGMLParser):
if self.scan_tag(tag):
for attr, value in attrs:
if self.scan_attr(attr):
if self.strip and value is not None:
value = strip_html5_whitespace(value)
url = self.process_value(value)
if url is not None:
link = Link(url=url, nofollow=rel_has_nofollow(dict(attrs).get('rel')))
@ -103,7 +108,8 @@ class SgmlLinkExtractor(FilteringLinkExtractor):
def __init__(self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(),
tags=('a', 'area'), attrs=('href',), canonicalize=True, unique=True,
process_value=None, deny_extensions=None, restrict_css=()):
process_value=None, deny_extensions=None, restrict_css=(),
strip=True):
warnings.warn(
"SgmlLinkExtractor is deprecated and will be removed in future releases. "
@ -118,7 +124,7 @@ class SgmlLinkExtractor(FilteringLinkExtractor):
with warnings.catch_warnings():
warnings.simplefilter('ignore', ScrapyDeprecationWarning)
lx = BaseSgmlLinkExtractor(tag=tag_func, attr=attr_func,
unique=unique, process_value=process_value)
unique=unique, process_value=process_value, strip=strip)
super(SgmlLinkExtractor, self).__init__(lx, allow=allow, deny=deny,
allow_domains=allow_domains, deny_domains=deny_domains,

View File

@ -7,7 +7,7 @@ from scrapy.utils.request import referer_str
SCRAPEDMSG = u"Scraped from %(src)s" + os.linesep + "%(item)s"
DROPPEDMSG = u"Dropped: %(exception)s" + os.linesep + "%(item)s"
CRAWLEDMSG = u"Crawled (%(status)s) %(request)s (referer: %(referer)s)%(flags)s"
CRAWLEDMSG = u"Crawled (%(status)s) %(request)s%(request_flags)s (referer: %(referer)s)%(response_flags)s"
class LogFormatter(object):
@ -32,15 +32,17 @@ class LogFormatter(object):
"""
def crawled(self, request, response, spider):
flags = ' %s' % str(response.flags) if response.flags else ''
request_flags = ' %s' % str(request.flags) if request.flags else ''
response_flags = ' %s' % str(response.flags) if response.flags else ''
return {
'level': logging.DEBUG,
'msg': CRAWLEDMSG,
'args': {
'status': response.status,
'request': request,
'request_flags' : request_flags,
'referer': referer_str(request),
'flags': flags,
'response_flags': response_flags,
}
}

View File

@ -16,8 +16,11 @@ class CachingThreadedResolver(ThreadedResolver):
def getHostByName(self, name, timeout=None):
if name in dnscache:
return defer.succeed(dnscache[name])
if not timeout:
timeout = self.timeout
# in Twisted<=16.6, getHostByName() is always called with
# a default timeout of 60s (actually passed as (1, 3, 11, 45) tuple),
# so the input argument above is simply overridden
# to enforce Scrapy's DNS_TIMEOUT setting's value
timeout = (self.timeout,)
d = super(CachingThreadedResolver, self).getHostByName(name, timeout)
d.addCallback(self._cache_result, name)
return d

View File

@ -114,8 +114,8 @@ class BaseSettings(MutableMapping):
"""
Get a setting value as a boolean.
``1``, ``'1'``, and ``True`` return ``True``, while ``0``, ``'0'``,
``False`` and ``None`` return ``False``.
``1``, ``'1'``, `True`` and ``'True'`` return ``True``,
while ``0``, ``'0'``, ``False``, ``'False'`` and ``None`` return ``False``.
For example, settings populated through environment variables set to
``'0'`` will return ``False`` when using this method.
@ -126,7 +126,17 @@ class BaseSettings(MutableMapping):
:param default: the value to return if no setting is found
:type default: any
"""
return bool(int(self.get(name, default)))
got = self.get(name, default)
try:
return bool(int(got))
except ValueError:
if got in ("True", "true"):
return True
if got in ("False", "false"):
return False
raise ValueError("Supported values for boolean settings "
"are 0/1, True/False, '0'/'1', "
"'True'/'False' and 'true'/'false'")
def getint(self, name, default=0):
"""

View File

@ -161,6 +161,10 @@ FEED_EXPORTERS_BASE = {
FILES_STORE_S3_ACL = 'private'
FTP_USER = 'anonymous'
FTP_PASSWORD = 'guest'
FTP_PASSIVE_MODE = True
HTTPCACHE_ENABLED = False
HTTPCACHE_DIR = 'httpcache'
HTTPCACHE_IGNORE_MISSING = False
@ -174,6 +178,7 @@ HTTPCACHE_DBM_MODULE = 'anydbm' if six.PY2 else 'dbm'
HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.DummyPolicy'
HTTPCACHE_GZIP = False
HTTPPROXY_ENABLED = True
HTTPPROXY_AUTH_ENCODING = 'latin-1'
IMAGES_STORE_S3_ACL = 'private'
@ -207,7 +212,7 @@ MEMDEBUG_ENABLED = False # enable memory debugging
MEMDEBUG_NOTIFY = [] # send memory debugging report by mail at engine shutdown
MEMUSAGE_CHECK_INTERVAL_SECONDS = 60.0
MEMUSAGE_ENABLED = False
MEMUSAGE_ENABLED = True
MEMUSAGE_LIMIT_MB = 0
MEMUSAGE_NOTIFY_MAIL = []
MEMUSAGE_REPORT = False

View File

@ -46,6 +46,10 @@ class HttpErrorMiddleware(object):
def process_spider_exception(self, response, exception, spider):
if isinstance(exception, HttpError):
spider.crawler.stats.inc_value('httperror/response_ignored_count')
spider.crawler.stats.inc_value(
'httperror/response_ignored_status_count/%s' % response.status
)
logger.info(
"Ignoring response %(response)r: HTTP status code is not handled or not allowed",
{'response': response}, extra={'spider': spider},

View File

@ -12,6 +12,7 @@ from scrapy.utils.trackref import object_ref
from scrapy.utils.url import url_is_from_spider
from scrapy.utils.deprecate import create_deprecated_class
from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.deprecate import method_is_overridden
class Spider(object_ref):
@ -66,10 +67,23 @@ class Spider(object_ref):
crawler.signals.connect(self.close, signals.spider_closed)
def start_requests(self):
for url in self.start_urls:
yield self.make_requests_from_url(url)
cls = self.__class__
if method_is_overridden(cls, Spider, 'make_requests_from_url'):
warnings.warn(
"Spider.make_requests_from_url method is deprecated; it "
"won't be called in future Scrapy releases. Please "
"override Spider.start_requests method instead (see %s.%s)." % (
cls.__module__, cls.__name__
),
)
for url in self.start_urls:
yield self.make_requests_from_url(url)
else:
for url in self.start_urls:
yield Request(url, dont_filter=True)
def make_requests_from_url(self, url):
""" This method is deprecated. """
return Request(url, dont_filter=True)
def parse(self, response):

View File

@ -48,6 +48,11 @@ class CrawlSpider(Spider):
def process_results(self, response, results):
return results
def _build_request(self, rule, link):
r = Request(url=link.url, callback=self._response_downloaded)
r.meta.update(rule=rule, link_text=link.text)
return r
def _requests_to_follow(self, response):
if not isinstance(response, HtmlResponse):
return
@ -59,8 +64,7 @@ class CrawlSpider(Spider):
links = rule.process_links(links)
for link in links:
seen.add(link)
r = Request(url=link.url, callback=self._response_downloaded)
r.meta.update(rule=n, link_text=link.text)
r = self._build_request(n, link)
yield rule.process_request(r)
def _response_downloaded(self, response):

View File

@ -20,14 +20,14 @@ class ${ProjectName}SpiderMiddleware(object):
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(response, spider):
def process_spider_input(self, response, spider):
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
def process_spider_output(response, result, spider):
def process_spider_output(self, response, result, spider):
# Called with the results returned from the Spider, after
# it has processed the response.
@ -35,7 +35,7 @@ class ${ProjectName}SpiderMiddleware(object):
for i in result:
yield i
def process_spider_exception(response, exception, spider):
def process_spider_exception(self, response, exception, spider):
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
@ -43,7 +43,7 @@ class ${ProjectName}SpiderMiddleware(object):
# or Item objects.
pass
def process_start_requests(start_requests, spider):
def process_start_requests(self, start_requests, spider):
# Called with the start requests of the spider, and works
# similarly to the process_spider_output() method, except
# that it doesnt have a response associated.

View File

@ -65,7 +65,7 @@ ROBOTSTXT_OBEY = True
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
#ITEM_PIPELINES = {
# '$project_name.pipelines.SomePipeline': 300,
# '$project_name.pipelines.${ProjectName}Pipeline': 300,
#}
# Enable and configure the AutoThrottle extension (disabled by default)

View File

@ -3,8 +3,8 @@ import scrapy
class $classname(scrapy.Spider):
name = "$name"
allowed_domains = ["$domain"]
name = '$name'
allowed_domains = ['$domain']
start_urls = ['http://$domain/']
def parse(self, response):

View File

@ -1,5 +1,6 @@
import os
import sys
import numbers
from operator import itemgetter
import six
@ -34,6 +35,13 @@ def build_component_list(compdict, custom=None, convert=update_classpath):
_check_components(compdict)
return {convert(k): v for k, v in six.iteritems(compdict)}
def _validate_values(compdict):
"""Fail if a value in the components dict is not a real number or None."""
for name, value in six.iteritems(compdict):
if value is not None and not isinstance(value, numbers.Real):
raise ValueError('Invalid value {} for component {}, please provide ' \
'a real number or None instead'.format(value, name))
# BEGIN Backwards compatibility for old (base, custom) call signature
if isinstance(custom, (list, tuple)):
_check_components(custom)
@ -43,6 +51,7 @@ def build_component_list(compdict, custom=None, convert=update_classpath):
compdict.update(custom)
# END Backwards compatibility
_validate_values(compdict)
compdict = without_none_values(_map_keys(compdict))
return [k for k, v in sorted(six.iteritems(compdict), key=itemgetter(1))]

View File

@ -156,3 +156,35 @@ def update_classpath(path):
ScrapyDeprecationWarning)
return new_path
return path
def method_is_overridden(subclass, base_class, method_name):
"""
Return True if a method named ``method_name`` of a ``base_class``
is overridden in a ``subclass``.
>>> class Base(object):
... def foo(self):
... pass
>>> class Sub1(Base):
... pass
>>> class Sub2(Base):
... def foo(self):
... pass
>>> class Sub3(Sub1):
... def foo(self):
... pass
>>> class Sub4(Sub2):
... pass
>>> method_is_overridden(Sub1, Base, 'foo')
False
>>> method_is_overridden(Sub2, Base, 'foo')
True
>>> method_is_overridden(Sub3, Base, 'foo')
True
>>> method_is_overridden(Sub4, Base, 'foo')
True
"""
base_method = getattr(base_class, method_name)
sub_method = getattr(subclass, method_name)
return base_method.__code__ is not sub_method.__code__

View File

@ -5,6 +5,7 @@ import six
from scrapy.http import Request
from scrapy.utils.python import to_unicode, to_native_str
from scrapy.utils.misc import load_object
def request_to_dict(request, spider=None):
@ -31,7 +32,10 @@ def request_to_dict(request, spider=None):
'_encoding': request._encoding,
'priority': request.priority,
'dont_filter': request.dont_filter,
'flags': request.flags
}
if type(request) is not Request:
d['_class'] = request.__module__ + '.' + request.__class__.__name__
return d
@ -47,7 +51,8 @@ def request_from_dict(d, spider=None):
eb = d['errback']
if eb and spider:
eb = _get_method(spider, eb)
return Request(
request_cls = load_object(d['_class']) if '_class' in d else Request
return request_cls(
url=to_native_str(d['url']),
callback=cb,
errback=eb,
@ -58,7 +63,8 @@ def request_from_dict(d, spider=None):
meta=d['meta'],
encoding=d['_encoding'],
priority=d['priority'],
dont_filter=d['dont_filter'])
dont_filter=d['dont_filter'],
flags=d.get('flags'))
def _find_method(obj, func):

View File

@ -1,3 +1,5 @@
:orphan:
Scrapy Enhancement Proposals
============================

View File

@ -36,19 +36,20 @@ setup(
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Software Development :: Libraries :: Application Frameworks',
'Topic :: Software Development :: Libraries :: Python Modules',
],
install_requires=[
'Twisted>=13.1.0',
'w3lib>=1.15.0',
'w3lib>=1.17.0',
'queuelib',
'lxml',
'pyOpenSSL',
'cssselect>=0.9',
'six>=1.5.2',
'parsel>=0.9.5',
'parsel>=1.1',
'PyDispatcher>=2.0.5',
'service_identity',
],

View File

@ -26,9 +26,12 @@ try:
except ImportError:
import mock
tests_datadir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'sample_data')
tests_datadir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'sample_data')
def get_testdata(*paths):
"""Return test data"""
path = os.path.join(tests_datadir, *paths)
return open(path, 'rb').read()
with open(path, 'rb') as f:
return f.read()

View File

@ -2,34 +2,18 @@ from __future__ import print_function
import sys, time, random, os, json
from six.moves.urllib.parse import urlencode
from subprocess import Popen, PIPE
from twisted.web.server import Site, NOT_DONE_YET
from twisted.web.resource import Resource
from twisted.internet import reactor, defer, ssl
from scrapy import twisted_version
from twisted.web.test.test_webclient import PayloadResource
from twisted.web.server import GzipEncoderFactory
from twisted.web.resource import EncodingResourceWrapper
from twisted.internet import reactor, ssl
from twisted.internet.task import deferLater
from scrapy.utils.python import to_bytes, to_unicode
if twisted_version < (11, 0, 0):
def deferLater(clock, delay, func, *args, **kw):
def _cancel_method():
_cancel_cb(None)
d.errback(Exception())
def _cancel_cb(result):
if cl.active():
cl.cancel()
return result
d = defer.Deferred()
d.cancel = _cancel_method
d.addCallback(lambda ignored: func(*args, **kw))
d.addBoth(_cancel_cb)
cl = clock.callLater(delay, d.callback, None)
return d
else:
from twisted.internet.task import deferLater
def getarg(request, name, default=None, type=None):
if name in request.args:
value = request.args[name][0]
@ -174,13 +158,8 @@ class Root(Resource):
self.putChild(b"drop", Drop())
self.putChild(b"raw", Raw())
self.putChild(b"echo", Echo())
if twisted_version > (12, 3, 0):
from twisted.web.test.test_webclient import PayloadResource
from twisted.web.server import GzipEncoderFactory
from twisted.web.resource import EncodingResourceWrapper
self.putChild(b"payload", PayloadResource())
self.putChild(b"xpayload", EncodingResourceWrapper(PayloadResource(), [GzipEncoderFactory()]))
self.putChild(b"payload", PayloadResource())
self.putChild(b"xpayload", EncodingResourceWrapper(PayloadResource(), [GzipEncoderFactory()]))
def getChild(self, name, request):
return self

View File

@ -8,3 +8,4 @@ botocore
# optional for shell wrapper tests
bpython
ipython
brotlipy

View File

@ -6,6 +6,7 @@ pytest==2.9.2
pytest-twisted
pytest-cov==2.2.1
jmespath
brotlipy
testfixtures
# optional for shell wrapper tests
bpython

Binary file not shown.

View File

@ -13,6 +13,7 @@
<a href='sample3.html'>sample 3 repetition</a>
<a href='http://www.google.com/something'></a>
<a href='http://example.com/innertag.html'><b>inner</b> tag</a>
<a href=' page 4.html '>href with whitespaces</a>
</div>
</body>
</html>

View File

@ -170,10 +170,7 @@ class DuplicateStartRequestsSpider(Spider):
for i in range(0, self.distinct_urls):
for j in range(0, self.dupe_factor):
url = "http://localhost:8998/echo?headers=1&body=test%d" % i
yield self.make_requests_from_url(url)
def make_requests_from_url(self, url):
return Request(url, dont_filter=self.dont_filter)
yield Request(url, dont_filter=self.dont_filter)
def __init__(self, url="http://localhost:8998", *args, **kwargs):
super(DuplicateStartRequestsSpider, self).__init__(*args, **kwargs)

View File

@ -1,5 +1,4 @@
import json
import socket
import logging
from testfixtures import LogCapture
@ -9,7 +8,6 @@ from twisted.trial.unittest import TestCase
from scrapy.http import Request
from scrapy.crawler import CrawlerRunner
from scrapy.utils.python import to_unicode
from tests import mock
from tests.spiders import FollowAllSpider, DelaySpider, SimpleSpider, \
BrokenStartRequestsSpider, SingleRequestSpider, DuplicateStartRequestsSpider
from tests.mockserver import MockServer
@ -91,12 +89,11 @@ class CrawlTestCase(TestCase):
@defer.inlineCallbacks
def test_retry_dns_error(self):
with mock.patch('socket.gethostbyname',
side_effect=socket.gaierror(-5, 'No address associated with hostname')):
crawler = self.runner.create_crawler(SimpleSpider)
with LogCapture() as l:
yield crawler.crawl("http://example.com/")
self._assert_retried(l)
crawler = self.runner.create_crawler(SimpleSpider)
with LogCapture() as l:
# try to fetch the homepage of a non-existent domain
yield crawler.crawl("http://dns.resolution.invalid./")
self._assert_retried(l)
@defer.inlineCallbacks
def test_start_requests_bug_before_yield(self):

View File

@ -1,10 +1,12 @@
import os
import six
import contextlib
import shutil
try:
from unittest import mock
except ImportError:
import mock
import shutil
from twisted.trial import unittest
from twisted.protocols.policies import WrappingFactory
@ -17,7 +19,6 @@ from twisted.web.test.test_webclient import ForeverTakingResource, \
from twisted.cred import portal, checkers, credentials
from w3lib.url import path_to_file_uri
from scrapy import twisted_version
from scrapy.core.downloader.handlers import DownloadHandlers
from scrapy.core.downloader.handlers.file import FileDownloadHandler
from scrapy.core.downloader.handlers.http import HTTPDownloadHandler, HttpDownloadHandler
@ -85,11 +86,13 @@ class FileTestCase(unittest.TestCase):
def setUp(self):
self.tmpname = self.mktemp()
fd = open(self.tmpname + '^', 'w')
fd.write('0123456789')
fd.close()
with open(self.tmpname + '^', 'w') as f:
f.write('0123456789')
self.download_request = FileDownloadHandler(Settings()).download_request
def tearDown(self):
os.unlink(self.tmpname + '^')
def test_download(self):
def _test(response):
self.assertEquals(response.url, request.url)
@ -135,10 +138,10 @@ class HttpTestCase(unittest.TestCase):
certfile = 'keys/cert.pem'
def setUp(self):
name = self.mktemp()
os.mkdir(name)
FilePath(name).child("file").setContent(b"0123456789")
r = static.File(name)
self.tmpname = self.mktemp()
os.mkdir(self.tmpname)
FilePath(self.tmpname).child("file").setContent(b"0123456789")
r = static.File(self.tmpname)
r.putChild(b"redirect", util.Redirect(b"/file"))
r.putChild(b"wait", ForeverTakingResource())
r.putChild(b"hang-after-headers", ForeverTakingResource(write=True))
@ -166,6 +169,7 @@ class HttpTestCase(unittest.TestCase):
yield self.port.stopListening()
if hasattr(self.download_handler, 'close'):
yield self.download_handler.close()
shutil.rmtree(self.tmpname)
def getURL(self, path):
return "%s://%s:%d/%s" % (self.scheme, self.host, self.portno, path)
@ -281,8 +285,6 @@ class Https10TestCase(Http10TestCase):
class Http11TestCase(HttpTestCase):
"""HTTP 1.1 test case"""
download_handler_cls = HTTP11DownloadHandler
if twisted_version < (11, 1, 0):
skip = 'HTTP1.1 not supported in twisted < 11.1.0'
def test_download_without_maxsize_limit(self):
request = Request(self.getURL('file'))
@ -366,8 +368,6 @@ class Https11InvalidDNSId(Https11TestCase):
class Http11MockServerTestCase(unittest.TestCase):
"""HTTP 1.1 test case with MockServer"""
if twisted_version < (11, 1, 0):
skip = 'HTTP1.1 not supported in twisted < 11.1.0'
def setUp(self):
self.mockserver = MockServer()
@ -396,31 +396,27 @@ class Http11MockServerTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_download_gzip_response(self):
crawler = get_crawler(SingleRequestSpider)
body = b'1' * 100 # PayloadResource requires body length to be 100
request = Request('http://localhost:8998/payload', method='POST',
body=body, meta={'download_maxsize': 50})
yield crawler.crawl(seed=request)
failure = crawler.spider.meta['failure']
# download_maxsize < 100, hence the CancelledError
self.assertIsInstance(failure.value, defer.CancelledError)
if twisted_version > (12, 3, 0):
crawler = get_crawler(SingleRequestSpider)
body = b'1'*100 # PayloadResource requires body length to be 100
request = Request('http://localhost:8998/payload', method='POST', body=body, meta={'download_maxsize': 50})
if six.PY2:
request.headers.setdefault(b'Accept-Encoding', b'gzip,deflate')
request = request.replace(url='http://localhost:8998/xpayload')
yield crawler.crawl(seed=request)
failure = crawler.spider.meta['failure']
# download_maxsize < 100, hence the CancelledError
self.assertIsInstance(failure.value, defer.CancelledError)
if six.PY2:
request.headers.setdefault(b'Accept-Encoding', b'gzip,deflate')
request = request.replace(url='http://localhost:8998/xpayload')
yield crawler.crawl(seed=request)
# download_maxsize = 50 is enough for the gzipped response
failure = crawler.spider.meta.get('failure')
self.assertTrue(failure == None)
reason = crawler.spider.meta['close_reason']
self.assertTrue(reason, 'finished')
else:
# See issue https://twistedmatrix.com/trac/ticket/8175
raise unittest.SkipTest("xpayload only enabled for PY2")
# download_maxsize = 50 is enough for the gzipped response
failure = crawler.spider.meta.get('failure')
self.assertTrue(failure == None)
reason = crawler.spider.meta['close_reason']
self.assertTrue(reason, 'finished')
else:
raise unittest.SkipTest("xpayload and payload endpoint only enabled for twisted > 12.3.0")
# See issue https://twistedmatrix.com/trac/ticket/8175
raise unittest.SkipTest("xpayload only enabled for PY2")
class UriResource(resource.Resource):
@ -500,8 +496,6 @@ class Http10ProxyTestCase(HttpProxyTestCase):
class Http11ProxyTestCase(HttpProxyTestCase):
download_handler_cls = HTTP11DownloadHandler
if twisted_version < (11, 1, 0):
skip = 'HTTP1.1 not supported in twisted < 11.1.0'
@defer.inlineCallbacks
def test_download_with_proxy_https_timeout(self):
@ -687,13 +681,12 @@ class S3TestCase(unittest.TestCase):
b'AWS 0PN5J17HBGZHT7JJ3X82:+CfvG8EZ3YccOrRVMXNaK2eKZmM=')
class FTPTestCase(unittest.TestCase):
class BaseFTPTestCase(unittest.TestCase):
username = "scrapy"
password = "passwd"
req_meta = {"ftp_user": username, "ftp_password": password}
if twisted_version < (10, 2, 0):
skip = "Twisted pre 10.2.0 doesn't allow to set home path other than /home"
if six.PY3:
skip = "Twisted missing ftp support for PY3"
@ -722,6 +715,9 @@ class FTPTestCase(unittest.TestCase):
self.download_handler = FTPDownloadHandler(Settings())
self.addCleanup(self.port.stopListening)
def tearDown(self):
shutil.rmtree(self.directory)
def _add_test_callbacks(self, deferred, callback=None, errback=None):
def _clean(data):
self.download_handler.client.transport.loseConnection()
@ -735,7 +731,7 @@ class FTPTestCase(unittest.TestCase):
def test_ftp_download_success(self):
request = Request(url="ftp://127.0.0.1:%s/file.txt" % self.portNum,
meta={"ftp_user": self.username, "ftp_password": self.password})
meta=self.req_meta)
d = self.download_handler.download_request(request, None)
def _test(r):
@ -747,7 +743,7 @@ class FTPTestCase(unittest.TestCase):
def test_ftp_download_path_with_spaces(self):
request = Request(
url="ftp://127.0.0.1:%s/file with spaces.txt" % self.portNum,
meta={"ftp_user": self.username, "ftp_password": self.password}
meta=self.req_meta
)
d = self.download_handler.download_request(request, None)
@ -759,7 +755,7 @@ class FTPTestCase(unittest.TestCase):
def test_ftp_download_notexist(self):
request = Request(url="ftp://127.0.0.1:%s/notexist.txt" % self.portNum,
meta={"ftp_user": self.username, "ftp_password": self.password})
meta=self.req_meta)
d = self.download_handler.download_request(request, None)
def _test(r):
@ -768,8 +764,10 @@ class FTPTestCase(unittest.TestCase):
def test_ftp_local_filename(self):
local_fname = "/tmp/file.txt"
meta = {"ftp_local_filename": local_fname}
meta.update(self.req_meta)
request = Request(url="ftp://127.0.0.1:%s/file.txt" % self.portNum,
meta={"ftp_user": self.username, "ftp_password": self.password, "ftp_local_filename": local_fname})
meta=meta)
d = self.download_handler.download_request(request, None)
def _test(r):
@ -781,13 +779,52 @@ class FTPTestCase(unittest.TestCase):
os.remove(local_fname)
return self._add_test_callbacks(d, _test)
class FTPTestCase(BaseFTPTestCase):
def test_invalid_credentials(self):
from twisted.protocols.ftp import ConnectionLost
meta = dict(self.req_meta)
meta.update({"ftp_password": 'invalid'})
request = Request(url="ftp://127.0.0.1:%s/file.txt" % self.portNum,
meta={"ftp_user": self.username, "ftp_password": 'invalid'})
meta=meta)
d = self.download_handler.download_request(request, None)
def _test(r):
self.assertEqual(r.type, ConnectionLost)
return self._add_test_callbacks(d, errback=_test)
class AnonymousFTPTestCase(BaseFTPTestCase):
username = "anonymous"
req_meta = {}
def setUp(self):
from twisted.protocols.ftp import FTPRealm, FTPFactory
from scrapy.core.downloader.handlers.ftp import FTPDownloadHandler
# setup dir and test file
self.directory = self.mktemp()
os.mkdir(self.directory)
fp = FilePath(self.directory)
fp.child('file.txt').setContent("I have the power!")
fp.child('file with spaces.txt').setContent("Moooooooooo power!")
# setup server for anonymous access
realm = FTPRealm(anonymousRoot=self.directory)
p = portal.Portal(realm)
p.registerChecker(checkers.AllowAnonymousAccess(),
credentials.IAnonymous)
self.factory = FTPFactory(portal=p,
userAnonymous=self.username)
self.port = reactor.listenTCP(0, self.factory, interface="127.0.0.1")
self.portNum = self.port.getHost().port
self.download_handler = FTPDownloadHandler(Settings())
self.addCleanup(self.port.stopListening)
def tearDown(self):
shutil.rmtree(self.directory)

View File

@ -1,11 +1,12 @@
from io import BytesIO
from unittest import TestCase
from os.path import join, abspath, dirname
from unittest import TestCase, SkipTest
from os.path import join
from gzip import GzipFile
from scrapy.spiders import Spider
from scrapy.http import Response, Request, HtmlResponse
from scrapy.downloadermiddlewares.httpcompression import HttpCompressionMiddleware
from scrapy.downloadermiddlewares.httpcompression import HttpCompressionMiddleware, \
ACCEPTED_ENCODINGS
from tests import tests_datadir
from w3lib.encoding import resolve_encoding
@ -17,8 +18,10 @@ FORMAT = {
'x-gzip': ('html-gzip.bin', 'gzip'),
'rawdeflate': ('html-rawdeflate.bin', 'deflate'),
'zlibdeflate': ('html-zlibdeflate.bin', 'deflate'),
'br': ('html-br.bin', 'br')
}
class HttpCompressionTest(TestCase):
def setUp(self):
@ -50,7 +53,8 @@ class HttpCompressionTest(TestCase):
request = Request('http://scrapytest.org')
assert 'Accept-Encoding' not in request.headers
self.mw.process_request(request, self.spider)
self.assertEqual(request.headers.get('Accept-Encoding'), b'gzip,deflate')
self.assertEqual(request.headers.get('Accept-Encoding'),
b','.join(ACCEPTED_ENCODINGS))
def test_process_response_gzip(self):
response = self._getresponse('gzip')
@ -62,6 +66,19 @@ class HttpCompressionTest(TestCase):
assert newresponse.body.startswith(b'<!DOCTYPE')
assert 'Content-Encoding' not in newresponse.headers
def test_process_response_br(self):
try:
import brotli
except ImportError:
raise SkipTest("no brotli")
response = self._getresponse('br')
request = response.request
self.assertEqual(response.headers['Content-Encoding'], b'br')
newresponse = self.mw.process_response(request, response, self.spider)
assert newresponse is not response
assert newresponse.body.startswith(b"<!DOCTYPE")
assert 'Content-Encoding' not in newresponse.headers
def test_process_response_rawdeflate(self):
response = self._getresponse('rawdeflate')
request = response.request

View File

@ -1,11 +1,14 @@
import os
import sys
from functools import partial
from twisted.trial.unittest import TestCase, SkipTest
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
from scrapy.exceptions import NotConfigured
from scrapy.http import Response, Request
from scrapy.spiders import Spider
from scrapy.crawler import Crawler
from scrapy.settings import Settings
spider = Spider('foo')
@ -20,9 +23,10 @@ class TestDefaultHeadersMiddleware(TestCase):
def tearDown(self):
os.environ = self._oldenv
def test_no_proxies(self):
os.environ = {}
self.assertRaises(NotConfigured, HttpProxyMiddleware)
def test_not_enabled(self):
settings = Settings({'HTTPPROXY_ENABLED': False})
crawler = Crawler(spider, settings)
self.assertRaises(NotConfigured, partial(HttpProxyMiddleware.from_crawler, crawler))
def test_no_enviroment_proxies(self):
os.environ = {'dummy_proxy': 'reset_env_and_do_not_raise'}
@ -47,6 +51,13 @@ class TestDefaultHeadersMiddleware(TestCase):
self.assertEquals(req.url, url)
self.assertEquals(req.meta.get('proxy'), proxy)
def test_proxy_precedence_meta(self):
os.environ['http_proxy'] = 'https://proxy.com'
mw = HttpProxyMiddleware()
req = Request('http://scrapytest.org', meta={'proxy': 'https://new.proxy:3128'})
assert mw.process_request(req, spider) is None
self.assertEquals(req.meta, {'proxy': 'https://new.proxy:3128'})
def test_proxy_auth(self):
os.environ['http_proxy'] = 'https://user:pass@proxy:3128'
mw = HttpProxyMiddleware()
@ -54,6 +65,11 @@ class TestDefaultHeadersMiddleware(TestCase):
assert mw.process_request(req, spider) is None
self.assertEquals(req.meta, {'proxy': 'https://proxy:3128'})
self.assertEquals(req.headers.get('Proxy-Authorization'), b'Basic dXNlcjpwYXNz')
# proxy from request.meta
req = Request('http://scrapytest.org', meta={'proxy': 'https://username:password@proxy:3128'})
assert mw.process_request(req, spider) is None
self.assertEquals(req.meta, {'proxy': 'https://proxy:3128'})
self.assertEquals(req.headers.get('Proxy-Authorization'), b'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')
def test_proxy_auth_empty_passwd(self):
os.environ['http_proxy'] = 'https://user:@proxy:3128'
@ -62,6 +78,11 @@ class TestDefaultHeadersMiddleware(TestCase):
assert mw.process_request(req, spider) is None
self.assertEquals(req.meta, {'proxy': 'https://proxy:3128'})
self.assertEquals(req.headers.get('Proxy-Authorization'), b'Basic dXNlcjo=')
# proxy from request.meta
req = Request('http://scrapytest.org', meta={'proxy': 'https://username:@proxy:3128'})
assert mw.process_request(req, spider) is None
self.assertEquals(req.meta, {'proxy': 'https://proxy:3128'})
self.assertEquals(req.headers.get('Proxy-Authorization'), b'Basic dXNlcm5hbWU6')
def test_proxy_auth_encoding(self):
# utf-8 encoding
@ -72,6 +93,12 @@ class TestDefaultHeadersMiddleware(TestCase):
self.assertEquals(req.meta, {'proxy': 'https://proxy:3128'})
self.assertEquals(req.headers.get('Proxy-Authorization'), b'Basic bcOhbjpwYXNz')
# proxy from request.meta
req = Request('http://scrapytest.org', meta={'proxy': u'https://\u00FCser:pass@proxy:3128'})
assert mw.process_request(req, spider) is None
self.assertEquals(req.meta, {'proxy': 'https://proxy:3128'})
self.assertEquals(req.headers.get('Proxy-Authorization'), b'Basic w7xzZXI6cGFzcw==')
# default latin-1 encoding
mw = HttpProxyMiddleware(auth_encoding='latin-1')
req = Request('http://scrapytest.org')
@ -79,15 +106,21 @@ class TestDefaultHeadersMiddleware(TestCase):
self.assertEquals(req.meta, {'proxy': 'https://proxy:3128'})
self.assertEquals(req.headers.get('Proxy-Authorization'), b'Basic beFuOnBhc3M=')
# proxy from request.meta, latin-1 encoding
req = Request('http://scrapytest.org', meta={'proxy': u'https://\u00FCser:pass@proxy:3128'})
assert mw.process_request(req, spider) is None
self.assertEquals(req.meta, {'proxy': 'https://proxy:3128'})
self.assertEquals(req.headers.get('Proxy-Authorization'), b'Basic /HNlcjpwYXNz')
def test_proxy_already_seted(self):
os.environ['http_proxy'] = http_proxy = 'https://proxy.for.http:3128'
os.environ['http_proxy'] = 'https://proxy.for.http:3128'
mw = HttpProxyMiddleware()
req = Request('http://noproxy.com', meta={'proxy': None})
assert mw.process_request(req, spider) is None
assert 'proxy' in req.meta and req.meta['proxy'] is None
def test_no_proxy(self):
os.environ['http_proxy'] = http_proxy = 'https://proxy.for.http:3128'
os.environ['http_proxy'] = 'https://proxy.for.http:3128'
mw = HttpProxyMiddleware()
os.environ['no_proxy'] = '*'
@ -104,3 +137,9 @@ class TestDefaultHeadersMiddleware(TestCase):
req = Request('http://noproxy.com')
assert mw.process_request(req, spider) is None
assert 'proxy' not in req.meta
# proxy from meta['proxy'] takes precedence
os.environ['no_proxy'] = '*'
req = Request('http://noproxy.com', meta={'proxy': 'http://proxy.com'})
assert mw.process_request(req, spider) is None
self.assertEquals(req.meta, {'proxy': 'http://proxy.com'})

View File

@ -5,7 +5,6 @@ from twisted.internet.error import TimeoutError, DNSLookupError, \
ConnectionLost, TCPTimedOutError
from twisted.web.client import ResponseFailed
from scrapy import twisted_version
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.spiders import Spider
from scrapy.http import Request, Response
@ -74,9 +73,7 @@ class RetryTest(unittest.TestCase):
def test_twistederrors(self):
exceptions = [defer.TimeoutError, TCPTimedOutError, TimeoutError,
DNSLookupError, ConnectionRefusedError, ConnectionDone,
ConnectError, ConnectionLost]
if twisted_version >= (11, 1, 0): # http11 available
exceptions.append(ResponseFailed)
ConnectError, ConnectionLost, ResponseFailed]
for exc in exceptions:
req = Request('http://www.scrapytest.org/%s' % exc.__name__)

View File

@ -66,8 +66,8 @@ class TestSpider(Spider):
class TestDupeFilterSpider(TestSpider):
def make_requests_from_url(self, url):
return Request(url) # dont_filter=False
def start_requests(self):
return (Request(url) for url in self.start_urls) # no dont_filter=True
class DictItemsSpider(TestSpider):

View File

@ -57,8 +57,11 @@ class FileFeedStorageTest(unittest.TestCase):
file.write(b"content")
yield storage.store(file)
self.assertTrue(os.path.exists(path))
with open(path, 'rb') as fp:
self.assertEqual(fp.read(), b"content")
try:
with open(path, 'rb') as fp:
self.assertEqual(fp.read(), b"content")
finally:
os.unlink(path)
class FTPFeedStorageTest(unittest.TestCase):
@ -79,12 +82,15 @@ class FTPFeedStorageTest(unittest.TestCase):
file.write(b"content")
yield storage.store(file)
self.assertTrue(os.path.exists(path))
with open(path, 'rb') as fp:
self.assertEqual(fp.read(), b"content")
# again, to check s3 objects are overwritten
yield storage.store(BytesIO(b"new content"))
with open(path, 'rb') as fp:
self.assertEqual(fp.read(), b"new content")
try:
with open(path, 'rb') as fp:
self.assertEqual(fp.read(), b"content")
# again, to check s3 objects are overwritten
yield storage.store(BytesIO(b"new content"))
with open(path, 'rb') as fp:
self.assertEqual(fp.read(), b"new content")
finally:
os.unlink(path)
class BlockingFeedStorageTest(unittest.TestCase):

View File

@ -556,7 +556,6 @@ class FormRequestTest(RequestTest):
fs = _qs(req, to_unicode=True, encoding='latin1')
self.assertTrue(fs[u'price in \u00a5'])
def test_from_response_multiple_forms_clickdata(self):
response = _buildresponse(
"""<form name="form1">
@ -989,7 +988,7 @@ class FormRequestTest(RequestTest):
"""
<html>
<head>
<base href="http://b.com/">
<base href=" http://b.com/">
</head>
<body>
<form action="test_form">
@ -1002,6 +1001,11 @@ class FormRequestTest(RequestTest):
req = self.request_class.from_response(response)
self.assertEqual(req.url, 'http://b.com/test_form')
def test_spaces_in_action(self):
resp = _buildresponse('<body><form action=" path\n"></form></body>')
req = self.request_class.from_response(resp)
self.assertEqual(req.url, 'http://example.com/path')
def test_from_response_css(self):
response = _buildresponse(
"""<form action="post.php" method="POST">
@ -1023,12 +1027,14 @@ class FormRequestTest(RequestTest):
self.assertRaises(ValueError, self.request_class.from_response,
response, formcss="input[name='abc']")
def _buildresponse(body, **kwargs):
kwargs.setdefault('body', body)
kwargs.setdefault('url', 'http://example.com')
kwargs.setdefault('encoding', 'utf-8')
return HtmlResponse(**kwargs)
def _qs(req, encoding='utf-8', to_unicode=False):
if req.method == 'POST':
qs = req.body

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import unittest
import six
@ -8,6 +9,8 @@ from scrapy.http import (Request, Response, TextResponse, HtmlResponse,
from scrapy.selector import Selector
from scrapy.utils.python import to_native_str
from scrapy.exceptions import NotSupported
from scrapy.link import Link
from tests import get_testdata
class BaseResponseTest(unittest.TestCase):
@ -140,6 +143,38 @@ class BaseResponseTest(unittest.TestCase):
r.css('body')
r.xpath('//body')
def test_follow_url_absolute(self):
self._assert_followed_url('http://foo.example.com',
'http://foo.example.com')
def test_follow_url_relative(self):
self._assert_followed_url('foo',
'http://example.com/foo')
def test_follow_link(self):
self._assert_followed_url(Link('http://example.com/foo'),
'http://example.com/foo')
def test_follow_whitespace_url(self):
self._assert_followed_url('foo ',
'http://example.com/foo%20')
def test_follow_whitespace_link(self):
self._assert_followed_url(Link('http://example.com/foo '),
'http://example.com/foo%20')
def _assert_followed_url(self, follow_obj, target_url, response=None):
if response is None:
response = self._links_response()
req = response.follow(follow_obj)
self.assertEqual(req.url, target_url)
return req
def _links_response(self):
body = get_testdata('link_extractor', 'sgml_linkextractor.html')
resp = self.response_class('http://example.com/index', body=body)
return resp
class TextResponseTest(BaseResponseTest):
@ -320,6 +355,20 @@ class TextResponseTest(BaseResponseTest):
response.selector.css("title::text").extract(),
)
def test_selector_shortcuts_kwargs(self):
body = b"<html><head><title>Some page</title><body><p class=\"content\">A nice paragraph.</p></body></html>"
response = self.response_class("http://www.example.com", body=body)
self.assertEqual(
response.xpath("normalize-space(//p[@class=$pclass])", pclass="content").extract(),
response.xpath("normalize-space(//p[@class=\"content\"])").extract(),
)
self.assertEqual(
response.xpath("//title[count(following::p[@class=$pclass])=$pcount]/text()",
pclass="content", pcount=1).extract(),
response.xpath("//title[count(following::p[@class=\"content\"])=1]/text()").extract(),
)
def test_urljoin_with_base_url(self):
"""Test urljoin shortcut which also evaluates base-url through get_base_url()."""
body = b'<html><body><base href="https://example.net"></body></html>'
@ -337,6 +386,89 @@ class TextResponseTest(BaseResponseTest):
absolute = 'http://www.example.com/elsewhere/test'
self.assertEqual(joined, absolute)
def test_follow_selector(self):
resp = self._links_response()
urls = [
'http://example.com/sample2.html',
'http://example.com/sample3.html',
'http://example.com/sample3.html',
'http://www.google.com/something',
'http://example.com/innertag.html'
]
# select <a> elements
for sellist in [resp.css('a'), resp.xpath('//a')]:
for sel, url in zip(sellist, urls):
self._assert_followed_url(sel, url, response=resp)
# href attributes should work
for sellist in [resp.css('a::attr(href)'), resp.xpath('//a/@href')]:
for sel, url in zip(sellist, urls):
self._assert_followed_url(sel, url, response=resp)
# non-a elements are not supported
self.assertRaises(ValueError, resp.follow, resp.css('div')[0])
def test_follow_selector_list(self):
resp = self._links_response()
self.assertRaisesRegexp(ValueError, 'SelectorList',
resp.follow, resp.css('a'))
def test_follow_selector_invalid(self):
resp = self._links_response()
self.assertRaisesRegexp(ValueError, 'Unsupported',
resp.follow, resp.xpath('count(//div)')[0])
def test_follow_selector_attribute(self):
resp = self._links_response()
for src in resp.css('img::attr(src)'):
self._assert_followed_url(src, 'http://example.com/sample2.jpg')
def test_follow_selector_no_href(self):
resp = self.response_class(
url='http://example.com',
body=b'<html><body><a name=123>click me</a></body></html>',
)
self.assertRaisesRegexp(ValueError, 'no href',
resp.follow, resp.css('a')[0])
def test_follow_whitespace_selector(self):
resp = self.response_class(
'http://example.com',
body=b'''<html><body><a href=" foo\n">click me</a></body></html>'''
)
self._assert_followed_url(resp.css('a')[0],
'http://example.com/foo',
response=resp)
self._assert_followed_url(resp.css('a::attr(href)')[0],
'http://example.com/foo',
response=resp)
def test_follow_encoding(self):
resp1 = self.response_class(
'http://example.com',
encoding='utf8',
body=u'<html><body><a href="foo?привет">click me</a></body></html>'.encode('utf8')
)
req = self._assert_followed_url(
resp1.css('a')[0],
'http://example.com/foo?%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82',
response=resp1,
)
self.assertEqual(req.encoding, 'utf8')
resp2 = self.response_class(
'http://example.com',
encoding='cp1251',
body=u'<html><body><a href="foo?привет">click me</a></body></html>'.encode('cp1251')
)
req = self._assert_followed_url(
resp2.css('a')[0],
'http://example.com/foo?%EF%F0%E8%E2%E5%F2',
response=resp2,
)
self.assertEqual(req.encoding, 'cp1251')
class HtmlResponseTest(TextResponseTest):
@ -428,3 +560,21 @@ class XmlResponseTest(TextResponseTest):
response.xpath("//elem/text()").extract(),
response.selector.xpath("//elem/text()").extract(),
)
def test_selector_shortcuts_kwargs(self):
body = b'''<?xml version="1.0" encoding="utf-8"?>
<xml xmlns:somens="http://scrapy.org">
<somens:elem>value</somens:elem>
</xml>'''
response = self.response_class("http://www.example.com", body=body)
self.assertEqual(
response.xpath("//s:elem/text()", namespaces={'s': 'http://scrapy.org'}).extract(),
response.selector.xpath("//s:elem/text()", namespaces={'s': 'http://scrapy.org'}).extract(),
)
response.selector.register_namespace('s2', 'http://scrapy.org')
self.assertEqual(
response.xpath("//s1:elem/text()", namespaces={'s1': 'http://scrapy.org'}).extract(),
response.selector.xpath("//s2:elem/text()").extract(),
)

View File

@ -1,8 +1,14 @@
import sys
import unittest
from scrapy.item import Item, Field
import six
from scrapy.item import ABCMeta, Item, ItemMeta, Field
from tests import mock
PY36_PLUS = (sys.version_info.major >= 3) and (sys.version_info.minor >= 6)
class ItemTest(unittest.TestCase):
@ -244,5 +250,49 @@ class ItemTest(unittest.TestCase):
self.assertNotEqual(item['name'], copied_item['name'])
class ItemMetaTest(unittest.TestCase):
def test_new_method_propagates_classcell(self):
new_mock = mock.Mock(side_effect=ABCMeta.__new__)
base = ItemMeta.__bases__[0]
with mock.patch.object(base, '__new__', new_mock):
class MyItem(Item):
if not PY36_PLUS:
# This attribute is an internal attribute in Python 3.6+
# and must be propagated properly. See
# https://docs.python.org/3.6/reference/datamodel.html#creating-the-class-object
# In <3.6, we add a dummy attribute just to ensure the
# __new__ method propagates it correctly.
__classcell__ = object()
def f(self):
# For rationale of this see:
# https://github.com/python/cpython/blob/ee1a81b77444c6715cbe610e951c655b6adab88b/Lib/test/test_super.py#L222
return __class__
MyItem()
(first_call, second_call) = new_mock.call_args_list[-2:]
mcs, class_name, bases, attrs = first_call[0]
assert '__classcell__' not in attrs
mcs, class_name, bases, attrs = second_call[0]
assert '__classcell__' in attrs
class ItemMetaClassCellRegression(unittest.TestCase):
def test_item_meta_classcell_regression(self):
class MyItem(six.with_metaclass(ItemMeta, Item)):
def __init__(self, *args, **kwargs):
# This call to super() trigger the __classcell__ propagation
# requirement. When not done properly raises an error:
# TypeError: __class__ set to <class '__main__.MyItem'>
# defining 'MyItem' as <class '__main__.MyItem'>
super(MyItem, self).__init__(*args, **kwargs)
if __name__ == "__main__":
unittest.main()

View File

@ -32,6 +32,7 @@ class Base:
Link(url='http://example.com/sample3.html', text=u'sample 3 text'),
Link(url='http://www.google.com/something', text=u''),
Link(url='http://example.com/innertag.html', text=u'inner tag'),
Link(url='http://example.com/page%204.html', text=u'href with whitespaces'),
])
def test_extract_filter_allow(self):
@ -281,6 +282,7 @@ class Base:
Link(url='http://example.com/sample3.html', text=u'sample 3 text'),
Link(url='http://www.google.com/something', text=u''),
Link(url='http://example.com/innertag.html', text=u'inner tag'),
Link(url='http://example.com/page%204.html', text=u'href with whitespaces'),
])
lx = self.extractor_cls(attrs=("href","src"), tags=("a","area","img"), deny_extensions=())
@ -291,6 +293,7 @@ class Base:
Link(url='http://example.com/sample3.html', text=u'sample 3 text'),
Link(url='http://www.google.com/something', text=u''),
Link(url='http://example.com/innertag.html', text=u'inner tag'),
Link(url='http://example.com/page%204.html', text=u'href with whitespaces'),
])
lx = self.extractor_cls(attrs=None)

View File

@ -117,12 +117,14 @@ class HtmlParserLinkExtractorTestCase(unittest.TestCase):
def test_extraction(self):
# Default arguments
lx = HtmlParserLinkExtractor()
self.assertEqual(lx.extract_links(self.response),
[Link(url='http://example.com/sample2.html', text=u'sample 2'),
Link(url='http://example.com/sample3.html', text=u'sample 3 text'),
Link(url='http://example.com/sample3.html', text=u'sample 3 repetition'),
Link(url='http://www.google.com/something', text=u''),
Link(url='http://example.com/innertag.html', text=u'inner tag'),])
self.assertEqual(lx.extract_links(self.response), [
Link(url='http://example.com/sample2.html', text=u'sample 2'),
Link(url='http://example.com/sample3.html', text=u'sample 3 text'),
Link(url='http://example.com/sample3.html', text=u'sample 3 repetition'),
Link(url='http://www.google.com/something', text=u''),
Link(url='http://example.com/innertag.html', text=u'inner tag'),
Link(url='http://example.com/page%204.html', text=u'href with whitespaces'),
])
def test_link_wrong_href(self):
html = """
@ -220,3 +222,9 @@ class RegexLinkExtractorTestCase(unittest.TestCase):
self.assertEqual([link for link in lx.extract_links(response)], [
Link(url='http://b.com/test.html', text=u'', nofollow=False),
])
@unittest.expectedFailure
def test_extraction(self):
# RegexLinkExtractor doesn't parse URLs with leading/trailing
# whitespaces correctly.
super(RegexLinkExtractorTestCase, self).test_extraction()

View File

@ -36,6 +36,14 @@ class LoggingContribTest(unittest.TestCase):
self.assertEqual(logline,
"Crawled (200) <GET http://www.example.com> (referer: http://example.com) ['cached']")
def test_flags_in_request(self):
req = Request("http://www.example.com", flags=['test','flag'])
res = Response("http://www.example.com")
logkws = self.formatter.crawled(req, res, self.spider)
logline = logkws['msg'] % logkws['args']
self.assertEqual(logline,
"Crawled (200) <GET http://www.example.com> ['test', 'flag'] (referer: None)")
def test_dropped(self):
item = {}
exception = Exception(u"\u2018")

View File

@ -211,9 +211,15 @@ class BaseSettingsTest(unittest.TestCase):
'TEST_ENABLED1': '1',
'TEST_ENABLED2': True,
'TEST_ENABLED3': 1,
'TEST_ENABLED4': 'True',
'TEST_ENABLED5': 'true',
'TEST_ENABLED_WRONG': 'on',
'TEST_DISABLED1': '0',
'TEST_DISABLED2': False,
'TEST_DISABLED3': 0,
'TEST_DISABLED4': 'False',
'TEST_DISABLED5': 'false',
'TEST_DISABLED_WRONG': 'off',
'TEST_INT1': 123,
'TEST_INT2': '123',
'TEST_FLOAT1': 123.45,
@ -231,11 +237,15 @@ class BaseSettingsTest(unittest.TestCase):
self.assertTrue(settings.getbool('TEST_ENABLED1'))
self.assertTrue(settings.getbool('TEST_ENABLED2'))
self.assertTrue(settings.getbool('TEST_ENABLED3'))
self.assertTrue(settings.getbool('TEST_ENABLED4'))
self.assertTrue(settings.getbool('TEST_ENABLED5'))
self.assertFalse(settings.getbool('TEST_ENABLEDx'))
self.assertTrue(settings.getbool('TEST_ENABLEDx', True))
self.assertFalse(settings.getbool('TEST_DISABLED1'))
self.assertFalse(settings.getbool('TEST_DISABLED2'))
self.assertFalse(settings.getbool('TEST_DISABLED3'))
self.assertFalse(settings.getbool('TEST_DISABLED4'))
self.assertFalse(settings.getbool('TEST_DISABLED5'))
self.assertEqual(settings.getint('TEST_INT1'), 123)
self.assertEqual(settings.getint('TEST_INT2'), 123)
self.assertEqual(settings.getint('TEST_INTx'), 0)
@ -258,6 +268,8 @@ class BaseSettingsTest(unittest.TestCase):
self.assertEqual(settings.getdict('TEST_DICT3'), {})
self.assertEqual(settings.getdict('TEST_DICT3', {'key1': 5}), {'key1': 5})
self.assertRaises(ValueError, settings.getdict, 'TEST_LIST1')
self.assertRaises(ValueError, settings.getbool, 'TEST_ENABLED_WRONG')
self.assertRaises(ValueError, settings.getbool, 'TEST_DISABLED_WRONG')
def test_getpriority(self):
settings = BaseSettings({'key': 'value'}, priority=99)

View File

@ -345,7 +345,7 @@ Sitemap: /sitemap-relative-url.xml
'http://www.example.com/sitemap-relative-url.xml'])
class BaseSpiderDeprecationTest(unittest.TestCase):
class DeprecationTest(unittest.TestCase):
def test_basespider_is_deprecated(self):
with warnings.catch_warnings(record=True) as w:
@ -399,6 +399,29 @@ class BaseSpiderDeprecationTest(unittest.TestCase):
assert isinstance(CrawlSpider(name='foo'), Spider)
assert isinstance(CrawlSpider(name='foo'), BaseSpider)
def test_make_requests_from_url_deprecated(self):
class MySpider4(Spider):
name = 'spider1'
start_urls = ['http://example.com']
if __name__ == '__main__':
unittest.main()
class MySpider5(Spider):
name = 'spider2'
start_urls = ['http://example.com']
def make_requests_from_url(self, url):
return Request(url + "/foo", dont_filter=True)
with warnings.catch_warnings(record=True) as w:
# spider without overridden make_requests_from_url method
# doesn't issue a warning
spider1 = MySpider4()
self.assertEqual(len(list(spider1.start_requests())), 1)
self.assertEqual(len(w), 0)
# spider with overridden make_requests_from_url issues a warning,
# but the method still works
spider2 = MySpider5()
requests = list(spider2.start_requests())
self.assertEqual(len(requests), 1)
self.assertEqual(requests[0].url, 'http://example.com/foo')
self.assertEqual(len(w), 1)

View File

@ -60,7 +60,8 @@ def _responses(request, status_codes):
class TestHttpErrorMiddleware(TestCase):
def setUp(self):
self.spider = Spider('foo')
crawler = get_crawler(Spider)
self.spider = Spider.from_crawler(crawler, name='foo')
self.mw = HttpErrorMiddleware(Settings({}))
self.req = Request('http://scrapytest.org')
self.res200, self.res404 = _responses(self.req, [200, 404])
@ -73,10 +74,10 @@ class TestHttpErrorMiddleware(TestCase):
def test_process_spider_exception(self):
self.assertEquals([],
self.mw.process_spider_exception(self.res404, \
self.mw.process_spider_exception(self.res404,
HttpError(self.res404), self.spider))
self.assertEquals(None,
self.mw.process_spider_exception(self.res404, \
self.mw.process_spider_exception(self.res404,
Exception(), self.spider))
def test_handle_httpstatus_list(self):
@ -173,6 +174,12 @@ class TestHttpErrorMiddlewareIntegrational(TrialTestCase):
self.assertEqual(crawler.spider.parsed, {'200'})
self.assertEqual(crawler.spider.failed, {'404', '402', '500'})
get_value = crawler.stats.get_value
self.assertEqual(get_value('httperror/response_ignored_count'), 3)
self.assertEqual(get_value('httperror/response_ignored_status_count/404'), 1)
self.assertEqual(get_value('httperror/response_ignored_status_count/402'), 1)
self.assertEqual(get_value('httperror/response_ignored_status_count/500'), 1)
@defer.inlineCallbacks
def test_logging(self):
crawler = get_crawler(_HttpErrorSpider)

View File

@ -1,5 +1,6 @@
import os
from datetime import datetime
import shutil
from twisted.trial import unittest
from scrapy.extensions.spiderstate import SpiderState
@ -13,20 +14,23 @@ class SpiderStateTest(unittest.TestCase):
def test_store_load(self):
jobdir = self.mktemp()
os.mkdir(jobdir)
spider = Spider(name='default')
dt = datetime.now()
try:
spider = Spider(name='default')
dt = datetime.now()
ss = SpiderState(jobdir)
ss.spider_opened(spider)
spider.state['one'] = 1
spider.state['dt'] = dt
ss.spider_closed(spider)
ss = SpiderState(jobdir)
ss.spider_opened(spider)
spider.state['one'] = 1
spider.state['dt'] = dt
ss.spider_closed(spider)
spider2 = Spider(name='default')
ss2 = SpiderState(jobdir)
ss2.spider_opened(spider2)
self.assertEqual(spider.state, {'one': 1, 'dt': dt})
ss2.spider_closed(spider2)
spider2 = Spider(name='default')
ss2 = SpiderState(jobdir)
ss2.spider_opened(spider2)
self.assertEqual(spider.state, {'one': 1, 'dt': dt})
ss2.spider_closed(spider2)
finally:
shutil.rmtree(jobdir)
def test_state_attribute(self):
# state attribute must be present if jobdir is not set, to provide a

View File

@ -62,6 +62,27 @@ class BuildComponentListTest(unittest.TestCase):
self.assertRaises(ValueError, build_component_list, duplicate_bs,
convert=lambda x: x.lower())
def test_valid_numbers(self):
# work well with None and numeric values
d = {'a': 10, 'b': None, 'c': 15, 'd': 5.0}
self.assertEqual(build_component_list(d, convert=lambda x: x),
['d', 'a', 'c'])
d = {'a': 33333333333333333333, 'b': 11111111111111111111, 'c': 22222222222222222222}
self.assertEqual(build_component_list(d, convert=lambda x: x),
['b', 'c', 'a'])
# raise exception for invalid values
d = {'one': '5'}
self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
d = {'one': '1.0'}
self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
d = {'one': [1, 2, 3]}
self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
d = {'one': {'a': 'a', 'b': 2}}
self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
d = {'one': 'lorem ipsum',}
self.assertRaises(ValueError, build_component_list, {}, d, convert=lambda x: x)
class UtilsConfTestCase(unittest.TestCase):

View File

@ -31,5 +31,8 @@ class ProjectUtilsTest(unittest.TestCase):
def test_data_path_inside_project(self):
with inside_a_project() as proj_path:
expected = os.path.join(proj_path, '.scrapy', 'somepath')
self.assertEquals(expected, data_path('somepath'))
self.assertEquals(
os.path.realpath(expected),
os.path.realpath(data_path('somepath'))
)
self.assertEquals('/absolute/path', data_path('/absolute/path'))

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import unittest
from scrapy.http import Request
from scrapy.http import Request, FormRequest
from scrapy.spiders import Spider
from scrapy.utils.reqser import request_to_dict, request_from_dict
@ -25,7 +25,8 @@ class RequestSerializationTest(unittest.TestCase):
cookies={'currency': u'руб'},
encoding='latin-1',
priority=20,
meta={'a': 'b'})
meta={'a': 'b'},
flags=['testFlag'])
self._assert_serializes_ok(r)
def test_latin1_body(self):
@ -42,6 +43,7 @@ class RequestSerializationTest(unittest.TestCase):
self._assert_same_request(request, request2)
def _assert_same_request(self, r1, r2):
self.assertEqual(r1.__class__, r2.__class__)
self.assertEqual(r1.url, r2.url)
self.assertEqual(r1.callback, r2.callback)
self.assertEqual(r1.errback, r2.errback)
@ -53,6 +55,13 @@ class RequestSerializationTest(unittest.TestCase):
self.assertEqual(r1._encoding, r2._encoding)
self.assertEqual(r1.priority, r2.priority)
self.assertEqual(r1.dont_filter, r2.dont_filter)
self.assertEqual(r1.flags, r2.flags)
def test_request_class(self):
r = FormRequest("http://www.example.com")
self._assert_serializes_ok(r, spider=self.spider)
r = CustomRequest("http://www.example.com")
self._assert_serializes_ok(r, spider=self.spider)
def test_callback_serialization(self):
r = Request("http://www.example.com", callback=self.spider.parse_item,
@ -77,3 +86,7 @@ class TestSpider(Spider):
def handle_error(self, failure):
pass
class CustomRequest(Request):
pass

View File

@ -4,7 +4,7 @@ Tests borrowed from the twisted.web.client tests.
"""
import os
import six
from six.moves.urllib.parse import urlparse
import shutil
from twisted.trial import unittest
from twisted.web import server, static, util, resource
@ -12,6 +12,7 @@ from twisted.internet import reactor, defer
from twisted.test.proto_helpers import StringTransport
from twisted.python.filepath import FilePath
from twisted.protocols.policies import WrappingFactory
from twisted.internet.defer import inlineCallbacks
from scrapy.core.downloader import webclient as client
from scrapy.http import Request, Headers
@ -229,10 +230,10 @@ class WebClientTestCase(unittest.TestCase):
return reactor.listenTCP(0, site, interface="127.0.0.1")
def setUp(self):
name = self.mktemp()
os.mkdir(name)
FilePath(name).child("file").setContent(b"0123456789")
r = static.File(name)
self.tmpname = self.mktemp()
os.mkdir(self.tmpname)
FilePath(self.tmpname).child("file").setContent(b"0123456789")
r = static.File(self.tmpname)
r.putChild(b"redirect", util.Redirect(b"/file"))
r.putChild(b"wait", ForeverTakingResource())
r.putChild(b"error", ErrorResource())
@ -246,8 +247,10 @@ class WebClientTestCase(unittest.TestCase):
self.port = self._listen(self.wrapper)
self.portno = self.port.getHost().port
@inlineCallbacks
def tearDown(self):
return self.port.stopListening()
yield self.port.stopListening()
shutil.rmtree(self.tmpname)
def getURL(self, path):
return "http://127.0.0.1:%d/%s" % (self.portno, path)
@ -266,7 +269,6 @@ class WebClientTestCase(unittest.TestCase):
getPage(self.getURL("host"), headers={"Host": "www.example.com"}).addCallback(
self.assertEquals, to_bytes("www.example.com"))])
def test_getPage(self):
"""
L{client.getPage} returns a L{Deferred} which is called back with
@ -276,7 +278,6 @@ class WebClientTestCase(unittest.TestCase):
d.addCallback(self.assertEquals, b"0123456789")
return d
def test_getPageHead(self):
"""
L{client.getPage} returns a L{Deferred} which is called back with
@ -289,7 +290,6 @@ class WebClientTestCase(unittest.TestCase):
_getPage("head").addCallback(self.assertEqual, b""),
_getPage("HEAD").addCallback(self.assertEqual, b"")])
def test_timeoutNotTriggering(self):
"""
When a non-zero timeout is passed to L{getPage} and the page is
@ -301,7 +301,6 @@ class WebClientTestCase(unittest.TestCase):
self.assertEquals, to_bytes("127.0.0.1:%d" % self.portno))
return d
def test_timeoutTriggering(self):
"""
When a non-zero timeout is passed to L{getPage} and that many
@ -351,7 +350,7 @@ class WebClientTestCase(unittest.TestCase):
b' </head>\n <body bgcolor="#FFFFFF" text="#000000">\n '
b'<a href="/file">click here</a>\n </body>\n</html>\n')
def test_Encoding(self):
def test_encoding(self):
""" Test that non-standart body encoding matches
Content-Encoding header """
body = b'\xd0\x81\xd1\x8e\xd0\xaf'

21
tox.ini
View File

@ -21,16 +21,16 @@ passenv =
commands =
py.test --cov=scrapy --cov-report= {posargs:scrapy tests}
[testenv:precise]
[testenv:trusty]
basepython = python2.7
deps =
pyOpenSSL==0.13
lxml==2.3.2
Twisted==11.1.0
boto==2.2.2
Pillow<2.0
lxml==3.3.3
Twisted==13.2.0
boto==2.20.1
Pillow==2.3.0
cssselect==0.9.1
zope.interface==3.6.1
zope.interface==4.0.5
-rtests/requirements.txt
[testenv:jessie]
@ -54,6 +54,11 @@ commands =
pip install -U https://github.com/scrapy/queuelib/archive/master.zip#egg=queuelib
py.test --cov=scrapy --cov-report= {posargs:scrapy tests}
[testenv:pypy]
basepython = pypy
commands =
py.test {posargs:scrapy tests}
[testenv:py33]
basepython = python3.3
deps =
@ -70,6 +75,10 @@ deps = {[testenv:py33]deps}
basepython = python3.5
deps = {[testenv:py33]deps}
[testenv:py36]
basepython = python3.6
deps = {[testenv:py33]deps}
[docs]
changedir = docs
deps =