Merge remote-tracking branch 'origin/master' into feed-path

This commit is contained in:
Adrian Chaves 2026-07-10 16:09:07 +02:00
commit 87b90a66f8
170 changed files with 3127 additions and 979 deletions

View File

@ -79,9 +79,6 @@ jobs:
- python-version: "3.14"
env:
TOXENV: botocore
- python-version: "3.14"
env:
TOXENV: mitmproxy
steps:
- uses: actions/checkout@v6
@ -97,6 +94,9 @@ jobs:
sudo apt-get update
sudo apt-get install libxml2-dev libxslt-dev
- name: Install mitmproxy
run: pipx install mitmproxy
- name: Run tests
env: ${{ matrix.env }}
run: |

View File

@ -6,7 +6,7 @@ exclude: |
)
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.2
rev: v0.15.20
hooks:
- id: ruff-check
args: [ --fix ]
@ -16,7 +16,7 @@ repos:
hooks:
- id: blacken-docs
additional_dependencies:
- black==25.9.0
- black==26.5.1
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:

View File

@ -4,8 +4,8 @@
| Version | Supported |
| ------- | ------------------ |
| 2.16.x | :white_check_mark: |
| < 2.16.x | :x: |
| 2.17.x | :white_check_mark: |
| < 2.17.x | :x: |
## Reporting a Vulnerability

View File

@ -11,7 +11,7 @@ from scrapy.utils.reactor import set_asyncio_event_loop_policy
from scrapy.utils.reactorless import install_reactor_import_hook
from tests.keys import generate_keys
from tests.mockserver.http import MockServer
from tests.mockserver.mitm_proxy import MitmProxy
from tests.mockserver.mitm_proxy import MitmProxy, mitmdump_cmd
if TYPE_CHECKING:
from collections.abc import Generator
@ -127,7 +127,6 @@ def pytest_runtest_setup(item):
"uvloop",
"botocore",
"boto3",
"mitmproxy",
]
for module in optional_deps:
@ -137,6 +136,9 @@ def pytest_runtest_setup(item):
except ImportError:
pytest.skip(f"{module} is not installed")
if item.get_closest_marker("requires_mitmproxy") and mitmdump_cmd() is None:
pytest.skip("mitmdump is not available")
# Generate localhost certificate files, needed by some tests
generate_keys()

View File

@ -1,68 +0,0 @@
:orphan:
======================================
Scrapy documentation quick start guide
======================================
This file provides a quick guide on how to compile the Scrapy documentation.
Setup the environment
---------------------
To compile the documentation you need Sphinx Python library. To install it
and all its dependencies run the following command from this dir
::
pip install -r requirements.txt
Compile the documentation
-------------------------
To compile the documentation (to classic HTML output) run the following command
from this dir::
make html
Documentation will be generated (in HTML format) inside the ``build/html`` dir.
View the documentation
----------------------
To view the documentation run the following command::
make htmlview
This command will fire up your default browser and open the main page of your
(previously generated) HTML documentation.
Start over
----------
To clean up all generated documentation files and start from scratch run::
make clean
Keep in mind that this command won't touch any documentation source files.
Recreating documentation on the fly
-----------------------------------
There is a way to recreate the doc automatically when you make changes, you
need to install watchdog (``pip install watchdog``) and then use::
make watch
Alternative method using tox
----------------------------
To compile the documentation to HTML run the following command::
tox -e docs
Documentation will be generated inside the ``docs/_build/all`` dir.

View File

@ -323,9 +323,10 @@ deprecation removals are documented in the :ref:`release notes <news>`.
Tests
=====
Tests are implemented using the :doc:`Twisted unit-testing framework
<twisted:development/test-standard>`. Running tests requires
:doc:`tox <tox:index>`.
Tests are implemented using pytest_. Running tests requires :doc:`tox
<tox:index>`.
.. _pytest: https://pytest.org
.. _running-tests:
@ -371,6 +372,21 @@ To see coverage report install :doc:`coverage <coverage:index>`
see output of ``coverage --help`` for more options like html or xml report.
Some tests need a ``mitmdump`` executable (from mitmproxy_) to test against a
fully featured proxy server; they are skipped when one cannot be found
(``mitmproxy`` is intentionally not a test dependency that would be installed
into test venvs, as that sometimes leads to various dependency conflicts).
To run these tests, make ``mitmdump`` available in one of these ways:
* install ``mitmproxy`` so that ``mitmdump`` is on your ``PATH``, e.g. with
pipx_ (``pipx install mitmproxy``) or uv_ (``uv tool install mitmproxy``);
* have uv_ installed, in which case the tests will run
``uvx --from mitmproxy mitmdump``;
* set the ``MITMDUMP`` environment variable to the path of a ``mitmdump``
executable.
Writing tests
-------------
@ -398,3 +414,6 @@ And their unit-tests are in::
.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist
.. _help wanted issues: https://github.com/scrapy/scrapy/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
.. _test coverage: https://app.codecov.io/gh/scrapy/scrapy
.. _mitmproxy: https://mitmproxy.org/
.. _pipx: https://pipx.pypa.io/
.. _uv: https://docs.astral.sh/uv/

View File

@ -285,7 +285,8 @@ consume a lot of memory.
In order to avoid parsing all the entire feed at once in memory, you can use
the :func:`~scrapy.utils.iterators.xmliter_lxml` and
:func:`~scrapy.utils.iterators.csviter` functions. In fact, this is what
:class:`~scrapy.spiders.XMLFeedSpider` uses.
:class:`~scrapy.spiders.XMLFeedSpider` and
:class:`~scrapy.spiders.CSVFeedSpider` use.
.. autofunction:: scrapy.utils.iterators.xmliter_lxml
@ -360,10 +361,11 @@ method for this purpose. For example:
def process_spider_output(self, response, result):
for item_or_request in result:
if isinstance(item_or_request, Request):
yield item_or_request
continue
adapter = ItemAdapter(item)
adapter = ItemAdapter(item_or_request)
for _ in range(adapter["multiply_by"]):
yield deepcopy(item)
yield deepcopy(item_or_request)
Does Scrapy support IPv6 addresses?
-----------------------------------
@ -409,7 +411,6 @@ How can I make a blank request?
from scrapy import Request
blank_request = Request("data:,")
In this case, the URL is set to a data URI scheme. Data URLs allow you to include data

View File

@ -24,7 +24,7 @@ Having trouble? We'd like to help!
* Ask or search questions in `StackOverflow using the scrapy tag`_.
* Ask or search questions in the `Scrapy subreddit`_.
* Search for questions on the archives of the `scrapy-users mailing list`_.
* Ask a question in the `#scrapy IRC channel`_,
* Ask a question in the `#scrapy IRC channel`_.
* Report bugs with Scrapy in our `issue tracker`_.
* Join the Discord community `Scrapy Discord`_.
@ -91,15 +91,15 @@ Basic concepts
:doc:`topics/selectors`
Extract the data from web pages using XPath.
:doc:`topics/shell`
Test your extraction code in an interactive environment.
:doc:`topics/items`
Define the data you want to scrape.
:doc:`topics/loaders`
Populate your items with the extracted data.
:doc:`topics/shell`
Test your extraction code in an interactive environment.
:doc:`topics/item-pipeline`
Post-process and store your scraped data.
@ -151,6 +151,7 @@ Solving specific problems
topics/debug
topics/contracts
topics/practices
topics/security
topics/broad-crawls
topics/developer-tools
topics/dynamic-content
@ -175,6 +176,10 @@ Solving specific problems
:doc:`topics/practices`
Get familiar with some Scrapy common practices.
:doc:`topics/security`
Understand the security implications of Scrapy defaults and how to harden
them.
:doc:`topics/broad-crawls`
Tune Scrapy for crawling a lot domains in parallel.

View File

@ -90,9 +90,10 @@ to figure these settings out automatically.
.. note::
This is using :ref:`feed exports <topics-feed-exports>` to generate the
JSON file, you can easily change the export format (XML or CSV, for example) or the
storage backend (FTP or `Amazon S3`_, for example). You can also write an
:ref:`item pipeline <topics-item-pipeline>` to store the items in a database.
JSON Lines file, you can easily change the export format (XML or CSV, for
example) or the storage backend (FTP or `Amazon S3`_, for example). You can
also write an :ref:`item pipeline <topics-item-pipeline>` to store the
items in a database.
.. _topics-whatelse:

View File

@ -3,6 +3,330 @@
Release notes
=============
.. _release-2.17.0:
Scrapy 2.17.0 (2026-07-07)
--------------------------
Highlights:
- Security bug fixes
- HTTP/2 and SOCKS proxy support for ``HttpxDownloadHandler``
- Improved settings for changing allowed TLS versions
Security bug fixes
~~~~~~~~~~~~~~~~~~
- ``s3://`` requests now use HTTPS by default, instead of plaintext HTTP.
Previously, :class:`~scrapy.core.downloader.handlers.s3.S3DownloadHandler`
sent signed S3 requests over plaintext HTTP unless
``request.meta["is_secure"]`` was set to a true value, exposing the request
path, the AWS ``Authorization`` header, the ``X-Amz-Security-Token`` header
(when using temporary credentials), and the response contents to network
attackers, who could also tamper with responses. See the `76g3-c3x4-crvx`_
security advisory for details.
To restore the previous behavior for a given request, set
``request.meta["is_secure"]`` to ``False``.
.. _76g3-c3x4-crvx: https://github.com/scrapy/scrapy/security/advisories/GHSA-76g3-c3x4-crvx
Deprecations
~~~~~~~~~~~~
- The ``DOWNLOADER_CLIENT_TLS_METHOD`` setting is deprecated. You should use
the :setting:`DOWNLOAD_TLS_MIN_VERSION` and/or
:setting:`DOWNLOAD_TLS_MAX_VERSION` settings instead if you want to change
the TLS method selection.
(:issue:`3288`, :issue:`6546`)
- The following spider attributes are deprecated in favor of settings:
- ``http_user`` (use :setting:`HTTPAUTH_USER`)
- ``http_pass`` (use :setting:`HTTPAUTH_PASS`)
- ``http_auth_domain`` (use :setting:`HTTPAUTH_DOMAIN`)
(:issue:`7590`)
- The ``scrapy.commands.ScrapyCommand.help()`` method is deprecated. It was
never called by Scrapy.
(:issue:`7626`, :issue:`7633`)
- The following TLS-related functions and constants, intended for internal
use, are deprecated:
- ``scrapy.core.downloader.tls.METHOD_TLS``
- ``scrapy.core.downloader.tls.METHOD_TLSv10``
- ``scrapy.core.downloader.tls.METHOD_TLSv11``
- ``scrapy.core.downloader.tls.METHOD_TLSv12``
- ``scrapy.core.downloader.tls.openssl_methods``
- ``scrapy.core.downloader.tls.DEFAULT_CIPHERS``
- ``scrapy.utils.ssl.ffi_buf_to_string()``
- ``scrapy.utils.ssl.get_temp_key_info()``
- ``scrapy.utils.ssl.x509name_to_string()``
(:issue:`6546`, :issue:`7619`, :issue:`7665`)
- The ``CRAWLSPIDER_FOLLOW_LINKS`` setting is deprecated. You can set
``follow=False`` in your rules to achieve the same effect.
(:issue:`7592`)
- Instantiating
:class:`~scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware`
without a ``crawler`` argument is deprecated.
(:issue:`7655`)
- Instantiating
:class:`~scrapy.spidermiddlewares.referer.RefererMiddleware` without a
``settings`` argument is deprecated.
(:issue:`7664`)
New features
~~~~~~~~~~~~
- Added support for HTTP/2 requests to
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`. It
requires setting the new :setting:`HTTPX_HTTP2_ENABLED` setting to
``True``.
(:issue:`7575`)
- Added support for SOCKS proxies to
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`.
(:issue:`747`, :issue:`7575`)
- Added :setting:`DOWNLOAD_TLS_MIN_VERSION` and
:setting:`DOWNLOAD_TLS_MAX_VERSION` settings as replacements for the
``DOWNLOADER_CLIENT_TLS_METHOD`` setting (which is now deprecated).
Compared to the old setting, they support specifying a range of allowed
versions and support newer TLS versions.
(:issue:`4821`, :issue:`6546`)
- Added :setting:`HTTPAUTH_USER`, :setting:`HTTPAUTH_PASS` and
:setting:`HTTPAUTH_DOMAIN` settings and :reqmeta:`http_user`,
:reqmeta:`http_pass` and :reqmeta:`http_auth_domain` meta keys as more
flexible ways to set HTTP authentication data.
(:issue:`7590`)
- Added a :reqmeta:`verbatim_url` meta key that can be set to ``True`` to
skip request URL canonicalization.
(:issue:`7473`)
- Added ``deny_tags`` and ``deny_attrs`` arguments to :class:`LinkExtractor
<scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor>`.
(:issue:`6321`, :issue:`7679`)
- :attr:`scrapy.Item.fields` now returns the fields in the definition order
instead of the alphabetical one.
(:issue:`7015`, :issue:`7694`)
- Added a :setting:`RETRY_GIVE_UP_LOG_LEVEL` setting, a
:reqmeta:`give_up_log_level` meta key and a ``give_up_log_level`` argument
of the
:func:`~scrapy.downloadermiddlewares.retry.get_retry_request` function that
allow changing the log level of the message logged when the retry limit has
been reached.
(:issue:`4622`, :issue:`5297`, :issue:`7567`)
- It's now possible to set :setting:`DOWNLOADER_CLIENT_TLS_CIPHERS` to
``None`` to use the default ciphers of the underlying TLS implementation.
(:issue:`7499`, :issue:`7665`)
Improvements
~~~~~~~~~~~~
- :class:`~scrapy.FormRequest` is no longer deprecated, only its
``from_response()`` method is still deprecated.
(:issue:`7561`, :issue:`7671`)
- Switched the item definition in the default project template from a
:class:`scrapy.item.Item` to a dataclass.
(:issue:`7493`, :issue:`7513`)
- Fixed deprecation warnings with pyOpenSSL 26.3.0.
(:issue:`7619`)
- Removed the runtime warnings for :attr:`Spider.allowed_domains
<scrapy.Spider.allowed_domains>` containing URLs or domains with ports
instead of just domains and for spider classes having a ``start_url``
attribute instead of :class:`~scrapy.spiders.Spider.start_urls`. Please use
:doc:`scrapy-lint <scrapy-lint:index>` to find mistakes in your spider code
instead.
(:issue:`4421`, :issue:`7627`)
- :func:`scrapy.utils.test.get_crawler` now disables
:setting:`TELNETCONSOLE_ENABLED` by default.
(:issue:`7644`)
- Other code refactoring and improvements.
(:issue:`7409`, :issue:`7593`, :issue:`7594`, :issue:`7611`, :issue:`7649`)
Bug fixes
~~~~~~~~~
- :class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler` no
longer ignores proxy credentials for redirected or retried requests.
(:issue:`7601`, :issue:`7630`)
- :class:`~scrapy.extensions.feedexport.GCSFeedStorage` now closes the
temporary file after the upload.
(:issue:`7546`)
- Fixed ``scrapy shell <URL>`` running a full spider crawl when there is a
spider for the requested URL. This bug was introduced in Scrapy 2.13.0.
(:issue:`7552`, :issue:`7557`)
- The :setting:`IMAGES_STORE_S3_ACL` and :setting:`IMAGES_STORE_GCS_ACL`
settings are no longer ignored. This bug was introduced in Scrapy 2.12.0.
(:issue:`7597`, :issue:`7614`)
- :class:`~scrapy.core.downloader.handlers.ftp.FTPDownloadHandler` now closes
the connection after making the request.
(:issue:`7602`, :issue:`7667`)
- Removed the deprecated ``spider`` argument from the pipeline defined in the
default project template.
(:issue:`7676`)
- Fixed ``scrapy genspider --edit`` not working.
(:issue:`7260`, :issue:`7683`)
- When a :class:`~scrapy.crawler.Crawler` instance is passed to
:meth:`AsyncCrawlerRunner.create_crawler()
<scrapy.crawler.AsyncCrawlerRunner.create_crawler>` or
:meth:`CrawlerRunner.create_crawler()
<scrapy.crawler.CrawlerRunner.create_crawler>`, settings from both classes
are now merged, previously only the settings from the
:class:`~scrapy.crawler.Crawler` instance were used.
(:issue:`1280`, :issue:`7647`)
- Fixed several issues with cookie handling in
:func:`scrapy.utils.request.request_to_curl`.
(:issue:`7603`, :issue:`7675`, :issue:`7684`)
- Fixed :class:`scrapy.resolver.CachingThreadedResolver` not disabling the
cache when :setting:`DNSCACHE_ENABLED` is set to ``False``.
(:issue:`7663`)
- Fixed :func:`scrapy.utils.response.open_in_browser` not removing comments
when looking for the ``<base>`` tag.
(:issue:`7506`)
- Fixed checking for deprecated methods in custom :setting:`ITEM_PROCESSOR`
implementations.
(:issue:`7589`)
- Fixed :func:`scrapy.utils.url.strip_url` corrupting some URLs with
credentials.
(:issue:`7604`, :issue:`7605`)
- :func:`scrapy.utils.misc.rel_has_nofollow` now ignores the case when
looking for "nofollow" strings.
(:issue:`7632`)
- Fixed an exception in :class:`scrapy.utils.sitemap.Sitemap` when parsing
some malformed sitemaps.
(:issue:`7686`, :issue:`7687`)
Documentation
~~~~~~~~~~~~~
- Mentioned :doc:`scrapy-lint <scrapy-lint:index>` in the docs.
(:issue:`4421`, :issue:`7627`)
- Added the docs about :ref:`security considerations <security>`.
(:issue:`7389`, :issue:`7678`)
- Improved the :ref:`item pipeline docs <topics-item-pipeline>`.
(:issue:`2350`, :issue:`7676`)
- Documented which stats are collected by
:class:`~scrapy.extensions.corestats.CoreStats`.
(:issue:`7421`)
- Switched documentation examples from using :class:`scrapy.item.Item` to
using dataclasses.
(:issue:`7493`, :issue:`7513`)
- Added feature comparison tables to the :ref:`download handler
<download-handlers-ref>` docs.
(:issue:`7575`)
- Improved the docs for :ref:`logging settings <logging-settings>`.
(:issue:`6909`, :issue:`7668`)
- Documented a way to :ref:`improve startup time and memory usage
<large-project-startup>` by using :setting:`SPIDER_MODULES`.
(:issue:`7576`, :issue:`7600`)
- Clarified handling of the ``type`` argument of :class:`~scrapy.Selector`.
(:issue:`7704`)
- Other documentation improvements and fixes.
(:issue:`4954`,
:issue:`6120`,
:issue:`7286`,
:issue:`7564`,
:issue:`7573`,
:issue:`7598`,
:issue:`7599`,
:issue:`7698`)
Quality assurance
~~~~~~~~~~~~~~~~~
- Fixed deprecation warnings with pytest 9.1.0.
(:issue:`7621`)
- Type hints improvements and fixes.
(:issue:`6958`, :issue:`7586`)
- CI and test improvements and fixes.
(:issue:`5954`,
:issue:`7002`,
:issue:`7017`,
:issue:`7247`,
:issue:`7508`,
:issue:`7545`,
:issue:`7566`,
:issue:`7574`,
:issue:`7585`,
:issue:`7595`,
:issue:`7608`,
:issue:`7610`,
:issue:`7612`,
:issue:`7616`,
:issue:`7625`,
:issue:`7637`,
:issue:`7639`,
:issue:`7640`,
:issue:`7641`,
:issue:`7642`,
:issue:`7643`,
:issue:`7644`,
:issue:`7645`,
:issue:`7646`,
:issue:`7654`,
:issue:`7655`,
:issue:`7664`,
:issue:`7672`,
:issue:`7677`,
:issue:`7680`,
:issue:`7682`,
:issue:`7692`)
.. _release-2.16.0:
Scrapy 2.16.0 (2026-05-19)
@ -711,7 +1035,7 @@ Deprecations
New features
~~~~~~~~~~~~
- Added a new setting, :setting:`REFERER_POLICIES`, to allow customizing
- Added a new setting, :setting:`REFERRER_POLICIES`, to allow customizing
supported referrer policies.
Bug fixes
@ -5722,7 +6046,7 @@ The following changes may impact custom priority queue classes:
* A new keyword parameter has been added: ``key``. It is a string
that is always an empty string for memory queues and indicates the
:setting:`JOB_DIR` value for disk queues.
:setting:`JOBDIR` value for disk queues.
* The parameter for disk queues that contains data from the previous
crawl, ``startprios`` or ``slot_startprios``, is now passed as a
@ -6243,8 +6567,8 @@ Backward-incompatible changes
``429``, you must override :setting:`RETRY_HTTP_CODES` accordingly.
* :class:`~scrapy.crawler.Crawler`,
:class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>` and
:class:`CrawlerRunner.create_crawler <scrapy.crawler.CrawlerRunner.create_crawler>`
:meth:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>` and
:meth:`CrawlerRunner.create_crawler <scrapy.crawler.CrawlerRunner.create_crawler>`
no longer accept a :class:`~scrapy.spiders.Spider` subclass instance, they
only accept a :class:`~scrapy.spiders.Spider` subclass now.
@ -6276,7 +6600,7 @@ New features
``scrapy.pqueues.DownloaderAwarePriorityQueue``, may be
:ref:`enabled <broad-crawls-scheduler-priority-queue>` for a significant
scheduling improvement on crawls targeting multiple web domains, at the
cost of no :setting:`CONCURRENT_REQUESTS_PER_IP` support (:issue:`3520`)
cost of no ``CONCURRENT_REQUESTS_PER_IP`` support (:issue:`3520`)
* A new :attr:`.Request.cb_kwargs` attribute
provides a cleaner way to pass keyword arguments to callback methods
@ -8755,7 +9079,7 @@ New features and settings
- In request errbacks, offending requests are now received in ``failure.request`` attribute (:rev:`2738`)
- Big downloader refactoring to support per domain/ip concurrency limits (:rev:`2732`)
- ``CONCURRENT_REQUESTS_PER_SPIDER`` setting has been deprecated and replaced by:
- :setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`, :setting:`CONCURRENT_REQUESTS_PER_IP`
- :setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`, ``CONCURRENT_REQUESTS_PER_IP``
- check the documentation for more details
- Added builtin caching DNS resolver (:rev:`2728`)
- Moved Amazon AWS-related components/extensions (SQS spider queue, SimpleDB stats collector) to a separate project: [scaws](https://github.com/scrapinghub/scaws) (:rev:`2706`, :rev:`2714`)

View File

@ -168,7 +168,6 @@ Use a fallback component:
from scrapy.utils.misc import build_from_crawler, load_object
FALLBACK_SETTING = "MY_FALLBACK_DOWNLOAD_HANDLER"

View File

@ -105,6 +105,9 @@ Scrapy API requires passing a Deferred to it) using the following helpers:
.. autofunction:: scrapy.utils.defer.deferred_from_coro
.. autofunction:: scrapy.utils.defer.deferred_f_from_coro_f
The following function helps with a reverse wrapping:
.. autofunction:: scrapy.utils.defer.ensure_awaitable
@ -190,7 +193,7 @@ in future Scrapy versions. The following features are not available:
:class:`~scrapy.crawler.CrawlerProcess`
(:class:`~scrapy.crawler.AsyncCrawlerProcess` and
:class:`~scrapy.crawler.AsyncCrawlerRunner` are available)
* Twisted-specific DNS resolvers (the :setting:`DNS_RESOLVER` setting)
* Twisted-specific DNS resolvers (the :setting:`TWISTED_DNS_RESOLVER` setting)
* User and 3rd-party code that requires a reactor (see :ref:`below
<asyncio-without-reactor-migrate>` for examples)
@ -315,8 +318,7 @@ implementations, :class:`~asyncio.ProactorEventLoop` (default) and
:class:`~asyncio.SelectorEventLoop` works with Twisted.
Scrapy changes the event loop class to :class:`~asyncio.SelectorEventLoop`
automatically when you change the :setting:`TWISTED_REACTOR` setting or call
:func:`~scrapy.utils.reactor.install_reactor`.
automatically when installing the asyncio reactor.
.. note:: Other libraries you use may require
:class:`~asyncio.ProactorEventLoop`, e.g. because it supports

View File

@ -88,6 +88,8 @@ server) is, and this extension builds on that premise.
Prevent specific requests from triggering slot delay adjustments
================================================================
.. versionadded:: 2.12.0
AutoThrottle adjusts the delay of download slots based on the latencies of
responses that belong to that download slot. The only exceptions are non-200
responses, which are only taken into account to increase that delay, but

View File

@ -199,6 +199,7 @@ Global commands:
* :command:`fetch`
* :command:`view`
* :command:`version`
* :command:`bench`
Project-only commands:
@ -207,7 +208,6 @@ Project-only commands:
* :command:`list`
* :command:`edit`
* :command:`parse`
* :command:`bench`
.. command:: startproject
@ -476,7 +476,7 @@ Supported options:
* ``--spider=SPIDER``: bypass spider autodetection and force use of specific spider
* ``--a NAME=VALUE``: set spider argument (may be repeated)
* ``-a NAME=VALUE``: set spider argument (may be repeated)
* ``--callback`` or ``-c``: spider method to use as callback for parsing the
response

View File

@ -268,5 +268,5 @@ You can also send multiple requests in parallel:
yield {
"h1": response.css("h1::text").get(),
"price": responses[0].css(".price::text").get(),
"price2": responses[1].css(".color::text").get(),
"color": responses[1].css(".color::text").get(),
}

View File

@ -39,6 +39,10 @@ for additional schemes and to replace or disable default ones:
"sftp": "my.download_handlers.SftpHandler",
}
.. seealso:: :ref:`security-unencrypted-protocols` and
:ref:`security-local-resources`, for the security implications of the
default ``http``, ``ftp``, ``file`` and ``data`` handlers.
Replacing HTTP(S) download handlers
-----------------------------------
@ -325,13 +329,13 @@ Other limitations:
HTTPX_HTTP2_ENABLED
^^^^^^^^^^^^^^^^^^^
.. versionadded:: 2.17.0
Default: ``False``
Whether to enable HTTP/2 support in this handler. The ``httpx[http2]`` extra
needs to be installed if you want to enable this setting.
.. versionadded:: VERSION
Built-in non-HTTP download handlers reference
=============================================

View File

@ -351,6 +351,8 @@ HttpAuthMiddleware
HTTPAUTH_USER
~~~~~~~~~~~~~
.. versionadded:: 2.17.0
Default: ``""``
The username to use for HTTP basic authentication, applied to all requests
@ -361,6 +363,8 @@ whose URL matches :setting:`HTTPAUTH_DOMAIN`.
HTTPAUTH_PASS
~~~~~~~~~~~~~
.. versionadded:: 2.17.0
Default: ``""``
The password to use for HTTP basic authentication.
@ -370,6 +374,8 @@ The password to use for HTTP basic authentication.
HTTPAUTH_DOMAIN
~~~~~~~~~~~~~~~
.. versionadded:: 2.17.0
Default: ``None``
The domain (and its subdomains) to which HTTP basic authentication credentials
@ -379,6 +385,8 @@ that this risks leaking credentials to unrelated domains.
This setting must be explicitly configured whenever :setting:`HTTPAUTH_USER`
or :setting:`HTTPAUTH_PASS` is set.
.. seealso:: :ref:`security-credential-leakage`
.. _Basic access authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
@ -497,7 +505,7 @@ Filesystem storage backend (default)
* ``response_body`` - the plain response body
* ``response_headers`` - the request headers (in raw HTTP format)
* ``response_headers`` - the response headers (in raw HTTP format)
* ``meta`` - some metadata of this cache resource in Python ``repr()``
format (grep-friendly format)
@ -539,7 +547,7 @@ defines the methods described below.
.. method:: open_spider(spider)
This method gets called after a spider has been opened for crawling. It handles
the :signal:`open_spider <spider_opened>` signal.
the :signal:`spider_opened` signal.
:param spider: the spider which has been opened
:type spider: :class:`~scrapy.Spider` object
@ -547,7 +555,7 @@ defines the methods described below.
.. method:: close_spider(spider)
This method gets called after a spider has been closed. It handles
the :signal:`close_spider <spider_closed>` signal.
the :signal:`spider_closed` signal.
:param spider: the spider which has been closed
:type spider: :class:`~scrapy.Spider` object
@ -806,7 +814,6 @@ HttpProxyMiddleware settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. setting:: HTTPPROXY_ENABLED
.. setting:: HTTPPROXY_AUTH_ENCODING
HTTPPROXY_ENABLED
^^^^^^^^^^^^^^^^^
@ -815,6 +822,8 @@ Default: ``True``
Whether or not to enable the :class:`HttpProxyMiddleware`.
.. setting:: HTTPPROXY_AUTH_ENCODING
HTTPPROXY_AUTH_ENCODING
^^^^^^^^^^^^^^^^^^^^^^^
@ -976,7 +985,7 @@ Whether the Meta Refresh middleware will be enabled.
METAREFRESH_IGNORE_TAGS
^^^^^^^^^^^^^^^^^^^^^^^
Default: ``[]``
Default: ``["noscript"]``
Meta tags within these tags are ignored.
@ -1083,7 +1092,7 @@ Default::
'twisted.internet.error.ConnectionDone',
'twisted.internet.error.ConnectError',
'twisted.internet.error.ConnectionLost',
IOError,
OSError,
'scrapy.core.downloader.handlers.http11.TunnelError',
]
@ -1102,6 +1111,8 @@ exception propagation, see
RETRY_GIVE_UP_LOG_LEVEL
^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 2.17.0
Default: ``"ERROR"``
:ref:`Logging level <levels>` used for the message logged when a request

View File

@ -133,7 +133,7 @@ data from it depends on the type of response:
.. code-block:: python
selector = Selector(data["html"])
selector = Selector(text=data["html"])
- If the response is JavaScript, or HTML with a ``<script/>`` element
containing the desired data, see :ref:`topics-parsing-javascript`.

View File

@ -31,7 +31,7 @@ For example:
.. code-block:: python
def parse_page(self, response):
if "Bandwidth exceeded" in response.body:
if "Bandwidth exceeded" in response.text:
raise CloseSpider("bandwidth_exceeded")
DontCloseSpider

View File

@ -153,7 +153,7 @@ output examples, which assume you're exporting these two items:
BaseItemExporter
----------------
.. class:: BaseItemExporter(fields_to_export=None, export_empty_fields=False, encoding='utf-8', indent=0, dont_fail=False)
.. class:: BaseItemExporter(fields_to_export=None, export_empty_fields=False, encoding=None, indent=None, dont_fail=False)
This is the (abstract) base class for all Item Exporters. It provides
support for common features used by all (concrete) Item Exporters, such as
@ -239,7 +239,7 @@ BaseItemExporter
.. attribute:: indent
Amount of spaces used to indent the output on each level. Defaults to ``0``.
Amount of spaces used to indent the output on each level. Defaults to ``None``.
* ``indent=None`` selects the most compact representation,
all items in the same line with no indentation
@ -328,7 +328,7 @@ CsvItemExporter
:param join_multivalued: The char (or chars) that will be used for joining
multi-valued fields, if found.
:type include_headers_line: str
:type join_multivalued: str
:param errors: The optional string that specifies how encoding and decoding
errors are to be handled. For more information see
@ -342,14 +342,14 @@ CsvItemExporter
A typical output of this exporter would be::
product,price
name,price
Color TV,1200
DVD player,200
PickleItemExporter
------------------
.. class:: PickleItemExporter(file, protocol=0, **kwargs)
.. class:: PickleItemExporter(file, protocol=4, **kwargs)
Exports items in pickle format to the given file-like object.

View File

@ -341,9 +341,6 @@ closing the spider. If the spider generates more than that number of errors,
it will be closed with the reason ``closespider_errorcount``. If zero (or non
set), spiders won't be closed by number of errors.
.. module:: scrapy.extensions.debug
:synopsis: Extensions for debugging Scrapy
.. module:: scrapy.extensions.periodic_log
:synopsis: Periodic stats logging
@ -418,7 +415,7 @@ Example extension configuration:
custom_settings = {
"LOG_LEVEL": "INFO",
"PERIODIC_LOG_STATS": {
"include": ["downloader/", "scheduler/", "log_count/", "item_scraped_count/"],
"include": ["downloader/", "scheduler/", "log_count/", "item_scraped_count"],
},
"PERIODIC_LOG_DELTA": {"include": ["downloader/"]},
"PERIODIC_LOG_TIMING_ENABLED": True,
@ -463,6 +460,9 @@ Default: ``False``
Debugging extensions
--------------------
.. module:: scrapy.extensions.debug
:synopsis: Extensions for debugging Scrapy
Stack trace dump extension
~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -528,10 +528,6 @@ safe numeric encoding (``\uXXXX`` sequences) for historic reasons.
Use ``"utf-8"`` if you want UTF-8 for JSON too.
.. versionchanged:: 2.8
The :command:`startproject` command now sets this setting to
``"utf-8"`` in the generated ``settings.py`` file.
.. setting:: FEED_EXPORT_FIELDS
FEED_EXPORT_FIELDS
@ -619,6 +615,7 @@ Default:
"file": "scrapy.extensions.feedexport.FileFeedStorage",
"stdout": "scrapy.extensions.feedexport.StdoutFeedStorage",
"s3": "scrapy.extensions.feedexport.S3FeedStorage",
"gs": "scrapy.extensions.feedexport.GCSFeedStorage",
"ftp": "scrapy.extensions.feedexport.FTPFeedStorage",
}

View File

@ -57,6 +57,8 @@ Any of these methods may be defined as a coroutine function (``async def``).
Item pipeline example
=====================
.. _price-pipeline-example:
Price validation and dropping items with no prices
--------------------------------------------------
@ -246,6 +248,8 @@ returns multiples items with the same id:
return item
.. _activating-item-pipeline:
Activating an Item Pipeline component
=====================================
@ -262,3 +266,110 @@ To activate an Item Pipeline component you must add its class to the
The integer values you assign to classes in this setting determine the
order in which they run: items go through from lower valued to higher
valued classes. It's customary to define these numbers in the 0-1000 range.
A complete example
==================
The examples above show item pipeline components on their own. In a project, a
pipeline is one of four pieces that work together: the :ref:`item
<topics-items>` your spider produces, the :ref:`spider <topics-spiders>` that
yields it, the pipeline that processes it, and the :setting:`ITEM_PIPELINES`
setting that enables the pipeline.
The following example wires those pieces together to validate the price of
books scraped from `books.toscrape.com`_, reusing the ``PricePipeline`` from
:ref:`price-pipeline-example` above.
Define the item in ``myproject/items.py``:
.. code-block:: python
from dataclasses import dataclass
@dataclass
class BookItem:
title: str
price: float
Yield instances of that item from your spider, e.g. in
``myproject/spiders/books.py``:
.. skip: next
.. code-block:: python
import scrapy
from myproject.items import BookItem
class BooksSpider(scrapy.Spider):
name = "books"
start_urls = ["https://books.toscrape.com/"]
def parse(self, response):
for book in response.css("article.product_pod"):
yield BookItem(
title=book.css("h3 a::attr(title)").get(),
price=float(book.css("p.price_color::text").re_first(r"[\d.]+")),
)
Put the ``PricePipeline`` shown earlier in ``myproject/pipelines.py``, and
enable it in ``myproject/settings.py``:
.. code-block:: python
ITEM_PIPELINES = {
"myproject.pipelines.PricePipeline": 300,
}
With these pieces in place, every ``BookItem`` that ``BooksSpider`` yields
passes through ``PricePipeline`` before it reaches the :ref:`feed exports
<topics-feed-exports>` or any other output.
.. _books.toscrape.com: https://books.toscrape.com/
Common pitfalls
===============
The pipeline does not run
-------------------------
A pipeline component only runs if its class is listed in the
:setting:`ITEM_PIPELINES` setting, normally in your project's
:file:`settings.py` file (see :ref:`activating-item-pipeline`). Adding it to
the spider or elsewhere has no effect.
To confirm that Scrapy loaded your pipeline, look for a line like this near the
start of the crawl log::
[scrapy.middleware] INFO: Enabled item pipelines:
['myproject.pipelines.PricePipeline']
If your pipeline is missing from that list, check that its import path matches
the :setting:`ITEM_PIPELINES` entry, and that the setting is not being
overridden, for example by :attr:`~scrapy.Spider.custom_settings` or by a
redefinition of :setting:`ITEM_PIPELINES` in :file:`settings.py`.
The item is not returned
------------------------
:meth:`process_item` must return the item (or raise
:exc:`~scrapy.exceptions.DropItem`). A common mistake is to modify the item but
forget to return it:
.. code-block:: python
def process_item(self, item):
ItemAdapter(item)["price"] *= 1.15
# Bug: returns None, so the next component gets None instead of the item.
Return the item so that the next component, and the rest of Scrapy, can keep
processing it:
.. code-block:: python
def process_item(self, item):
ItemAdapter(item)["price"] *= 1.15
return item

View File

@ -376,7 +376,9 @@ Creating dicts from items:
>>> dict(product) # create a dict from all populated values
{'price': 1000, 'name': 'Desktop PC'}
Creating items from dicts:
Creating items from dicts:
.. code-block:: pycon
>>> Product({"name": "Laptop PC", "price": 1500})
Product(price=1500, name='Laptop PC')

View File

@ -93,7 +93,7 @@ You can get the oldest object of each class using the
Which objects are tracked?
--------------------------
The objects tracked by ``trackrefs`` are all from these classes (and all its
The objects tracked by ``trackref`` are all from these classes (and all its
subclasses):
* :class:`scrapy.Request`
@ -187,7 +187,7 @@ Here are the functions available in the :mod:`~scrapy.utils.trackref` module.
Inherit from this class if you want to track live
instances with the ``trackref`` module.
.. function:: print_live_refs(class_name, ignore=NoneType)
.. function:: print_live_refs(ignore=NoneType)
Print a report of live references, grouped by class name.
@ -203,9 +203,9 @@ Here are the functions available in the :mod:`~scrapy.utils.trackref` module.
.. function:: iter_all(class_name)
Return an iterator over all objects alive with the given class name, or
``None`` if none is found. Use :func:`print_live_refs` first to get a list
of all tracked live objects per class name.
Return an iterator over all objects alive with the given class name. Use
:func:`print_live_refs` first to get a list of all tracked live objects
per class name.
.. skip: end

View File

@ -47,108 +47,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=False, 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.
:param allow: a single regular expression (or list of regular expressions)
that the (absolute) urls must match in order to be extracted. If not
given (or empty), it will match all links.
:type allow: str or list
:param deny: a single regular expression (or list of regular expressions)
that the (absolute) urls must match in order to be excluded (i.e. not
extracted). It has precedence over the ``allow`` parameter. If not
given (or empty) it won't exclude any links.
:type deny: str or list
:param allow_domains: a single value or a list of string containing
domains which will be considered for extracting the links
:type allow_domains: str or list
:param deny_domains: a single value or a list of strings containing
domains which won't be considered for extracting the links
:type deny_domains: str or list
:param deny_extensions: a single value or list of strings containing
extensions that should be ignored when extracting links.
If not given, it will default to
:data:`scrapy.linkextractors.IGNORED_EXTENSIONS`.
:type deny_extensions: list
:param restrict_xpaths: is an XPath (or list of XPath's) which defines
regions inside the response where links should be extracted from.
If given, only the text selected by those XPath will be scanned for
links.
:type restrict_xpaths: str or list
:param restrict_css: a CSS selector (or list of selectors) which defines
regions inside the response where links should be extracted from.
Has the same behaviour as ``restrict_xpaths``.
:type restrict_css: str or list
:param restrict_text: a single regular expression (or list of regular expressions)
that the link's text must match in order to be extracted. If not
given (or empty), it will match all links. If a list of regular expressions is
given, the link will be extracted if it matches at least one.
:type restrict_text: str or list
:param tags: a tag or a list of tags to consider when extracting links.
Defaults to ``('a', 'area')``.
:type tags: str or list
:param attrs: an attribute or list of attributes which should be considered when looking
for links to extract (only for those tags specified in the ``tags``
parameter). Defaults to ``('href',)``
:type attrs: list
:param canonicalize: canonicalize each extracted url (using
w3lib.url.canonicalize_url). Defaults to ``False``.
Note that canonicalize_url is meant for duplicate checking;
it can change the URL visible at server side, so the response can be
different for requests with canonicalized and raw URLs. If you're
using LinkExtractor to follow links it is more robust to
keep the default ``canonicalize=False``.
:type canonicalize: bool
:param unique: whether duplicate filtering should be applied to extracted
links.
:type unique: bool
:param process_value: a function which receives each value extracted from
the tag and attributes scanned and can modify the value and return a
new one, or return ``None`` to ignore the link altogether. If not
given, ``process_value`` defaults to ``lambda x: x``.
.. highlight:: html
For example, to extract links from this code::
<a href="javascript:goToPage('../other/page.html'); return false">Link text</a>
.. highlight:: python
You can use the following function in ``process_value``:
.. code-block:: python
def process_value(value):
m = re.search(r"javascript:goToPage\('(.*?)'", value)
if m:
return m.group(1)
:type process_value: collections.abc.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: bool
.. autoclass:: LxmlLinkExtractor
.. automethod:: extract_links

View File

@ -76,7 +76,7 @@ data that will be assigned to the ``name`` field later.
Afterwards, similar calls are used for ``price`` and ``stock`` fields
(the latter using a CSS selector with the :meth:`~ItemLoader.add_css` method),
and finally the ``last_update`` field is populated directly with a literal value
and finally the ``last_updated`` field is populated directly with a literal value
(``today``) using a different method: :meth:`~ItemLoader.add_value`.
Finally, when all data is collected, the :meth:`ItemLoader.load_item` method is
@ -273,8 +273,8 @@ The precedence order, for both input and output processors, is as follows:
1. Item Loader field-specific attributes: ``field_in`` and ``field_out`` (most
precedence)
2. Field metadata (``input_processor`` and ``output_processor`` key)
3. Item Loader defaults: :meth:`ItemLoader.default_input_processor` and
:meth:`ItemLoader.default_output_processor` (least precedence)
3. Item Loader defaults: :attr:`ItemLoader.default_input_processor` and
:attr:`ItemLoader.default_output_processor` (least precedence)
See also: :ref:`topics-loaders-extending`.
@ -323,8 +323,8 @@ There are several ways to modify Item Loader context values:
loader = ItemLoader(product, unit="cm")
3. On Item Loader declaration, for those input/output processors that support
instantiating them with an Item Loader context. :class:`~processor.MapCompose` is one of
them:
instantiating them with an Item Loader context.
:class:`~itemloaders.processors.MapCompose` is one of them:
.. code-block:: python

View File

@ -4,11 +4,6 @@
Logging
=======
.. note::
:mod:`scrapy.log` has been deprecated alongside its functions in favor of
explicit calls to the Python standard logging. Keep reading to learn more
about the new logging system.
Scrapy uses :mod:`logging` for event logging. We'll
provide some simple examples to get you started, but for more advanced
use-cases it's strongly suggested to read thoroughly its documentation.

View File

@ -371,11 +371,12 @@ For the Images Pipeline, set :setting:`IMAGES_URLS_FIELD` and/or
If you need something more complex and want to override the custom pipeline
behaviour, see :ref:`topics-media-pipeline-override`.
If you have multiple image pipelines inheriting from ImagePipeline and you want
to have different settings in different pipelines you can set setting keys
preceded with uppercase name of your pipeline class. E.g. if your pipeline is
called MyPipeline and you want to have custom IMAGES_URLS_FIELD you define
setting MYPIPELINE_IMAGES_URLS_FIELD and your custom settings will be used.
If you have multiple image pipelines inheriting from :class:`ImagesPipeline`
and you want to have different settings in different pipelines you can set
setting keys preceded with uppercase name of your pipeline class. E.g. if your
pipeline is called ``MyPipeline`` and you want to have custom
:setting:`IMAGES_URLS_FIELD` you define setting
``MYPIPELINE_IMAGES_URLS_FIELD`` and your custom settings will be used.
Additional features

View File

@ -63,7 +63,7 @@ Request objects
.. invisible-code-block: python
from scrapy.http import Request
from scrapy import Request
1. Using a dict:
@ -247,7 +247,7 @@ Request objects
Return a new Request which is a copy of this Request. See also:
:ref:`topics-request-response-ref-request-callback-arguments`.
.. method:: Request.replace([url, method, headers, body, cookies, meta, flags, encoding, priority, dont_filter, callback, errback, cb_kwargs])
.. method:: Request.replace([url, method, headers, body, cookies, meta, flags, encoding, priority, dont_filter, callback, errback, cb_kwargs, cls])
Return a Request object with the same members, except for those members
given new values by whichever keyword arguments are specified. The
@ -717,6 +717,7 @@ Those are:
* :reqmeta:`download_fail_on_dataloss`
* :reqmeta:`download_latency`
* :reqmeta:`download_maxsize`
* :reqmeta:`download_slot`
* :reqmeta:`download_warnsize`
* :reqmeta:`download_timeout`
* ``ftp_password`` (See :setting:`FTP_PASSWORD` for more info)
@ -806,6 +807,8 @@ Whether or not to fail on broken responses. See:
give_up_log_level
-----------------
.. versionadded:: 2.17.0
:ref:`Logging level <levels>` used for the message logged when a request
exceeds its retries. See :setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
@ -814,6 +817,8 @@ exceeds its retries. See :setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
http_auth_domain
----------------
.. versionadded:: 2.17.0
Overrides :setting:`HTTPAUTH_DOMAIN` for this request.
.. reqmeta:: http_pass
@ -821,6 +826,8 @@ Overrides :setting:`HTTPAUTH_DOMAIN` for this request.
http_pass
---------
.. versionadded:: 2.17.0
Overrides :setting:`HTTPAUTH_PASS` for this request.
.. reqmeta:: http_user
@ -828,6 +835,8 @@ Overrides :setting:`HTTPAUTH_PASS` for this request.
http_user
---------
.. versionadded:: 2.17.0
Overrides :setting:`HTTPAUTH_USER` for this request.
.. reqmeta:: max_retry_times
@ -844,6 +853,8 @@ The meta key is used set retry times per request. When set, the
verbatim_url
------------
.. versionadded:: 2.17.0
Set this key to ``True`` to keep the request URL as passed to
:class:`~scrapy.Request`, without URL percent-encoding.
@ -854,7 +865,6 @@ characters that would otherwise be canonicalized get different fingerprints.
In this mode, the ``keep_fragments`` parameter is ignored, and it is
effectively true.
.. _topics-stop-response-download:
Stopping the download of a Response
@ -911,6 +921,11 @@ Request subclasses
Here is the list of built-in :class:`~scrapy.Request` subclasses. You can also subclass
it to implement your own custom functionality.
FormRequest
-----------
.. autoclass:: scrapy.FormRequest
JsonRequest
-----------
@ -1071,7 +1086,7 @@ Response objects
.. attribute:: Response.flags
A list that contains flags for this response. Flags are labels used for
tagging Responses. For example: ``'cached'``, ``'redirected``', etc. And
tagging Responses. For example: ``'cached'``, ``'redirected'``', etc. And
they're shown on the string representation of the Response (``__str__()``
method) which is used by the engine for logging.
@ -1105,7 +1120,7 @@ Response objects
Returns a new Response which is a copy of this Response.
.. method:: Response.replace([url, status, headers, body, request, flags, cls])
.. method:: Response.replace([url, status, headers, body, request, flags, certificate, ip_address, protocol, cls])
Returns a Response object with the same members, except for those members
given new values by whichever keyword arguments are specified. The

207
docs/topics/security.rst Normal file
View File

@ -0,0 +1,207 @@
.. _security:
========
Security
========
Scrapy defaults are optimized for web scraping, not for the security posture
that you might expect from software that handles untrusted input or runs in a
shared or exposed environment. Some common security practices are unnecessary
for many scraping use cases, and a few can even prevent valid ones (for
example, sites that you must scrape may use misconfigured TLS certificates or
serve content over unencrypted protocols).
This page highlights the Scrapy defaults that have security implications, so
that you can make an informed decision about whether to keep them, and explains
how to harden them along with the trade-offs involved.
.. note::
None of the options below are silver bullets. Which of them make sense
depends on your threat model: whether the URLs you crawl come from trusted
sources, whether the machine running Scrapy is exposed to a network you do
not control, whether the data you handle is sensitive, and so on.
.. _security-untrusted-responses:
Treat responses as untrusted input
==================================
Regardless of any setting, remember that response data comes from servers you
do not control, even when you trust the site you are crawling, as responses may
be tampered with in transit or the server itself may be compromised.
Never pass response data to functions that can execute code or otherwise act on
their input in an unsafe way, such as :func:`eval`, :func:`exec`, or
:func:`pickle.loads`, and be careful when writing response data to paths
derived from the response itself.
TLS connections
===============
.. _security-certificate-verification:
Certificate verification
------------------------
By default Scrapy does **not** verify the TLS certificate of HTTPS servers, as
controlled by the :setting:`DOWNLOAD_VERIFY_CERTIFICATES` setting (default:
``False``).
This default favors reach over security: many sites that are otherwise fine to
scrape have expired, self-signed, or otherwise invalid certificates, and
verifying certificates would make requests to them fail.
If the integrity of the connection matters to you (for example, to detect
man-in-the-middle attacks), set:
.. code-block:: python
DOWNLOAD_VERIFY_CERTIFICATES = True
* **Pro:** requests to servers with invalid or untrusted certificates fail
instead of silently succeeding, protecting you from some man-in-the-middle
attacks.
* **Con:** you can no longer scrape sites with misconfigured certificates
without re-disabling verification for them.
.. _security-tls-protocols-ciphers:
Protocol versions and ciphers
-----------------------------
You can restrict the TLS protocol versions that Scrapy accepts through the
:setting:`DOWNLOAD_TLS_MIN_VERSION` and :setting:`DOWNLOAD_TLS_MAX_VERSION`
settings, e.g. to reject obsolete protocol versions.
By default Scrapy uses the OpenSSL ``DEFAULT`` cipher list
(:setting:`DOWNLOADER_CLIENT_TLS_CIPHERS`), which favors compatibility and still
allows some older, weaker ciphers. Set it to ``None`` to instead use the curated
cipher list of the underlying TLS implementation (Twisted), which excludes weak
ciphers:
.. code-block:: python
DOWNLOADER_CLIENT_TLS_CIPHERS = None
* **Pro:** connections that would negotiate a weak cipher fail instead of
succeeding.
* **Con:** you can no longer connect to servers that only support the excluded
ciphers.
.. _security-unencrypted-protocols:
Unencrypted protocols
=====================
By default Scrapy enables download handlers for unencrypted protocols, namely
``http://`` and ``ftp://`` (see :setting:`DOWNLOAD_HANDLERS_BASE`). Data sent
and received over these protocols, including any credentials, travels in plain
text and can be read or modified by anyone on the network path.
If you only crawl over encrypted protocols, you can disable the unencrypted
ones so that no request can accidentally be sent unencrypted:
.. code-block:: python
DOWNLOAD_HANDLERS = {
"http": None,
"ftp": None,
}
* **Pro:** a misconfigured or maliciously-redirected request cannot leak data
over an unencrypted connection, as such requests fail instead.
* **Con:** you can no longer crawl resources that are only available over those
protocols.
Note that disabling the ``http`` handler also prevents plain-HTTP requests that
result from following an ``http://`` redirect or link, which is often the point
of disabling it.
.. _security-local-resources:
Local and non-network resources
===============================
By default Scrapy enables download handlers for the ``file://`` and ``data:``
schemes (see :setting:`DOWNLOAD_HANDLERS_BASE`). The ``file://`` handler reads
arbitrary files from the local filesystem, limited only by the permissions of
the process running Scrapy.
This is convenient (for example, to parse a local HTML file), but it is a risk
if any of the URLs you schedule come from an untrusted source: a crafted
``file:///etc/passwd`` URL could read local files.
If you do not need them, disable these handlers:
.. code-block:: python
DOWNLOAD_HANDLERS = {
"file": None,
"data": None,
}
* **Pro:** crawled URLs cannot be used to read local files or inline data.
* **Con:** you can no longer fetch ``file://`` or ``data:`` URLs.
More generally, if you crawl URLs from untrusted sources, consider validating
their schemes (and, where applicable, their hosts) before scheduling requests,
to avoid server-side request forgery (SSRF) and similar issues.
.. _security-telnet:
Telnet console
==============
Scrapy enables the :ref:`telnet console <topics-telnetconsole>` by default
(:setting:`TELNETCONSOLE_ENABLED`). The telnet console is a Python shell
running inside the Scrapy process, so anyone who can connect to it can run
arbitrary code in that process.
By default the console binds to ``127.0.0.1`` (:setting:`TELNETCONSOLE_HOST`)
and is protected by a username (:setting:`TELNETCONSOLE_USERNAME`, default
``scrapy``) and an automatically generated password
(:setting:`TELNETCONSOLE_PASSWORD`), so it is only reachable from the local
machine.
.. warning::
Telnet does not provide any transport-layer security, so the
username/password authentication does not protect the credentials or the
session from anyone able to observe the traffic. Never expose the telnet
console over an untrusted network by changing :setting:`TELNETCONSOLE_HOST`
to a non-local address.
If you do not use the telnet console, disable it entirely:
.. code-block:: python
TELNETCONSOLE_ENABLED = False
* **Pro:** removes a local code-execution surface and one less listening port.
* **Con:** you can no longer :ref:`inspect and control a running crawler
<topics-telnetconsole>` through it.
.. _security-credential-leakage:
Credential leakage across domains
=================================
Some Scrapy features attach credentials or other sensitive headers to requests,
and a crawl that spans multiple domains can leak them to unintended hosts:
* HTTP authentication credentials set through
:class:`~scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware` are only
sent to the domain set in :setting:`HTTPAUTH_DOMAIN`. Leave this set to the
intended domain rather than ``None`` so that credentials are not sent to
every domain you crawl.
* The ``Referer`` header may disclose the URLs you crawl to other sites. The
default :setting:`REFERRER_POLICY` already avoids sending the referrer from
HTTPS to HTTP, but you can tighten it further (for example, to
``same-origin`` or ``no-referrer``) if needed.

View File

@ -308,7 +308,7 @@ Examples:
* ``*::text`` selects all descendant text nodes of the current selector context:
..skip: next
.. skip: next
.. code-block:: pycon
>>> response.css("#images *::text").getall()
@ -634,8 +634,7 @@ Example:
.. code-block:: pycon
>>> from scrapy import Selector
>>> sel = Selector(
... text="""
>>> sel = Selector(text="""
... <ul class="list">
... <li>1</li>
... <li>2</li>
@ -645,8 +644,8 @@ Example:
... <li>4</li>
... <li>5</li>
... <li>6</li>
... </ul>"""
... )
... </ul>""")
...
>>> xp = lambda x: sel.xpath(x).getall()
This gets all first ``<li>`` elements under whatever it is its parent:
@ -948,11 +947,9 @@ with groups of itemscopes and corresponding itemprops:
>>> sel = Selector(text=doc, type="html")
>>> for scope in sel.xpath("//div[@itemscope]"):
... print("current scope:", scope.xpath("@itemtype").getall())
... props = scope.xpath(
... """
... props = scope.xpath("""
... set:difference(./descendant::*/@itemprop,
... .//*[@itemscope]/*/@itemprop)"""
... )
... .//*[@itemscope]/*/@itemprop)""")
... print(f" properties: {props.getall()}")
... print("")
...

View File

@ -409,77 +409,6 @@ Default: ``{}``
A dict containing paths to the add-ons enabled in your project and their
priorities. For more information, see :ref:`topics-addons`.
.. setting:: AWS_ACCESS_KEY_ID
AWS_ACCESS_KEY_ID
-----------------
Default: ``None``
The AWS access key used by code that requires access to `Amazon Web services`_,
such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`.
.. setting:: AWS_SECRET_ACCESS_KEY
AWS_SECRET_ACCESS_KEY
---------------------
Default: ``None``
The AWS secret key used by code that requires access to `Amazon Web services`_,
such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`.
.. setting:: AWS_SESSION_TOKEN
AWS_SESSION_TOKEN
-----------------
Default: ``None``
The AWS security token used by code that requires access to `Amazon Web services`_,
such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`, when using
`temporary security credentials`_.
.. _temporary security credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html
.. setting:: AWS_ENDPOINT_URL
AWS_ENDPOINT_URL
----------------
Default: ``None``
Endpoint URL used for S3-like storage, for example Minio or s3.scality.
.. setting:: AWS_USE_SSL
AWS_USE_SSL
-----------
Default: ``None``
Use this option if you want to disable SSL connection for communication with
S3 or S3-like storage. By default SSL will be used.
.. setting:: AWS_VERIFY
AWS_VERIFY
----------
Default: ``None``
Verify SSL connection between Scrapy and S3 or S3-like storage. By default
SSL verification will occur.
.. setting:: AWS_REGION_NAME
AWS_REGION_NAME
---------------
Default: ``None``
The name of the region associated with the AWS client.
.. setting:: ASYNCIO_EVENT_LOOP
ASYNCIO_EVENT_LOOP
@ -508,6 +437,77 @@ Note that the event loop class must inherit from :class:`asyncio.AbstractEventLo
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
.. setting:: AWS_ACCESS_KEY_ID
AWS_ACCESS_KEY_ID
-----------------
Default: ``None``
The AWS access key used by code that requires access to `Amazon Web services`_,
such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`.
.. setting:: AWS_ENDPOINT_URL
AWS_ENDPOINT_URL
----------------
Default: ``None``
Endpoint URL used for S3-like storage, for example Minio or s3.scality.
.. setting:: AWS_REGION_NAME
AWS_REGION_NAME
---------------
Default: ``None``
The name of the region associated with the AWS client.
.. setting:: AWS_SECRET_ACCESS_KEY
AWS_SECRET_ACCESS_KEY
---------------------
Default: ``None``
The AWS secret key used by code that requires access to `Amazon Web services`_,
such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`.
.. setting:: AWS_SESSION_TOKEN
AWS_SESSION_TOKEN
-----------------
Default: ``None``
The AWS security token used by code that requires access to `Amazon Web services`_,
such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`, when using
`temporary security credentials`_.
.. _temporary security credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html
.. setting:: AWS_USE_SSL
AWS_USE_SSL
-----------
Default: ``None``
Use this option if you want to disable SSL connection for communication with
S3 or S3-like storage. By default SSL will be used.
.. setting:: AWS_VERIFY
AWS_VERIFY
----------
Default: ``None``
Verify SSL connection between Scrapy and S3 or S3-like storage. By default
SSL verification will occur.
.. setting:: BOT_NAME
BOT_NAME
@ -593,7 +593,7 @@ When writing an item pipeline, you can force a different log level by setting
DEFAULT_ITEM_CLASS
------------------
Default: ``'scrapy.Item'``
Default: ``'scrapy.item.Item'``
The default class that will be used for instantiating items in the :ref:`the
Scrapy shell <topics-shell>`.
@ -687,7 +687,7 @@ Whether to enable DNS in-memory cache.
:class:`~scrapy.resolver.CachingThreadedResolver` and
:class:`~scrapy.resolver.CachingHostnameResolver`. It has no effect when
:setting:`TWISTED_REACTOR_ENABLED` is ``False``, and may have no effect
either when :setting:`DNS_RESOLVER` is set to a different resolver.
either when :setting:`TWISTED_DNS_RESOLVER` is set to a different resolver.
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
@ -702,25 +702,6 @@ DNS in-memory cache size, see :setting:`DNSCACHE_ENABLED`.
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
.. setting:: TWISTED_DNS_RESOLVER
TWISTED_DNS_RESOLVER
--------------------
Default: ``'scrapy.resolver.CachingThreadedResolver'``
The class to be used by Twisted to resolve DNS names. The default
``scrapy.resolver.CachingThreadedResolver`` supports specifying a timeout for
DNS requests via the :setting:`DNS_TIMEOUT` setting, but works only with IPv4
addresses. Scrapy provides an alternative resolver,
``scrapy.resolver.CachingHostnameResolver``, which supports IPv4/IPv6 addresses but does not
take the :setting:`DNS_TIMEOUT` setting into account.
.. note::
This setting has no effect when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
.. setting:: DNS_TIMEOUT
DNS_TIMEOUT
@ -734,7 +715,7 @@ Timeout for processing of DNS queries in seconds. Float is supported.
This setting is only used by
:class:`~scrapy.resolver.CachingThreadedResolver`. It has no effect when
:setting:`TWISTED_REACTOR_ENABLED` is ``False``, and may have no effect
either when :setting:`DNS_RESOLVER` is set to a different resolver.
either when :setting:`TWISTED_DNS_RESOLVER` is set to a different resolver.
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
@ -766,6 +747,9 @@ specific cipher that is not included in ``DEFAULT`` if a website requires it.
Set this setting to ``None`` to use the default ciphers of the underlying TLS
implementation.
.. versionchanged:: 2.17.0
Added support for setting this to ``None``.
.. _OpenSSL cipher list format: https://docs.openssl.org/master/man1/openssl-ciphers/#cipher-list-format
.. note::
@ -774,11 +758,15 @@ implementation.
handler <topics-download-handlers>`, so it's not guaranteed to be supported
by all 3rd-party handlers.
.. seealso:: :ref:`security-tls-protocols-ciphers`
.. setting:: DOWNLOAD_TLS_MAX_VERSION
DOWNLOAD_TLS_MAX_VERSION
------------------------
.. versionadded:: 2.17.0
Default: ``None``
Use this setting to change the maximum version of the TLS protocol allowed to
@ -807,11 +795,15 @@ modern environments.
by all 3rd-party handlers. Additionally, the set of supported TLS versions
depends on the TLS implementation being used by the handler.
.. seealso:: :ref:`security-tls-protocols-ciphers`
.. setting:: DOWNLOAD_TLS_MIN_VERSION
DOWNLOAD_TLS_MIN_VERSION
------------------------
.. versionadded:: 2.17.0
Default: ``None``
Use this setting to change the minimum version of the TLS protocol allowed to
@ -819,6 +811,8 @@ be used by Scrapy.
See :setting:`DOWNLOAD_TLS_MAX_VERSION` for the details and limitations.
.. seealso:: :ref:`security-tls-protocols-ciphers`
.. setting:: DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING
DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING
@ -979,6 +973,9 @@ enabled in your project.
See :setting:`DOWNLOAD_HANDLERS_BASE` for example format.
.. seealso:: :ref:`security-unencrypted-protocols` and
:ref:`security-local-resources`
.. setting:: DOWNLOAD_HANDLERS_BASE
DOWNLOAD_HANDLERS_BASE
@ -1026,6 +1023,9 @@ handler (without replacement), place this in your ``settings.py``:
"ftp": None,
}
.. seealso:: :ref:`security-unencrypted-protocols` and
:ref:`security-local-resources`
.. setting:: DOWNLOAD_SLOTS
@ -1184,6 +1184,8 @@ when making a request and abort the request if the verification fails.
certificate problems are logged when this setting is set to ``False``)
depends on its implementation.
.. seealso:: :ref:`security-certificate-verification`
.. setting:: DUPEFILTER_CLASS
DUPEFILTER_CLASS
@ -1309,6 +1311,7 @@ Default:
{
"scrapy.extensions.corestats.CoreStats": 0,
"scrapy.extensions.logcount.LogCount": 0,
"scrapy.extensions.telnet.TelnetConsole": 0,
"scrapy.extensions.memusage.MemoryUsage": 0,
"scrapy.extensions.memdebug.MemoryDebugger": 0,
@ -1331,6 +1334,8 @@ and the :ref:`list of available extensions <topics-extensions-ref>`.
FEED_TEMPDIR
------------
Default: ``None``
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>`.
@ -1340,6 +1345,8 @@ temporary files before uploading with :ref:`FTP feed storage <topics-feed-storag
FEED_STORAGE_GCS_ACL
--------------------
Default: ``""``
The Access Control List (ACL) used when storing items to :ref:`Google Cloud Storage <topics-feed-storage-gcs>`.
For more information on how to set this value, please refer to the column *JSON API* in `Google Cloud documentation <https://docs.cloud.google.com/storage/docs/access-control/lists>`_.
@ -1608,6 +1615,8 @@ The following special items are also supported:
- ``Python``
- ``pyOpenSSL``
.. setting:: LOGSTATS_INTERVAL
LOGSTATS_INTERVAL
@ -1735,7 +1744,7 @@ significant similarities in the time between their requests.
The randomization policy is the same used by `wget`_ ``--random-wait`` option.
If :setting:`DOWNLOAD_DELAY` is zero (default) this option has no effect.
If :setting:`DOWNLOAD_DELAY` is zero this option has no effect.
.. _wget: https://www.gnu.org/software/wget/manual/wget.html
@ -1796,7 +1805,7 @@ The parser backend to use for parsing ``robots.txt`` files. For more information
.. setting:: ROBOTSTXT_USER_AGENT
ROBOTSTXT_USER_AGENT
^^^^^^^^^^^^^^^^^^^^
--------------------
Default: ``None``
@ -1951,6 +1960,8 @@ Default:
{
"scrapy.contracts.default.UrlContract": 1,
"scrapy.contracts.default.CallbackKeywordArgumentsContract": 1,
"scrapy.contracts.default.MetadataContract": 1,
"scrapy.contracts.default.ReturnsContract": 2,
"scrapy.contracts.default.ScrapesContract": 3,
}
@ -2015,6 +2026,7 @@ Default:
.. code-block:: python
{
"scrapy.spidermiddlewares.start.StartSpiderMiddleware": 25,
"scrapy.spidermiddlewares.httperror.HttpErrorMiddleware": 50,
"scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
"scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
@ -2074,6 +2086,8 @@ Default: ``True`` (``False`` when :setting:`TWISTED_REACTOR_ENABLED` is ``False`
A boolean which specifies if the :ref:`telnet console <topics-telnetconsole>`
will be enabled (provided its extension is also enabled).
.. seealso:: :ref:`security-telnet`
.. setting:: TEMPLATES_DIR
TEMPLATES_DIR
@ -2088,6 +2102,25 @@ command.
The project name must not conflict with the name of custom files or directories
in the ``project`` subdirectory.
.. setting:: TWISTED_DNS_RESOLVER
TWISTED_DNS_RESOLVER
--------------------
Default: ``'scrapy.resolver.CachingThreadedResolver'``
The class to be used by Twisted to resolve DNS names. The default
``scrapy.resolver.CachingThreadedResolver`` supports specifying a timeout for
DNS requests via the :setting:`DNS_TIMEOUT` setting, but works only with IPv4
addresses. Scrapy provides an alternative resolver,
``scrapy.resolver.CachingHostnameResolver``, which supports IPv4/IPv6 addresses but does not
take the :setting:`DNS_TIMEOUT` setting into account.
.. note::
This setting has no effect when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
.. setting:: TWISTED_REACTOR_ENABLED
TWISTED_REACTOR_ENABLED
@ -2229,7 +2262,7 @@ URLLENGTH_LIMIT
Default: ``2083``
Scope: ``spidermiddlewares.urllength``
Scope: ``scrapy.spidermiddlewares.urllength``
The maximum URL length to allow for crawled URLs.

View File

@ -60,6 +60,8 @@ Let's take an example using :ref:`coroutines <topics-coroutines>`:
.. skip: next
.. code-block:: python
import json
import scrapy
import treq

View File

@ -46,7 +46,7 @@ previous (or subsequent) middleware being applied.
If you want to disable a builtin middleware (the ones defined in
:setting:`SPIDER_MIDDLEWARES_BASE`, and enabled by default) you must define it
in your project :setting:`SPIDER_MIDDLEWARES` setting and assign ``None`` as its
value. For example, if you want to disable the off-site middleware:
value. For example, if you want to disable the referer middleware:
.. code-block:: python
@ -355,6 +355,8 @@ Default: ``'scrapy.spidermiddlewares.referer.DefaultReferrerPolicy'``
using the special ``"referrer_policy"`` :ref:`Request.meta <topics-request-meta>` key,
with the same acceptable values as for the ``REFERRER_POLICY`` setting.
.. seealso:: :ref:`security-credential-leakage`
Acceptable values for REFERRER_POLICY
*************************************

View File

@ -208,7 +208,7 @@ scrapy.Spider
:param response: the response to parse
:type response: :class:`~scrapy.http.Response`
.. method:: log(message, [level, component])
.. method:: log(message, [level])
Wrapper that sends a log message through the Spider's :attr:`logger`,
kept for backward compatibility. For more information see
@ -335,8 +335,8 @@ The above example can also be written as follows:
If you are :ref:`running Scrapy from a script <run-from-script>`, you can
specify spider arguments when calling
:class:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
:class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
:meth:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
:meth:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
.. skip: next
.. code-block:: python
@ -593,7 +593,7 @@ Let's now take a look at an example CrawlSpider with rules:
This spider would start crawling example.com's home page, collecting category
links, and item links, parsing the latter with the ``parse_item`` method. For
each item response, some data will be extracted from the HTML using XPath, and
an :class:`~scrapy.Item` will be filled with it.
a dictionary will be filled with it.
XMLFeedSpider
-------------
@ -953,6 +953,7 @@ Combine SitemapSpider with other sources of urls:
.. code-block:: python
from scrapy import Request
from scrapy.spiders import SitemapSpider

View File

@ -29,14 +29,16 @@ disable it if you want. For more information about the extension itself see
.. note::
This feature is not supported when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
.. seealso:: :ref:`security-telnet`
.. highlight:: none
How to access the telnet console
================================
The telnet console listens in the TCP port defined in the
:setting:`TELNETCONSOLE_PORT` setting, which defaults to ``6023``. To access
the console you need to type::
The telnet console listens on the first available TCP port from the range
defined in the :setting:`TELNETCONSOLE_PORT` setting, which defaults to
``[6023, 6073]``. To access the console you need to type::
telnet localhost 6023
Trying localhost...
@ -190,6 +192,8 @@ Default: ``'127.0.0.1'``
The interface the telnet console should listen on
.. seealso:: :ref:`security-telnet`
.. setting:: TELNETCONSOLE_USERNAME

View File

@ -39,8 +39,8 @@ API stability
API stability was one of the major goals for the *1.0* release.
Methods or functions that start with a single dash (``_``) are private and
should never be relied as stable.
Methods or functions that start with a single underscore (``_``) are private
and should never be relied upon as stable.
Also, keep in mind that stable doesn't mean complete: stable APIs could grow
new methods or functionality but the existing methods should keep working the

View File

@ -94,7 +94,82 @@ untyped_calls_exclude = [
[[tool.mypy.overrides]]
module = "tests.*"
allow_untyped_defs = true
allow_incomplete_defs = true # 48 errors
allow_incomplete_defs = true # 59 errors
# TODO
[[tool.mypy.overrides]]
module = [
"tests.mockserver.*",
"tests.spiders",
"tests.test_closespider",
"tests.test_cmdline",
"tests.test_contracts",
"tests.test_core_downloader",
"tests.test_downloader_handler_twisted_ftp",
"tests.test_downloadermiddleware_cookies",
"tests.test_downloadermiddleware_httpauth",
"tests.test_downloadermiddleware_httpcache",
"tests.test_downloadermiddleware_httpcompression",
"tests.test_downloadermiddleware_httpproxy",
"tests.test_downloadermiddleware_offsite",
"tests.test_downloadermiddleware_redirect",
"tests.test_downloadermiddleware_redirect_base",
"tests.test_downloadermiddleware_redirect_metarefresh",
"tests.test_downloadermiddleware_retry",
"tests.test_downloadermiddleware_robotstxt",
"tests.test_downloadermiddleware_stats",
"tests.test_downloaderslotssettings",
"tests.test_dupefilters",
"tests.test_engine_loop",
"tests.test_exporters",
"tests.test_extension_statsmailer",
"tests.test_extension_throttle",
"tests.test_feedexport",
"tests.test_feedexport_postprocess",
"tests.test_feedexport_storages",
"tests.test_feedexport_uri_params",
"tests.test_http2_client_protocol",
"tests.test_http_headers",
"tests.test_http_request",
"tests.test_http_request_form",
"tests.test_http_response",
"tests.test_http_response_text",
"tests.test_item",
"tests.test_link",
"tests.test_linkextractors",
"tests.test_loader",
"tests.test_logformatter",
"tests.test_logstats",
"tests.test_mail",
"tests.test_pipeline_crawl",
"tests.test_pipeline_files",
"tests.test_pipeline_images",
"tests.test_pipeline_media",
"tests.test_pipelines",
"tests.test_pqueues",
"tests.test_request_attribute_binding",
"tests.test_request_cb_kwargs",
"tests.test_request_dict",
"tests.test_request_left",
"tests.test_robotstxt_interface",
"tests.test_scheduler_base",
"tests.test_settings",
"tests.test_spider",
"tests.test_spider_crawl",
"tests.test_spidermiddleware_output_chain",
"tests.test_spidermiddleware_process_start",
"tests.test_spider_sitemap",
"tests.test_squeues",
"tests.test_squeues_request",
"tests.test_stats",
"tests.test_utils_datatypes",
"tests.test_utils_decorators",
"tests.test_utils_defer",
"tests.test_utils_deprecate",
"tests.test_utils_misc.test_return_with_argument_inside_generator",
"tests.test_utils_python",
"tests.test_utils_request",
]
check_untyped_defs = false
# Interface classes are hard to support
@ -137,7 +212,7 @@ module = [
ignore_missing_imports = true
[tool.bumpversion]
current_version = "2.16.0"
current_version = "2.17.0"
commit = true
tag = true
tag_name = "{new_version}"
@ -269,11 +344,13 @@ markers = [
"requires_uvloop: marks tests as only enabled when uvloop is known to be working",
"requires_botocore: marks tests that need botocore (but not boto3)",
"requires_boto3: marks tests that need botocore and boto3",
"requires_mitmproxy: marks tests that need mitmproxy",
"requires_mitmproxy: marks tests that need a mitmdump executable",
"requires_internet: marks tests that need real Internet access",
]
filterwarnings = [
"ignore::DeprecationWarning:twisted.web.static",
# Twisted doesn't close failed sockets after CannotListenError: https://github.com/twisted/twisted/issues/6108
"ignore:Exception ignored in. <socket\\.socket.*laddr=..0\\.0\\.0\\.0., 0.:pytest.PytestUnraisableExceptionWarning",
]
[tool.ruff.lint]

View File

@ -1 +1 @@
2.16.0
2.17.0

View File

@ -64,7 +64,7 @@ class AddonManager:
:param settings: The :class:`~scrapy.settings.BaseSettings` object from \
which to read the early add-on configuration
:type settings: :class:`~scrapy.settings.Settings`
:type settings: :class:`~scrapy.settings.BaseSettings`
"""
for clspath in build_component_list(settings["ADDONS"]):
addoncls = load_object(clspath)

View File

@ -73,7 +73,7 @@ class ScrapyCommand(ABC):
def long_desc(self) -> str:
"""A long description of the command. Return short description when not
available. It cannot contain newlines since contents will be formatted
by optparser which removes newlines and wraps text.
by argparse which removes newlines and wraps text.
"""
return self.short_desc()

View File

@ -1,12 +1,29 @@
import argparse
from __future__ import annotations
import os
import shlex
import subprocess
import sys
from typing import Any, ClassVar
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar
from scrapy.commands import ScrapyCommand
from scrapy.exceptions import UsageError
from scrapy.spiderloader import get_spider_loader
if TYPE_CHECKING:
import argparse
def _edit_file(editor: str, file_path: str | os.PathLike[str]) -> int:
"""Open ``file_path`` with ``editor`` and return the editor exit code.
``editor`` may include arguments (e.g. ``"code -w"``); it is split with
:func:`shlex.split` and the file is passed as a separate argument, so no
shell is involved.
"""
return subprocess.call([*shlex.split(editor), os.fspath(file_path)]) # noqa: S603
class Command(ScrapyCommand):
requires_project = True
@ -45,4 +62,4 @@ class Command(ScrapyCommand):
sfile = sys.modules[spidercls.__module__].__file__
assert sfile
sfile = sfile.replace(".pyc", ".py")
self.exitcode = os.system(f'{editor} "{sfile}"') # noqa: S605
self.exitcode = _edit_file(editor, Path(sfile))

View File

@ -1,6 +1,5 @@
from __future__ import annotations
import os
import shutil
import string
from importlib import import_module
@ -10,12 +9,14 @@ from urllib.parse import urlparse
import scrapy
from scrapy.commands import ScrapyCommand
from scrapy.commands.edit import _edit_file
from scrapy.exceptions import UsageError
from scrapy.spiderloader import get_spider_loader
from scrapy.utils.template import render_templatefile, string_camelcase
if TYPE_CHECKING:
import argparse
import os
def sanitize_module_name(module_name: str) -> str:
@ -118,9 +119,11 @@ class Command(ScrapyCommand):
template_file = self._find_template(opts.template)
if template_file:
self._genspider(module, name, url, opts.template, template_file)
spider_file = self._genspider(
module, name, url, opts.template, template_file
)
if opts.edit:
self.exitcode = os.system(f'scrapy edit "{name}"') # noqa: S605
self.exitcode = _edit_file(self.settings["EDITOR"], spider_file)
def _generate_template_variables(
self,
@ -148,7 +151,7 @@ class Command(ScrapyCommand):
url: str,
template_name: str,
template_file: str | os.PathLike[str],
) -> None:
) -> Path:
"""Generate the spider module, based on the given template"""
assert self.settings is not None
tvars = self._generate_template_variables(module, name, url, template_name)
@ -168,6 +171,7 @@ class Command(ScrapyCommand):
)
if spiders_module:
print(f"in module:\n {spiders_module.__name__}.{module}")
return Path(spider_file)
def _find_template(self, template: str) -> Path | None:
template_file = Path(self.templates_dir, f"{template}.tmpl")

View File

@ -47,8 +47,13 @@ class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
instance.
The purpose of this custom class is to provide a ``creatorForNetloc()``
method that returns a ``_ScrapyClientTLSOptions`` instance configured based
on TLS settings provided to the factory.
method that returns:
- a ``_ScrapyClientTLSOptions26`` or ``_ScrapyClientTLSOptions`` instance
configured based on TLS settings provided to the factory (when the
certificate verification is disabled);
- a result of ``optionsForClientTLS()`` called with those TLS settings
(when the certificate verification is enabled).
"""
def __init__(

View File

@ -2,9 +2,9 @@
An asynchronous FTP file download handler for scrapy which somehow emulates an http response.
FTP connection parameters are passed using the request meta field:
- ftp_user (required)
- ftp_password (required)
- ftp_passive (by default, enabled) sets FTP connection passive mode
- ftp_user (optional, falls back to FTP_USER)
- ftp_password (optional, falls back to FTP_PASSWORD)
- ftp_passive (optional, falls back to FTP_PASSIVE_MODE) sets FTP connection passive mode
- ftp_local_filename
- If not given, file data will come in the response.body, as a normal scrapy Response,
which will imply that the entire file will be on memory.

View File

@ -104,7 +104,6 @@ class HTTP11DownloadHandler(BaseHttpDownloadHandler):
self._disconnect_timeout: int = 1
async def download_request(self, request: Request) -> Response:
"""Return a deferred for the HTTP download"""
if hasattr(self._crawler.spider, "download_maxsize"): # pragma: no cover
warn_on_deprecated_spider_attribute("download_maxsize", "DOWNLOAD_MAXSIZE")
if hasattr(self._crawler.spider, "download_warnsize"): # pragma: no cover
@ -283,7 +282,7 @@ def _tunnel_request_data(
class _TunnelingAgent(Agent):
"""An agent that uses a L{TunnelingTCP4ClientEndpoint} to make HTTPS
"""An agent that uses a ``_TunnelingTCP4ClientEndpoint`` to make HTTPS
downloads. It may look strange that we have chosen to subclass Agent and not
ProxyAgent but consider that after the tunnel is opened the proxy is
transparent to the client; thus the agent should behave like there is no
@ -545,7 +544,8 @@ class _ScrapyAgent:
expected_size, maxsize, request, expected=True
)
logger.warning(warning_msg)
txresponse._transport.loseConnection()
# Abort connection immediately.
txresponse._transport._producer.abortConnection()
raise DownloadCancelledError(warning_msg)
if warnsize and expected_size > warnsize:

View File

@ -49,7 +49,7 @@ class S3DownloadHandler(BaseDownloadHandler):
async def download_request(self, request: Request) -> Response:
p = urlparse_cached(request)
scheme = "https" if request.meta.get("is_secure") else "http"
scheme = "http" if request.meta.get("is_secure") is False else "https"
bucket = p.hostname
path = p.path + "?" + p.query if p.query else p.path
url = f"{scheme}://{bucket}.s3.amazonaws.com{path}"

View File

@ -114,11 +114,7 @@ class H2ConnectionPool:
d.errback(ResponseFailed(errors))
def close_connections(self) -> None:
"""Close all the HTTP/2 connections and remove them from pool
Returns:
Deferred that fires when all connections have been closed
"""
"""Close all the HTTP/2 connections and remove them from pool."""
for conn in self._connections.values():
assert conn.transport is not None # typing
conn.transport.abortConnection()

View File

@ -101,7 +101,7 @@ class H2ClientProtocol(Protocol, TimeoutMixin):
uri is used to verify that incoming client requests have correct
base URL.
settings -- Scrapy project settings
conn_lost_deferred -- Deferred fires with the reason: Failure to notify
conn_lost_deferred -- Deferred that fires with the list of underlying exceptions to notify
that connection was lost
tls_verbose_logging -- Whether to log TLS details
"""
@ -375,7 +375,7 @@ class H2ClientProtocol(Protocol, TimeoutMixin):
def _handle_events(self, events: list[Event]) -> None:
"""Private method which acts as a bridge between the events
received from the HTTP/2 data and IH2EventsHandler
received from the HTTP/2 data and the handlers in this class.
Arguments:
events -- A list of events that the remote peer triggered by sending data

View File

@ -131,10 +131,10 @@ class Scheduler(BaseScheduler):
(:setting:`SCHEDULER_PRIORITY_QUEUE`) that sort requests by
:attr:`~scrapy.http.Request.priority`.
By default, a single, memory-based priority queue is used for all requests.
When using :setting:`JOBDIR`, a disk-based priority queue is also created,
By default, memory-based priority queues are used for all requests.
When using :setting:`JOBDIR`, disk-based priority queues are also created,
and only unserializable requests are stored in the memory-based priority
queue. For a given priority value, requests in memory take precedence over
queues. For a given priority value, requests in memory take precedence over
requests in disk.
Each priority queue stores requests in separate internal queues, one per
@ -209,8 +209,8 @@ class Scheduler(BaseScheduler):
-------------------------
While pending requests are below the configured values of
:setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`
or :setting:`CONCURRENT_REQUESTS_PER_IP`, those requests are sent
:setting:`CONCURRENT_REQUESTS` or
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN`, those requests are sent
concurrently.
As a result, the first few requests of a crawl may not follow the desired
@ -342,7 +342,7 @@ class Scheduler(BaseScheduler):
def open(self, spider: Spider) -> Deferred[None] | None:
"""
(1) initialize the memory queue
(2) initialize the disk queue if the ``jobdir`` attribute is a valid directory
(2) initialize the disk queue if the ``jobdir`` argument wasn't empty
(3) return the result of the dupefilter's ``open`` method
"""
self.spider: Spider = spider

View File

@ -441,7 +441,7 @@ class Scraper:
self, output: Any, response: Response | Failure
) -> Deferred[None]:
"""Process each Request/Item (given in the output parameter) returned
from the given spider.
from the spider.
Items are sent to the item pipelines, requests are scheduled.
"""
@ -451,7 +451,7 @@ class Scraper:
self, output: Any, response: Response | Failure
) -> None:
"""Process each Request/Item (given in the output parameter) returned
from the given spider.
from the spider.
Items are sent to the item pipelines, requests are scheduled.
"""

View File

@ -531,8 +531,8 @@ class AsyncCrawlerRunner(CrawlerRunnerBase):
"""
Run a crawler with the provided arguments.
It will call the given Crawler's :meth:`~Crawler.crawl` method, while
keeping track of it so it can be stopped later.
It will call the given Crawler's :meth:`~Crawler.crawl_async` method,
while keeping track of it so it can be stopped later.
If ``crawler_or_spidercls`` isn't a :class:`~scrapy.crawler.Crawler`
instance, this method will try to create one using this parameter as
@ -773,7 +773,7 @@ class CrawlerProcess(CrawlerProcessBase, CrawlerRunner):
"""
This method starts a :mod:`~twisted.internet.reactor`, adjusts its pool
size to :setting:`REACTOR_THREADPOOL_MAXSIZE`, and installs a DNS
resolver based on :setting:`DNSCACHE_ENABLED`.
resolver based on :setting:`TWISTED_DNS_RESOLVER`.
If ``stop_after_crawl`` is True, the reactor will be stopped after all
crawlers have finished, using :meth:`join`.
@ -875,10 +875,10 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
When using a reactor it adjusts its pool size to
:setting:`REACTOR_THREADPOOL_MAXSIZE` and installs a DNS resolver based
on :setting:`DNSCACHE_ENABLED`.
on :setting:`TWISTED_DNS_RESOLVER`.
If ``stop_after_crawl`` is True, the reactor will be stopped after all
crawlers have finished, using :meth:`join`.
If ``stop_after_crawl`` is True, the reactor/event loop will be stopped
after all crawlers have finished, using :meth:`join`.
:param bool stop_after_crawl: stop or not the reactor when all
crawlers have finished

View File

@ -12,6 +12,7 @@ from scrapy.http.cookies import CookieJar
from scrapy.utils.decorators import _warn_spider_arg
from scrapy.utils.httpobj import urlparse_cached
from scrapy.utils.python import to_unicode
from scrapy.utils.request import _decode_cookie, _to_verbose_cookies
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
@ -134,29 +135,10 @@ class CookiesMiddleware:
Given a dict consisting of cookie components, return its string representation.
Decode from bytes if necessary.
"""
decoded = {}
decoded = _decode_cookie(cookie, request)
if decoded is None:
return None
flags = set()
for key in ("name", "value", "path", "domain"):
value = cookie.get(key)
if value is None:
if key in {"name", "value"}:
msg = f"Invalid cookie found in request {request}: {cookie} ('{key}' is missing)"
logger.warning(msg)
return None
continue
if isinstance(value, (bool, float, int, str)):
decoded[key] = str(value)
else:
assert isinstance(value, bytes)
try:
decoded[key] = value.decode("utf8")
except UnicodeDecodeError:
logger.warning(
"Non UTF-8 encoded cookie found in request %s: %s",
request,
cookie,
)
decoded[key] = value.decode("latin1", errors="replace")
for flag in ("secure",):
value = cookie.get(flag, _UNSET)
if value is _UNSET or not value:
@ -177,11 +159,7 @@ class CookiesMiddleware:
"""
if not request.cookies:
return ()
cookies: Iterable[VerboseCookie]
if isinstance(request.cookies, dict):
cookies = tuple({"name": k, "value": v} for k, v in request.cookies.items())
else:
cookies = request.cookies
cookies: Iterable[VerboseCookie] = _to_verbose_cookies(request.cookies)
for cookie in cookies:
cookie.setdefault("secure", urlparse_cached(request).scheme == "https")
formatted = filter(None, (self._format_cookie(c, request) for c in cookies))

View File

@ -60,7 +60,7 @@ else:
class HttpCompressionMiddleware:
"""This middleware allows compressed (gzip, deflate) traffic to be
"""This middleware allows compressed (gzip, deflate etc.) traffic to be
sent/received from websites"""
def __init__(

View File

@ -196,10 +196,7 @@ class BaseRedirectMiddleware:
class RedirectMiddleware(BaseRedirectMiddleware):
"""
Handle redirection of requests based on response status
and meta-refresh html tag.
"""
"""Handle redirection of requests based on response status."""
@_warn_spider_arg
def process_response(
@ -251,6 +248,8 @@ class RedirectMiddleware(BaseRedirectMiddleware):
class MetaRefreshMiddleware(BaseRedirectMiddleware):
"""Handle redirection of requests based on meta-refresh html tag."""
enabled_setting = "METAREFRESH_ENABLED"
def __init__(self, settings: BaseSettings):

View File

@ -5,9 +5,6 @@ problems such as a connection timeout or HTTP 500 error.
You can change the behaviour of this middleware by modifying the scraping settings:
RETRY_TIMES - how many times to retry a failed page
RETRY_HTTP_CODES - which HTTP response codes to retry
Failed pages are collected on the scraping process and rescheduled at the end,
once the spider has finished crawling all regular (non-failed) pages.
"""
from __future__ import annotations
@ -70,8 +67,9 @@ def get_retry_request(
and :ref:`stats <topics-stats>`, and to provide extra logging context (see
:func:`logging.debug`).
*reason* is a string or an :class:`Exception` object that indicates the
reason why the request needs to be retried. It is used to name retry stats.
*reason* is a string, an :class:`Exception` subclass or an
:class:`Exception` object that indicates the reason why the request needs
to be retried. It is used to name retry stats.
*max_retry_times* is a number that determines the maximum number of times
that *request* can be retried. If not specified or ``None``, the number is
@ -89,6 +87,9 @@ def get_retry_request(
message logged when a request exceeds its retries. See
:setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
.. versionadded:: 2.17.0
The *give_up_log_level* parameter.
*stats_base_key* is a string to be used as the base key for the
retry-related job stats
"""

View File

@ -115,7 +115,7 @@ class UsageError(Exception):
class ScrapyDeprecationWarning(Warning):
"""Warning category for deprecated features, since the default
DeprecationWarning is silenced on Python 2.7+
:exc:`DeprecationWarning` is silenced.
"""

View File

@ -5,6 +5,7 @@ Item Exporters are used to export/serialize items into different formats.
from __future__ import annotations
import csv
import logging
import marshal
import pickle
import pprint
@ -24,6 +25,8 @@ from scrapy.utils.serialize import ScrapyJSONEncoder
if TYPE_CHECKING:
from json import JSONEncoder
logger = logging.getLogger(__name__)
__all__ = [
"BaseItemExporter",
"CsvItemExporter",
@ -171,7 +174,15 @@ class XmlItemExporter(BaseItemExporter):
super().__init__(**kwargs)
if not self.encoding:
self.encoding = "utf-8"
self.xg = XMLGenerator(file, encoding=self.encoding)
# copied from xml.sax.saxutils._gettextwriter()
self.stream = TextIOWrapper(
file,
encoding=self.encoding,
errors="xmlcharrefreplace",
newline="\n",
write_through=True,
)
self.xg = XMLGenerator(self.stream, encoding=self.encoding)
def _beautify_newline(self, new_item: bool = False) -> None:
if self.indent is not None and (self.indent > 0 or new_item):
@ -199,6 +210,7 @@ class XmlItemExporter(BaseItemExporter):
def finish_exporting(self) -> None:
self.xg.endElement(self.root_element)
self.xg.endDocument()
self.stream.detach() # Avoid closing the wrapped file.
def _export_xml_field(self, name: str, serialized_value: Any, depth: int) -> None:
self._beautify_indent(depth=depth)
@ -245,6 +257,8 @@ class CsvItemExporter(BaseItemExporter):
self.csv_writer = csv.writer(self.stream, **self._kwargs)
self._headers_not_written = True
self._join_multivalued = join_multivalued
self._autodetected_fields = False
self._data_loss_warned = False
def serialize_field(
self, field: Mapping[str, Any] | Field, name: str, value: Any
@ -265,6 +279,22 @@ class CsvItemExporter(BaseItemExporter):
self._headers_not_written = False
self._write_headers_and_set_fields_to_export(item)
if (
self._autodetected_fields
and self.fields_to_export is not None
and not self._data_loss_warned
):
item_fields = ItemAdapter(item).field_names()
dropped_fields = set(item_fields) - set(self.fields_to_export)
if dropped_fields:
dropped_fields_display = sorted(dropped_fields)
logger.warning(
f"CSVExporter dropped fields {dropped_fields_display}. "
f"To avoid this, fully configure your FEED_EXPORT_FIELDS setting. "
f"See: https://docs.scrapy.org/en/latest/topics/feed-exports.html#feed-export-fields",
)
self._data_loss_warned = True
fields = self._get_serialized_fields(item, default_value="", include_empty=True)
values = list(self._build_row(x for _, x in fields))
self.csv_writer.writerow(values)
@ -284,6 +314,7 @@ class CsvItemExporter(BaseItemExporter):
if not self.fields_to_export:
# use declared field names, or keys if the item is a dict
self.fields_to_export = ItemAdapter(item).field_names()
self._autodetected_fields = True
fields: Iterable[str]
if isinstance(self.fields_to_export, Mapping):
fields = self.fields_to_export.values()

View File

@ -250,20 +250,22 @@ class S3FeedStorage(BlockingFeedStorage):
def _store_in_thread(self, file: IO[bytes]) -> None:
file.seek(0)
if self.acl:
self.s3_client.upload_fileobj(
Bucket=self.bucketname,
Key=self.keyname,
Fileobj=file,
ExtraArgs={"ACL": self.acl},
)
else:
self.s3_client.upload_fileobj(
Bucket=self.bucketname,
Key=self.keyname,
Fileobj=file,
)
file.close()
try:
if self.acl:
self.s3_client.upload_fileobj(
Bucket=self.bucketname,
Key=self.keyname,
Fileobj=file,
ExtraArgs={"ACL": self.acl},
)
else:
self.s3_client.upload_fileobj(
Bucket=self.bucketname,
Key=self.keyname,
Fileobj=file,
)
finally:
file.close()
class GCSFeedStorage(BlockingFeedStorage):

View File

@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
class LogStats:
"""Log basic scraping stats periodically like:
* RPM - Requests per Minute
* RPM - Responses per Minute
* IPM - Items per Minute
"""

View File

@ -116,7 +116,7 @@ class AutoThrottle:
# It works better with problematic sites.
new_delay = max(target_delay, new_delay)
# Make sure self.mindelay <= new_delay <= self.max_delay
# Make sure self.mindelay <= new_delay <= self.maxdelay
new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
# Dont adjust delay if response status != 200 and new delay is smaller

View File

@ -5,11 +5,9 @@ Use this module (instead of the more specific ones) when importing Headers,
Request and Response outside this module.
"""
from warnings import catch_warnings, filterwarnings
from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.http.headers import Headers
from scrapy.http.request import Request
from scrapy.http.request.form import FormRequest
from scrapy.http.request.json_request import JsonRequest
from scrapy.http.request.rpc import XmlRpcRequest
from scrapy.http.response import Response
@ -17,19 +15,6 @@ from scrapy.http.response.html import HtmlResponse
from scrapy.http.response.json import JsonResponse
from scrapy.http.response.text import TextResponse
from scrapy.http.response.xml import XmlResponse
from scrapy.utils.deprecate import create_deprecated_class
with catch_warnings():
filterwarnings("ignore", category=ScrapyDeprecationWarning)
from scrapy.http.request.form import FormRequest as _FormRequest
FormRequest = create_deprecated_class(
name="FormRequest",
new_class=_FormRequest,
subclass_warn_message="{cls} inherits from deprecated class {old}, use the form2request library instead.",
instance_warn_message="{cls} is deprecated, use the form2request library instead.",
)
__all__ = [
"FormRequest",

View File

@ -136,9 +136,9 @@ class _DummyLock:
class WrappedRequest:
"""Wraps a scrapy Request class with methods defined by urllib2.Request class to interact with CookieJar class
see http://docs.python.org/library/urllib2.html#urllib2.Request
"""Wraps a :class:`scrapy.Request` class with methods defined by
the :class:`urllib.request.Request` class to interact with
the :class:`http.cookiejar.CookieJar` class.
"""
def __init__(self, request: Request):

View File

@ -51,7 +51,7 @@ class VerboseCookie(TypedDict):
secure: NotRequired[bool]
CookiesT: TypeAlias = dict[str, str] | list[VerboseCookie]
CookiesT: TypeAlias = dict[str | bytes, str | bytes] | list[VerboseCookie]
RequestTypeVar = TypeVar("RequestTypeVar", bound="Request")

View File

@ -32,19 +32,61 @@ if TYPE_CHECKING:
from scrapy.http.response.text import TextResponse
warn(
"The entire scrapy.http.request.form module is deprecated. Use the "
"form2request library instead.",
ScrapyDeprecationWarning,
stacklevel=2,
)
FormdataVType: TypeAlias = str | Iterable[str]
FormdataKVType: TypeAlias = tuple[str, FormdataVType]
FormdataType: TypeAlias = dict[str, FormdataVType] | list[FormdataKVType] | None
class FormRequest(Request):
"""A :class:`~scrapy.Request` subclass with a ``formdata`` parameter that
url-encodes the given data and assigns it to the request, which makes it
convenient to send arbitrary form data via HTTP POST or GET without an HTML
``<form>`` element to parse.
.. note:: To build a request from an HTML ``<form>`` element found in a
response, use :doc:`form2request <form2request:index>` instead. See
:ref:`form`.
The remaining arguments are the same as for the :class:`~scrapy.Request`
class and are not documented here.
:param formdata: a dictionary (or iterable of (key, value) tuples)
containing HTML form data which will be url-encoded. If
:attr:`~scrapy.Request.method` is not given and ``formdata`` is
provided, the method is set to ``"POST"`` and the data is assigned to
the request body; if the method is ``"GET"``, the data is added to the
URL query string instead.
:type formdata: dict or collections.abc.Iterable
To send data via HTTP POST, simulating an HTML form submission, return a
:class:`~scrapy.FormRequest` object from your spider:
.. skip: next
.. code-block:: python
return [
FormRequest(
url="http://www.example.com/post/action",
formdata={"name": "John Doe", "age": "27"},
callback=self.after_post,
)
]
To send the data in the URL query string instead, use the ``GET`` method:
.. skip: next
.. code-block:: python
return [
FormRequest(
url="http://www.example.com/search",
method="GET",
formdata={"q": "keyword", "page": "1"},
callback=self.parse_results,
)
]
"""
__slots__ = ()
valid_form_methods: ClassVar[list[str]] = ["GET", "POST"]
@ -84,6 +126,13 @@ class FormRequest(Request):
formcss: str | None = None,
**kwargs: Any,
) -> Self:
warn(
"FormRequest.from_response() is deprecated. Use the form2request "
"library instead.",
ScrapyDeprecationWarning,
stacklevel=2,
)
kwargs.setdefault("encoding", response.encoding)
if formcss is not None:

View File

@ -1,6 +1,7 @@
"""
This module implements the HtmlResponse class which adds encoding
discovering through HTML encoding declarations to the TextResponse class.
This module implements the :class:`HtmlResponse` class which is used as a
content type marker by :class:`~scrapy.selector.Selector` and can be used in
``isinstance()`` checks.
See documentation in docs/topics/request-response.rst
"""

View File

@ -1,6 +1,7 @@
"""
This module implements the XmlResponse class which adds encoding
discovering through XML encoding declarations to the TextResponse class.
This module implements the :class:`XmlResponse` class which is used as a
content type marker by :class:`~scrapy.selector.Selector` and can be used in
``isinstance()`` checks.
See documentation in docs/topics/request-response.rst
"""

View File

@ -1,7 +1,7 @@
"""
Scrapy Item
See documentation in docs/topics/item.rst
See documentation in docs/topics/items.rst
"""
from __future__ import annotations
@ -25,6 +25,25 @@ class Field(dict[str, Any]):
"""Container of field metadata"""
def _ordered_field_names(cls: type) -> list[str]:
"""Return the names of the :class:`Field` attributes of *cls* in definition
order.
Fields declared in base classes come first, ordered from the topmost base
to the most derived class. Within each class, fields keep their definition
order. A field redefined in a subclass keeps the position of its first
definition.
"""
names: list[str] = []
seen: set[str] = set()
for base in reversed(cls.__mro__):
for name, value in vars(base).items():
if isinstance(value, Field) and name not in seen:
seen.add(name)
names.append(name)
return names
class ItemMeta(ABCMeta):
"""Metaclass_ of :class:`Item` that handles field definitions.
@ -39,13 +58,9 @@ class ItemMeta(ABCMeta):
_class = super().__new__(mcs, "x_" + class_name, new_bases, attrs)
fields = getattr(_class, "fields", {})
new_attrs = {}
for n in dir(_class):
v = getattr(_class, n)
if isinstance(v, Field):
fields[n] = v
elif n in attrs:
new_attrs[n] = attrs[n]
for n in _ordered_field_names(_class):
fields[n] = getattr(_class, n)
new_attrs = {n: v for n, v in attrs.items() if not isinstance(v, Field)}
new_attrs["fields"] = fields
new_attrs["_class"] = _class
@ -80,6 +95,14 @@ class Item(MutableMapping[str, Any], object_ref, metaclass=ItemMeta):
#: those populated. The keys are the field names and the values are the
#: :class:`Field` objects used in the :ref:`Item declaration
#: <topics-items-declaring>`.
#:
#: Fields are kept in definition order: fields declared in base classes
#: come first, followed by fields declared in subclasses, and a field
#: redefined in a subclass keeps the position of its first definition.
#:
#: .. versionchanged:: 2.17.0
#: Fields are now returned in definition order rather than alphabetical
#: order.
fields: dict[str, Field]
def __init__(self, *args: Any, **kwargs: Any):

View File

@ -9,7 +9,9 @@ its documentation in: docs/topics/link-extractors.rst
class Link:
"""Link objects represent an extracted link by the LinkExtractor.
Using the anchor tag sample below to illustrate the parameters::
Using the anchor tag sample below to illustrate the parameters:
.. code-block:: html
<a href="https://example.com/nofollow.html#foo" rel="nofollow">Dont follow this one</a>

View File

@ -57,6 +57,18 @@ def _canonicalize_link_url(link: Link) -> str:
return canonicalize_url(link.url, keep_fragments=True)
def _name_matches(allowed: set[str], denied: set[str], name: str) -> bool:
"""Return whether a tag or attribute *name* should be considered.
A name matches when it is allowed and not denied. A name is allowed if it is
listed in *allowed*, or if *allowed* contains the ``"*"`` wildcard, which
matches every name. *denied* has precedence over *allowed*.
"""
if name in denied:
return False
return "*" in allowed or name in allowed
class LxmlParserLinkExtractor:
def __init__(
self,
@ -162,6 +174,126 @@ _RegexOrSeveral: TypeAlias = _Regex | Iterable[_Regex]
class LxmlLinkExtractor:
r"""LxmlLinkExtractor is the recommended link extractor with handy filtering
options. It is implemented using lxml's robust HTMLParser.
:param allow: a single regular expression (or list of regular expressions)
that the (absolute) urls must match in order to be extracted. If not
given (or empty), it will match all links.
:type allow: str or list
:param deny: a single regular expression (or list of regular expressions)
that the (absolute) urls must match in order to be excluded (i.e. not
extracted). It has precedence over the ``allow`` parameter. If not
given (or empty) it won't exclude any links.
:type deny: str or list
:param allow_domains: a single value or a list of string containing
domains which will be considered for extracting the links
:type allow_domains: str or list
:param deny_domains: a single value or a list of strings containing
domains which won't be considered for extracting the links
:type deny_domains: str or list
:param deny_extensions: a single value or list of strings containing
extensions that should be ignored when extracting links.
If not given, it will default to
:data:`scrapy.linkextractors.IGNORED_EXTENSIONS`.
:type deny_extensions: list
:param restrict_xpaths: is an XPath (or list of XPath's) which defines
regions inside the response where links should be extracted from.
If given, only the text selected by those XPath will be scanned for
links.
:type restrict_xpaths: str or list
:param restrict_css: a CSS selector (or list of selectors) which defines
regions inside the response where links should be extracted from.
Has the same behaviour as ``restrict_xpaths``.
:type restrict_css: str or list
:param restrict_text: a single regular expression (or list of regular
expressions) that the link's text must match in order to be extracted.
If not given (or empty), it will match all links. If a list of regular
expressions is given, the link will be extracted if it matches at least
one.
:type restrict_text: str or list
:param tags: a tag or a list of tags to consider when extracting links.
Defaults to ``('a', 'area')``. Use ``'*'`` to consider every tag.
:type tags: str or list
:param attrs: an attribute or list of attributes which should be considered
when looking for links to extract (only for those tags specified in the
``tags`` parameter). Defaults to ``('href',)``. Use ``'*'`` to consider
every attribute.
:type attrs: list
:param deny_tags: a tag or a list of tags that should not be considered when
extracting links. It has precedence over the ``tags`` parameter, so it
can be combined with ``tags='*'`` to consider every tag except a few.
Defaults to ``()`` (no tag is excluded).
.. versionadded:: 2.17.0
:type deny_tags: str or list
:param deny_attrs: an attribute or a list of attributes that should not be
considered when looking for links to extract. It has precedence over the
``attrs`` parameter, so it can be combined with ``attrs='*'`` to consider
every attribute except a few. Defaults to ``()`` (no attribute is
excluded).
.. versionadded:: 2.17.0
:type deny_attrs: str or list
:param canonicalize: canonicalize each extracted url (using
w3lib.url.canonicalize_url). Defaults to ``False``.
Note that canonicalize_url is meant for duplicate checking;
it can change the URL visible at server side, so the response can be
different for requests with canonicalized and raw URLs. If you're
using LinkExtractor to follow links it is more robust to
keep the default ``canonicalize=False``.
:type canonicalize: bool
:param unique: whether duplicate filtering should be applied to extracted
links.
:type unique: bool
:param process_value: a function which receives each value extracted from
the tag and attributes scanned and can modify the value and return a
new one, or return ``None`` to ignore the link altogether. If not
given, ``process_value`` defaults to ``lambda x: x``.
.. highlight:: html
For example, to extract links from this code::
<a href="javascript:goToPage('../other/page.html'); return false">Link text</a>
.. highlight:: python
You can use the following function in ``process_value``:
.. code-block:: python
def process_value(value):
m = re.search(r"javascript:goToPage\('(.*?)'", value)
if m:
return m.group(1)
:type process_value: collections.abc.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: bool
"""
_csstranslator = HTMLTranslator()
def __init__(
@ -180,11 +312,17 @@ class LxmlLinkExtractor:
restrict_css: str | Iterable[str] = (),
strip: bool = True,
restrict_text: _RegexOrSeveral | None = None,
deny_tags: str | Iterable[str] = (),
deny_attrs: str | Iterable[str] = (),
):
tags, attrs = set(arg_to_iter(tags)), set(arg_to_iter(attrs))
deny_tags, deny_attrs = (
set(arg_to_iter(deny_tags)),
set(arg_to_iter(deny_attrs)),
)
self.link_extractor = LxmlParserLinkExtractor(
tag=partial(operator.contains, tags),
attr=partial(operator.contains, attrs),
tag=partial(_name_matches, tags, deny_tags),
attr=partial(_name_matches, attrs, deny_attrs),
unique=unique,
process=process_value,
strip=strip,

View File

@ -58,18 +58,20 @@ class LogFormatter:
logging an action the method must return ``None``.
Here is an example on how to create a custom log formatter to lower the severity level of
the log message when an item is dropped from the pipeline::
the log message when an item is dropped from the pipeline:
class PoliteLogFormatter(logformatter.LogFormatter):
def dropped(self, item, exception, response, spider):
return {
'level': logging.INFO, # lowering the level from logging.WARNING
'msg': "Dropped: %(exception)s" + os.linesep + "%(item)s",
'args': {
'exception': exception,
'item': item,
}
}
.. code-block:: python
class PoliteLogFormatter(logformatter.LogFormatter):
def dropped(self, item, exception, response, spider):
return {
"level": logging.INFO, # lowering the level from logging.WARNING
"msg": "Dropped: %(exception)s" + os.linesep + "%(item)s",
"args": {
"exception": exception,
"item": item,
},
}
"""
def crawled(

View File

@ -1,7 +1,5 @@
"""
Mail sending helpers
See documentation in docs/topics/email.rst
"""
from __future__ import annotations

View File

@ -1,7 +1,7 @@
"""
Item pipeline
See documentation in docs/item-pipeline.rst
See documentation in docs/topics/item-pipeline.rst
"""
from __future__ import annotations

View File

@ -423,7 +423,7 @@ class FTPFilesStore:
class FilesPipeline(MediaPipeline):
"""Abstract pipeline that implement the file downloading
"""Pipeline that implements file downloading.
This pipeline tries to minimize network transfers and file processing,
doing stat of the files and determining if file is new, up-to-date or

View File

@ -47,7 +47,7 @@ class ImageException(FileException):
class ImagesPipeline(FilesPipeline):
"""Abstract pipeline that implement the image thumbnail generation logic"""
"""Pipeline that implements the handling logic specific to images."""
MEDIA_NAME: str = "image"

View File

@ -92,9 +92,10 @@ class ScrapyPriorityQueue:
- The :data:`~scrapy.Request.priority` of the request.
For each combination of the above seen, this class creates an instance of
*downstream_queue_cls* with *key* set to a subdirectory of the persistence
directory, named as the request priority (e.g. ``1``), with an ``s`` suffix
in case of a start request (e.g. ``1s``).
*downstream_queue_cls* (or *start_queue_cls* for start requests if it was
passed) with *key* set to a subdirectory of the persistence directory,
named as the negated request priority (e.g. ``-1``), with an ``s`` suffix
in case of a start request (e.g. ``-1s``).
"""
@classmethod
@ -141,10 +142,14 @@ class ScrapyPriorityQueue:
q = self.qfactory(priority)
if q:
self.queues[priority] = q
else:
q.close()
if self._start_queue_cls:
q = self._sqfactory(priority)
if q:
self._start_queues[priority] = q
else:
q.close()
self.curprio = min(startprios)

View File

@ -1,10 +1,6 @@
"""
XPath selectors based on lxml
"""
from __future__ import annotations
from typing import Any
from typing import Any, Literal
from parsel import Selector as _ParselSelector
@ -18,13 +14,10 @@ __all__ = ["Selector", "SelectorList"]
_NOT_SET = object()
def _st(response: TextResponse | None, st: str | None) -> str:
if st is None:
return "xml" if isinstance(response, XmlResponse) else "html"
return st
SelectorType = Literal["html", "xml", "json", "text"]
def _response_from_text(text: str | bytes, st: str | None) -> TextResponse:
def _response_from_text(text: str | bytes, st: SelectorType | None) -> TextResponse:
rt: type[TextResponse] = XmlResponse if st == "xml" else HtmlResponse
return rt(url="about:blank", encoding="utf-8", body=to_bytes(text, "utf-8"))
@ -49,23 +42,16 @@ class Selector(_ParselSelector, object_ref):
``response`` isn't available. Using ``text`` and ``response`` together is
undefined behavior.
``type`` defines the selector type, it can be ``"html"``, ``"xml"``, ``"json"``
or ``None`` (default).
``type`` defines the selector type, it can be ``"html"``, ``"xml"``,
``"json"``, ``"text"`` or ``None`` (default). It's passed to
:class:`parsel.Selector` and its meaning is defined there. However, when
``type`` is ``None``, it is set to ``"xml"`` for an
:class:`~scrapy.http.XmlResponse` and to ``"html"`` otherwise before
passing it to :class:`parsel.Selector`.
If ``type`` is ``None``, the selector automatically chooses the best type
based on ``response`` type (see below), or defaults to ``"html"`` in case it
is used together with ``text``.
If ``type`` is ``None`` and a ``response`` is passed, the selector type is
inferred from the response type as follows:
* ``"html"`` for :class:`~scrapy.http.HtmlResponse` type
* ``"xml"`` for :class:`~scrapy.http.XmlResponse` type
* ``"json"`` for :class:`~scrapy.http.TextResponse` type
* ``"html"`` for anything else
Otherwise, if ``type`` is set, the selector type will be forced and no
detection will occur.
.. note:: JSON selector support requires ``parsel`` 1.8.0 or higher. With
older versions setting ``type`` to ``"json"`` or ``"text"`` is not
supported.
"""
__slots__ = ["response"]
@ -75,7 +61,7 @@ class Selector(_ParselSelector, object_ref):
self,
response: TextResponse | None = None,
text: str | None = None,
type: str | None = None, # noqa: A002
type: SelectorType | None = None, # noqa: A002
root: Any | None = _NOT_SET,
**kwargs: Any,
):
@ -84,10 +70,11 @@ class Selector(_ParselSelector, object_ref):
f"{self.__class__.__name__}.__init__() received both response and text"
)
st = _st(response, type)
if type is None:
type = "xml" if isinstance(response, XmlResponse) else "html" # noqa: A001
if text is not None:
response = _response_from_text(text, st)
response = _response_from_text(text, type)
if response is not None:
text = response.text
@ -98,4 +85,4 @@ class Selector(_ParselSelector, object_ref):
if root is not _NOT_SET:
kwargs["root"] = root
super().__init__(text=text, type=st, **kwargs)
super().__init__(text=text, type=type, **kwargs)

View File

@ -454,9 +454,10 @@ class BaseSettings(MutableMapping[str, Any]):
"""
Store a key/value attribute with a given priority.
Settings should be populated *before* configuring the Crawler object
(through the :meth:`~scrapy.crawler.Crawler.configure` method),
otherwise they won't have any effect.
Settings should be populated *before* the Crawler object applies them
(in the :meth:`~scrapy.crawler.Crawler.crawl_async` or
:meth:`~scrapy.crawler.Crawler.crawl` method), otherwise they won't
have any effect.
:param name: the setting name
:type name: str
@ -613,7 +614,7 @@ class BaseSettings(MutableMapping[str, Any]):
"""
Make a deep copy of current settings.
This method returns a new instance of the :class:`Settings` class,
This method returns a new instance of this class,
populated with the same values and their priorities.
Modifications to the new object won't be reflected on the original
@ -658,7 +659,7 @@ class BaseSettings(MutableMapping[str, Any]):
Make a copy of current settings and convert to a dict.
This method returns a new dict populated with the same values
and their priorities as the current settings.
as the current settings.
Modifications to the returned dict won't be reflected on the original
settings.

View File

@ -240,7 +240,7 @@ BOT_NAME = "scrapybot"
CLOSESPIDER_ERRORCOUNT = 0
CLOSESPIDER_ITEMCOUNT = 0
CLOSESPIDER_PAGECOUNT = 0
CLOSESPIDER_TIMEOUT = 0
CLOSESPIDER_TIMEOUT = 0.0
CLOSESPIDER_PAGECOUNT_NO_ITEM = 0
CLOSESPIDER_TIMEOUT_NO_ITEM = 0

View File

@ -75,7 +75,7 @@ if TYPE_CHECKING:
# running event loop.
#
# Side note: it should be possible to remove _request_deferred() by using
# engine.download_async() instead of engine.schedule(), losing the usual stuff
# engine.download_async() instead of engine.crawl(), losing the usual stuff
# like spider middlewares (none of which should be important).
#
# Other architecture problems:
@ -188,7 +188,8 @@ class Shell:
async def _schedule(self, request: Request, spider: Spider | None) -> Response:
"""Send the request to the engine, wait for the result.
Runs in the reactor thread.
Runs in the reactor thread when using the reactor, or in the asyncio
event loop thread otherwise.
"""
if not self.spider:
await self._open_spider(spider)

View File

@ -75,7 +75,7 @@ class BaseSpiderMiddleware:
) -> Request | None:
"""Return a processed request from the spider output.
This method is called with a single request from the start seeds or the
This method is called with a single request from ``start()`` or the
spider output. It should return the same or a different request, or
``None`` to ignore it.
@ -84,7 +84,7 @@ class BaseSpiderMiddleware:
:param response: the response being processed
:type response: :class:`~scrapy.http.Response` object or ``None`` for
start seeds
start requests
:return: the processed request or ``None``
"""
@ -93,7 +93,7 @@ class BaseSpiderMiddleware:
def get_processed_item(self, item: Any, response: Response | None) -> Any:
"""Return a processed item from the spider output.
This method is called with a single item from the start seeds or the
This method is called with a single item from ``start()`` or the
spider output. It should return the same or a different item, or
``None`` to ignore it.
@ -102,7 +102,7 @@ class BaseSpiderMiddleware:
:param response: the response being processed
:type response: :class:`~scrapy.http.Response` object or ``None`` for
start seeds
start items
:return: the processed item or ``None``
"""

View File

@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
class HttpError(IgnoreRequest):
"""A non-200 response was filtered"""
"""A non-2xx response was filtered"""
def __init__(self, response: Response, *args: Any, **kwargs: Any):
self.response = response

View File

@ -92,7 +92,7 @@ class ReferrerPolicy(ABC):
)
def origin(self, url: str) -> str | None:
"""Return serialized origin (scheme, host, path) for a request or response URL."""
"""Return serialized origin (scheme, host, port) for a request or response URL."""
return self.strip_url(url, origin_only=True)
def potentially_trustworthy(self, url: str) -> bool:

View File

@ -54,17 +54,21 @@ class XMLFeedSpider(Spider):
return response
def parse_node(self, response: Response, selector: Selector) -> Any:
"""This method must be overridden with your custom spider functionality"""
"""This method is called for the nodes matching the provided tag name
(itertag). Receives the response and an Selector for each node.
This method must return either an item, a request, or a list
containing any of them.
This method must be overridden with your custom spider functionality.
"""
if hasattr(self, "parse_item"): # backward compatibility
return self.parse_item(response, selector)
raise NotImplementedError
def parse_nodes(self, response: Response, nodes: Iterable[Selector]) -> Any:
"""This method is called for the nodes matching the provided tag name
(itertag). Receives the response and an Selector for each node.
Overriding this method is mandatory. Otherwise, you spider won't work.
This method must return either an item, a request, or a list
containing any of them.
(itertag). Receives the response and an iterable of Selectors.
"""
for selector in nodes:
@ -113,6 +117,9 @@ class CSVFeedSpider(Spider):
It receives a CSV file in a response; iterates through each of its rows,
and calls parse_row with a dict containing each field's data.
This spider also gives the opportunity to override adapt_response and
process_results methods for pre and post-processing purposes.
You can set some options regarding the CSV file, such as the delimiter, quotechar
and the file's headers.
"""
@ -136,16 +143,14 @@ class CSVFeedSpider(Spider):
return response
def parse_row(self, response: Response, row: dict[str, str]) -> Any:
"""This method must be overridden with your custom spider functionality"""
"""Receives a response and a dict (representing each row) with a key for
each provided (or detected) header of the CSV file.
This method must be overridden with your custom spider functionality.
"""
raise NotImplementedError
def parse_rows(self, response: Response) -> Any:
"""Receives a response and a dict (representing each row) with a key for
each provided (or detected) header of the CSV file. This spider also
gives the opportunity to override adapt_response and
process_results methods for pre and post-processing purposes.
"""
for row in csviter(
response, self.delimiter, self.headers, quotechar=self.quotechar
):

View File

@ -9,5 +9,5 @@ from itemadapter import ItemAdapter
class ${ProjectName}Pipeline:
def process_item(self, item, spider):
def process_item(self, item):
return item

View File

@ -1,8 +1,6 @@
"""
This module contains data types used by Scrapy which are not included in the
Python Standard Library.
This module must not depend on any module outside the Standard Library.
"""
from __future__ import annotations
@ -152,7 +150,9 @@ class LocalCache(OrderedDict[_KT, _VT]):
self.limit: int | None = limit
def __setitem__(self, key: _KT, value: _VT) -> None:
if self.limit:
if self.limit is not None:
if self.limit == 0:
return
while len(self) >= self.limit:
self.popitem(last=False)
super().__setitem__(key, value)

View File

@ -103,7 +103,7 @@ async def _defer_sleep_async() -> None:
def defer_result(result: Any) -> Deferred[Any]: # pragma: no cover
warnings.warn(
"scrapy.utils.defer.defer_result() is deprecated, use"
" twisted.internet.defer.success() and twisted.internet.defer.fail(),"
" twisted.internet.defer.succeed() and twisted.internet.defer.fail(),"
" plus an explicit sleep if needed, or explicit reactor.callLater().",
category=ScrapyDeprecationWarning,
stacklevel=2,
@ -469,22 +469,22 @@ def _maybeDeferred_coro(
def deferred_to_future(d: Deferred[_T]) -> Future[_T]:
"""Return an :class:`asyncio.Future` object that wraps *d*.
This function requires
:class:`~twisted.internet.asyncioreactor.AsyncioSelectorReactor` to be
installed.
This function requires an installed asyncio reactor or a running asyncio
event loop, see :ref:`using-asyncio`.
When :ref:`using the asyncio reactor <install-asyncio>`, you cannot await
on :class:`~twisted.internet.defer.Deferred` objects from :ref:`Scrapy
callables defined as coroutines <coroutine-support>`, you can only await on
``Future`` objects. Wrapping ``Deferred`` objects into ``Future`` objects
allows you to wait on them::
In this state you cannot await on :class:`~twisted.internet.defer.Deferred`
objects from :ref:`Scrapy callables defined as coroutines
<coroutine-support>`, you can only await on ``Future`` objects. Wrapping
``Deferred`` objects into ``Future`` objects allows you to wait on them:
.. code-block:: python
class MySpider(Spider):
...
async def parse(self, response):
additional_request = scrapy.Request('https://example.org/price')
deferred = self.crawler.engine.download(additional_request)
additional_response = await deferred_to_future(deferred)
deferred = some_dfd_helper()
result = await deferred_to_future(deferred)
.. versionchanged:: 2.14
This function no longer installs an asyncio loop if called before the
@ -492,7 +492,10 @@ def deferred_to_future(d: Deferred[_T]) -> Future[_T]:
in this case.
"""
if not is_asyncio_available():
raise RuntimeError("deferred_to_future() requires AsyncioSelectorReactor.")
raise RuntimeError(
"deferred_to_future() requires an installed asyncio reactor"
" or a running asyncio event loop."
)
return d.asFuture(asyncio.get_event_loop())
@ -501,23 +504,26 @@ def maybe_deferred_to_future(d: Deferred[_T]) -> Deferred[_T] | Future[_T]:
defined as a coroutine <coroutine-support>`.
What you can await in Scrapy callables defined as coroutines depends on the
value of :setting:`TWISTED_REACTOR`:
value of :setting:`TWISTED_REACTOR` and :setting:`TWISTED_REACTOR_ENABLED`:
- When :ref:`using the asyncio reactor <install-asyncio>`, you can only
await on :class:`asyncio.Future` objects.
- When :ref:`using the asyncio reactor <install-asyncio>`, or :ref:`not
using a reactor at all <asyncio-without-reactor>`, you can only await
on :class:`asyncio.Future` objects.
- When not using the asyncio reactor, you can only await on
:class:`~twisted.internet.defer.Deferred` objects.
- When :ref:`using a non-asyncio reactor <disable-asyncio>`, you can only
await on :class:`~twisted.internet.defer.Deferred` objects.
If you want to write code that uses ``Deferred`` objects but works with any
reactor, use this function on all ``Deferred`` objects::
If you want to write code that uses ``Deferred`` objects but works in both
of these states, use this function on all ``Deferred`` objects:
.. code-block:: python
class MySpider(Spider):
...
async def parse(self, response):
additional_request = scrapy.Request('https://example.org/price')
deferred = self.crawler.engine.download(additional_request)
additional_response = await maybe_deferred_to_future(deferred)
deferred = some_dfd_helper()
result = await maybe_deferred_to_future(deferred)
"""
if not is_asyncio_available():
return d

View File

@ -43,15 +43,18 @@ def create_deprecated_class(
It can be used to rename a base class in a library. For example, if we
have
class OldName(SomeClass):
# ...
.. code-block:: python
and we want to rename it to NewName, we can do the following::
class OldName(SomeClass): ...
class NewName(SomeClass):
# ...
and we want to rename it to NewName, we can do the following:
OldName = create_deprecated_class('OldName', NewName)
.. code-block:: python
class NewName(SomeClass): ...
OldName = create_deprecated_class("OldName", NewName)
Then, if user class inherits from OldName, warning is issued. Also, if
some code uses ``issubclass(sub, OldName)`` or ``isinstance(sub(), OldName)``

View File

@ -1,4 +1,5 @@
import posixpath
from contextlib import closing
from ftplib import FTP, error_perm
from posixpath import dirname
from typing import IO
@ -32,7 +33,7 @@ def ftp_store_file(
"""Opens a FTP connection with passed credentials,sets current directory
to the directory extracted from given path, then uploads the file to server
"""
with FTP() as ftp:
with FTP() as ftp, closing(file):
ftp.connect(host, port)
ftp.login(username, password)
if use_active_mode:
@ -42,4 +43,3 @@ def ftp_store_file(
ftp_makedirs_cwd(ftp, dirname)
command = "STOR" if overwrite else "APPE"
ftp.storbinary(f"{command} {filename}", file)
file.close()

View File

@ -149,11 +149,12 @@ def install_scrapy_root_handler(settings: Settings) -> None:
def _uninstall_scrapy_root_handler() -> None:
global _scrapy_root_handler # noqa: PLW0603
if (
_scrapy_root_handler is not None
and _scrapy_root_handler in logging.root.handlers
):
if _scrapy_root_handler is None:
return
if _scrapy_root_handler in logging.root.handlers:
logging.root.removeHandler(_scrapy_root_handler)
_scrapy_root_handler.close()
_scrapy_root_handler = None
@ -247,8 +248,7 @@ def logformatter_adapter(
) -> tuple[int, str, dict[str, Any] | tuple[Any, ...]]:
"""
Helper that takes the dictionary output from the methods in LogFormatter
and adapts it into a tuple of positional arguments for logger.log calls,
handling backward compatibility as well.
and adapts it into a tuple of positional arguments for logger.log calls.
"""
level = logkws.get("level", logging.INFO)

View File

@ -7,6 +7,7 @@ from __future__ import annotations
import hashlib
import json
import logging
from typing import TYPE_CHECKING, Any, Protocol
from urllib.parse import urlunparse
from weakref import WeakKeyDictionary
@ -25,6 +26,9 @@ if TYPE_CHECKING:
from typing_extensions import Self
from scrapy.crawler import Crawler
from scrapy.http.request import CookiesT, VerboseCookie
logger = logging.getLogger(__name__)
_fingerprint_cache: WeakKeyDictionary[
@ -179,17 +183,52 @@ def _get_method(obj: Any, name: Any) -> Any:
raise ValueError(f"Method {name!r} not found in: {obj}") from None
def _cookie_value_to_unicode(value: str | bytes | float) -> str:
if isinstance(value, bytes):
return value.decode()
return str(value)
def _to_verbose_cookies(cookies: CookiesT) -> list[VerboseCookie]:
"""Return a list of verbose cookies from ``request.cookies``.
The list of dicts form is returned as is, the dict one is converted first.
"""
if isinstance(cookies, dict):
return [{"name": k, "value": v} for k, v in cookies.items()]
return cookies
def _decode_cookie(cookie: VerboseCookie, request: Request) -> dict[str, str] | None:
"""Return a dict with non-flag verbose cookie values converted to strings.
``name``, ``value``, ``path``, ``domain`` are included, ``secure`` isn't.
"""
decoded = {}
for key in ("name", "value", "path", "domain"):
value = cookie.get(key)
if value is None:
if key in {"name", "value"}:
logger.warning(
f"Invalid cookie found in request {request}:"
f" {cookie} ('{key}' is missing)"
)
return None
continue
if isinstance(value, (bool, float, int, str)):
decoded[key] = str(value)
else:
assert isinstance(value, bytes)
try:
decoded[key] = value.decode("utf8")
except UnicodeDecodeError:
logger.warning(
f"Non UTF-8 encoded cookie found in request {request}: {cookie}",
)
decoded[key] = value.decode("latin1", errors="replace")
return decoded
def request_to_curl(request: Request) -> str:
"""
Converts a :class:`~scrapy.Request` object to a curl command.
:param :class:`~scrapy.Request`: Request object to be converted
:param request: Request object to be converted
:return: string containing the curl command
"""
method = request.method
@ -201,19 +240,14 @@ def request_to_curl(request: Request) -> str:
)
url = request.url
cookies = ""
if request.cookies:
if isinstance(request.cookies, dict):
cookie = "; ".join(f"{k}={v}" for k, v in request.cookies.items())
cookies = f"--cookie '{cookie}'"
elif isinstance(request.cookies, list):
cookie = "; ".join(
f"{_cookie_value_to_unicode(c['name'])}={_cookie_value_to_unicode(c['value'])}"
if "name" in c and "value" in c
else f"{next(iter(c.keys()))}={next(iter(c.values()))}"
for c in request.cookies
)
cookies = f"--cookie '{cookie}'"
cookie_list: list[VerboseCookie] = _to_verbose_cookies(request.cookies)
pairs = [
f"{decoded['name']}={decoded['value']}"
for c in cookie_list
if (decoded := _decode_cookie(c, request)) is not None
]
cookies = f"--cookie '{'; '.join(pairs)}'" if pairs else ""
curl_cmd = f"curl -X {method} {url} {data} {headers} {cookies}".strip()
return " ".join(curl_cmd.split())

View File

@ -88,7 +88,7 @@ def open_in_browser(
def parse_details(self, response):
if "item name" not in response.body:
if "item name" not in response.text:
open_in_browser(response)
"""
# circular imports

View File

@ -39,7 +39,7 @@ def send_catch_log(
*arguments: TypingAny,
**named: TypingAny,
) -> list[tuple[TypingAny, TypingAny]]:
"""Like ``pydispatcher.robust.sendRobust()`` but it also logs errors and returns
"""Like ``pydispatch.robust.sendRobust()`` but it also logs errors and returns
Failures instead of exceptions.
"""
dont_log = named.pop("dont_log", ())
@ -172,9 +172,8 @@ async def _send_catch_log_asyncio(
Returns a coroutine that completes once all signal handlers have finished.
This function requires
:class:`~twisted.internet.asyncioreactor.AsyncioSelectorReactor` to be
installed.
This function requires an installed asyncio reactor or a running asyncio
event loop.
.. versionadded:: 2.14
"""

View File

@ -97,10 +97,11 @@ class Sitemap:
@staticmethod
def _get_tag_name(elem: lxml.etree._Element) -> str:
if TYPE_CHECKING:
assert isinstance(elem.tag, str)
_, _, localname = elem.tag.partition("}")
return localname or elem.tag
tag = elem.tag
if not isinstance(tag, str):
return ""
_, _, localname = tag.partition("}")
return localname or tag
def sitemap_urls_from_robots(

View File

@ -69,6 +69,7 @@ def get_crawler(
# When needed, useful settings can be added here, e.g. ones that prevent
# deprecation warnings.
settings: dict[str, Any] = {
"TELNETCONSOLE_ENABLED": False,
**get_reactor_settings(),
**(settings_dict or {}),
}

View File

@ -4,9 +4,7 @@ references to live object instances.
If you want live objects for a particular class to be tracked, you only have to
subclass from object_ref (instead of object).
About performance: This library has a minimal performance impact when enabled,
and no performance penalty at all when disabled (as object_ref becomes just an
alias to object in that case).
This library has a minimal performance impact.
.. note:: PyPy uses a tracing garbage collector, so objects may
remain in the ``live_refs`` longer than expected, even after they

View File

@ -117,8 +117,8 @@ def strip_url(
- ``strip_credentials`` removes "user:password@"
- ``strip_default_port`` removes ":80" (resp. ":443", ":21")
from http:// (resp. https://, ftp://) URLs
- ``origin_only`` replaces path component with "/", also dropping
query and fragment components ; it also strips credentials
- ``origin_only`` replaces the path component with "/", also dropping
the query component; it also strips credentials
- ``strip_fragment`` drops any #fragment component
"""

View File

@ -9,7 +9,7 @@ from scrapy.utils.defer import deferred_from_coro
class UppercasePipeline:
async def _open_spider(self, spider):
async def _open_spider(self, spider: Spider) -> None:
spider.logger.info("async pipeline opened!")
await asyncio.sleep(0.1)

View File

@ -0,0 +1,31 @@
import asyncio
import sys
from twisted.internet import asyncioreactor
import scrapy
from scrapy.crawler import AsyncCrawlerProcess
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
loop = asyncio.SelectorEventLoop()
asyncio.set_event_loop(loop)
asyncioreactor.install(loop)
class NoRequestsSpider(scrapy.Spider):
name = "no_request"
async def start(self):
return
yield
process = AsyncCrawlerProcess(
settings={
"TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
"ASYNCIO_EVENT_LOOP": "asyncio.SelectorEventLoop",
}
)
process.crawl(NoRequestsSpider)
process.start()

Some files were not shown because too many files have changed in this diff Show More