mirror of https://github.com/scrapy/scrapy.git
Compare commits
49 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
b2d4eedea8 | |
|
|
01447f9965 | |
|
|
0cbb20e8e8 | |
|
|
8b5147ae2e | |
|
|
bc5b5fb1f6 | |
|
|
e7d8b34e73 | |
|
|
ad816d2b3a | |
|
|
5a65bdcc18 | |
|
|
59ebb26e60 | |
|
|
7e8b58a2b2 | |
|
|
a5bc43e34c | |
|
|
f3693aa8ba | |
|
|
25e6884e2f | |
|
|
cec86f216e | |
|
|
13be37e4b1 | |
|
|
e710b9c18e | |
|
|
96195e4a61 | |
|
|
e4ae4aad52 | |
|
|
58ed9fdccc | |
|
|
41bb09741a | |
|
|
0b578c1cbf | |
|
|
abbc024bbe | |
|
|
67e5282684 | |
|
|
8489b3dad8 | |
|
|
628a3afbbd | |
|
|
56dee203e9 | |
|
|
1157b3e235 | |
|
|
a54c438da1 | |
|
|
64358f547b | |
|
|
ca21306df7 | |
|
|
1ddf024b89 | |
|
|
394c2797f3 | |
|
|
e322905255 | |
|
|
4ee3676464 | |
|
|
36bf1185e5 | |
|
|
b1b9efb473 | |
|
|
1c5404dce5 | |
|
|
4d4a04f318 | |
|
|
e80f94fe8a | |
|
|
7faf20c6b5 | |
|
|
a591d15c04 | |
|
|
11d7a05a6f | |
|
|
d8ba1571e7 | |
|
|
b3670369b8 | |
|
|
c9446931a8 | |
|
|
9d9950df69 | |
|
|
61f99f2df1 | |
|
|
bdf3067935 | |
|
|
c5ec881f1d |
|
|
@ -79,9 +79,6 @@ jobs:
|
||||||
- python-version: "3.14"
|
- python-version: "3.14"
|
||||||
env:
|
env:
|
||||||
TOXENV: botocore
|
TOXENV: botocore
|
||||||
- python-version: "3.14"
|
|
||||||
env:
|
|
||||||
TOXENV: mitmproxy
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
@ -97,6 +94,9 @@ jobs:
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install libxml2-dev libxslt-dev
|
sudo apt-get install libxml2-dev libxslt-dev
|
||||||
|
|
||||||
|
- name: Install mitmproxy
|
||||||
|
run: pipx install mitmproxy
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
env: ${{ matrix.env }}
|
env: ${{ matrix.env }}
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ exclude: |
|
||||||
)
|
)
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.15.2
|
rev: v0.15.20
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args: [ --fix ]
|
args: [ --fix ]
|
||||||
|
|
@ -16,7 +16,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- black==25.9.0
|
- black==26.5.1
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v6.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
|
|
|
||||||
25
conftest.py
25
conftest.py
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib
|
import os
|
||||||
|
from importlib.util import find_spec
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@ from scrapy.utils.reactor import set_asyncio_event_loop_policy
|
||||||
from scrapy.utils.reactorless import install_reactor_import_hook
|
from scrapy.utils.reactorless import install_reactor_import_hook
|
||||||
from tests.keys import generate_keys
|
from tests.keys import generate_keys
|
||||||
from tests.mockserver.http import MockServer
|
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:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
@ -50,9 +51,7 @@ if not H2_ENABLED:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
if find_spec("httpx2") is None and find_spec("httpx") is None:
|
||||||
import httpx # noqa: F401
|
|
||||||
except ImportError:
|
|
||||||
collect_ignore.append("scrapy/core/downloader/handlers/_httpx.py")
|
collect_ignore.append("scrapy/core/downloader/handlers/_httpx.py")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -127,16 +126,16 @@ def pytest_runtest_setup(item):
|
||||||
"uvloop",
|
"uvloop",
|
||||||
"botocore",
|
"botocore",
|
||||||
"boto3",
|
"boto3",
|
||||||
"mitmproxy",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for module in optional_deps:
|
for module in optional_deps:
|
||||||
if item.get_closest_marker(f"requires_{module}"):
|
if item.get_closest_marker(f"requires_{module}") and find_spec(module) is None:
|
||||||
try:
|
pytest.skip(f"{module} is not installed")
|
||||||
importlib.import_module(module)
|
|
||||||
except ImportError:
|
if item.get_closest_marker("requires_mitmproxy") and mitmdump_cmd() is None:
|
||||||
pytest.skip(f"{module} is not installed")
|
pytest.skip("mitmdump is not available")
|
||||||
|
|
||||||
|
|
||||||
# Generate localhost certificate files, needed by some tests
|
# Generate localhost certificate files, needed by some tests (but only once if xdist is used)
|
||||||
generate_keys()
|
if "PYTEST_XDIST_WORKER" not in os.environ:
|
||||||
|
generate_keys()
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -158,6 +158,7 @@ scrapy_intersphinx_enable = [
|
||||||
"itemloaders",
|
"itemloaders",
|
||||||
"parsel",
|
"parsel",
|
||||||
"pytest",
|
"pytest",
|
||||||
|
"pypug",
|
||||||
"scrapy-lint",
|
"scrapy-lint",
|
||||||
"sphinx",
|
"sphinx",
|
||||||
"tox",
|
"tox",
|
||||||
|
|
|
||||||
|
|
@ -323,9 +323,10 @@ deprecation removals are documented in the :ref:`release notes <news>`.
|
||||||
Tests
|
Tests
|
||||||
=====
|
=====
|
||||||
|
|
||||||
Tests are implemented using the :doc:`Twisted unit-testing framework
|
Tests are implemented using pytest_. Running tests requires :doc:`tox
|
||||||
<twisted:development/test-standard>`. Running tests requires
|
<tox:index>`.
|
||||||
:doc:`tox <tox:index>`.
|
|
||||||
|
.. _pytest: https://pytest.org
|
||||||
|
|
||||||
.. _running-tests:
|
.. _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.
|
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
|
Writing tests
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
@ -398,3 +414,6 @@ And their unit-tests are in::
|
||||||
.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist
|
.. _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
|
.. _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
|
.. _test coverage: https://app.codecov.io/gh/scrapy/scrapy
|
||||||
|
.. _mitmproxy: https://mitmproxy.org/
|
||||||
|
.. _pipx: https://pipx.pypa.io/
|
||||||
|
.. _uv: https://docs.astral.sh/uv/
|
||||||
|
|
|
||||||
20
docs/faq.rst
20
docs/faq.rst
|
|
@ -136,7 +136,7 @@ middleware with a :ref:`custom downloader middleware
|
||||||
<topics-downloader-middleware-custom>` that requires less memory. For example:
|
<topics-downloader-middleware-custom>` that requires less memory. For example:
|
||||||
|
|
||||||
- If your domain names are similar enough, use your own regular expression
|
- If your domain names are similar enough, use your own regular expression
|
||||||
instead joining the strings in :attr:`~scrapy.Spider.allowed_domains` into
|
instead of joining the strings in :attr:`~scrapy.Spider.allowed_domains` into
|
||||||
a complex regular expression.
|
a complex regular expression.
|
||||||
|
|
||||||
- If you can meet the installation requirements, use pyre2_ instead of
|
- If you can meet the installation requirements, use pyre2_ instead of
|
||||||
|
|
@ -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
|
In order to avoid parsing all the entire feed at once in memory, you can use
|
||||||
the :func:`~scrapy.utils.iterators.xmliter_lxml` and
|
the :func:`~scrapy.utils.iterators.xmliter_lxml` and
|
||||||
:func:`~scrapy.utils.iterators.csviter` functions. In fact, this is what
|
: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
|
.. autofunction:: scrapy.utils.iterators.xmliter_lxml
|
||||||
|
|
||||||
|
|
@ -331,8 +332,8 @@ section of the site (which varies each time). In that case, the credentials to
|
||||||
log in would be settings, while the url of the section to scrape would be a
|
log in would be settings, while the url of the section to scrape would be a
|
||||||
spider argument.
|
spider argument.
|
||||||
|
|
||||||
I'm scraping a XML document and my XPath selector doesn't return any items
|
I'm scraping an XML document and my XPath selector doesn't return any items
|
||||||
--------------------------------------------------------------------------
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
You may need to remove namespaces. See :ref:`removing-namespaces`.
|
You may need to remove namespaces. See :ref:`removing-namespaces`.
|
||||||
|
|
||||||
|
|
@ -360,10 +361,11 @@ method for this purpose. For example:
|
||||||
def process_spider_output(self, response, result):
|
def process_spider_output(self, response, result):
|
||||||
for item_or_request in result:
|
for item_or_request in result:
|
||||||
if isinstance(item_or_request, Request):
|
if isinstance(item_or_request, Request):
|
||||||
|
yield item_or_request
|
||||||
continue
|
continue
|
||||||
adapter = ItemAdapter(item)
|
adapter = ItemAdapter(item_or_request)
|
||||||
for _ in range(adapter["multiply_by"]):
|
for _ in range(adapter["multiply_by"]):
|
||||||
yield deepcopy(item)
|
yield deepcopy(item_or_request)
|
||||||
|
|
||||||
Does Scrapy support IPv6 addresses?
|
Does Scrapy support IPv6 addresses?
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
@ -382,8 +384,9 @@ How to deal with ``<class 'ValueError'>: filedescriptor out of range in select()
|
||||||
----------------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
This issue `has been reported`_ to appear when running broad crawls in macOS, where the default
|
This issue `has been reported`_ to appear when running broad crawls in macOS, where the default
|
||||||
Twisted reactor is :class:`twisted.internet.selectreactor.SelectReactor`. Switching to a
|
Twisted reactor was :class:`twisted.internet.selectreactor.SelectReactor` at that time.
|
||||||
different reactor is possible by using the :setting:`TWISTED_REACTOR` setting.
|
If you have switched to this reactor using the :setting:`TWISTED_REACTOR` setting you can switch
|
||||||
|
to a different one in the same way.
|
||||||
|
|
||||||
|
|
||||||
.. _faq-stop-response-download:
|
.. _faq-stop-response-download:
|
||||||
|
|
@ -409,7 +412,6 @@ How can I make a blank request?
|
||||||
|
|
||||||
from scrapy import Request
|
from scrapy import Request
|
||||||
|
|
||||||
|
|
||||||
blank_request = Request("data:,")
|
blank_request = Request("data:,")
|
||||||
|
|
||||||
In this case, the URL is set to a data URI scheme. Data URLs allow you to include data
|
In this case, the URL is set to a data URI scheme. Data URLs allow you to include data
|
||||||
|
|
|
||||||
|
|
@ -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 `StackOverflow using the scrapy tag`_.
|
||||||
* Ask or search questions in the `Scrapy subreddit`_.
|
* Ask or search questions in the `Scrapy subreddit`_.
|
||||||
* Search for questions on the archives of the `scrapy-users mailing list`_.
|
* 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`_.
|
* Report bugs with Scrapy in our `issue tracker`_.
|
||||||
* Join the Discord community `Scrapy Discord`_.
|
* Join the Discord community `Scrapy Discord`_.
|
||||||
|
|
||||||
|
|
@ -91,15 +91,15 @@ Basic concepts
|
||||||
:doc:`topics/selectors`
|
:doc:`topics/selectors`
|
||||||
Extract the data from web pages using XPath.
|
Extract the data from web pages using XPath.
|
||||||
|
|
||||||
:doc:`topics/shell`
|
|
||||||
Test your extraction code in an interactive environment.
|
|
||||||
|
|
||||||
:doc:`topics/items`
|
:doc:`topics/items`
|
||||||
Define the data you want to scrape.
|
Define the data you want to scrape.
|
||||||
|
|
||||||
:doc:`topics/loaders`
|
:doc:`topics/loaders`
|
||||||
Populate your items with the extracted data.
|
Populate your items with the extracted data.
|
||||||
|
|
||||||
|
:doc:`topics/shell`
|
||||||
|
Test your extraction code in an interactive environment.
|
||||||
|
|
||||||
:doc:`topics/item-pipeline`
|
:doc:`topics/item-pipeline`
|
||||||
Post-process and store your scraped data.
|
Post-process and store your scraped data.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,56 @@ just like any other Python package.
|
||||||
(See :ref:`platform-specific guides <intro-install-platform-notes>`
|
(See :ref:`platform-specific guides <intro-install-platform-notes>`
|
||||||
below for non-Python dependencies that you may need to install beforehand).
|
below for non-Python dependencies that you may need to install beforehand).
|
||||||
|
|
||||||
|
.. _extras:
|
||||||
|
|
||||||
|
Optional extras
|
||||||
|
===============
|
||||||
|
|
||||||
|
Scrapy provides optional :ref:`extras <pypug:dependency-specifiers-extras>`
|
||||||
|
that install additional dependencies to enable specific features. To install
|
||||||
|
Scrapy with one or more extras, list them in square brackets:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pip install scrapy[s3,images]
|
||||||
|
|
||||||
|
The following extras are available:
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Extra
|
||||||
|
- Provides
|
||||||
|
* - ``bpython``
|
||||||
|
- :ref:`bpython shell <shell-config>`
|
||||||
|
* - ``brotli``
|
||||||
|
- :ref:`Brotli response decompression <http-compression>`
|
||||||
|
* - ``gcs``
|
||||||
|
- :ref:`Google Cloud Storage <topics-feed-storage-gcs>` for
|
||||||
|
:ref:`feed exports <topics-feed-exports>` and
|
||||||
|
:ref:`media pipelines <media-pipeline-gcs>`
|
||||||
|
* - ``httpx``
|
||||||
|
- :ref:`httpx-handler`, including its HTTP/2 and SOCKS proxy support
|
||||||
|
* - ``images``
|
||||||
|
- :ref:`Images pipeline <images-pipeline>`
|
||||||
|
* - ``ipython``
|
||||||
|
- :ref:`IPython shell <shell-config>`
|
||||||
|
* - ``ptpython``
|
||||||
|
- :ref:`ptpython shell <shell-config>`
|
||||||
|
* - ``robotparser``
|
||||||
|
- :ref:`Robotexclusionrulesparser robots.txt parsing <rerp-parser>`
|
||||||
|
* - ``s3``
|
||||||
|
- :ref:`Amazon S3 <topics-feed-storage-s3>` storage for
|
||||||
|
:ref:`feed exports <topics-feed-exports>`,
|
||||||
|
:ref:`media pipelines <media-pipelines-s3>`, and
|
||||||
|
:ref:`S3 downloads <s3-handler>`
|
||||||
|
* - ``twisted-http2``
|
||||||
|
- :ref:`twisted-http2-handler`
|
||||||
|
* - ``uvloop``
|
||||||
|
- `uvloop <https://github.com/MagicStack/uvloop>`_ event loop
|
||||||
|
* - ``zstd``
|
||||||
|
- :ref:`Zstandard response decompression <http-compression>`
|
||||||
|
|
||||||
|
|
||||||
.. _intro-install-platform-notes:
|
.. _intro-install-platform-notes:
|
||||||
|
|
||||||
|
|
@ -230,8 +280,8 @@ Installing Scrapy with PyPy on Windows is not tested.
|
||||||
You can check that Scrapy is installed correctly by running ``scrapy bench``.
|
You can check that Scrapy is installed correctly by running ``scrapy bench``.
|
||||||
If this command gives errors such as
|
If this command gives errors such as
|
||||||
``TypeError: ... got 2 unexpected keyword arguments``, this means
|
``TypeError: ... got 2 unexpected keyword arguments``, this means
|
||||||
that setuptools was unable to pick up one PyPy-specific dependency.
|
that the ``PyPyDispatcher`` dependency wasn't installed. To fix this issue, run
|
||||||
To fix this issue, run ``pip install 'PyPyDispatcher>=2.1.0'``.
|
``pip install 'PyPyDispatcher>=2.1.0'``.
|
||||||
|
|
||||||
|
|
||||||
.. _intro-install-troubleshooting:
|
.. _intro-install-troubleshooting:
|
||||||
|
|
|
||||||
|
|
@ -83,16 +83,17 @@ While this enables you to do very fast crawls (sending multiple concurrent
|
||||||
requests at the same time, in a fault-tolerant way) Scrapy also gives you
|
requests at the same time, in a fault-tolerant way) Scrapy also gives you
|
||||||
control over the politeness of the crawl through :ref:`a few settings
|
control over the politeness of the crawl through :ref:`a few settings
|
||||||
<topics-settings-ref>`. You can do things like setting a download delay between
|
<topics-settings-ref>`. You can do things like setting a download delay between
|
||||||
each request, limiting the amount of concurrent requests per domain or per IP, and
|
each request, limiting the amount of concurrent requests per domain, and
|
||||||
even :ref:`using an auto-throttling extension <topics-autothrottle>` that tries
|
even :ref:`using an auto-throttling extension <topics-autothrottle>` that tries
|
||||||
to figure these settings out automatically.
|
to figure these settings out automatically.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
This is using :ref:`feed exports <topics-feed-exports>` to generate the
|
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
|
JSON Lines file, you can easily change the export format (XML or CSV, for
|
||||||
storage backend (FTP or `Amazon S3`_, for example). You can also write an
|
example) or the storage backend (FTP or `Amazon S3`_, for example). You can
|
||||||
:ref:`item pipeline <topics-item-pipeline>` to store the items in a database.
|
also write an :ref:`item pipeline <topics-item-pipeline>` to store the
|
||||||
|
items in a database.
|
||||||
|
|
||||||
|
|
||||||
.. _topics-whatelse:
|
.. _topics-whatelse:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,28 @@
|
||||||
Release notes
|
Release notes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
Scrapy VERSION (unreleased)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Backward-incompatible changes
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- The following runtime usage of zope.interface_ interfaces is removed:
|
||||||
|
|
||||||
|
- :class:`~scrapy.spiderloader.SpiderLoader` and
|
||||||
|
:class:`~scrapy.spiderloader.DummySpiderLoader` are no longer marked
|
||||||
|
as implementing the ``ISpiderLoader`` interface.
|
||||||
|
|
||||||
|
- :func:`~scrapy.spiderloader.get_spider_loader` no longer checks that the
|
||||||
|
configured spider loader implements the ``ISpiderLoader`` interface.
|
||||||
|
|
||||||
|
- :class:`~scrapy.extensions.feedexport.BlockingFeedStorage`,
|
||||||
|
:class:`~scrapy.extensions.feedexport.FileFeedStorage` and
|
||||||
|
:class:`~scrapy.extensions.feedexport.StdoutFeedStorage` are no longer
|
||||||
|
marked as implementing the ``IFeedStorage`` interface.
|
||||||
|
|
||||||
|
(:issue:`6585`, :issue:`7731`)
|
||||||
|
|
||||||
.. _release-2.17.0:
|
.. _release-2.17.0:
|
||||||
|
|
||||||
Scrapy 2.17.0 (2026-07-07)
|
Scrapy 2.17.0 (2026-07-07)
|
||||||
|
|
@ -1035,7 +1057,7 @@ Deprecations
|
||||||
New features
|
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.
|
supported referrer policies.
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
|
|
@ -2643,7 +2665,7 @@ Deprecation removals
|
||||||
(:issue:`6109`, :issue:`6116`)
|
(:issue:`6109`, :issue:`6116`)
|
||||||
|
|
||||||
- A custom class assigned to the :setting:`SPIDER_LOADER_CLASS` setting that
|
- A custom class assigned to the :setting:`SPIDER_LOADER_CLASS` setting that
|
||||||
does not implement the :class:`~scrapy.interfaces.ISpiderLoader` interface
|
does not implement the ``ISpiderLoader`` interface
|
||||||
will now raise a :exc:`zope.interface.verify.DoesNotImplement` exception at
|
will now raise a :exc:`zope.interface.verify.DoesNotImplement` exception at
|
||||||
run time. Non-compliant classes have been triggering a deprecation warning
|
run time. Non-compliant classes have been triggering a deprecation warning
|
||||||
since Scrapy 1.0.0.
|
since Scrapy 1.0.0.
|
||||||
|
|
@ -5133,7 +5155,7 @@ Bug fixes
|
||||||
* The system file mode creation mask no longer affects the permissions of
|
* The system file mode creation mask no longer affects the permissions of
|
||||||
files generated using the :command:`startproject` command (:issue:`4722`)
|
files generated using the :command:`startproject` command (:issue:`4722`)
|
||||||
|
|
||||||
* :func:`scrapy.utils.iterators.xmliter` now supports namespaced node names
|
* ``scrapy.utils.iterators.xmliter`` now supports namespaced node names
|
||||||
(:issue:`861`, :issue:`4746`)
|
(:issue:`861`, :issue:`4746`)
|
||||||
|
|
||||||
* :class:`~scrapy.Request` objects can now have ``about:`` URLs, which can
|
* :class:`~scrapy.Request` objects can now have ``about:`` URLs, which can
|
||||||
|
|
@ -6046,7 +6068,7 @@ The following changes may impact custom priority queue classes:
|
||||||
|
|
||||||
* A new keyword parameter has been added: ``key``. It is a string
|
* A new keyword parameter has been added: ``key``. It is a string
|
||||||
that is always an empty string for memory queues and indicates the
|
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
|
* The parameter for disk queues that contains data from the previous
|
||||||
crawl, ``startprios`` or ``slot_startprios``, is now passed as a
|
crawl, ``startprios`` or ``slot_startprios``, is now passed as a
|
||||||
|
|
@ -6600,7 +6622,7 @@ New features
|
||||||
``scrapy.pqueues.DownloaderAwarePriorityQueue``, may be
|
``scrapy.pqueues.DownloaderAwarePriorityQueue``, may be
|
||||||
:ref:`enabled <broad-crawls-scheduler-priority-queue>` for a significant
|
:ref:`enabled <broad-crawls-scheduler-priority-queue>` for a significant
|
||||||
scheduling improvement on crawls targeting multiple web domains, at the
|
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
|
* A new :attr:`.Request.cb_kwargs` attribute
|
||||||
provides a cleaner way to pass keyword arguments to callback methods
|
provides a cleaner way to pass keyword arguments to callback methods
|
||||||
|
|
@ -9079,7 +9101,7 @@ New features and settings
|
||||||
- In request errbacks, offending requests are now received in ``failure.request`` attribute (:rev:`2738`)
|
- 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`)
|
- Big downloader refactoring to support per domain/ip concurrency limits (:rev:`2732`)
|
||||||
- ``CONCURRENT_REQUESTS_PER_SPIDER`` setting has been deprecated and replaced by:
|
- ``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
|
- check the documentation for more details
|
||||||
- Added builtin caching DNS resolver (:rev:`2728`)
|
- 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`)
|
- 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`)
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,14 @@ The ``ADDONS`` setting is a dict in which every key is an add-on class or its
|
||||||
import path and the value is its priority.
|
import path and the value is its priority.
|
||||||
|
|
||||||
This is an example where two add-ons are enabled in a project's
|
This is an example where two add-ons are enabled in a project's
|
||||||
``settings.py``::
|
``settings.py``:
|
||||||
|
|
||||||
|
.. skip: next
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
ADDONS = {
|
ADDONS = {
|
||||||
'path.to.someaddon': 0,
|
"path.to.someaddon": 0,
|
||||||
SomeAddonClass: 1,
|
SomeAddonClass: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +60,9 @@ the following methods:
|
||||||
:type settings: :class:`~scrapy.settings.BaseSettings`
|
:type settings: :class:`~scrapy.settings.BaseSettings`
|
||||||
|
|
||||||
The settings set by the add-on should use the ``addon`` priority (see
|
The settings set by the add-on should use the ``addon`` priority (see
|
||||||
:ref:`populating-settings` and :func:`scrapy.settings.BaseSettings.set`)::
|
:ref:`populating-settings` and :func:`scrapy.settings.BaseSettings.set`):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MyAddon:
|
class MyAddon:
|
||||||
def update_settings(self, settings):
|
def update_settings(self, settings):
|
||||||
|
|
@ -168,7 +174,6 @@ Use a fallback component:
|
||||||
|
|
||||||
from scrapy.utils.misc import build_from_crawler, load_object
|
from scrapy.utils.misc import build_from_crawler, load_object
|
||||||
|
|
||||||
|
|
||||||
FALLBACK_SETTING = "MY_FALLBACK_DOWNLOAD_HANDLER"
|
FALLBACK_SETTING = "MY_FALLBACK_DOWNLOAD_HANDLER"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,46 +172,15 @@ SpiderLoader API
|
||||||
.. module:: scrapy.spiderloader
|
.. module:: scrapy.spiderloader
|
||||||
:synopsis: The spider loader
|
:synopsis: The spider loader
|
||||||
|
|
||||||
.. class:: SpiderLoader
|
Custom spider loaders can be employed by specifying their path in the
|
||||||
|
:setting:`SPIDER_LOADER_CLASS` project setting. They must implement
|
||||||
|
:class:`SpiderLoaderProtocol`.
|
||||||
|
|
||||||
This class is in charge of retrieving and handling the spider classes
|
.. autoclass:: SpiderLoaderProtocol
|
||||||
defined across the project.
|
:members:
|
||||||
|
|
||||||
Custom spider loaders can be employed by specifying their path in the
|
.. autoclass:: SpiderLoader
|
||||||
:setting:`SPIDER_LOADER_CLASS` project setting. They must fully implement
|
:members:
|
||||||
the :class:`scrapy.interfaces.ISpiderLoader` interface to guarantee an
|
|
||||||
errorless execution.
|
|
||||||
|
|
||||||
.. method:: from_settings(settings)
|
|
||||||
|
|
||||||
This class method is used by Scrapy to create an instance of the class.
|
|
||||||
It's called with the current project settings, and it loads the spiders
|
|
||||||
found recursively in the modules of the :setting:`SPIDER_MODULES`
|
|
||||||
setting.
|
|
||||||
|
|
||||||
:param settings: project settings
|
|
||||||
:type settings: :class:`~scrapy.settings.Settings` instance
|
|
||||||
|
|
||||||
.. method:: load(spider_name)
|
|
||||||
|
|
||||||
Get the Spider class with the given name. It'll look into the previously
|
|
||||||
loaded spiders for a spider class with name ``spider_name`` and will raise
|
|
||||||
a KeyError if not found.
|
|
||||||
|
|
||||||
:param spider_name: spider class name
|
|
||||||
:type spider_name: str
|
|
||||||
|
|
||||||
.. method:: list()
|
|
||||||
|
|
||||||
Get the names of the available spiders in the project.
|
|
||||||
|
|
||||||
.. method:: find_by_request(request)
|
|
||||||
|
|
||||||
List the spiders' names that can handle the given request. Will try to
|
|
||||||
match the request's url against the domains of the spiders.
|
|
||||||
|
|
||||||
:param request: queried request
|
|
||||||
:type request: :class:`~scrapy.Request` instance
|
|
||||||
|
|
||||||
.. autoclass:: DummySpiderLoader
|
.. autoclass:: DummySpiderLoader
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ asyncio
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Scrapy supports :mod:`asyncio` natively. New projects created with
|
Scrapy supports :mod:`asyncio` natively. New projects created with
|
||||||
:command:`scrapy startproject` have asyncio enabled by default, and you can use
|
:command:`startproject` have asyncio enabled by default, and you can use
|
||||||
:mod:`asyncio` and :mod:`asyncio`-powered libraries in any :doc:`coroutine
|
:mod:`asyncio` and :mod:`asyncio`-powered libraries in any :doc:`coroutine
|
||||||
<coroutines>`.
|
<coroutines>`.
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ no additional setup is needed.
|
||||||
Configuring the asyncio reactor
|
Configuring the asyncio reactor
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
New projects generated with :command:`scrapy startproject` have the asyncio
|
New projects generated with :command:`startproject` have the asyncio
|
||||||
reactor configured by default. No manual setup is needed.
|
reactor configured by default. No manual setup is needed.
|
||||||
|
|
||||||
The :setting:`TWISTED_REACTOR` setting controls which Twisted reactor Scrapy
|
The :setting:`TWISTED_REACTOR` setting controls which Twisted reactor Scrapy
|
||||||
|
|
@ -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_from_coro
|
||||||
.. autofunction:: scrapy.utils.defer.deferred_f_from_coro_f
|
.. autofunction:: scrapy.utils.defer.deferred_f_from_coro_f
|
||||||
|
|
||||||
|
The following function helps with a reverse wrapping:
|
||||||
|
|
||||||
.. autofunction:: scrapy.utils.defer.ensure_awaitable
|
.. autofunction:: scrapy.utils.defer.ensure_awaitable
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -147,6 +150,12 @@ Using Scrapy without a Twisted reactor
|
||||||
.. warning::
|
.. warning::
|
||||||
This is currently experimental and may not be suitable for production use.
|
This is currently experimental and may not be suitable for production use.
|
||||||
|
|
||||||
|
.. note:: As the Twisted download handlers cannot be used without a reactor,
|
||||||
|
the default download handler in this mode is
|
||||||
|
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`. You
|
||||||
|
will need to additionally install the :ref:`httpx <extras>` extra to use
|
||||||
|
it, unless you switch to some different handler.
|
||||||
|
|
||||||
It's possible to use Scrapy without installing a Twisted reactor at all, by
|
It's possible to use Scrapy without installing a Twisted reactor at all, by
|
||||||
setting the :setting:`TWISTED_REACTOR_ENABLED` setting to ``False``. In this
|
setting the :setting:`TWISTED_REACTOR_ENABLED` setting to ``False``. In this
|
||||||
mode Scrapy will use the asyncio event loop directly, and most of the Scrapy
|
mode Scrapy will use the asyncio event loop directly, and most of the Scrapy
|
||||||
|
|
@ -190,7 +199,7 @@ in future Scrapy versions. The following features are not available:
|
||||||
:class:`~scrapy.crawler.CrawlerProcess`
|
:class:`~scrapy.crawler.CrawlerProcess`
|
||||||
(:class:`~scrapy.crawler.AsyncCrawlerProcess` and
|
(:class:`~scrapy.crawler.AsyncCrawlerProcess` and
|
||||||
:class:`~scrapy.crawler.AsyncCrawlerRunner` are available)
|
: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
|
* User and 3rd-party code that requires a reactor (see :ref:`below
|
||||||
<asyncio-without-reactor-migrate>` for examples)
|
<asyncio-without-reactor-migrate>` for examples)
|
||||||
|
|
||||||
|
|
@ -218,7 +227,8 @@ for its differences and limitations compared to
|
||||||
|
|
||||||
Additionally, :class:`~scrapy.crawler.AsyncCrawlerProcess` will install a
|
Additionally, :class:`~scrapy.crawler.AsyncCrawlerProcess` will install a
|
||||||
:term:`meta path finder` that prevents :mod:`twisted.internet.reactor` from
|
:term:`meta path finder` that prevents :mod:`twisted.internet.reactor` from
|
||||||
being imported.
|
being imported. It will be uninstalled when :meth:`AsyncCrawlerProcess.start()
|
||||||
|
<scrapy.crawler.AsyncCrawlerProcess.start>` exits.
|
||||||
|
|
||||||
.. _asyncio-without-reactor-migrate:
|
.. _asyncio-without-reactor-migrate:
|
||||||
|
|
||||||
|
|
@ -315,8 +325,7 @@ implementations, :class:`~asyncio.ProactorEventLoop` (default) and
|
||||||
:class:`~asyncio.SelectorEventLoop` works with Twisted.
|
:class:`~asyncio.SelectorEventLoop` works with Twisted.
|
||||||
|
|
||||||
Scrapy changes the event loop class to :class:`~asyncio.SelectorEventLoop`
|
Scrapy changes the event loop class to :class:`~asyncio.SelectorEventLoop`
|
||||||
automatically when you change the :setting:`TWISTED_REACTOR` setting or call
|
automatically when installing the asyncio reactor.
|
||||||
:func:`~scrapy.utils.reactor.install_reactor`.
|
|
||||||
|
|
||||||
.. note:: Other libraries you use may require
|
.. note:: Other libraries you use may require
|
||||||
:class:`~asyncio.ProactorEventLoop`, e.g. because it supports
|
:class:`~asyncio.ProactorEventLoop`, e.g. because it supports
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ AutoThrottle algorithm adjusts download delays based on the following rules:
|
||||||
.. _download-latency:
|
.. _download-latency:
|
||||||
|
|
||||||
In Scrapy, the download latency is measured as the time elapsed between
|
In Scrapy, the download latency is measured as the time elapsed between
|
||||||
establishing the TCP connection and receiving the HTTP headers.
|
sending the request and receiving the HTTP headers.
|
||||||
|
|
||||||
Note that these latencies are very hard to measure accurately in a cooperative
|
Note that these latencies are very hard to measure accurately in a cooperative
|
||||||
multitasking environment because Scrapy may be busy processing a spider
|
multitasking environment because Scrapy may be busy processing a spider
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,7 @@ Global commands:
|
||||||
* :command:`fetch`
|
* :command:`fetch`
|
||||||
* :command:`view`
|
* :command:`view`
|
||||||
* :command:`version`
|
* :command:`version`
|
||||||
|
* :command:`bench`
|
||||||
|
|
||||||
Project-only commands:
|
Project-only commands:
|
||||||
|
|
||||||
|
|
@ -207,7 +208,6 @@ Project-only commands:
|
||||||
* :command:`list`
|
* :command:`list`
|
||||||
* :command:`edit`
|
* :command:`edit`
|
||||||
* :command:`parse`
|
* :command:`parse`
|
||||||
* :command:`bench`
|
|
||||||
|
|
||||||
.. command:: startproject
|
.. command:: startproject
|
||||||
|
|
||||||
|
|
@ -309,11 +309,25 @@ Usage examples::
|
||||||
* parse_item
|
* parse_item
|
||||||
|
|
||||||
$ scrapy check
|
$ scrapy check
|
||||||
[FAILED] first_spider:parse_item
|
F.F.
|
||||||
>>> 'RetailPricex' field is missing
|
======================================================================
|
||||||
|
FAIL: [first_spider] parse (@returns post-hook)
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
scrapy.exceptions.ContractFail: Returned 92 requests, expected 0..4
|
||||||
|
|
||||||
[FAILED] first_spider:parse
|
======================================================================
|
||||||
>>> Returned 92 requests, expected 0..4
|
FAIL: [first_spider] parse_item (@scrapes post-hook)
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
scrapy.exceptions.ContractFail: Missing fields: RetailPricex
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Ran 4 contracts in 0.174s
|
||||||
|
|
||||||
|
FAILED (failures=2)
|
||||||
|
|
||||||
.. skip: end
|
.. skip: end
|
||||||
|
|
||||||
|
|
@ -377,7 +391,7 @@ Supported options:
|
||||||
|
|
||||||
* ``--spider=SPIDER``: bypass spider autodetection and force use of specific spider
|
* ``--spider=SPIDER``: bypass spider autodetection and force use of specific spider
|
||||||
|
|
||||||
* ``--headers``: print the response's HTTP headers instead of the response's body
|
* ``--headers``: print the request's and response's HTTP headers instead of the response's body
|
||||||
|
|
||||||
* ``--no-redirect``: do not follow HTTP 3xx redirects (default is to follow them)
|
* ``--no-redirect``: do not follow HTTP 3xx redirects (default is to follow them)
|
||||||
|
|
||||||
|
|
@ -387,15 +401,19 @@ Usage examples::
|
||||||
[ ... html content here ... ]
|
[ ... html content here ... ]
|
||||||
|
|
||||||
$ scrapy fetch --nolog --headers http://www.example.com/
|
$ scrapy fetch --nolog --headers http://www.example.com/
|
||||||
{'Accept-Ranges': ['bytes'],
|
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
||||||
'Age': ['1263 '],
|
> Accept-Language: en
|
||||||
'Connection': ['close '],
|
> User-Agent: Scrapy/2.16.0 (+https://scrapy.org)
|
||||||
'Content-Length': ['596'],
|
> Accept-Encoding: gzip, deflate, br
|
||||||
'Content-Type': ['text/html; charset=UTF-8'],
|
>
|
||||||
'Date': ['Wed, 18 Aug 2010 23:59:46 GMT'],
|
< Date: Wed, 08 Jul 2026 06:15:01 GMT
|
||||||
'Etag': ['"573c1-254-48c9c87349680"'],
|
< Content-Type: text/html
|
||||||
'Last-Modified': ['Fri, 30 Jul 2010 15:30:18 GMT'],
|
< Server: cloudflare
|
||||||
'Server': ['Apache/2.2.3 (CentOS)']}
|
< Last-Modified: Wed, 01 Jul 2026 17:50:18 GMT
|
||||||
|
< Allow: GET, HEAD
|
||||||
|
< Cf-Cache-Status: HIT
|
||||||
|
< Age: 8184
|
||||||
|
< Cf-Ray: a17cf3b80eddf141-DME
|
||||||
|
|
||||||
.. command:: view
|
.. command:: view
|
||||||
|
|
||||||
|
|
@ -476,7 +494,7 @@ Supported options:
|
||||||
|
|
||||||
* ``--spider=SPIDER``: bypass spider autodetection and force use of specific spider
|
* ``--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
|
* ``--callback`` or ``-c``: spider method to use as callback for parsing the
|
||||||
response
|
response
|
||||||
|
|
@ -605,7 +623,10 @@ shouldn't matter to the user running the command, but when the user :ref:`needs
|
||||||
a non-default Twisted reactor <disable-asyncio>`, it may be important.
|
a non-default Twisted reactor <disable-asyncio>`, it may be important.
|
||||||
|
|
||||||
Scrapy decides which of these two classes to use based on the value of the
|
Scrapy decides which of these two classes to use based on the value of the
|
||||||
:setting:`TWISTED_REACTOR` setting. If the setting value is the default one
|
:setting:`TWISTED_REACTOR` and :setting:`TWISTED_REACTOR_ENABLED` settings.
|
||||||
|
With :setting:`TWISTED_REACTOR_ENABLED` set to ``False`` it will use
|
||||||
|
:class:`~scrapy.crawler.AsyncCrawlerProcess`. Otherwise, if the
|
||||||
|
:setting:`TWISTED_REACTOR` value is the default one
|
||||||
(``'twisted.internet.asyncioreactor.AsyncioSelectorReactor'``),
|
(``'twisted.internet.asyncioreactor.AsyncioSelectorReactor'``),
|
||||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` will be used, otherwise
|
:class:`~scrapy.crawler.AsyncCrawlerProcess` will be used, otherwise
|
||||||
:class:`~scrapy.crawler.CrawlerProcess` will be used. The :ref:`spider settings
|
:class:`~scrapy.crawler.CrawlerProcess` will be used. The :ref:`spider settings
|
||||||
|
|
|
||||||
|
|
@ -30,43 +30,15 @@ You can use the following contracts:
|
||||||
|
|
||||||
.. module:: scrapy.contracts.default
|
.. module:: scrapy.contracts.default
|
||||||
|
|
||||||
.. class:: UrlContract
|
.. autoclass:: UrlContract
|
||||||
|
|
||||||
This contract (``@url``) sets the sample URL used when checking other
|
.. autoclass:: CallbackKeywordArgumentsContract
|
||||||
contract conditions for this spider. This contract is mandatory. All
|
|
||||||
callbacks lacking this contract are ignored when running the checks::
|
|
||||||
|
|
||||||
@url url
|
.. autoclass:: MetadataContract
|
||||||
|
|
||||||
.. class:: CallbackKeywordArgumentsContract
|
.. autoclass:: ReturnsContract
|
||||||
|
|
||||||
This contract (``@cb_kwargs``) sets the :attr:`cb_kwargs <scrapy.Request.cb_kwargs>`
|
.. autoclass:: ScrapesContract
|
||||||
attribute for the sample request. It must be a valid JSON dictionary.
|
|
||||||
::
|
|
||||||
|
|
||||||
@cb_kwargs {"arg1": "value1", "arg2": "value2", ...}
|
|
||||||
|
|
||||||
.. class:: MetadataContract
|
|
||||||
|
|
||||||
This contract (``@meta``) sets the :attr:`meta <scrapy.Request.meta>`
|
|
||||||
attribute for the sample request. It must be a valid JSON dictionary.
|
|
||||||
::
|
|
||||||
|
|
||||||
@meta {"arg1": "value1", "arg2": "value2", ...}
|
|
||||||
|
|
||||||
.. class:: ReturnsContract
|
|
||||||
|
|
||||||
This contract (``@returns``) sets lower and upper bounds for the items and
|
|
||||||
requests returned by the spider. The upper bound is optional::
|
|
||||||
|
|
||||||
@returns item(s)|request(s) [min [max]]
|
|
||||||
|
|
||||||
.. class:: ScrapesContract
|
|
||||||
|
|
||||||
This contract (``@scrapes``) checks that all the items returned by the
|
|
||||||
callback have the specified fields::
|
|
||||||
|
|
||||||
@scrapes field_1 field_2 ...
|
|
||||||
|
|
||||||
Use the :command:`check` command to run the contract checks.
|
Use the :command:`check` command to run the contract checks.
|
||||||
|
|
||||||
|
|
@ -89,30 +61,16 @@ override three methods:
|
||||||
|
|
||||||
.. module:: scrapy.contracts
|
.. module:: scrapy.contracts
|
||||||
|
|
||||||
.. class:: Contract(method, *args)
|
.. autoclass:: Contract
|
||||||
|
|
||||||
:param method: callback function to which the contract is associated
|
.. automethod:: adjust_request_args
|
||||||
:type method: collections.abc.Callable
|
|
||||||
|
|
||||||
:param args: list of arguments passed into the docstring (whitespace
|
.. method:: pre_process(response)
|
||||||
separated)
|
|
||||||
:type args: list
|
|
||||||
|
|
||||||
.. method:: Contract.adjust_request_args(args)
|
|
||||||
|
|
||||||
This receives a ``dict`` as an argument containing default arguments
|
|
||||||
for request object. :class:`~scrapy.Request` is used by default,
|
|
||||||
but this can be changed with the ``request_cls`` attribute.
|
|
||||||
If multiple contracts in chain have this attribute defined, the last one is used.
|
|
||||||
|
|
||||||
Must return the same or a modified version of it.
|
|
||||||
|
|
||||||
.. method:: Contract.pre_process(response)
|
|
||||||
|
|
||||||
This allows hooking in various checks on the response received from the
|
This allows hooking in various checks on the response received from the
|
||||||
sample request, before it's being passed to the callback.
|
sample request, before it's being passed to the callback.
|
||||||
|
|
||||||
.. method:: Contract.post_process(output)
|
.. method:: post_process(output)
|
||||||
|
|
||||||
This allows processing the output of the callback. Iterators are
|
This allows processing the output of the callback. Iterators are
|
||||||
converted to lists before being passed to this hook.
|
converted to lists before being passed to this hook.
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Supported callables
|
||||||
The following callables may be defined as coroutines using ``async def``, and
|
The following callables may be defined as coroutines using ``async def``, and
|
||||||
hence use coroutine syntax (e.g. ``await``, ``async for``, ``async with``):
|
hence use coroutine syntax (e.g. ``await``, ``async for``, ``async with``):
|
||||||
|
|
||||||
- The :meth:`~scrapy.spiders.Spider.start` spider method, which *must* be
|
- The :meth:`~scrapy.Spider.start` spider method, which *must* be
|
||||||
defined as an :term:`asynchronous generator`.
|
defined as an :term:`asynchronous generator`.
|
||||||
|
|
||||||
.. versionadded:: 2.13
|
.. versionadded:: 2.13
|
||||||
|
|
@ -204,13 +204,15 @@ This means you can use many useful Python libraries providing such code:
|
||||||
Common use cases for asynchronous code include:
|
Common use cases for asynchronous code include:
|
||||||
|
|
||||||
* requesting data from websites, databases and other services (in
|
* requesting data from websites, databases and other services (in
|
||||||
:meth:`~scrapy.spiders.Spider.start`, callbacks, pipelines and
|
:meth:`~scrapy.Spider.start`, callbacks, pipelines and
|
||||||
middlewares);
|
middlewares);
|
||||||
* storing data in databases (in pipelines and middlewares);
|
* storing data in databases (in pipelines and middlewares);
|
||||||
* delaying the spider initialization until some external event (in the
|
* delaying the spider initialization until some external event (in the
|
||||||
:signal:`spider_opened` handler);
|
:signal:`spider_opened` handler);
|
||||||
* calling asynchronous Scrapy methods like :meth:`ExecutionEngine.download`
|
* calling asynchronous Scrapy methods like
|
||||||
(see :ref:`the screenshot pipeline example<ScreenshotPipeline>`).
|
:meth:`ExecutionEngine.download_async()
|
||||||
|
<scrapy.core.engine.ExecutionEngine.download_async>` (see :ref:`the
|
||||||
|
screenshot pipeline example <ScreenshotPipeline>`).
|
||||||
|
|
||||||
.. _aio-libs: https://github.com/aio-libs
|
.. _aio-libs: https://github.com/aio-libs
|
||||||
|
|
||||||
|
|
@ -268,5 +270,5 @@ You can also send multiple requests in parallel:
|
||||||
yield {
|
yield {
|
||||||
"h1": response.css("h1::text").get(),
|
"h1": response.css("h1::text").get(),
|
||||||
"price": responses[0].css(".price::text").get(),
|
"price": responses[0].css(".price::text").get(),
|
||||||
"price2": responses[1].css(".color::text").get(),
|
"color": responses[1].css(".color::text").get(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,6 @@ also request each page to get every quote on the site:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class QuoteSpider(scrapy.Spider):
|
class QuoteSpider(scrapy.Spider):
|
||||||
|
|
@ -256,7 +255,7 @@ also request each page to get every quote on the site:
|
||||||
start_urls = ["https://quotes.toscrape.com/api/quotes?page=1"]
|
start_urls = ["https://quotes.toscrape.com/api/quotes?page=1"]
|
||||||
|
|
||||||
def parse(self, response):
|
def parse(self, response):
|
||||||
data = json.loads(response.text)
|
data = response.json()
|
||||||
for quote in data["quotes"]:
|
for quote in data["quotes"]:
|
||||||
yield {"quote": quote["text"]}
|
yield {"quote": quote["text"]}
|
||||||
if data["has_next"]:
|
if data["has_next"]:
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ the following API:
|
||||||
If ``True``, the handler will only be instantiated when the first
|
If ``True``, the handler will only be instantiated when the first
|
||||||
request handled by it needs to be downloaded.
|
request handled by it needs to be downloaded.
|
||||||
|
|
||||||
.. method:: download_request(request: Request) -> Response:
|
.. method:: download_request(request: Request) -> Response
|
||||||
:async:
|
:async:
|
||||||
|
|
||||||
Download the given request and return a response.
|
Download the given request and return a response.
|
||||||
|
|
@ -173,6 +173,8 @@ of this package for more information.
|
||||||
H2DownloadHandler
|
H2DownloadHandler
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
.. note:: Requires the :ref:`twisted-http2 <extras>` extra.
|
||||||
|
|
||||||
.. autoclass:: scrapy.core.downloader.handlers.http2.H2DownloadHandler
|
.. autoclass:: scrapy.core.downloader.handlers.http2.H2DownloadHandler
|
||||||
|
|
||||||
| Supported scheme: ``https``.
|
| Supported scheme: ``https``.
|
||||||
|
|
@ -185,9 +187,6 @@ for them.
|
||||||
|
|
||||||
It's implemented using :mod:`twisted.web.client` and the ``h2`` library.
|
It's implemented using :mod:`twisted.web.client` and the ``h2`` library.
|
||||||
|
|
||||||
For this handler to work you need to install the ``Twisted[http2]`` extra
|
|
||||||
dependency.
|
|
||||||
|
|
||||||
If you want to use this handler you need to replace the default one for the
|
If you want to use this handler you need to replace the default one for the
|
||||||
``https`` scheme:
|
``https`` scheme:
|
||||||
|
|
||||||
|
|
@ -273,9 +272,13 @@ Other limitations:
|
||||||
|
|
||||||
- HTTPS proxies to HTTPS destinations are not supported.
|
- HTTPS proxies to HTTPS destinations are not supported.
|
||||||
|
|
||||||
|
.. _httpx-handler:
|
||||||
|
|
||||||
HttpxDownloadHandler
|
HttpxDownloadHandler
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
.. note:: Requires the :ref:`httpx <extras>` extra.
|
||||||
|
|
||||||
.. versionadded:: 2.15.0
|
.. versionadded:: 2.15.0
|
||||||
|
|
||||||
.. autoclass:: scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler
|
.. autoclass:: scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler
|
||||||
|
|
@ -288,7 +291,9 @@ HttpxDownloadHandler
|
||||||
This handler supports ``http://host/path`` and ``https://host/path`` URLs and
|
This handler supports ``http://host/path`` and ``https://host/path`` URLs and
|
||||||
uses the HTTP/1.1 or HTTP/2 protocol for them.
|
uses the HTTP/1.1 or HTTP/2 protocol for them.
|
||||||
|
|
||||||
It's implemented using the ``httpx`` library and needs it to be installed.
|
It's implemented using the httpx2_ library.
|
||||||
|
|
||||||
|
.. _httpx2: https://httpx2.pydantic.dev/
|
||||||
|
|
||||||
If you want to use this handler you need to replace the default ones for the
|
If you want to use this handler you need to replace the default ones for the
|
||||||
``http`` and ``https`` schemes:
|
``http`` and ``https`` schemes:
|
||||||
|
|
@ -311,8 +316,8 @@ Features and limitations
|
||||||
|
|
||||||
=========================== =======================================
|
=========================== =======================================
|
||||||
HTTP proxies Yes
|
HTTP proxies Yes
|
||||||
SOCKS proxies Yes (SOCKS5; requires ``httpx[socks]``)
|
SOCKS proxies Yes (SOCKS5)
|
||||||
HTTP/2 Yes (requires ``httpx[http2]``)
|
HTTP/2 Yes
|
||||||
``response.certificate`` DER bytes
|
``response.certificate`` DER bytes
|
||||||
Per-request ``bindaddress`` No (not supported by the library)
|
Per-request ``bindaddress`` No (not supported by the library)
|
||||||
TLS implementation Standard library ``ssl``
|
TLS implementation Standard library ``ssl``
|
||||||
|
|
@ -333,8 +338,7 @@ HTTPX_HTTP2_ENABLED
|
||||||
|
|
||||||
Default: ``False``
|
Default: ``False``
|
||||||
|
|
||||||
Whether to enable HTTP/2 support in this handler. The ``httpx[http2]`` extra
|
Whether to enable HTTP/2 support in this handler.
|
||||||
needs to be installed if you want to enable this setting.
|
|
||||||
|
|
||||||
Built-in non-HTTP download handlers reference
|
Built-in non-HTTP download handlers reference
|
||||||
=============================================
|
=============================================
|
||||||
|
|
@ -378,9 +382,13 @@ This handler supports ``ftp://host/path`` FTP URIs.
|
||||||
|
|
||||||
It's implemented using :mod:`twisted.protocols.ftp`.
|
It's implemented using :mod:`twisted.protocols.ftp`.
|
||||||
|
|
||||||
|
.. _s3-handler:
|
||||||
|
|
||||||
S3DownloadHandler
|
S3DownloadHandler
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
.. note:: Requires the :ref:`s3 <extras>` extra.
|
||||||
|
|
||||||
.. autoclass:: scrapy.core.downloader.handlers.s3.S3DownloadHandler
|
.. autoclass:: scrapy.core.downloader.handlers.s3.S3DownloadHandler
|
||||||
|
|
||||||
| Supported scheme: ``s3``.
|
| Supported scheme: ``s3``.
|
||||||
|
|
@ -390,4 +398,6 @@ S3DownloadHandler
|
||||||
|
|
||||||
This handler supports ``s3://bucket/path`` S3 URIs.
|
This handler supports ``s3://bucket/path`` S3 URIs.
|
||||||
|
|
||||||
It's implemented using the ``botocore`` library and needs it to be installed.
|
It's implemented using the botocore_ library.
|
||||||
|
|
||||||
|
.. _botocore: https://github.com/boto/botocore
|
||||||
|
|
|
||||||
|
|
@ -505,7 +505,7 @@ Filesystem storage backend (default)
|
||||||
|
|
||||||
* ``response_body`` - the plain response body
|
* ``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()``
|
* ``meta`` - some metadata of this cache resource in Python ``repr()``
|
||||||
format (grep-friendly format)
|
format (grep-friendly format)
|
||||||
|
|
@ -547,7 +547,7 @@ defines the methods described below.
|
||||||
.. method:: open_spider(spider)
|
.. method:: open_spider(spider)
|
||||||
|
|
||||||
This method gets called after a spider has been opened for crawling. It handles
|
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
|
:param spider: the spider which has been opened
|
||||||
:type spider: :class:`~scrapy.Spider` object
|
:type spider: :class:`~scrapy.Spider` object
|
||||||
|
|
@ -555,7 +555,7 @@ defines the methods described below.
|
||||||
.. method:: close_spider(spider)
|
.. method:: close_spider(spider)
|
||||||
|
|
||||||
This method gets called after a spider has been closed. It handles
|
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
|
:param spider: the spider which has been closed
|
||||||
:type spider: :class:`~scrapy.Spider` object
|
:type spider: :class:`~scrapy.Spider` object
|
||||||
|
|
@ -564,6 +564,10 @@ defines the methods described below.
|
||||||
|
|
||||||
Return response if present in cache, or ``None`` otherwise.
|
Return response if present in cache, or ``None`` otherwise.
|
||||||
|
|
||||||
|
If this method raises an exception, e.g. because the cache entry is
|
||||||
|
corrupted, the middleware logs a warning and handles the request as a
|
||||||
|
cache miss.
|
||||||
|
|
||||||
:param spider: the spider which generated the request
|
:param spider: the spider which generated the request
|
||||||
:type spider: :class:`~scrapy.Spider` object
|
:type spider: :class:`~scrapy.Spider` object
|
||||||
|
|
||||||
|
|
@ -591,8 +595,8 @@ In order to use your storage backend, set:
|
||||||
HTTPCache middleware settings
|
HTTPCache middleware settings
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The :class:`HttpCacheMiddleware` can be configured through the following
|
:class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware` can be
|
||||||
settings:
|
configured through the following settings:
|
||||||
|
|
||||||
.. setting:: HTTPCACHE_ENABLED
|
.. setting:: HTTPCACHE_ENABLED
|
||||||
|
|
||||||
|
|
@ -727,6 +731,8 @@ We assume that the spider will not issue Cache-Control directives
|
||||||
in requests unless it actually needs them, so directives in requests are
|
in requests unless it actually needs them, so directives in requests are
|
||||||
not filtered.
|
not filtered.
|
||||||
|
|
||||||
|
.. _http-compression:
|
||||||
|
|
||||||
HttpCompressionMiddleware
|
HttpCompressionMiddleware
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|
@ -738,14 +744,12 @@ HttpCompressionMiddleware
|
||||||
This middleware allows compressed (gzip, deflate) traffic to be
|
This middleware allows compressed (gzip, deflate) traffic to be
|
||||||
sent/received from web sites.
|
sent/received from web sites.
|
||||||
|
|
||||||
This middleware also supports decoding `brotli-compressed`_ as well as
|
This middleware also supports decoding `brotli-compressed`_ responses with
|
||||||
`zstd-compressed`_ responses, provided that `brotli`_ or `zstandard`_ is
|
the :ref:`brotli <extras>` extra, and `zstd-compressed`_
|
||||||
installed, respectively.
|
responses with the :ref:`zstd <extras>` extra.
|
||||||
|
|
||||||
.. _brotli-compressed: https://www.ietf.org/rfc/rfc7932.txt
|
.. _brotli-compressed: https://www.ietf.org/rfc/rfc7932.txt
|
||||||
.. _brotli: https://pypi.org/project/Brotli/
|
|
||||||
.. _zstd-compressed: https://www.ietf.org/rfc/rfc8478.txt
|
.. _zstd-compressed: https://www.ietf.org/rfc/rfc8478.txt
|
||||||
.. _zstandard: https://pypi.org/project/zstandard/
|
|
||||||
|
|
||||||
|
|
||||||
HttpCompressionMiddleware Settings
|
HttpCompressionMiddleware Settings
|
||||||
|
|
@ -814,7 +818,6 @@ HttpProxyMiddleware settings
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. setting:: HTTPPROXY_ENABLED
|
.. setting:: HTTPPROXY_ENABLED
|
||||||
.. setting:: HTTPPROXY_AUTH_ENCODING
|
|
||||||
|
|
||||||
HTTPPROXY_ENABLED
|
HTTPPROXY_ENABLED
|
||||||
^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
@ -823,6 +826,8 @@ Default: ``True``
|
||||||
|
|
||||||
Whether or not to enable the :class:`HttpProxyMiddleware`.
|
Whether or not to enable the :class:`HttpProxyMiddleware`.
|
||||||
|
|
||||||
|
.. setting:: HTTPPROXY_AUTH_ENCODING
|
||||||
|
|
||||||
HTTPPROXY_AUTH_ENCODING
|
HTTPPROXY_AUTH_ENCODING
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
@ -867,9 +872,9 @@ OffsiteMiddleware
|
||||||
.. reqmeta:: allow_offsite
|
.. reqmeta:: allow_offsite
|
||||||
|
|
||||||
If the request has the :attr:`~scrapy.Request.dont_filter` attribute set to
|
If the request has the :attr:`~scrapy.Request.dont_filter` attribute set to
|
||||||
``True`` or :attr:`Request.meta` has ``allow_offsite`` set to ``True``, then
|
``True`` or :attr:`Request.meta <scrapy.Request.meta>` has ``allow_offsite``
|
||||||
the OffsiteMiddleware will allow the request even if its domain is not listed
|
set to ``True``, then the OffsiteMiddleware will allow the request even if
|
||||||
in allowed domains.
|
its domain is not listed in allowed domains.
|
||||||
|
|
||||||
RedirectMiddleware
|
RedirectMiddleware
|
||||||
------------------
|
------------------
|
||||||
|
|
@ -984,7 +989,7 @@ Whether the Meta Refresh middleware will be enabled.
|
||||||
METAREFRESH_IGNORE_TAGS
|
METAREFRESH_IGNORE_TAGS
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Default: ``[]``
|
Default: ``["noscript"]``
|
||||||
|
|
||||||
Meta tags within these tags are ignored.
|
Meta tags within these tags are ignored.
|
||||||
|
|
||||||
|
|
@ -1014,17 +1019,6 @@ RetryMiddleware
|
||||||
A middleware to retry failed requests that are potentially caused by
|
A middleware to retry failed requests that are potentially caused by
|
||||||
temporary problems such as a connection timeout or HTTP 500 error.
|
temporary problems such as a connection timeout or HTTP 500 error.
|
||||||
|
|
||||||
Failed pages are collected on the scraping process and rescheduled at the
|
|
||||||
end, once the spider has finished crawling all regular (non failed) pages.
|
|
||||||
|
|
||||||
The :class:`RetryMiddleware` can be configured through the following
|
|
||||||
settings (see the settings documentation for more info):
|
|
||||||
|
|
||||||
* :setting:`RETRY_ENABLED`
|
|
||||||
* :setting:`RETRY_TIMES`
|
|
||||||
* :setting:`RETRY_HTTP_CODES`
|
|
||||||
* :setting:`RETRY_EXCEPTIONS`
|
|
||||||
|
|
||||||
.. reqmeta:: dont_retry
|
.. reqmeta:: dont_retry
|
||||||
|
|
||||||
If :attr:`Request.meta <scrapy.Request.meta>` has ``dont_retry`` key
|
If :attr:`Request.meta <scrapy.Request.meta>` has ``dont_retry`` key
|
||||||
|
|
@ -1091,7 +1085,7 @@ Default::
|
||||||
'twisted.internet.error.ConnectionDone',
|
'twisted.internet.error.ConnectionDone',
|
||||||
'twisted.internet.error.ConnectError',
|
'twisted.internet.error.ConnectError',
|
||||||
'twisted.internet.error.ConnectionLost',
|
'twisted.internet.error.ConnectionLost',
|
||||||
IOError,
|
OSError,
|
||||||
'scrapy.core.downloader.handlers.http11.TunnelError',
|
'scrapy.core.downloader.handlers.http11.TunnelError',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1249,8 +1243,7 @@ Based on `Robotexclusionrulesparser <https://pypi.org/project/robotexclusionrule
|
||||||
|
|
||||||
In order to use this parser:
|
In order to use this parser:
|
||||||
|
|
||||||
* Install ``Robotexclusionrulesparser`` by running
|
* Install the :ref:`robotparser <extras>` extra.
|
||||||
``pip install robotexclusionrulesparser``
|
|
||||||
|
|
||||||
* Set :setting:`ROBOTSTXT_PARSER` setting to
|
* Set :setting:`ROBOTSTXT_PARSER` setting to
|
||||||
``scrapy.robotstxt.RerpRobotParser``
|
``scrapy.robotstxt.RerpRobotParser``
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ data from it depends on the type of response:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
selector = Selector(data["html"])
|
selector = Selector(text=data["html"])
|
||||||
|
|
||||||
- If the response is JavaScript, or HTML with a ``<script/>`` element
|
- If the response is JavaScript, or HTML with a ``<script/>`` element
|
||||||
containing the desired data, see :ref:`topics-parsing-javascript`.
|
containing the desired data, see :ref:`topics-parsing-javascript`.
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,25 @@
|
||||||
.. _topics-exceptions:
|
.. _topics-exceptions:
|
||||||
|
.. _topics-exceptions-ref:
|
||||||
|
|
||||||
==========
|
==========
|
||||||
Exceptions
|
Exceptions
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
Here's a list of all exceptions included in Scrapy and their usage, except for
|
||||||
|
the :ref:`download handler exceptions <download-handlers-exceptions>`.
|
||||||
|
|
||||||
.. module:: scrapy.exceptions
|
.. module:: scrapy.exceptions
|
||||||
:synopsis: Scrapy exceptions
|
|
||||||
|
|
||||||
.. _topics-exceptions-ref:
|
.. autoexception:: CloseSpider
|
||||||
|
|
||||||
Built-in Exceptions reference
|
.. autoexception:: DontCloseSpider
|
||||||
=============================
|
|
||||||
|
|
||||||
Here's a list of all exceptions included in Scrapy and their usage.
|
.. autoexception:: DropItem
|
||||||
|
|
||||||
|
.. autoexception:: IgnoreRequest
|
||||||
|
|
||||||
CloseSpider
|
.. autoexception:: NotConfigured
|
||||||
-----------
|
|
||||||
|
|
||||||
.. exception:: CloseSpider(reason='cancelled')
|
.. autoexception:: NotSupported
|
||||||
|
|
||||||
This exception can be raised from a spider callback to request the spider to be
|
.. autoexception:: StopDownload
|
||||||
closed/stopped. Supported arguments:
|
|
||||||
|
|
||||||
:param reason: the reason for closing
|
|
||||||
:type reason: str
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
def parse_page(self, response):
|
|
||||||
if "Bandwidth exceeded" in response.body:
|
|
||||||
raise CloseSpider("bandwidth_exceeded")
|
|
||||||
|
|
||||||
DontCloseSpider
|
|
||||||
---------------
|
|
||||||
|
|
||||||
.. exception:: DontCloseSpider
|
|
||||||
|
|
||||||
This exception can be raised in a :signal:`spider_idle` signal handler to
|
|
||||||
prevent the spider from being closed.
|
|
||||||
|
|
||||||
DropItem
|
|
||||||
--------
|
|
||||||
|
|
||||||
.. exception:: DropItem
|
|
||||||
|
|
||||||
The exception that must be raised by item pipeline stages to stop processing an
|
|
||||||
Item. For more information see :ref:`topics-item-pipeline`.
|
|
||||||
|
|
||||||
IgnoreRequest
|
|
||||||
-------------
|
|
||||||
|
|
||||||
.. exception:: IgnoreRequest
|
|
||||||
|
|
||||||
This exception can be raised by the Scheduler or any downloader middleware to
|
|
||||||
indicate that the request should be ignored.
|
|
||||||
|
|
||||||
NotConfigured
|
|
||||||
-------------
|
|
||||||
|
|
||||||
.. exception:: NotConfigured
|
|
||||||
|
|
||||||
This exception can be raised by some components to indicate that they will
|
|
||||||
remain disabled. Those components include:
|
|
||||||
|
|
||||||
- Extensions
|
|
||||||
- Item pipelines
|
|
||||||
- Downloader middlewares
|
|
||||||
- Spider middlewares
|
|
||||||
|
|
||||||
The exception must be raised in the component's ``__init__`` method.
|
|
||||||
|
|
||||||
NotSupported
|
|
||||||
------------
|
|
||||||
|
|
||||||
.. exception:: NotSupported
|
|
||||||
|
|
||||||
This exception is raised to indicate an unsupported feature.
|
|
||||||
|
|
||||||
StopDownload
|
|
||||||
-------------
|
|
||||||
|
|
||||||
.. exception:: StopDownload(fail=True)
|
|
||||||
|
|
||||||
Raised from a :class:`~scrapy.signals.bytes_received` or :class:`~scrapy.signals.headers_received`
|
|
||||||
signal handler to indicate that no further bytes should be downloaded for a response.
|
|
||||||
|
|
||||||
The ``fail`` boolean parameter controls which method will handle the resulting
|
|
||||||
response:
|
|
||||||
|
|
||||||
* If ``fail=True`` (default), the request errback is called. The response object is
|
|
||||||
available as the ``response`` attribute of the ``StopDownload`` exception,
|
|
||||||
which is in turn stored as the ``value`` attribute of the received
|
|
||||||
:class:`~twisted.python.failure.Failure` object. This means that in an errback
|
|
||||||
defined as ``def errback(self, failure)``, the response can be accessed though
|
|
||||||
``failure.value.response``.
|
|
||||||
|
|
||||||
* If ``fail=False``, the request callback is called instead.
|
|
||||||
|
|
||||||
In both cases, the response could have its body truncated: the body contains
|
|
||||||
all bytes received up until the exception is raised, including the bytes
|
|
||||||
received in the signal handler that raises the exception. Also, the response
|
|
||||||
object is marked with ``"download_stopped"`` in its :attr:`~scrapy.http.Response.flags`
|
|
||||||
attribute.
|
|
||||||
|
|
||||||
.. note:: ``fail`` is a keyword-only parameter, i.e. raising
|
|
||||||
``StopDownload(False)`` or ``StopDownload(True)`` will raise
|
|
||||||
a :class:`TypeError`.
|
|
||||||
|
|
||||||
See the documentation for the :class:`~scrapy.signals.bytes_received` and
|
|
||||||
:class:`~scrapy.signals.headers_received` signals
|
|
||||||
and the :ref:`topics-stop-response-download` topic for additional information and examples.
|
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ output examples, which assume you're exporting these two items:
|
||||||
BaseItemExporter
|
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
|
This is the (abstract) base class for all Item Exporters. It provides
|
||||||
support for common features used by all (concrete) Item Exporters, such as
|
support for common features used by all (concrete) Item Exporters, such as
|
||||||
|
|
@ -211,13 +211,17 @@ BaseItemExporter
|
||||||
|
|
||||||
- ``None`` (all fields [2]_, default)
|
- ``None`` (all fields [2]_, default)
|
||||||
|
|
||||||
- A list of fields::
|
- A list of fields:
|
||||||
|
|
||||||
['field1', 'field2']
|
.. code-block:: python
|
||||||
|
|
||||||
- A dict where keys are fields and values are output names::
|
["field1", "field2"]
|
||||||
|
|
||||||
{'field1': 'Field 1', 'field2': 'Field 2'}
|
- A dict where keys are fields and values are output names:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
{"field1": "Field 1", "field2": "Field 2"}
|
||||||
|
|
||||||
.. [1] Not all exporters respect the specified field order.
|
.. [1] Not all exporters respect the specified field order.
|
||||||
.. [2] When using :ref:`item objects <item-types>` that do not expose
|
.. [2] When using :ref:`item objects <item-types>` that do not expose
|
||||||
|
|
@ -239,7 +243,7 @@ BaseItemExporter
|
||||||
|
|
||||||
.. attribute:: indent
|
.. 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,
|
* ``indent=None`` selects the most compact representation,
|
||||||
all items in the same line with no indentation
|
all items in the same line with no indentation
|
||||||
|
|
@ -273,7 +277,9 @@ XmlItemExporter
|
||||||
The additional keyword arguments of this ``__init__`` method are passed to the
|
The additional keyword arguments of this ``__init__`` method are passed to the
|
||||||
:class:`BaseItemExporter` ``__init__`` method.
|
:class:`BaseItemExporter` ``__init__`` method.
|
||||||
|
|
||||||
A typical output of this exporter would be::
|
A typical output of this exporter would be:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<items>
|
<items>
|
||||||
|
|
@ -291,11 +297,17 @@ XmlItemExporter
|
||||||
exported by serializing each value inside a ``<value>`` element. This is for
|
exported by serializing each value inside a ``<value>`` element. This is for
|
||||||
convenience, as multi-valued fields are very common.
|
convenience, as multi-valued fields are very common.
|
||||||
|
|
||||||
For example, the item::
|
For example, the item:
|
||||||
|
|
||||||
Item(name=['John', 'Doe'], age='23')
|
.. skip: next
|
||||||
|
|
||||||
Would be serialized as::
|
.. code-block:: python
|
||||||
|
|
||||||
|
Item(name=["John", "Doe"], age="23")
|
||||||
|
|
||||||
|
Would be serialized as:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<items>
|
<items>
|
||||||
|
|
@ -328,7 +340,7 @@ CsvItemExporter
|
||||||
|
|
||||||
:param join_multivalued: The char (or chars) that will be used for joining
|
:param join_multivalued: The char (or chars) that will be used for joining
|
||||||
multi-valued fields, if found.
|
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
|
:param errors: The optional string that specifies how encoding and decoding
|
||||||
errors are to be handled. For more information see
|
errors are to be handled. For more information see
|
||||||
|
|
@ -342,14 +354,14 @@ CsvItemExporter
|
||||||
|
|
||||||
A typical output of this exporter would be::
|
A typical output of this exporter would be::
|
||||||
|
|
||||||
product,price
|
name,price
|
||||||
Color TV,1200
|
Color TV,1200
|
||||||
DVD player,200
|
DVD player,200
|
||||||
|
|
||||||
PickleItemExporter
|
PickleItemExporter
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
.. class:: PickleItemExporter(file, protocol=0, **kwargs)
|
.. class:: PickleItemExporter(file, protocol=4, **kwargs)
|
||||||
|
|
||||||
Exports items in pickle format to the given file-like object.
|
Exports items in pickle format to the given file-like object.
|
||||||
|
|
||||||
|
|
@ -379,10 +391,12 @@ PprintItemExporter
|
||||||
The additional keyword arguments of this ``__init__`` method are passed to the
|
The additional keyword arguments of this ``__init__`` method are passed to the
|
||||||
:class:`BaseItemExporter` ``__init__`` method.
|
:class:`BaseItemExporter` ``__init__`` method.
|
||||||
|
|
||||||
A typical output of this exporter would be::
|
A typical output of this exporter would be:
|
||||||
|
|
||||||
{'name': 'Color TV', 'price': '1200'}
|
.. code-block:: python
|
||||||
{'name': 'DVD player', 'price': '200'}
|
|
||||||
|
{"name": "Color TV", "price": "1200"}
|
||||||
|
{"name": "DVD player", "price": "200"}
|
||||||
|
|
||||||
Longer lines (when present) are pretty-formatted.
|
Longer lines (when present) are pretty-formatted.
|
||||||
|
|
||||||
|
|
@ -400,7 +414,9 @@ JsonItemExporter
|
||||||
:param file: the file-like object to use for exporting the data. Its ``write`` method should
|
:param file: the file-like object to use for exporting the data. Its ``write`` method should
|
||||||
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
|
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
|
||||||
|
|
||||||
A typical output of this exporter would be::
|
A typical output of this exporter would be:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
[{"name": "Color TV", "price": "1200"},
|
[{"name": "Color TV", "price": "1200"},
|
||||||
{"name": "DVD player", "price": "200"}]
|
{"name": "DVD player", "price": "200"}]
|
||||||
|
|
@ -429,7 +445,9 @@ JsonLinesItemExporter
|
||||||
:param file: the file-like object to use for exporting the data. Its ``write`` method should
|
:param file: the file-like object to use for exporting the data. Its ``write`` method should
|
||||||
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
|
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
|
||||||
|
|
||||||
A typical output of this exporter would be::
|
A typical output of this exporter would be:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
{"name": "Color TV", "price": "1200"}
|
{"name": "Color TV", "price": "1200"}
|
||||||
{"name": "DVD player", "price": "200"}
|
{"name": "DVD player", "price": "200"}
|
||||||
|
|
|
||||||
|
|
@ -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
|
it will be closed with the reason ``closespider_errorcount``. If zero (or non
|
||||||
set), spiders won't be closed by number of errors.
|
set), spiders won't be closed by number of errors.
|
||||||
|
|
||||||
.. module:: scrapy.extensions.debug
|
|
||||||
:synopsis: Extensions for debugging Scrapy
|
|
||||||
|
|
||||||
.. module:: scrapy.extensions.periodic_log
|
.. module:: scrapy.extensions.periodic_log
|
||||||
:synopsis: Periodic stats logging
|
:synopsis: Periodic stats logging
|
||||||
|
|
||||||
|
|
@ -418,7 +415,7 @@ Example extension configuration:
|
||||||
custom_settings = {
|
custom_settings = {
|
||||||
"LOG_LEVEL": "INFO",
|
"LOG_LEVEL": "INFO",
|
||||||
"PERIODIC_LOG_STATS": {
|
"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_DELTA": {"include": ["downloader/"]},
|
||||||
"PERIODIC_LOG_TIMING_ENABLED": True,
|
"PERIODIC_LOG_TIMING_ENABLED": True,
|
||||||
|
|
@ -463,6 +460,9 @@ Default: ``False``
|
||||||
Debugging extensions
|
Debugging extensions
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
.. module:: scrapy.extensions.debug
|
||||||
|
:synopsis: Extensions for debugging Scrapy
|
||||||
|
|
||||||
Stack trace dump extension
|
Stack trace dump extension
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,6 @@ Marshal
|
||||||
- Value for the ``format`` key in the :setting:`FEEDS` setting: ``marshal``
|
- Value for the ``format`` key in the :setting:`FEEDS` setting: ``marshal``
|
||||||
- Exporter used: :class:`~scrapy.exporters.MarshalItemExporter`
|
- Exporter used: :class:`~scrapy.exporters.MarshalItemExporter`
|
||||||
|
|
||||||
|
|
||||||
.. _topics-feed-storage:
|
.. _topics-feed-storage:
|
||||||
|
|
||||||
Storages
|
Storages
|
||||||
|
|
@ -106,14 +105,13 @@ The storages backends supported out of the box are:
|
||||||
|
|
||||||
- :ref:`topics-feed-storage-fs`
|
- :ref:`topics-feed-storage-fs`
|
||||||
- :ref:`topics-feed-storage-ftp`
|
- :ref:`topics-feed-storage-ftp`
|
||||||
- :ref:`topics-feed-storage-s3` (requires boto3_)
|
- :ref:`topics-feed-storage-s3` (requires the :ref:`s3 <extras>` extra)
|
||||||
- :ref:`topics-feed-storage-gcs` (requires `google-cloud-storage`_)
|
- :ref:`topics-feed-storage-gcs` (requires the :ref:`gcs <extras>` extra)
|
||||||
- :ref:`topics-feed-storage-stdout`
|
- :ref:`topics-feed-storage-stdout`
|
||||||
|
|
||||||
Some storage backends may be unavailable if the required external libraries are
|
Some storage backends may be unavailable if the required :ref:`extras <extras>`
|
||||||
not available. For example, the S3 backend is only available if the boto3_
|
are not installed. For example, the S3 backend requires the :ref:`s3 <extras>`
|
||||||
library is installed.
|
extra.
|
||||||
|
|
||||||
|
|
||||||
.. _topics-feed-uri-params:
|
.. _topics-feed-uri-params:
|
||||||
|
|
||||||
|
|
@ -143,6 +141,11 @@ Here are some examples to illustrate:
|
||||||
.. note:: :ref:`Spider arguments <spiderargs>` become spider attributes, hence
|
.. note:: :ref:`Spider arguments <spiderargs>` become spider attributes, hence
|
||||||
they can also be used as storage URI parameters.
|
they can also be used as storage URI parameters.
|
||||||
|
|
||||||
|
.. note:: Only ``%(...)s`` parameters are replaced. Any other percent
|
||||||
|
character is kept as-is, so percent-encoded URIs (e.g. ``%20`` for a
|
||||||
|
space or percent-encoded FTP credentials) and :class:`pathlib.Path`
|
||||||
|
keys containing ``%(...)s`` parameters both work as expected.
|
||||||
|
|
||||||
|
|
||||||
.. _topics-feed-storage-backends:
|
.. _topics-feed-storage-backends:
|
||||||
|
|
||||||
|
|
@ -161,7 +164,7 @@ The feeds are stored in the local filesystem.
|
||||||
- Required external libraries: none
|
- Required external libraries: none
|
||||||
|
|
||||||
Note that for the local filesystem storage (only) you can omit the scheme if
|
Note that for the local filesystem storage (only) you can omit the scheme if
|
||||||
you specify an absolute path like ``/tmp/export.csv`` (Unix systems only).
|
you specify a path (e.g. ``/tmp/export.csv``).
|
||||||
Alternatively you can also use a :class:`pathlib.Path` object.
|
Alternatively you can also use a :class:`pathlib.Path` object.
|
||||||
|
|
||||||
.. _topics-feed-storage-ftp:
|
.. _topics-feed-storage-ftp:
|
||||||
|
|
@ -204,7 +207,7 @@ The feeds are stored on `Amazon S3`_.
|
||||||
|
|
||||||
- ``s3://aws_key:aws_secret@mybucket/path/to/export.csv``
|
- ``s3://aws_key:aws_secret@mybucket/path/to/export.csv``
|
||||||
|
|
||||||
- Required external libraries: `boto3`_ >= 1.20.0
|
- Required extras: :ref:`s3 <extras>`
|
||||||
|
|
||||||
The AWS credentials can be passed as user/password in the URI, or they can be
|
The AWS credentials can be passed as user/password in the URI, or they can be
|
||||||
passed through the following settings:
|
passed through the following settings:
|
||||||
|
|
@ -244,7 +247,7 @@ The feeds are stored on `Google Cloud Storage`_.
|
||||||
|
|
||||||
- ``gs://mybucket/path/to/export.csv``
|
- ``gs://mybucket/path/to/export.csv``
|
||||||
|
|
||||||
- Required external libraries: `google-cloud-storage`_.
|
- Required extras: :ref:`gcs <extras>`
|
||||||
|
|
||||||
For more information about authentication, please refer to `Google Cloud documentation <https://docs.cloud.google.com/docs/authentication>`_.
|
For more information about authentication, please refer to `Google Cloud documentation <https://docs.cloud.google.com/docs/authentication>`_.
|
||||||
|
|
||||||
|
|
@ -261,7 +264,6 @@ storage backend is: ``True``.
|
||||||
|
|
||||||
This storage backend uses :ref:`delayed file delivery <delayed-file-delivery>`.
|
This storage backend uses :ref:`delayed file delivery <delayed-file-delivery>`.
|
||||||
|
|
||||||
.. _google-cloud-storage: https://docs.cloud.google.com/storage/docs/reference/libraries#client-libraries-install-python
|
|
||||||
|
|
||||||
|
|
||||||
.. _topics-feed-storage-stdout:
|
.. _topics-feed-storage-stdout:
|
||||||
|
|
@ -429,33 +431,37 @@ This setting is required for enabling the feed export feature.
|
||||||
|
|
||||||
See :ref:`topics-feed-storage-backends` for supported URI schemes.
|
See :ref:`topics-feed-storage-backends` for supported URI schemes.
|
||||||
|
|
||||||
For instance::
|
For instance:
|
||||||
|
|
||||||
|
.. skip: next
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
'items.json': {
|
"items.json": {
|
||||||
'format': 'json',
|
"format": "json",
|
||||||
'encoding': 'utf8',
|
"encoding": "utf8",
|
||||||
'store_empty': False,
|
"store_empty": False,
|
||||||
'item_classes': [MyItemClass1, 'myproject.items.MyItemClass2'],
|
"item_classes": [MyItemClass1, "myproject.items.MyItemClass2"],
|
||||||
'fields': None,
|
"fields": None,
|
||||||
'indent': 4,
|
"indent": 4,
|
||||||
'item_export_kwargs': {
|
"item_export_kwargs": {
|
||||||
'export_empty_fields': True,
|
"export_empty_fields": True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/home/user/documents/items.xml': {
|
"/home/user/documents/items.xml": {
|
||||||
'format': 'xml',
|
"format": "xml",
|
||||||
'fields': ['name', 'price'],
|
"fields": ["name", "price"],
|
||||||
'item_filter': MyCustomFilter1,
|
"item_filter": MyCustomFilter1,
|
||||||
'encoding': 'latin1',
|
"encoding": "latin1",
|
||||||
'indent': 8,
|
"indent": 8,
|
||||||
},
|
},
|
||||||
pathlib.Path('items.csv.gz'): {
|
pathlib.Path("items.csv.gz"): {
|
||||||
'format': 'csv',
|
"format": "csv",
|
||||||
'fields': ['price', 'name'],
|
"fields": ["price", "name"],
|
||||||
'item_filter': 'myproject.filters.MyCustomFilter2',
|
"item_filter": "myproject.filters.MyCustomFilter2",
|
||||||
'postprocessing': [MyPlugin1, 'scrapy.extensions.postprocessing.GzipPlugin'],
|
"postprocessing": [MyPlugin1, "scrapy.extensions.postprocessing.GzipPlugin"],
|
||||||
'gzip_compresslevel': 5,
|
"gzip_compresslevel": 5,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -615,6 +621,7 @@ Default:
|
||||||
"file": "scrapy.extensions.feedexport.FileFeedStorage",
|
"file": "scrapy.extensions.feedexport.FileFeedStorage",
|
||||||
"stdout": "scrapy.extensions.feedexport.StdoutFeedStorage",
|
"stdout": "scrapy.extensions.feedexport.StdoutFeedStorage",
|
||||||
"s3": "scrapy.extensions.feedexport.S3FeedStorage",
|
"s3": "scrapy.extensions.feedexport.S3FeedStorage",
|
||||||
|
"gs": "scrapy.extensions.feedexport.GCSFeedStorage",
|
||||||
"ftp": "scrapy.extensions.feedexport.FTPFeedStorage",
|
"ftp": "scrapy.extensions.feedexport.FTPFeedStorage",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -756,8 +763,8 @@ The function signature should be as follows:
|
||||||
:param spider: source spider of the feed items
|
:param spider: source spider of the feed items
|
||||||
:type spider: scrapy.Spider
|
:type spider: scrapy.Spider
|
||||||
|
|
||||||
.. caution:: The function should return a new dictionary, modifying
|
.. caution:: The function must return a new dictionary instead of modifying
|
||||||
the received ``params`` in-place is deprecated.
|
the received ``params`` in-place.
|
||||||
|
|
||||||
For example, to include the :attr:`name <scrapy.Spider.name>` of the
|
For example, to include the :attr:`name <scrapy.Spider.name>` of the
|
||||||
source spider in the feed URI:
|
source spider in the feed URI:
|
||||||
|
|
@ -784,6 +791,5 @@ source spider in the feed URI:
|
||||||
|
|
||||||
.. _URIs: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
|
.. _URIs: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
|
||||||
.. _Amazon S3: https://aws.amazon.com/s3/
|
.. _Amazon S3: https://aws.amazon.com/s3/
|
||||||
.. _boto3: https://github.com/boto/boto3
|
|
||||||
.. _Canned ACL: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
.. _Canned ACL: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||||
.. _Google Cloud Storage: https://cloud.google.com/storage/
|
.. _Google Cloud Storage: https://cloud.google.com/storage/
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ Write items to MongoDB
|
||||||
|
|
||||||
In this example we'll write items to MongoDB_ using pymongo_.
|
In this example we'll write items to MongoDB_ using pymongo_.
|
||||||
MongoDB address and database name are specified in Scrapy settings;
|
MongoDB address and database name are specified in Scrapy settings;
|
||||||
MongoDB collection is named after item class.
|
MongoDB collection is specified in a class attribute.
|
||||||
|
|
||||||
The main point of this example is to show how to :ref:`get the crawler
|
The main point of this example is to show how to :ref:`get the crawler
|
||||||
<from-crawler>` and how to clean up the resources properly.
|
<from-crawler>` and how to clean up the resources properly.
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ Item Types
|
||||||
|
|
||||||
Scrapy supports the following types of items, via the `itemadapter`_ library:
|
Scrapy supports the following types of items, via the `itemadapter`_ library:
|
||||||
:ref:`dictionaries <dict-items>`, :ref:`Item objects <item-objects>`,
|
:ref:`dictionaries <dict-items>`, :ref:`Item objects <item-objects>`,
|
||||||
:ref:`dataclass objects <dataclass-items>`, and :ref:`attrs objects <attrs-items>`.
|
:ref:`dataclass objects <dataclass-items>`, :ref:`attrs objects <attrs-items>`
|
||||||
|
and :ref:`Pydantic models <pydantic-items>`.
|
||||||
|
|
||||||
.. _itemadapter: https://github.com/scrapy/itemadapter
|
.. _itemadapter: https://github.com/scrapy/itemadapter
|
||||||
|
|
||||||
|
|
@ -61,8 +62,8 @@ its ``__init__`` method.
|
||||||
:class:`Item` also allows the defining of field metadata, which can be used to
|
:class:`Item` also allows the defining of field metadata, which can be used to
|
||||||
:ref:`customize serialization <topics-exporters-field-serialization>`.
|
:ref:`customize serialization <topics-exporters-field-serialization>`.
|
||||||
|
|
||||||
:mod:`trackref` tracks :class:`Item` objects to help find memory leaks
|
:mod:`scrapy.utils.trackref` tracks :class:`Item` objects to help find memory
|
||||||
(see :ref:`topics-leaks-trackrefs`).
|
leaks (see :ref:`topics-leaks-trackrefs`).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
@ -262,7 +263,7 @@ Creating items
|
||||||
|
|
||||||
>>> product = Product(name="Desktop PC", price=1000)
|
>>> product = Product(name="Desktop PC", price=1000)
|
||||||
>>> print(product)
|
>>> print(product)
|
||||||
Product(name='Desktop PC', price=1000)
|
{'name': 'Desktop PC', 'price': 1000}
|
||||||
|
|
||||||
|
|
||||||
Getting field values
|
Getting field values
|
||||||
|
|
@ -376,10 +377,12 @@ Creating dicts from items:
|
||||||
>>> dict(product) # create a dict from all populated values
|
>>> dict(product) # create a dict from all populated values
|
||||||
{'price': 1000, 'name': 'Desktop PC'}
|
{'price': 1000, 'name': 'Desktop PC'}
|
||||||
|
|
||||||
Creating items from dicts:
|
Creating items from dicts:
|
||||||
|
|
||||||
|
.. code-block:: pycon
|
||||||
|
|
||||||
>>> Product({"name": "Laptop PC", "price": 1500})
|
>>> Product({"name": "Laptop PC", "price": 1500})
|
||||||
Product(price=1500, name='Laptop PC')
|
{'name': 'Laptop PC', 'price': 1500}
|
||||||
|
|
||||||
>>> Product({"name": "Laptop PC", "lala": 1500}) # warning: unknown field in dict
|
>>> Product({"name": "Laptop PC", "lala": 1500}) # warning: unknown field in dict
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
|
|
|
||||||
|
|
@ -151,8 +151,8 @@ Where:
|
||||||
- :class:`~scrapy.pqueues.ScrapyPriorityQueue` creates the ``{priority}{s?}``
|
- :class:`~scrapy.pqueues.ScrapyPriorityQueue` creates the ``{priority}{s?}``
|
||||||
directories.
|
directories.
|
||||||
|
|
||||||
- :class:`scrapy.squeues.PickleLifoDiskQueue`, a subclass of
|
- :class:`scrapy.squeues.PickleFifoDiskQueue`, a subclass of
|
||||||
:class:`queuelib.LifoDiskQueue` that uses :mod:`pickle` to serialize
|
:class:`queuelib.FifoDiskQueue` that uses :mod:`pickle` to serialize
|
||||||
:class:`dict` representations of :class:`scrapy.Request` objects, creates
|
:class:`dict` representations of :class:`scrapy.Request` objects, creates
|
||||||
the ``info.json`` and ``q{00000}`` files.
|
the ``info.json`` and ``q{00000}`` files.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ Debugging memory leaks with ``trackref``
|
||||||
|
|
||||||
.. skip: start
|
.. skip: start
|
||||||
|
|
||||||
:mod:`trackref` is a module provided by Scrapy to debug the most common cases of
|
:mod:`scrapy.utils.trackref` is a module provided by Scrapy to debug the most
|
||||||
memory leaks. It basically tracks the references to all live Request,
|
common cases of memory leaks. It basically tracks the references to all live
|
||||||
Response, Item, Spider and Selector objects.
|
Request, Response, Item, Spider and Selector objects.
|
||||||
|
|
||||||
You can enter the telnet console and inspect how many objects (of the classes
|
You can enter the telnet console and inspect how many objects (of the classes
|
||||||
mentioned above) are currently alive using the ``prefs()`` function which is an
|
mentioned above) are currently alive using the ``prefs()`` function which is an
|
||||||
|
|
@ -93,7 +93,7 @@ You can get the oldest object of each class using the
|
||||||
Which objects are tracked?
|
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):
|
subclasses):
|
||||||
|
|
||||||
* :class:`scrapy.Request`
|
* :class:`scrapy.Request`
|
||||||
|
|
@ -106,10 +106,15 @@ A real example
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Let's see a concrete example of a hypothetical case of memory leaks.
|
Let's see a concrete example of a hypothetical case of memory leaks.
|
||||||
Suppose we have some spider with a line similar to this one::
|
Suppose we have some spider with a line similar to this one:
|
||||||
|
|
||||||
return Request(f"http://www.somenastyspider.com/product.php?pid={product_id}",
|
.. code-block:: python
|
||||||
callback=self.parse, cb_kwargs={'referer': response})
|
|
||||||
|
return Request(
|
||||||
|
f"http://www.somenastyspider.com/product.php?pid={product_id}",
|
||||||
|
callback=self.parse,
|
||||||
|
cb_kwargs={"referer": response},
|
||||||
|
)
|
||||||
|
|
||||||
That line is passing a response reference inside a request which effectively
|
That line is passing a response reference inside a request which effectively
|
||||||
ties the response lifetime to the requests' one, and that would definitely
|
ties the response lifetime to the requests' one, and that would definitely
|
||||||
|
|
@ -164,7 +169,7 @@ Too many spiders?
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
If your project has too many spiders executed in parallel,
|
If your project has too many spiders executed in parallel,
|
||||||
the output of :func:`prefs` can be difficult to read.
|
the output of ``prefs()`` can be difficult to read.
|
||||||
For this reason, that function has a ``ignore`` argument which can be used to
|
For this reason, that function has a ``ignore`` argument which can be used to
|
||||||
ignore a particular class (and all its subclasses). For
|
ignore a particular class (and all its subclasses). For
|
||||||
example, this won't show any live references to spiders:
|
example, this won't show any live references to spiders:
|
||||||
|
|
@ -182,30 +187,13 @@ scrapy.utils.trackref module
|
||||||
|
|
||||||
Here are the functions available in the :mod:`~scrapy.utils.trackref` module.
|
Here are the functions available in the :mod:`~scrapy.utils.trackref` module.
|
||||||
|
|
||||||
.. class:: object_ref
|
.. autoclass:: object_ref
|
||||||
|
|
||||||
Inherit from this class if you want to track live
|
.. autofunction:: print_live_refs(ignore=NoneType)
|
||||||
instances with the ``trackref`` module.
|
|
||||||
|
|
||||||
.. function:: print_live_refs(class_name, ignore=NoneType)
|
.. autofunction:: get_oldest
|
||||||
|
|
||||||
Print a report of live references, grouped by class name.
|
.. autofunction:: iter_all
|
||||||
|
|
||||||
:param ignore: if given, all objects from the specified class (or tuple of
|
|
||||||
classes) will be ignored.
|
|
||||||
:type ignore: type or tuple
|
|
||||||
|
|
||||||
.. function:: get_oldest(class_name)
|
|
||||||
|
|
||||||
Return the oldest object 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.
|
|
||||||
|
|
||||||
.. 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.
|
|
||||||
|
|
||||||
.. skip: end
|
.. skip: end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@ Link extractor reference
|
||||||
|
|
||||||
The link extractor class is
|
The link extractor class is
|
||||||
:class:`scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor`. For convenience it
|
:class:`scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor`. For convenience it
|
||||||
can also be imported as ``scrapy.linkextractors.LinkExtractor``::
|
can also be imported as ``scrapy.linkextractors.LinkExtractor``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from scrapy.linkextractors import LinkExtractor
|
from scrapy.linkextractors import LinkExtractor
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ data that will be assigned to the ``name`` field later.
|
||||||
|
|
||||||
Afterwards, similar calls are used for ``price`` and ``stock`` fields
|
Afterwards, similar calls are used for ``price`` and ``stock`` fields
|
||||||
(the latter using a CSS selector with the :meth:`~ItemLoader.add_css` method),
|
(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`.
|
(``today``) using a different method: :meth:`~ItemLoader.add_value`.
|
||||||
|
|
||||||
Finally, when all data is collected, the :meth:`ItemLoader.load_item` method is
|
Finally, when all data is collected, the :meth:`ItemLoader.load_item` method is
|
||||||
|
|
@ -264,7 +264,7 @@ metadata. Here is an example:
|
||||||
>>> il.add_value("name", ["Welcome to my", "<strong>website</strong>"])
|
>>> il.add_value("name", ["Welcome to my", "<strong>website</strong>"])
|
||||||
>>> il.add_value("price", ["€", "<span>1000</span>"])
|
>>> il.add_value("price", ["€", "<span>1000</span>"])
|
||||||
>>> il.load_item()
|
>>> il.load_item()
|
||||||
{'name': 'Welcome to my website', 'price': '1000'}
|
Product(name='Welcome to my website', price='1000')
|
||||||
|
|
||||||
.. skip: end
|
.. skip: end
|
||||||
|
|
||||||
|
|
@ -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
|
1. Item Loader field-specific attributes: ``field_in`` and ``field_out`` (most
|
||||||
precedence)
|
precedence)
|
||||||
2. Field metadata (``input_processor`` and ``output_processor`` key)
|
2. Field metadata (``input_processor`` and ``output_processor`` key)
|
||||||
3. Item Loader defaults: :meth:`ItemLoader.default_input_processor` and
|
3. Item Loader defaults: :attr:`ItemLoader.default_input_processor` and
|
||||||
:meth:`ItemLoader.default_output_processor` (least precedence)
|
:attr:`ItemLoader.default_output_processor` (least precedence)
|
||||||
|
|
||||||
See also: :ref:`topics-loaders-extending`.
|
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")
|
loader = ItemLoader(product, unit="cm")
|
||||||
|
|
||||||
3. On Item Loader declaration, for those input/output processors that support
|
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
|
instantiating them with an Item Loader context.
|
||||||
them:
|
:class:`~itemloaders.processors.MapCompose` is one of them:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
@ -350,7 +350,9 @@ When parsing related values from a subsection of a document, it can be
|
||||||
useful to create nested loaders. Imagine you're extracting details from
|
useful to create nested loaders. Imagine you're extracting details from
|
||||||
a footer of a page that looks something like:
|
a footer of a page that looks something like:
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<a class="social" href="https://facebook.com/whatever">Like Us</a>
|
<a class="social" href="https://facebook.com/whatever">Like Us</a>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,6 @@
|
||||||
Logging
|
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
|
Scrapy uses :mod:`logging` for event logging. We'll
|
||||||
provide some simple examples to get you started, but for more advanced
|
provide some simple examples to get you started, but for more advanced
|
||||||
use-cases it's strongly suggested to read thoroughly its documentation.
|
use-cases it's strongly suggested to read thoroughly its documentation.
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,10 @@ this:
|
||||||
2. The item is returned from the spider and goes to the item pipeline.
|
2. The item is returned from the spider and goes to the item pipeline.
|
||||||
|
|
||||||
3. When the item reaches the :class:`FilesPipeline`, the URLs in the
|
3. When the item reaches the :class:`FilesPipeline`, the URLs in the
|
||||||
``file_urls`` field are scheduled for download using the standard
|
``file_urls`` field are downloaded using the standard Scrapy downloader
|
||||||
Scrapy scheduler and downloader (which means the scheduler and downloader
|
(which means the downloader middlewares are used, but the spider middlewares
|
||||||
middlewares are reused), but with a higher priority, processing them before other
|
aren't). The item remains "locked" at that particular pipeline stage until
|
||||||
pages are scraped. The item remains "locked" at that particular pipeline stage
|
the files have finished downloading (or failed for some reason).
|
||||||
until the files have finish downloading (or fail for some reason).
|
|
||||||
|
|
||||||
4. When the files are downloaded, another field (``files``) will be populated
|
4. When the files are downloaded, another field (``files``) will be populated
|
||||||
with the results. This field will contain a list of dicts with information
|
with the results. This field will contain a list of dicts with information
|
||||||
|
|
@ -61,6 +60,8 @@ this:
|
||||||
Using the Images Pipeline
|
Using the Images Pipeline
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
.. note:: Requires the :ref:`images <extras>` extra.
|
||||||
|
|
||||||
Using the :class:`ImagesPipeline` is a lot like using the :class:`FilesPipeline`,
|
Using the :class:`ImagesPipeline` is a lot like using the :class:`FilesPipeline`,
|
||||||
except the default field names used are different: you use ``image_urls`` for
|
except the default field names used are different: you use ``image_urls`` for
|
||||||
the image URLs of an item and it will populate an ``images`` field for the information
|
the image URLs of an item and it will populate an ``images`` field for the information
|
||||||
|
|
@ -70,12 +71,6 @@ The advantage of using the :class:`ImagesPipeline` for image files is that you
|
||||||
can configure some extra functions like generating thumbnails and filtering
|
can configure some extra functions like generating thumbnails and filtering
|
||||||
the images based on their size.
|
the images based on their size.
|
||||||
|
|
||||||
The Images Pipeline requires Pillow_ 8.3.2 or greater. It is used for
|
|
||||||
thumbnailing and normalizing images to JPEG/RGB format.
|
|
||||||
|
|
||||||
.. _Pillow: https://github.com/python-pillow/Pillow
|
|
||||||
|
|
||||||
|
|
||||||
.. _topics-media-pipeline-enabling:
|
.. _topics-media-pipeline-enabling:
|
||||||
|
|
||||||
Enabling your Media Pipeline
|
Enabling your Media Pipeline
|
||||||
|
|
@ -232,12 +227,13 @@ set the :setting:`FEED_STORAGE_FTP_ACTIVE` setting to ``True``.
|
||||||
Amazon S3 storage
|
Amazon S3 storage
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
.. note:: Requires the :ref:`s3 <extras>` extra.
|
||||||
|
|
||||||
.. setting:: FILES_STORE_S3_ACL
|
.. setting:: FILES_STORE_S3_ACL
|
||||||
.. setting:: IMAGES_STORE_S3_ACL
|
.. setting:: IMAGES_STORE_S3_ACL
|
||||||
|
|
||||||
If botocore_ >= 1.13.45 is installed, :setting:`FILES_STORE` and
|
:setting:`FILES_STORE` and :setting:`IMAGES_STORE` can represent an Amazon S3
|
||||||
:setting:`IMAGES_STORE` can represent an Amazon S3 bucket. Scrapy will
|
bucket. Scrapy will automatically upload the files to the bucket.
|
||||||
automatically upload the files to the bucket.
|
|
||||||
|
|
||||||
For example, this is a valid :setting:`IMAGES_STORE` value:
|
For example, this is a valid :setting:`IMAGES_STORE` value:
|
||||||
|
|
||||||
|
|
@ -272,7 +268,6 @@ For self-hosting you also might feel the need not to use SSL and not to verify S
|
||||||
AWS_USE_SSL = False # or True (None by default)
|
AWS_USE_SSL = False # or True (None by default)
|
||||||
AWS_VERIFY = False # or True (None by default)
|
AWS_VERIFY = False # or True (None by default)
|
||||||
|
|
||||||
.. _botocore: https://github.com/boto/botocore
|
|
||||||
.. _canned ACLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
.. _canned ACLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||||
.. _Minio: https://github.com/minio/minio
|
.. _Minio: https://github.com/minio/minio
|
||||||
.. _Zenko CloudServer: https://www.zenko.io/cloudserver/
|
.. _Zenko CloudServer: https://www.zenko.io/cloudserver/
|
||||||
|
|
@ -283,13 +278,13 @@ For self-hosting you also might feel the need not to use SSL and not to verify S
|
||||||
Google Cloud Storage
|
Google Cloud Storage
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
.. note:: Requires the :ref:`gcs <extras>` extra.
|
||||||
|
|
||||||
.. setting:: FILES_STORE_GCS_ACL
|
.. setting:: FILES_STORE_GCS_ACL
|
||||||
.. setting:: IMAGES_STORE_GCS_ACL
|
.. setting:: IMAGES_STORE_GCS_ACL
|
||||||
|
|
||||||
:setting:`FILES_STORE` and :setting:`IMAGES_STORE` can represent a Google Cloud Storage
|
:setting:`FILES_STORE` and :setting:`IMAGES_STORE` can represent a Google Cloud
|
||||||
bucket. Scrapy will automatically upload the files to the bucket. (requires `google-cloud-storage`_ )
|
Storage bucket. Scrapy will automatically upload the files to the bucket.
|
||||||
|
|
||||||
.. _google-cloud-storage: https://docs.cloud.google.com/storage/docs/reference/libraries#client-libraries-install-python
|
|
||||||
|
|
||||||
For example, these are valid :setting:`IMAGES_STORE` and :setting:`GCS_PROJECT_ID` settings:
|
For example, these are valid :setting:`IMAGES_STORE` and :setting:`GCS_PROJECT_ID` settings:
|
||||||
|
|
||||||
|
|
@ -371,11 +366,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
|
If you need something more complex and want to override the custom pipeline
|
||||||
behaviour, see :ref:`topics-media-pipeline-override`.
|
behaviour, see :ref:`topics-media-pipeline-override`.
|
||||||
|
|
||||||
If you have multiple image pipelines inheriting from ImagePipeline and you want
|
If you have multiple image pipelines inheriting from :class:`ImagesPipeline`
|
||||||
to have different settings in different pipelines you can set setting keys
|
and you want to have different settings in different pipelines you can set
|
||||||
preceded with uppercase name of your pipeline class. E.g. if your pipeline is
|
setting keys preceded with uppercase name of your pipeline class. E.g. if your
|
||||||
called MyPipeline and you want to have custom IMAGES_URLS_FIELD you define
|
pipeline is called ``MyPipeline`` and you want to have custom
|
||||||
setting MYPIPELINE_IMAGES_URLS_FIELD and your custom settings will be used.
|
:setting:`IMAGES_URLS_FIELD` you define setting
|
||||||
|
``MYPIPELINE_IMAGES_URLS_FIELD`` and your custom settings will be used.
|
||||||
|
|
||||||
|
|
||||||
Additional features
|
Additional features
|
||||||
|
|
@ -470,7 +466,9 @@ When using the Images Pipeline, you can drop images which are too small, by
|
||||||
specifying the minimum allowed size in the :setting:`IMAGES_MIN_HEIGHT` and
|
specifying the minimum allowed size in the :setting:`IMAGES_MIN_HEIGHT` and
|
||||||
:setting:`IMAGES_MIN_WIDTH` settings.
|
:setting:`IMAGES_MIN_WIDTH` settings.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
IMAGES_MIN_HEIGHT = 110
|
IMAGES_MIN_HEIGHT = 110
|
||||||
IMAGES_MIN_WIDTH = 110
|
IMAGES_MIN_WIDTH = 110
|
||||||
|
|
@ -493,7 +491,9 @@ Allowing redirections
|
||||||
By default media pipelines ignore redirects, i.e. an HTTP redirection
|
By default media pipelines ignore redirects, i.e. an HTTP redirection
|
||||||
to a media file URL request will mean the media download is considered failed.
|
to a media file URL request will mean the media download is considered failed.
|
||||||
|
|
||||||
To handle media redirections, set this setting to ``True``::
|
To handle media redirections, set this setting to ``True``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
MEDIA_ALLOW_REDIRECTS = True
|
MEDIA_ALLOW_REDIRECTS = True
|
||||||
|
|
||||||
|
|
@ -547,10 +547,9 @@ See here the methods that you can override in your custom Files Pipeline:
|
||||||
|
|
||||||
.. method:: FilesPipeline.get_media_requests(item, info)
|
.. method:: FilesPipeline.get_media_requests(item, info)
|
||||||
|
|
||||||
As seen on the workflow, the pipeline will get the URLs of the images to
|
As seen on the workflow, the pipeline will get the requests for the files
|
||||||
download from the item. In order to do this, you can override the
|
to download from the item by calling this method. You can override it to
|
||||||
:meth:`~get_media_requests` method and return a Request for each
|
change what requests are returned:
|
||||||
file URL:
|
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
@ -590,8 +589,9 @@ See here the methods that you can override in your custom Files Pipeline:
|
||||||
* ``downloaded`` - file was downloaded.
|
* ``downloaded`` - file was downloaded.
|
||||||
* ``uptodate`` - file was not downloaded, as it was downloaded recently,
|
* ``uptodate`` - file was not downloaded, as it was downloaded recently,
|
||||||
according to the file expiration policy.
|
according to the file expiration policy.
|
||||||
* ``cached`` - file was already scheduled for download, by another item
|
* ``cached`` - file was taken from a cache (the response has a
|
||||||
sharing the same file.
|
``"cached"`` flag, e.g. from
|
||||||
|
:class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`).
|
||||||
|
|
||||||
The list of tuples received by :meth:`~item_completed` is
|
The list of tuples received by :meth:`~item_completed` is
|
||||||
guaranteed to retain the same order of the requests returned from the
|
guaranteed to retain the same order of the requests returned from the
|
||||||
|
|
@ -618,9 +618,6 @@ See here the methods that you can override in your custom Files Pipeline:
|
||||||
(False, Failure(...)),
|
(False, Failure(...)),
|
||||||
]
|
]
|
||||||
|
|
||||||
By default the :meth:`get_media_requests` method returns ``None`` which
|
|
||||||
means there are no files to download for the item.
|
|
||||||
|
|
||||||
.. method:: FilesPipeline.item_completed(results, item, info)
|
.. method:: FilesPipeline.item_completed(results, item, info)
|
||||||
|
|
||||||
The :meth:`FilesPipeline.item_completed` method called when all file
|
The :meth:`FilesPipeline.item_completed` method called when all file
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@ Run Scrapy from a script
|
||||||
You can use the :ref:`API <topics-api>` to run Scrapy from a script, instead of
|
You can use the :ref:`API <topics-api>` to run Scrapy from a script, instead of
|
||||||
the typical way of running Scrapy via ``scrapy crawl``.
|
the typical way of running Scrapy via ``scrapy crawl``.
|
||||||
|
|
||||||
Remember that Scrapy is built on top of the Twisted
|
Remember that Scrapy requires a Twisted reactor or (with
|
||||||
asynchronous networking library, so you need to run it inside the Twisted reactor.
|
:setting:`TWISTED_REACTOR_ENABLED` set to ``False``) an asyncio event loop, so
|
||||||
|
you need to run one of those in your script for it to work (helpers described
|
||||||
|
below can do it for you).
|
||||||
|
|
||||||
The first utility you can use to run your spiders is
|
The first utility you can use to run your spiders is
|
||||||
:class:`scrapy.crawler.AsyncCrawlerProcess` or
|
:class:`scrapy.crawler.AsyncCrawlerProcess` or
|
||||||
|
|
@ -245,6 +247,110 @@ Using :func:`asyncio.run` with :class:`~scrapy.crawler.AsyncCrawlerRunner`:
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|
||||||
|
.. _run-spiders-in-apps:
|
||||||
|
|
||||||
|
Running spiders inside existing applications
|
||||||
|
============================================
|
||||||
|
|
||||||
|
You may want to run Scrapy spiders inside an existing application. In simple
|
||||||
|
cases (e.g. task queues that spawn a process for every task, or applications
|
||||||
|
that can execute tasks synchronously in the same process) you can use the same
|
||||||
|
approach as for standalone scripts (see :ref:`run-from-script`). More complex
|
||||||
|
cases, e.g. asynchronous web applications, have additional caveats and
|
||||||
|
limitations.
|
||||||
|
|
||||||
|
If the application runs its own Twisted reactor, you can use
|
||||||
|
:class:`~scrapy.crawler.AsyncCrawlerRunner` or
|
||||||
|
:class:`~scrapy.crawler.CrawlerRunner` to run spiders using this reactor, see
|
||||||
|
:ref:`run-from-script` for examples.
|
||||||
|
|
||||||
|
If the application doesn't run a Twisted reactor or an asyncio event loop (for
|
||||||
|
example, a Django web app deployed with a WSGI server such as uWSGI), you can
|
||||||
|
use :class:`~scrapy.crawler.AsyncCrawlerProcess` with
|
||||||
|
:setting:`TWISTED_REACTOR_ENABLED` set to ``False``, so that Scrapy starts and
|
||||||
|
stops an asyncio event loop for every spider run:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import scrapy
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from scrapy.crawler import AsyncCrawlerProcess
|
||||||
|
|
||||||
|
|
||||||
|
class MySpider(scrapy.Spider):
|
||||||
|
# Your spider definition
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def crawl_view(request):
|
||||||
|
process = AsyncCrawlerProcess(settings={"TWISTED_REACTOR_ENABLED": False})
|
||||||
|
process.crawl(MySpider)
|
||||||
|
process.start() # returns when the spider finishes
|
||||||
|
return HttpResponse("Crawling finished")
|
||||||
|
|
||||||
|
If the application runs its own asyncio event loop (for example, a Django web
|
||||||
|
app deployed with an ASGI server such as uvicorn), you can use
|
||||||
|
:class:`~scrapy.crawler.AsyncCrawlerRunner` with
|
||||||
|
:setting:`TWISTED_REACTOR_ENABLED` set to ``False``, so that Scrapy uses the
|
||||||
|
existing event loop:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import scrapy
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from scrapy.crawler import AsyncCrawlerRunner
|
||||||
|
|
||||||
|
|
||||||
|
class MySpider(scrapy.Spider):
|
||||||
|
# Your spider definition
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
async def crawl_view(request):
|
||||||
|
runner = AsyncCrawlerRunner(settings={"TWISTED_REACTOR_ENABLED": False})
|
||||||
|
await runner.crawl(MySpider) # completes when the spider finishes
|
||||||
|
return HttpResponse("Crawling finished")
|
||||||
|
|
||||||
|
.. note:: Running Scrapy without a Twisted reactor is experimental and has
|
||||||
|
some limitations, described in :ref:`asyncio-without-reactor`.
|
||||||
|
|
||||||
|
.. _run-in-notebook:
|
||||||
|
|
||||||
|
Running spiders in Jupyter notebooks
|
||||||
|
====================================
|
||||||
|
|
||||||
|
You can run Scrapy spiders in Jupyter notebooks. You need to use
|
||||||
|
:class:`~scrapy.crawler.AsyncCrawlerRunner` with
|
||||||
|
:setting:`TWISTED_REACTOR_ENABLED` set to ``False`` for this, so that Scrapy
|
||||||
|
uses the event loop provided by the notebook kernel. As
|
||||||
|
:class:`~scrapy.crawler.AsyncCrawlerRunner` doesn't configure logging, and you
|
||||||
|
most likely want to see the spider log in the notebook, you should call
|
||||||
|
:func:`scrapy.utils.log.configure_logging`. Here is a full example, which
|
||||||
|
supports rerunning both as a single cell and as separate cells:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from scrapy import Spider
|
||||||
|
from scrapy.crawler import AsyncCrawlerRunner
|
||||||
|
from scrapy.utils.log import configure_logging
|
||||||
|
|
||||||
|
configure_logging()
|
||||||
|
|
||||||
|
|
||||||
|
class BooksSpider(Spider):
|
||||||
|
name = "books"
|
||||||
|
start_urls = ["https://books.toscrape.com"]
|
||||||
|
|
||||||
|
def parse(self, response):
|
||||||
|
for book in response.css("h3"):
|
||||||
|
yield {"title": book.css("a::attr(title)").get()}
|
||||||
|
|
||||||
|
|
||||||
|
runner = AsyncCrawlerRunner({"TWISTED_REACTOR_ENABLED": False})
|
||||||
|
await runner.crawl(BooksSpider)
|
||||||
|
|
||||||
|
.. note:: Running Scrapy without a Twisted reactor is experimental and has
|
||||||
|
some limitations, described in :ref:`asyncio-without-reactor`.
|
||||||
|
|
||||||
.. _run-multiple-spiders:
|
.. _run-multiple-spiders:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,9 @@ Request objects
|
||||||
Also mind that the :meth:`copy` and :meth:`replace` request methods
|
Also mind that the :meth:`copy` and :meth:`replace` request methods
|
||||||
:doc:`shallow-copy <library/copy>` request metadata.
|
:doc:`shallow-copy <library/copy>` request metadata.
|
||||||
|
|
||||||
|
.. seealso:: :class:`~scrapy.spidermiddlewares.metacopy.MetaCopyDetectionMiddleware`
|
||||||
|
for a built-in middleware that warns about this issue at run time.
|
||||||
|
|
||||||
.. autoattribute:: dont_filter
|
.. autoattribute:: dont_filter
|
||||||
|
|
||||||
.. autoattribute:: Request.attributes
|
.. autoattribute:: Request.attributes
|
||||||
|
|
@ -247,7 +250,7 @@ Request objects
|
||||||
Return a new Request which is a copy of this Request. See also:
|
Return a new Request which is a copy of this Request. See also:
|
||||||
:ref:`topics-request-response-ref-request-callback-arguments`.
|
: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
|
Return a Request object with the same members, except for those members
|
||||||
given new values by whichever keyword arguments are specified. The
|
given new values by whichever keyword arguments are specified. The
|
||||||
|
|
@ -257,6 +260,8 @@ Request objects
|
||||||
|
|
||||||
.. automethod:: from_curl
|
.. automethod:: from_curl
|
||||||
|
|
||||||
|
.. automethod:: to_curl
|
||||||
|
|
||||||
.. automethod:: to_dict
|
.. automethod:: to_dict
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -339,6 +344,8 @@ Other functions related to requests
|
||||||
|
|
||||||
.. autofunction:: scrapy.utils.request.request_from_dict
|
.. autofunction:: scrapy.utils.request.request_from_dict
|
||||||
|
|
||||||
|
.. autofunction:: scrapy.utils.httpobj.urlparse_cached
|
||||||
|
|
||||||
|
|
||||||
.. _topics-request-response-ref-request-callback-arguments:
|
.. _topics-request-response-ref-request-callback-arguments:
|
||||||
|
|
||||||
|
|
@ -436,7 +443,7 @@ errors if needed:
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_httpbin(self, response):
|
def parse_httpbin(self, response):
|
||||||
self.logger.info("Got successful response from {}".format(response.url))
|
self.logger.info(f"Got successful response from {response.url}")
|
||||||
# do something useful here...
|
# do something useful here...
|
||||||
|
|
||||||
def errback_httpbin(self, failure):
|
def errback_httpbin(self, failure):
|
||||||
|
|
@ -717,6 +724,7 @@ Those are:
|
||||||
* :reqmeta:`download_fail_on_dataloss`
|
* :reqmeta:`download_fail_on_dataloss`
|
||||||
* :reqmeta:`download_latency`
|
* :reqmeta:`download_latency`
|
||||||
* :reqmeta:`download_maxsize`
|
* :reqmeta:`download_maxsize`
|
||||||
|
* :reqmeta:`download_slot`
|
||||||
* :reqmeta:`download_warnsize`
|
* :reqmeta:`download_warnsize`
|
||||||
* :reqmeta:`download_timeout`
|
* :reqmeta:`download_timeout`
|
||||||
* ``ftp_password`` (See :setting:`FTP_PASSWORD` for more info)
|
* ``ftp_password`` (See :setting:`FTP_PASSWORD` for more info)
|
||||||
|
|
@ -1027,9 +1035,13 @@ Response objects
|
||||||
:meth:`~scrapy.http.headers.Headers.get` to return the last header value with
|
:meth:`~scrapy.http.headers.Headers.get` to return the last header value with
|
||||||
the specified name or :meth:`~scrapy.http.headers.Headers.getlist` to return
|
the specified name or :meth:`~scrapy.http.headers.Headers.getlist` to return
|
||||||
all header values with the specified name. For example, this call will give you
|
all header values with the specified name. For example, this call will give you
|
||||||
all cookies in the headers::
|
all cookies in the headers:
|
||||||
|
|
||||||
response.headers.getlist('Set-Cookie')
|
.. skip: next
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
response.headers.getlist("Set-Cookie")
|
||||||
|
|
||||||
.. attribute:: Response.body
|
.. attribute:: Response.body
|
||||||
|
|
||||||
|
|
@ -1085,7 +1097,7 @@ Response objects
|
||||||
.. attribute:: Response.flags
|
.. attribute:: Response.flags
|
||||||
|
|
||||||
A list that contains flags for this response. Flags are labels used for
|
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__()``
|
they're shown on the string representation of the Response (``__str__()``
|
||||||
method) which is used by the engine for logging.
|
method) which is used by the engine for logging.
|
||||||
|
|
||||||
|
|
@ -1100,8 +1112,8 @@ Response objects
|
||||||
|
|
||||||
The IP address of the server from which the Response originated.
|
The IP address of the server from which the Response originated.
|
||||||
|
|
||||||
This attribute is currently only populated by the HTTP 1.1 download
|
This attribute is currently only populated by the HTTP download
|
||||||
handler, i.e. for ``http(s)`` responses. For other handlers,
|
handlers, i.e. for ``http(s)`` responses. For other handlers,
|
||||||
:attr:`ip_address` is always ``None``.
|
:attr:`ip_address` is always ``None``.
|
||||||
|
|
||||||
.. attribute:: Response.protocol
|
.. attribute:: Response.protocol
|
||||||
|
|
@ -1119,7 +1131,7 @@ Response objects
|
||||||
|
|
||||||
Returns a new Response which is a copy of this Response.
|
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
|
Returns a Response object with the same members, except for those members
|
||||||
given new values by whichever keyword arguments are specified. The
|
given new values by whichever keyword arguments are specified. The
|
||||||
|
|
@ -1131,7 +1143,11 @@ Response objects
|
||||||
a possible relative url.
|
a possible relative url.
|
||||||
|
|
||||||
This is a wrapper over :func:`~urllib.parse.urljoin`, it's merely an alias for
|
This is a wrapper over :func:`~urllib.parse.urljoin`, it's merely an alias for
|
||||||
making this call::
|
making this call:
|
||||||
|
|
||||||
|
.. skip: next
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
urllib.parse.urljoin(response.url, url)
|
urllib.parse.urljoin(response.url, url)
|
||||||
|
|
||||||
|
|
@ -1220,21 +1236,31 @@ TextResponse objects
|
||||||
|
|
||||||
.. method:: TextResponse.jmespath(query)
|
.. method:: TextResponse.jmespath(query)
|
||||||
|
|
||||||
A shortcut to ``TextResponse.selector.jmespath(query)``::
|
.. skip: start
|
||||||
|
|
||||||
response.jmespath('object.[*]')
|
A shortcut to ``TextResponse.selector.jmespath(query)``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
response.jmespath("object.[*]")
|
||||||
|
|
||||||
.. method:: TextResponse.xpath(query)
|
.. method:: TextResponse.xpath(query)
|
||||||
|
|
||||||
A shortcut to ``TextResponse.selector.xpath(query)``::
|
A shortcut to ``TextResponse.selector.xpath(query)``:
|
||||||
|
|
||||||
response.xpath('//p')
|
.. code-block:: python
|
||||||
|
|
||||||
|
response.xpath("//p")
|
||||||
|
|
||||||
.. method:: TextResponse.css(query)
|
.. method:: TextResponse.css(query)
|
||||||
|
|
||||||
A shortcut to ``TextResponse.selector.css(query)``::
|
A shortcut to ``TextResponse.selector.css(query)``:
|
||||||
|
|
||||||
response.css('p')
|
.. code-block:: python
|
||||||
|
|
||||||
|
response.css("p")
|
||||||
|
|
||||||
|
.. skip: end
|
||||||
|
|
||||||
.. automethod:: TextResponse.follow
|
.. automethod:: TextResponse.follow
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ Examples:
|
||||||
|
|
||||||
* ``*::text`` selects all descendant text nodes of the current selector context:
|
* ``*::text`` selects all descendant text nodes of the current selector context:
|
||||||
|
|
||||||
..skip: next
|
.. skip: next
|
||||||
.. code-block:: pycon
|
.. code-block:: pycon
|
||||||
|
|
||||||
>>> response.css("#images *::text").getall()
|
>>> response.css("#images *::text").getall()
|
||||||
|
|
@ -634,8 +634,7 @@ Example:
|
||||||
.. code-block:: pycon
|
.. code-block:: pycon
|
||||||
|
|
||||||
>>> from scrapy import Selector
|
>>> from scrapy import Selector
|
||||||
>>> sel = Selector(
|
>>> sel = Selector(text="""
|
||||||
... text="""
|
|
||||||
... <ul class="list">
|
... <ul class="list">
|
||||||
... <li>1</li>
|
... <li>1</li>
|
||||||
... <li>2</li>
|
... <li>2</li>
|
||||||
|
|
@ -645,8 +644,8 @@ Example:
|
||||||
... <li>4</li>
|
... <li>4</li>
|
||||||
... <li>5</li>
|
... <li>5</li>
|
||||||
... <li>6</li>
|
... <li>6</li>
|
||||||
... </ul>"""
|
... </ul>""")
|
||||||
... )
|
...
|
||||||
>>> xp = lambda x: sel.xpath(x).getall()
|
>>> xp = lambda x: sel.xpath(x).getall()
|
||||||
|
|
||||||
This gets all first ``<li>`` elements under whatever it is its parent:
|
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")
|
>>> sel = Selector(text=doc, type="html")
|
||||||
>>> for scope in sel.xpath("//div[@itemscope]"):
|
>>> for scope in sel.xpath("//div[@itemscope]"):
|
||||||
... print("current scope:", scope.xpath("@itemtype").getall())
|
... print("current scope:", scope.xpath("@itemtype").getall())
|
||||||
... props = scope.xpath(
|
... props = scope.xpath("""
|
||||||
... """
|
|
||||||
... set:difference(./descendant::*/@itemprop,
|
... set:difference(./descendant::*/@itemprop,
|
||||||
... .//*[@itemscope]/*/@itemprop)"""
|
... .//*[@itemscope]/*/@itemprop)""")
|
||||||
... )
|
|
||||||
... print(f" properties: {props.getall()}")
|
... print(f" properties: {props.getall()}")
|
||||||
... print("")
|
... print("")
|
||||||
...
|
...
|
||||||
|
|
|
||||||
|
|
@ -409,6 +409,36 @@ Default: ``{}``
|
||||||
A dict containing paths to the add-ons enabled in your project and their
|
A dict containing paths to the add-ons enabled in your project and their
|
||||||
priorities. For more information, see :ref:`topics-addons`.
|
priorities. For more information, see :ref:`topics-addons`.
|
||||||
|
|
||||||
|
.. setting:: ASYNCIO_EVENT_LOOP
|
||||||
|
|
||||||
|
ASYNCIO_EVENT_LOOP
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
Import path of a given ``asyncio`` event loop class.
|
||||||
|
|
||||||
|
If the asyncio reactor is enabled (see :setting:`TWISTED_REACTOR`) or when
|
||||||
|
:ref:`running Scrapy without a reactor <asyncio-without-reactor>` this setting
|
||||||
|
can be used to specify the
|
||||||
|
asyncio event loop to be used with it. Set the setting to the import path of the
|
||||||
|
desired asyncio event loop class. If the setting is set to ``None`` the default asyncio
|
||||||
|
event loop will be used.
|
||||||
|
|
||||||
|
If you are installing the asyncio reactor manually using the :func:`~scrapy.utils.reactor.install_reactor`
|
||||||
|
function, you can use the ``event_loop_path`` parameter to indicate the import path of the event loop
|
||||||
|
class to be used.
|
||||||
|
|
||||||
|
Note that the event loop class must inherit from :class:`asyncio.AbstractEventLoop`.
|
||||||
|
|
||||||
|
.. caution:: Please be aware that, when using a non-default event loop
|
||||||
|
(either defined via :setting:`ASYNCIO_EVENT_LOOP` or installed with
|
||||||
|
:func:`~scrapy.utils.reactor.install_reactor`), Scrapy will call
|
||||||
|
:func:`asyncio.set_event_loop`, which will set the specified event loop
|
||||||
|
as the current loop for the current OS thread.
|
||||||
|
|
||||||
|
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||||
|
|
||||||
.. setting:: AWS_ACCESS_KEY_ID
|
.. setting:: AWS_ACCESS_KEY_ID
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID
|
AWS_ACCESS_KEY_ID
|
||||||
|
|
@ -419,6 +449,24 @@ Default: ``None``
|
||||||
The AWS access key used by code that requires access to `Amazon Web services`_,
|
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>`.
|
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
|
.. setting:: AWS_SECRET_ACCESS_KEY
|
||||||
|
|
||||||
AWS_SECRET_ACCESS_KEY
|
AWS_SECRET_ACCESS_KEY
|
||||||
|
|
@ -442,15 +490,6 @@ such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`, when using
|
||||||
|
|
||||||
.. _temporary security credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html
|
.. _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
|
.. setting:: AWS_USE_SSL
|
||||||
|
|
||||||
AWS_USE_SSL
|
AWS_USE_SSL
|
||||||
|
|
@ -471,43 +510,6 @@ Default: ``None``
|
||||||
Verify SSL connection between Scrapy and S3 or S3-like storage. By default
|
Verify SSL connection between Scrapy and S3 or S3-like storage. By default
|
||||||
SSL verification will occur.
|
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
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Default: ``None``
|
|
||||||
|
|
||||||
Import path of a given ``asyncio`` event loop class.
|
|
||||||
|
|
||||||
If the asyncio reactor is enabled (see :setting:`TWISTED_REACTOR`) this setting can be used to specify the
|
|
||||||
asyncio event loop to be used with it. Set the setting to the import path of the
|
|
||||||
desired asyncio event loop class. If the setting is set to ``None`` the default asyncio
|
|
||||||
event loop will be used.
|
|
||||||
|
|
||||||
If you are installing the asyncio reactor manually using the :func:`~scrapy.utils.reactor.install_reactor`
|
|
||||||
function, you can use the ``event_loop_path`` parameter to indicate the import path of the event loop
|
|
||||||
class to be used.
|
|
||||||
|
|
||||||
Note that the event loop class must inherit from :class:`asyncio.AbstractEventLoop`.
|
|
||||||
|
|
||||||
.. caution:: Please be aware that, when using a non-default event loop
|
|
||||||
(either defined via :setting:`ASYNCIO_EVENT_LOOP` or installed with
|
|
||||||
:func:`~scrapy.utils.reactor.install_reactor`), Scrapy will call
|
|
||||||
:func:`asyncio.set_event_loop`, which will set the specified event loop
|
|
||||||
as the current loop for the current OS thread.
|
|
||||||
|
|
||||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
|
||||||
|
|
||||||
.. setting:: BOT_NAME
|
.. setting:: BOT_NAME
|
||||||
|
|
||||||
BOT_NAME
|
BOT_NAME
|
||||||
|
|
@ -554,6 +556,8 @@ performed to any single domain.
|
||||||
See also: :ref:`topics-autothrottle` and its
|
See also: :ref:`topics-autothrottle` and its
|
||||||
:setting:`AUTOTHROTTLE_TARGET_CONCURRENCY` option.
|
:setting:`AUTOTHROTTLE_TARGET_CONCURRENCY` option.
|
||||||
|
|
||||||
|
It is possible to change this setting per domain by using
|
||||||
|
:setting:`DOWNLOAD_SLOTS`.
|
||||||
|
|
||||||
.. setting:: DEFAULT_DROPITEM_LOG_LEVEL
|
.. setting:: DEFAULT_DROPITEM_LOG_LEVEL
|
||||||
|
|
||||||
|
|
@ -593,7 +597,7 @@ When writing an item pipeline, you can force a different log level by setting
|
||||||
DEFAULT_ITEM_CLASS
|
DEFAULT_ITEM_CLASS
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Default: ``'scrapy.Item'``
|
Default: ``'scrapy.item.Item'``
|
||||||
|
|
||||||
The default class that will be used for instantiating items in the :ref:`the
|
The default class that will be used for instantiating items in the :ref:`the
|
||||||
Scrapy shell <topics-shell>`.
|
Scrapy shell <topics-shell>`.
|
||||||
|
|
@ -687,7 +691,7 @@ Whether to enable DNS in-memory cache.
|
||||||
:class:`~scrapy.resolver.CachingThreadedResolver` and
|
:class:`~scrapy.resolver.CachingThreadedResolver` and
|
||||||
:class:`~scrapy.resolver.CachingHostnameResolver`. It has no effect when
|
:class:`~scrapy.resolver.CachingHostnameResolver`. It has no effect when
|
||||||
:setting:`TWISTED_REACTOR_ENABLED` is ``False``, and may have no effect
|
: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>`.
|
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||||
|
|
||||||
|
|
@ -702,25 +706,6 @@ DNS in-memory cache size, see :setting:`DNSCACHE_ENABLED`.
|
||||||
|
|
||||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
.. 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
|
.. setting:: DNS_TIMEOUT
|
||||||
|
|
||||||
DNS_TIMEOUT
|
DNS_TIMEOUT
|
||||||
|
|
@ -734,7 +719,7 @@ Timeout for processing of DNS queries in seconds. Float is supported.
|
||||||
This setting is only used by
|
This setting is only used by
|
||||||
:class:`~scrapy.resolver.CachingThreadedResolver`. It has no effect when
|
:class:`~scrapy.resolver.CachingThreadedResolver`. It has no effect when
|
||||||
:setting:`TWISTED_REACTOR_ENABLED` is ``False``, and may have no effect
|
: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>`.
|
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||||
|
|
||||||
|
|
@ -914,7 +899,9 @@ Use :setting:`DOWNLOAD_DELAY` to throttle your crawling speed, to avoid hitting
|
||||||
servers too hard.
|
servers too hard.
|
||||||
|
|
||||||
Decimal numbers are supported. For example, to send a maximum of 4 requests
|
Decimal numbers are supported. For example, to send a maximum of 4 requests
|
||||||
every 10 seconds::
|
every 10 seconds:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
DOWNLOAD_DELAY = 2.5
|
DOWNLOAD_DELAY = 2.5
|
||||||
|
|
||||||
|
|
@ -936,9 +923,8 @@ desired.
|
||||||
|
|
||||||
This delay can be set per spider using :attr:`download_delay` spider attribute.
|
This delay can be set per spider using :attr:`download_delay` spider attribute.
|
||||||
|
|
||||||
It is also possible to change this setting per domain, although it requires
|
It is possible to change this setting per domain by using
|
||||||
non-trivial code. See the implementation of the :ref:`AutoThrottle
|
:setting:`DOWNLOAD_SLOTS`.
|
||||||
<topics-autothrottle>` extension for an example.
|
|
||||||
|
|
||||||
.. setting:: DOWNLOAD_BIND_ADDRESS
|
.. setting:: DOWNLOAD_BIND_ADDRESS
|
||||||
|
|
||||||
|
|
@ -1247,7 +1233,9 @@ the ``dont_filter`` parameter to ``True`` on the ``__init__`` method of a
|
||||||
specific :class:`~scrapy.Request` object that should not be filtered out.
|
specific :class:`~scrapy.Request` object that should not be filtered out.
|
||||||
|
|
||||||
A class assigned to :setting:`DUPEFILTER_CLASS` must implement the following
|
A class assigned to :setting:`DUPEFILTER_CLASS` must implement the following
|
||||||
interface::
|
interface:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class MyDupeFilter:
|
class MyDupeFilter:
|
||||||
|
|
||||||
|
|
@ -1330,6 +1318,7 @@ Default:
|
||||||
|
|
||||||
{
|
{
|
||||||
"scrapy.extensions.corestats.CoreStats": 0,
|
"scrapy.extensions.corestats.CoreStats": 0,
|
||||||
|
"scrapy.extensions.logcount.LogCount": 0,
|
||||||
"scrapy.extensions.telnet.TelnetConsole": 0,
|
"scrapy.extensions.telnet.TelnetConsole": 0,
|
||||||
"scrapy.extensions.memusage.MemoryUsage": 0,
|
"scrapy.extensions.memusage.MemoryUsage": 0,
|
||||||
"scrapy.extensions.memdebug.MemoryDebugger": 0,
|
"scrapy.extensions.memdebug.MemoryDebugger": 0,
|
||||||
|
|
@ -1352,6 +1341,8 @@ and the :ref:`list of available extensions <topics-extensions-ref>`.
|
||||||
FEED_TEMPDIR
|
FEED_TEMPDIR
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
The Feed Temp dir allows you to set a custom folder to save crawler
|
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
|
temporary files before uploading with :ref:`FTP feed storage <topics-feed-storage-ftp>` and
|
||||||
:ref:`Amazon S3 <topics-feed-storage-s3>`.
|
:ref:`Amazon S3 <topics-feed-storage-s3>`.
|
||||||
|
|
@ -1361,6 +1352,8 @@ temporary files before uploading with :ref:`FTP feed storage <topics-feed-storag
|
||||||
FEED_STORAGE_GCS_ACL
|
FEED_STORAGE_GCS_ACL
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
Default: ``""``
|
||||||
|
|
||||||
The Access Control List (ACL) used when storing items to :ref:`Google Cloud Storage <topics-feed-storage-gcs>`.
|
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>`_.
|
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>`_.
|
||||||
|
|
||||||
|
|
@ -1372,14 +1365,19 @@ FORCE_CRAWLER_PROCESS
|
||||||
Default: ``False``
|
Default: ``False``
|
||||||
|
|
||||||
If ``False``, :ref:`Scrapy commands that need a CrawlerProcess
|
If ``False``, :ref:`Scrapy commands that need a CrawlerProcess
|
||||||
<topics-commands-crawlerprocess>` will decide between using
|
<topics-commands-crawlerprocess>`, when :setting:`TWISTED_REACTOR_ENABLED`
|
||||||
|
is set to ``True``, will decide between using
|
||||||
:class:`scrapy.crawler.AsyncCrawlerProcess` and
|
:class:`scrapy.crawler.AsyncCrawlerProcess` and
|
||||||
:class:`scrapy.crawler.CrawlerProcess` based on the value of the
|
:class:`scrapy.crawler.CrawlerProcess` based on the value of the
|
||||||
:setting:`TWISTED_REACTOR` setting, but ignoring its value in :ref:`per-spider
|
:setting:`TWISTED_REACTOR` setting, but ignoring its value in :ref:`per-spider
|
||||||
settings <spider-settings>`.
|
settings <spider-settings>`.
|
||||||
|
|
||||||
If ``True``, these commands will always use
|
If ``True``, these commands will always use
|
||||||
:class:`~scrapy.crawler.CrawlerProcess`.
|
:class:`~scrapy.crawler.CrawlerProcess` when :setting:`TWISTED_REACTOR_ENABLED`
|
||||||
|
is set to ``True``.
|
||||||
|
|
||||||
|
When :setting:`TWISTED_REACTOR_ENABLED` is set to ``False``,
|
||||||
|
:class:`~scrapy.crawler.AsyncCrawlerProcess` will be used in all cases.
|
||||||
|
|
||||||
Set this to ``True`` if you want to set :setting:`TWISTED_REACTOR` to a
|
Set this to ``True`` if you want to set :setting:`TWISTED_REACTOR` to a
|
||||||
non-default value in :ref:`per-spider settings <spider-settings>`.
|
non-default value in :ref:`per-spider settings <spider-settings>`.
|
||||||
|
|
@ -1629,6 +1627,8 @@ The following special items are also supported:
|
||||||
|
|
||||||
- ``Python``
|
- ``Python``
|
||||||
|
|
||||||
|
- ``pyOpenSSL``
|
||||||
|
|
||||||
.. setting:: LOGSTATS_INTERVAL
|
.. setting:: LOGSTATS_INTERVAL
|
||||||
|
|
||||||
LOGSTATS_INTERVAL
|
LOGSTATS_INTERVAL
|
||||||
|
|
@ -1648,21 +1648,6 @@ Default: ``False``
|
||||||
|
|
||||||
Whether to enable memory debugging.
|
Whether to enable memory debugging.
|
||||||
|
|
||||||
.. setting:: MEMDEBUG_NOTIFY
|
|
||||||
|
|
||||||
MEMDEBUG_NOTIFY
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Default: ``[]``
|
|
||||||
|
|
||||||
When memory debugging is enabled a memory report will be sent to the specified
|
|
||||||
addresses if this setting is not empty, otherwise the report will be written to
|
|
||||||
the log.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
MEMDEBUG_NOTIFY = ['user@example.com']
|
|
||||||
|
|
||||||
.. setting:: MEMUSAGE_ENABLED
|
.. setting:: MEMUSAGE_ENABLED
|
||||||
|
|
||||||
MEMUSAGE_ENABLED
|
MEMUSAGE_ENABLED
|
||||||
|
|
@ -1736,9 +1721,11 @@ Default: ``"<project name>.spiders"`` (:ref:`fallback <default-settings>`: ``""`
|
||||||
|
|
||||||
Module where to create new spiders using the :command:`genspider` command.
|
Module where to create new spiders using the :command:`genspider` command.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
NEWSPIDER_MODULE = 'mybot.spiders_dev'
|
.. code-block:: python
|
||||||
|
|
||||||
|
NEWSPIDER_MODULE = "mybot.spiders_dev"
|
||||||
|
|
||||||
.. setting:: RANDOMIZE_DOWNLOAD_DELAY
|
.. setting:: RANDOMIZE_DOWNLOAD_DELAY
|
||||||
|
|
||||||
|
|
@ -1756,7 +1743,10 @@ significant similarities in the time between their requests.
|
||||||
|
|
||||||
The randomization policy is the same used by `wget`_ ``--random-wait`` option.
|
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.
|
||||||
|
|
||||||
|
It is possible to change this setting per domain by using
|
||||||
|
:setting:`DOWNLOAD_SLOTS`.
|
||||||
|
|
||||||
.. _wget: https://www.gnu.org/software/wget/manual/wget.html
|
.. _wget: https://www.gnu.org/software/wget/manual/wget.html
|
||||||
|
|
||||||
|
|
@ -1817,7 +1807,7 @@ The parser backend to use for parsing ``robots.txt`` files. For more information
|
||||||
.. setting:: ROBOTSTXT_USER_AGENT
|
.. setting:: ROBOTSTXT_USER_AGENT
|
||||||
|
|
||||||
ROBOTSTXT_USER_AGENT
|
ROBOTSTXT_USER_AGENT
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
--------------------
|
||||||
|
|
||||||
Default: ``None``
|
Default: ``None``
|
||||||
|
|
||||||
|
|
@ -1972,6 +1962,8 @@ Default:
|
||||||
|
|
||||||
{
|
{
|
||||||
"scrapy.contracts.default.UrlContract": 1,
|
"scrapy.contracts.default.UrlContract": 1,
|
||||||
|
"scrapy.contracts.default.CallbackKeywordArgumentsContract": 1,
|
||||||
|
"scrapy.contracts.default.MetadataContract": 1,
|
||||||
"scrapy.contracts.default.ReturnsContract": 2,
|
"scrapy.contracts.default.ReturnsContract": 2,
|
||||||
"scrapy.contracts.default.ScrapesContract": 3,
|
"scrapy.contracts.default.ScrapesContract": 3,
|
||||||
}
|
}
|
||||||
|
|
@ -2036,6 +2028,7 @@ Default:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
{
|
{
|
||||||
|
"scrapy.spidermiddlewares.start.StartSpiderMiddleware": 25,
|
||||||
"scrapy.spidermiddlewares.httperror.HttpErrorMiddleware": 50,
|
"scrapy.spidermiddlewares.httperror.HttpErrorMiddleware": 50,
|
||||||
"scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
|
"scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
|
||||||
"scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
|
"scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
|
||||||
|
|
@ -2111,6 +2104,25 @@ command.
|
||||||
The project name must not conflict with the name of custom files or directories
|
The project name must not conflict with the name of custom files or directories
|
||||||
in the ``project`` subdirectory.
|
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
|
.. setting:: TWISTED_REACTOR_ENABLED
|
||||||
|
|
||||||
TWISTED_REACTOR_ENABLED
|
TWISTED_REACTOR_ENABLED
|
||||||
|
|
@ -2149,6 +2161,9 @@ Default: ``"twisted.internet.asyncioreactor.AsyncioSelectorReactor"``
|
||||||
|
|
||||||
Import path of a given :mod:`~twisted.internet.reactor`.
|
Import path of a given :mod:`~twisted.internet.reactor`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
This setting has no effect when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
|
||||||
|
|
||||||
Scrapy will install this reactor if no other reactor is installed yet, such as
|
Scrapy will install this reactor if no other reactor is installed yet, such as
|
||||||
when the ``scrapy`` CLI program is invoked or when using the
|
when the ``scrapy`` CLI program is invoked or when using the
|
||||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` class or the
|
:class:`~scrapy.crawler.AsyncCrawlerProcess` class or the
|
||||||
|
|
@ -2185,7 +2200,7 @@ In order to use the reactor installed by Scrapy:
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.timeout = int(kwargs.pop("timeout", "60"))
|
self.timeout = int(kwargs.pop("timeout", "60"))
|
||||||
super(QuotesSpider, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
reactor.callLater(self.timeout, self.stop)
|
reactor.callLater(self.timeout, self.stop)
|
||||||
|
|
@ -2214,7 +2229,7 @@ which raises an exception, becomes:
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.timeout = int(kwargs.pop("timeout", "60"))
|
self.timeout = int(kwargs.pop("timeout", "60"))
|
||||||
super(QuotesSpider, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
@ -2252,7 +2267,7 @@ URLLENGTH_LIMIT
|
||||||
|
|
||||||
Default: ``2083``
|
Default: ``2083``
|
||||||
|
|
||||||
Scope: ``spidermiddlewares.urllength``
|
Scope: ``scrapy.spidermiddlewares.urllength``
|
||||||
|
|
||||||
The maximum URL length to allow for crawled URLs.
|
The maximum URL length to allow for crawled URLs.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,30 +17,35 @@ spider, without having to run the spider to test every change.
|
||||||
Once you get familiarized with the Scrapy shell, you'll see that it's an
|
Once you get familiarized with the Scrapy shell, you'll see that it's an
|
||||||
invaluable tool for developing and debugging your spiders.
|
invaluable tool for developing and debugging your spiders.
|
||||||
|
|
||||||
|
.. _shell-config:
|
||||||
|
|
||||||
Configuring the shell
|
Configuring the shell
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
If you have `IPython`_ installed, the Scrapy shell will use it (instead of the
|
With the :ref:`ptpython <extras>` extra, the Scrapy shell will use ptpython_
|
||||||
standard Python console). The `IPython`_ console is much more powerful and
|
instead of the :term:`REPL`. ptpython provides syntax highlighting, smart
|
||||||
provides smart auto-completion and colorized output, among other things.
|
auto-completion, and more.
|
||||||
|
|
||||||
We highly recommend you install `IPython`_, especially if you're working on
|
Failing that, with the :ref:`ipython <extras>` extra, the Scrapy shell will
|
||||||
Unix systems (where `IPython`_ excels). See the `IPython installation guide`_
|
use IPython_ instead. IPython provides smart auto-completion, colorized
|
||||||
for more info.
|
output, and more.
|
||||||
|
|
||||||
Scrapy also has support for `bpython`_, and will try to use it where `IPython`_
|
Scrapy also has support for `bpython`_ via the :ref:`bpython <extras>` extra,
|
||||||
is unavailable.
|
and will try to use it where neither ptpython nor IPython is available.
|
||||||
|
|
||||||
Through Scrapy's settings you can configure it to use any one of
|
Through Scrapy's settings you can configure it to use any one of
|
||||||
``ipython``, ``bpython`` or the standard ``python`` shell, regardless of which
|
``ptpython``, ``ipython``, ``bpython`` or the standard ``python`` shell,
|
||||||
are installed. This is done by setting the ``SCRAPY_PYTHON_SHELL`` environment
|
regardless of which are installed. This is done by setting the
|
||||||
variable; or by defining it in your :ref:`scrapy.cfg <topics-config-settings>`::
|
``SCRAPY_PYTHON_SHELL`` environment variable; or by defining it in your
|
||||||
|
:ref:`scrapy.cfg <topics-config-settings>`:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
[settings]
|
[settings]
|
||||||
shell = bpython
|
shell = bpython
|
||||||
|
|
||||||
|
.. _ptpython: https://github.com/prompt-toolkit/ptpython
|
||||||
.. _IPython: https://ipython.org/
|
.. _IPython: https://ipython.org/
|
||||||
.. _IPython installation guide: https://ipython.org/install/
|
|
||||||
.. _bpython: https://bpython-interpreter.org/
|
.. _bpython: https://bpython-interpreter.org/
|
||||||
|
|
||||||
Launch the shell
|
Launch the shell
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ Here is a simple example showing how you can catch signals and perform some acti
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler, *args, **kwargs):
|
def from_crawler(cls, crawler, *args, **kwargs):
|
||||||
spider = super(DmozSpider, cls).from_crawler(crawler, *args, **kwargs)
|
spider = super().from_crawler(crawler, *args, **kwargs)
|
||||||
crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
|
crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
|
||||||
return spider
|
return spider
|
||||||
|
|
||||||
|
|
@ -60,6 +60,8 @@ Let's take an example using :ref:`coroutines <topics-coroutines>`:
|
||||||
.. skip: next
|
.. skip: next
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import scrapy
|
import scrapy
|
||||||
import treq
|
import treq
|
||||||
|
|
||||||
|
|
@ -70,7 +72,7 @@ Let's take an example using :ref:`coroutines <topics-coroutines>`:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_crawler(cls, crawler, *args, **kwargs):
|
def from_crawler(cls, crawler, *args, **kwargs):
|
||||||
spider = super(SignalSpider, cls).from_crawler(crawler, *args, **kwargs)
|
spider = super().from_crawler(crawler, *args, **kwargs)
|
||||||
crawler.signals.connect(spider.item_scraped, signal=signals.item_scraped)
|
crawler.signals.connect(spider.item_scraped, signal=signals.item_scraped)
|
||||||
return spider
|
return spider
|
||||||
|
|
||||||
|
|
@ -452,7 +454,7 @@ bytes_received
|
||||||
.. signal:: bytes_received
|
.. signal:: bytes_received
|
||||||
.. function:: bytes_received(data, request, spider)
|
.. function:: bytes_received(data, request, spider)
|
||||||
|
|
||||||
Sent by the HTTP 1.1 and S3 download handlers when a group of bytes is
|
Sent by some download handlers when a group of bytes is
|
||||||
received for a specific request. This signal might be fired multiple
|
received for a specific request. This signal might be fired multiple
|
||||||
times for the same request, with partial data each time. For instance,
|
times for the same request, with partial data each time. For instance,
|
||||||
a possible scenario for a 25 kb response would be two signals fired
|
a possible scenario for a 25 kb response would be two signals fired
|
||||||
|
|
@ -480,7 +482,7 @@ headers_received
|
||||||
.. signal:: headers_received
|
.. signal:: headers_received
|
||||||
.. function:: headers_received(headers, body_length, request, spider)
|
.. function:: headers_received(headers, body_length, request, spider)
|
||||||
|
|
||||||
Sent by the HTTP 1.1 and S3 download handlers when the response headers are
|
Sent by some download handlers when the response headers are
|
||||||
available for a given request, before downloading any additional content.
|
available for a given request, before downloading any additional content.
|
||||||
|
|
||||||
Handlers for this signal can stop the download of a response while it
|
Handlers for this signal can stop the download of a response while it
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ previous (or subsequent) middleware being applied.
|
||||||
If you want to disable a builtin middleware (the ones defined in
|
If you want to disable a builtin middleware (the ones defined in
|
||||||
:setting:`SPIDER_MIDDLEWARES_BASE`, and enabled by default) you must define it
|
:setting:`SPIDER_MIDDLEWARES_BASE`, and enabled by default) you must define it
|
||||||
in your project :setting:`SPIDER_MIDDLEWARES` setting and assign ``None`` as its
|
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
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
@ -316,6 +316,35 @@ Default: ``False``
|
||||||
Pass all responses, regardless of its status code.
|
Pass all responses, regardless of its status code.
|
||||||
|
|
||||||
|
|
||||||
|
MetaCopyDetectionMiddleware
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. module:: scrapy.spidermiddlewares.metacopy
|
||||||
|
:synopsis: Meta Copy Detection Spider Middleware
|
||||||
|
|
||||||
|
.. class:: MetaCopyDetectionMiddleware
|
||||||
|
|
||||||
|
Warns when a spider yields a request that contains internal meta keys which
|
||||||
|
should not be copied from :attr:`response.meta <scrapy.http.Response.meta>`
|
||||||
|
into new requests. See :attr:`~scrapy.http.Request.meta` to learn why.
|
||||||
|
|
||||||
|
Only 1 warning is emitted per crawl.
|
||||||
|
|
||||||
|
MetaCopyDetectionMiddleware settings
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. setting:: META_COPY_WARN_SKIP_KEYS
|
||||||
|
|
||||||
|
META_COPY_WARN_SKIP_KEYS
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Default: ``[]``
|
||||||
|
|
||||||
|
A list of internal meta key names to exclude from the internal-keys check.
|
||||||
|
Use this when you intentionally copy one of the monitored keys and want to
|
||||||
|
suppress the resulting warning without disabling the middleware entirely.
|
||||||
|
|
||||||
|
|
||||||
RefererMiddleware
|
RefererMiddleware
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,12 +208,6 @@ scrapy.Spider
|
||||||
:param response: the response to parse
|
:param response: the response to parse
|
||||||
:type response: :class:`~scrapy.http.Response`
|
:type response: :class:`~scrapy.http.Response`
|
||||||
|
|
||||||
.. method:: log(message, [level, component])
|
|
||||||
|
|
||||||
Wrapper that sends a log message through the Spider's :attr:`logger`,
|
|
||||||
kept for backward compatibility. For more information see
|
|
||||||
:ref:`topics-logging-from-spiders`.
|
|
||||||
|
|
||||||
.. method:: closed(reason)
|
.. method:: closed(reason)
|
||||||
|
|
||||||
Called when the spider closes. This method provides a shortcut to
|
Called when the spider closes. This method provides a shortcut to
|
||||||
|
|
@ -314,7 +308,7 @@ Spiders can access arguments in their `__init__` methods:
|
||||||
name = "myspider"
|
name = "myspider"
|
||||||
|
|
||||||
def __init__(self, category=None, *args, **kwargs):
|
def __init__(self, category=None, *args, **kwargs):
|
||||||
super(MySpider, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.start_urls = [f"http://www.example.com/categories/{category}"]
|
self.start_urls = [f"http://www.example.com/categories/{category}"]
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
|
@ -335,8 +329,8 @@ The above example can also be written as follows:
|
||||||
|
|
||||||
If you are :ref:`running Scrapy from a script <run-from-script>`, you can
|
If you are :ref:`running Scrapy from a script <run-from-script>`, you can
|
||||||
specify spider arguments when calling
|
specify spider arguments when calling
|
||||||
:class:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
|
:meth:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
|
||||||
:class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
|
:meth:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
|
||||||
|
|
||||||
.. skip: next
|
.. skip: next
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
@ -593,7 +587,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
|
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
|
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
|
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
|
XMLFeedSpider
|
||||||
-------------
|
-------------
|
||||||
|
|
@ -614,7 +608,7 @@ XMLFeedSpider
|
||||||
|
|
||||||
A string which defines the iterator to use. It can be either:
|
A string which defines the iterator to use. It can be either:
|
||||||
|
|
||||||
- ``'iternodes'`` - a fast iterator based on regular expressions
|
- ``'iternodes'`` - a fast iterator based on ``lxml``
|
||||||
|
|
||||||
- ``'html'`` - an iterator which uses :class:`~scrapy.Selector`.
|
- ``'html'`` - an iterator which uses :class:`~scrapy.Selector`.
|
||||||
Keep in mind this uses DOM parsing and must load all DOM in memory
|
Keep in mind this uses DOM parsing and must load all DOM in memory
|
||||||
|
|
@ -628,9 +622,11 @@ XMLFeedSpider
|
||||||
|
|
||||||
.. attribute:: itertag
|
.. attribute:: itertag
|
||||||
|
|
||||||
A string with the name of the node (or element) to iterate in. Example::
|
A string with the name of the node (or element) to iterate in. Example:
|
||||||
|
|
||||||
itertag = 'product'
|
.. code-block:: python
|
||||||
|
|
||||||
|
itertag = "product"
|
||||||
|
|
||||||
.. attribute:: namespaces
|
.. attribute:: namespaces
|
||||||
|
|
||||||
|
|
@ -643,12 +639,17 @@ XMLFeedSpider
|
||||||
You can then specify nodes with namespaces in the :attr:`itertag`
|
You can then specify nodes with namespaces in the :attr:`itertag`
|
||||||
attribute.
|
attribute.
|
||||||
|
|
||||||
Example::
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from scrapy.spiders import XMLFeedSpider
|
||||||
|
|
||||||
|
|
||||||
class YourSpider(XMLFeedSpider):
|
class YourSpider(XMLFeedSpider):
|
||||||
|
|
||||||
namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]
|
namespaces = [("n", "http://www.sitemaps.org/schemas/sitemap/0.9")]
|
||||||
itertag = 'n:url'
|
itertag = "n:url"
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
Apart from these new attributes, this spider has the following overridable
|
Apart from these new attributes, this spider has the following overridable
|
||||||
|
|
@ -808,9 +809,11 @@ SitemapSpider
|
||||||
the regular expression. ``callback`` can be a string (indicating the
|
the regular expression. ``callback`` can be a string (indicating the
|
||||||
name of a spider method) or a callable.
|
name of a spider method) or a callable.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
sitemap_rules = [('/product/', 'parse_product')]
|
.. code-block:: python
|
||||||
|
|
||||||
|
sitemap_rules = [("/product/", "parse_product")]
|
||||||
|
|
||||||
Rules are applied in order, and only the first one that matches will be
|
Rules are applied in order, and only the first one that matches will be
|
||||||
used.
|
used.
|
||||||
|
|
@ -832,7 +835,9 @@ SitemapSpider
|
||||||
are links for the same website in another language passed within
|
are links for the same website in another language passed within
|
||||||
the same ``url`` block.
|
the same ``url`` block.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>http://example.com/</loc>
|
<loc>http://example.com/</loc>
|
||||||
|
|
@ -850,7 +855,9 @@ SitemapSpider
|
||||||
This is a filter function that could be overridden to select sitemap entries
|
This is a filter function that could be overridden to select sitemap entries
|
||||||
based on their attributes.
|
based on their attributes.
|
||||||
|
|
||||||
For example::
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>http://example.com/</loc>
|
<loc>http://example.com/</loc>
|
||||||
|
|
@ -953,6 +960,7 @@ Combine SitemapSpider with other sources of urls:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
from scrapy import Request
|
||||||
from scrapy.spiders import SitemapSpider
|
from scrapy.spiders import SitemapSpider
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ Collector, and can be accessed through the :attr:`~scrapy.crawler.Crawler.stats`
|
||||||
attribute of the :ref:`topics-api-crawler`, as illustrated by the examples in
|
attribute of the :ref:`topics-api-crawler`, as illustrated by the examples in
|
||||||
the :ref:`topics-stats-usecases` section below.
|
the :ref:`topics-stats-usecases` section below.
|
||||||
|
|
||||||
However, the Stats Collector is always available, so you can always import it
|
The Stats Collector API is always available, so you can always use it (to
|
||||||
in your module and use its API (to increment or set new stat keys), regardless
|
increment or set new stat keys), regardless
|
||||||
of whether the stats collection is enabled or not. If it's disabled, the API
|
of whether the stats collection is enabled or not. If it's disabled, the API
|
||||||
will still work but it won't collect anything. This is aimed at simplifying the
|
will still work but it won't collect anything. This is aimed at simplifying the
|
||||||
stats collector usage: you should spend no more than one line of code for
|
stats collector usage: you should spend no more than one line of code for
|
||||||
|
|
@ -21,9 +21,6 @@ using the Stats Collector from.
|
||||||
Another feature of the Stats Collector is that it's very efficient (when
|
Another feature of the Stats Collector is that it's very efficient (when
|
||||||
enabled) and extremely efficient (almost unnoticeable) when disabled.
|
enabled) and extremely efficient (almost unnoticeable) when disabled.
|
||||||
|
|
||||||
The Stats Collector keeps a stats table per open spider which is automatically
|
|
||||||
opened when the spider is opened, and closed when the spider is closed.
|
|
||||||
|
|
||||||
.. _topics-stats-usecases:
|
.. _topics-stats-usecases:
|
||||||
|
|
||||||
Common Stats Collector uses
|
Common Stats Collector uses
|
||||||
|
|
@ -87,37 +84,20 @@ Get all stats:
|
||||||
Available Stats Collectors
|
Available Stats Collectors
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
|
.. currentmodule:: scrapy.statscollectors
|
||||||
|
|
||||||
Besides the basic :class:`StatsCollector` there are other Stats Collectors
|
Besides the basic :class:`StatsCollector` there are other Stats Collectors
|
||||||
available in Scrapy which extend the basic Stats Collector. You can select
|
available in Scrapy which extend the basic Stats Collector. You can select
|
||||||
which Stats Collector to use through the :setting:`STATS_CLASS` setting. The
|
which Stats Collector to use through the :setting:`STATS_CLASS` setting. The
|
||||||
default Stats Collector used is the :class:`MemoryStatsCollector`.
|
default Stats Collector used is the :class:`MemoryStatsCollector`.
|
||||||
|
|
||||||
.. currentmodule:: scrapy.statscollectors
|
|
||||||
|
|
||||||
MemoryStatsCollector
|
MemoryStatsCollector
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
.. class:: MemoryStatsCollector
|
.. autoclass:: MemoryStatsCollector
|
||||||
|
:members:
|
||||||
A simple stats collector that keeps the stats of the last scraping run (for
|
|
||||||
each spider) in memory, after they're closed. The stats can be accessed
|
|
||||||
through the :attr:`spider_stats` attribute, which is a dict keyed by spider
|
|
||||||
domain name.
|
|
||||||
|
|
||||||
This is the default Stats Collector used in Scrapy.
|
|
||||||
|
|
||||||
.. attribute:: spider_stats
|
|
||||||
|
|
||||||
A dict of dicts (keyed by spider name) containing the stats of the last
|
|
||||||
scraping run for each spider.
|
|
||||||
|
|
||||||
DummyStatsCollector
|
DummyStatsCollector
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
.. class:: DummyStatsCollector
|
.. autoclass:: DummyStatsCollector
|
||||||
|
|
||||||
A Stats collector which does nothing but is very efficient (because it does
|
|
||||||
nothing). This stats collector can be set via the :setting:`STATS_CLASS`
|
|
||||||
setting, to disable stats collect in order to improve performance. However,
|
|
||||||
the performance penalty of stats collection is usually marginal compared to
|
|
||||||
other Scrapy workload like parsing pages.
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ disable it if you want. For more information about the extension itself see
|
||||||
How to access the telnet console
|
How to access the telnet console
|
||||||
================================
|
================================
|
||||||
|
|
||||||
The telnet console listens in the TCP port defined in the
|
The telnet console listens on the first available TCP port from the range
|
||||||
:setting:`TELNETCONSOLE_PORT` setting, which defaults to ``6023``. To access
|
defined in the :setting:`TELNETCONSOLE_PORT` setting, which defaults to
|
||||||
the console you need to type::
|
``[6023, 6073]``. To access the console you need to type::
|
||||||
|
|
||||||
telnet localhost 6023
|
telnet localhost 6023
|
||||||
Trying localhost...
|
Trying localhost...
|
||||||
|
|
@ -107,8 +107,8 @@ Here are some example tasks you can do with the telnet console:
|
||||||
View engine status
|
View engine status
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
You can use the ``est()`` method of the Scrapy engine to quickly show its state
|
You can use the ``est()`` method provided by the console to quickly show the
|
||||||
using the telnet console::
|
engine status::
|
||||||
|
|
||||||
telnet localhost 6023
|
telnet localhost 6023
|
||||||
>>> est()
|
>>> est()
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ API stability
|
||||||
|
|
||||||
API stability was one of the major goals for the *1.0* release.
|
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
|
Methods or functions that start with a single underscore (``_``) are private
|
||||||
should never be relied as stable.
|
and should never be relied upon as stable.
|
||||||
|
|
||||||
Also, keep in mind that stable doesn't mean complete: stable APIs could grow
|
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
|
new methods or functionality but the existing methods should keep working the
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,25 @@ Source = "https://github.com/scrapy/scrapy"
|
||||||
Tracker = "https://github.com/scrapy/scrapy/issues"
|
Tracker = "https://github.com/scrapy/scrapy/issues"
|
||||||
"Release notes" = "https://docs.scrapy.org/en/latest/news.html"
|
"Release notes" = "https://docs.scrapy.org/en/latest/news.html"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
bpython = ["bpython>=0.7.1"]
|
||||||
|
brotli = [
|
||||||
|
"brotli>=1.2.0; implementation_name != 'pypy'",
|
||||||
|
"brotlicffi>=1.2.0.0; implementation_name == 'pypy'",
|
||||||
|
]
|
||||||
|
gcs = ["google-cloud-storage>=1.29.0"]
|
||||||
|
httpx = ["httpx2[http2,socks]>=2.0.0"]
|
||||||
|
images = ["Pillow>=8.3.2"]
|
||||||
|
ipython = ["ipython>=7.1.0"]
|
||||||
|
ptpython = ["ptpython>=2.0.1"]
|
||||||
|
robotparser = ["robotexclusionrulesparser>=1.6.2"]
|
||||||
|
s3 = ["boto3>=1.20.0"]
|
||||||
|
twisted-http2 = ["Twisted[http2]>=21.7.0"]
|
||||||
|
uvloop = [
|
||||||
|
"uvloop>=0.16.0; platform_system != 'Windows' and implementation_name != 'pypy'",
|
||||||
|
]
|
||||||
|
zstd = ["zstandard>=0.16.0; implementation_name != 'pypy'"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
scrapy = "scrapy.cmdline:execute"
|
scrapy = "scrapy.cmdline:execute"
|
||||||
|
|
||||||
|
|
@ -94,7 +113,75 @@ untyped_calls_exclude = [
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "tests.*"
|
module = "tests.*"
|
||||||
allow_untyped_defs = true
|
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_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_linkextractors",
|
||||||
|
"tests.test_loader",
|
||||||
|
"tests.test_logformatter",
|
||||||
|
"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.utils.bases.http_request",
|
||||||
|
"tests.utils.bases.http_response",
|
||||||
|
"tests.utils.bases.spider",
|
||||||
|
]
|
||||||
check_untyped_defs = false
|
check_untyped_defs = false
|
||||||
|
|
||||||
# Interface classes are hard to support
|
# Interface classes are hard to support
|
||||||
|
|
@ -131,7 +218,6 @@ module = [
|
||||||
"pyftpdlib.*",
|
"pyftpdlib.*",
|
||||||
"pytest_twisted",
|
"pytest_twisted",
|
||||||
"robotexclusionrulesparser",
|
"robotexclusionrulesparser",
|
||||||
"testfixtures",
|
|
||||||
"zope.interface.*",
|
"zope.interface.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
@ -269,7 +355,7 @@ markers = [
|
||||||
"requires_uvloop: marks tests as only enabled when uvloop is known to be working",
|
"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_botocore: marks tests that need botocore (but not boto3)",
|
||||||
"requires_boto3: marks tests that need botocore and 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",
|
"requires_internet: marks tests that need real Internet access",
|
||||||
]
|
]
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
|
|
|
||||||
|
|
@ -225,13 +225,11 @@ def _run_command(cmd: ScrapyCommand, args: list[str], opts: argparse.Namespace)
|
||||||
def _run_command_profiled(
|
def _run_command_profiled(
|
||||||
cmd: ScrapyCommand, args: list[str], opts: argparse.Namespace
|
cmd: ScrapyCommand, args: list[str], opts: argparse.Namespace
|
||||||
) -> None:
|
) -> None:
|
||||||
if opts.profile:
|
sys.stderr.write(f"scrapy: writing cProfile stats to {opts.profile!r}\n")
|
||||||
sys.stderr.write(f"scrapy: writing cProfile stats to {opts.profile!r}\n")
|
|
||||||
loc = locals()
|
loc = locals()
|
||||||
p = cProfile.Profile()
|
p = cProfile.Profile()
|
||||||
p.runctx("cmd.run(args, opts)", globals(), loc)
|
p.runctx("cmd.run(args, opts)", globals(), loc)
|
||||||
if opts.profile:
|
p.dump_stats(opts.profile)
|
||||||
p.dump_stats(opts.profile)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,8 @@ class Command(BaseRunSpiderCommand):
|
||||||
self.first_response = response
|
self.first_response = response
|
||||||
|
|
||||||
cb = self._get_callback(spider=spider, opts=opts, response=response)
|
cb = self._get_callback(spider=spider, opts=opts, response=response)
|
||||||
|
assert response.request
|
||||||
|
response.request.callback = cb
|
||||||
|
|
||||||
# parse items and requests
|
# parse items and requests
|
||||||
depth: int = response.meta["_depth"]
|
depth: int = response.meta["_depth"]
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,16 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class Contract:
|
class Contract:
|
||||||
"""Abstract class for contracts"""
|
"""Base class for :ref:`custom contracts <topics-contracts>`.
|
||||||
|
|
||||||
|
*method* is the callback function to which the contract is associated.
|
||||||
|
|
||||||
|
*args* is the list of arguments passed into the docstring, separated by
|
||||||
|
whitespace.
|
||||||
|
|
||||||
|
Subclasses may override :meth:`adjust_request_args`, and define a
|
||||||
|
``pre_process`` method or a ``post_process`` method, or both.
|
||||||
|
"""
|
||||||
|
|
||||||
request_cls: type[Request] | None = None
|
request_cls: type[Request] | None = None
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -90,6 +99,13 @@ class Contract:
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def adjust_request_args(self, args: dict[str, Any]) -> dict[str, Any]:
|
def adjust_request_args(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Receive a ``dict`` with the default arguments for the sample request
|
||||||
|
and return it, either unmodified or with changes.
|
||||||
|
|
||||||
|
:class:`~scrapy.Request` is used by default, but this can be changed
|
||||||
|
with the ``request_cls`` attribute. If multiple contracts in the chain
|
||||||
|
define this attribute, the last one is used.
|
||||||
|
"""
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,15 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
# contracts
|
# contracts
|
||||||
class UrlContract(Contract):
|
class UrlContract(Contract):
|
||||||
"""Contract to set the url of the request (mandatory)
|
"""Sets (``@url``) the sample URL used when checking the other contract
|
||||||
@url http://scrapy.org
|
conditions of a callback.
|
||||||
|
|
||||||
|
This contract is mandatory: callbacks lacking it are ignored when running
|
||||||
|
the checks.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
@url url
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "url"
|
name = "url"
|
||||||
|
|
@ -27,10 +34,14 @@ class UrlContract(Contract):
|
||||||
|
|
||||||
|
|
||||||
class CallbackKeywordArgumentsContract(Contract):
|
class CallbackKeywordArgumentsContract(Contract):
|
||||||
"""Contract to set the keyword arguments for the request.
|
"""Sets (``@cb_kwargs``) the :attr:`cb_kwargs <scrapy.Request.cb_kwargs>`
|
||||||
The value should be a JSON-encoded dictionary, e.g.:
|
attribute of the sample request.
|
||||||
|
|
||||||
@cb_kwargs {"arg1": "some value"}
|
Its value must be a valid JSON dictionary.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
@cb_kwargs {"arg1": "value1", "arg2": "value2", ...}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "cb_kwargs"
|
name = "cb_kwargs"
|
||||||
|
|
@ -41,10 +52,14 @@ class CallbackKeywordArgumentsContract(Contract):
|
||||||
|
|
||||||
|
|
||||||
class MetadataContract(Contract):
|
class MetadataContract(Contract):
|
||||||
"""Contract to set metadata arguments for the request.
|
"""Sets (``@meta``) the :attr:`meta <scrapy.Request.meta>` attribute of the
|
||||||
The value should be JSON-encoded dictionary, e.g.:
|
sample request.
|
||||||
|
|
||||||
@meta {"arg1": "some value"}
|
Its value must be a valid JSON dictionary.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
@meta {"arg1": "value1", "arg2": "value2", ...}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "meta"
|
name = "meta"
|
||||||
|
|
@ -55,16 +70,29 @@ class MetadataContract(Contract):
|
||||||
|
|
||||||
|
|
||||||
class ReturnsContract(Contract):
|
class ReturnsContract(Contract):
|
||||||
"""Contract to check the output of a callback
|
"""Sets (``@returns``) lower and upper bounds for the items and requests
|
||||||
|
returned by a callback.
|
||||||
|
|
||||||
general form:
|
The upper bound is optional:
|
||||||
@returns request(s)/item(s) [min=1 [max]]
|
|
||||||
|
|
||||||
e.g.:
|
.. code-block:: none
|
||||||
@returns request
|
|
||||||
@returns request 2
|
@returns item(s)|request(s) [min [max]]
|
||||||
@returns request 2 10
|
|
||||||
@returns request 0 10
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
@returns request
|
||||||
|
@returns request 2
|
||||||
|
@returns request 2 10
|
||||||
|
@returns request 0 10
|
||||||
|
|
||||||
|
Set both bounds to the same value to require an exact number:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
@returns request 2 2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "returns"
|
name = "returns"
|
||||||
|
|
@ -115,8 +143,12 @@ class ReturnsContract(Contract):
|
||||||
|
|
||||||
|
|
||||||
class ScrapesContract(Contract):
|
class ScrapesContract(Contract):
|
||||||
"""Contract to check presence of fields in scraped items
|
"""Checks (``@scrapes``) that all items returned by a callback have the
|
||||||
@scrapes page_name page_body
|
specified fields.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
@scrapes field_1 field_2 ...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "scrapes"
|
name = "scrapes"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ from twisted.internet.ssl import (
|
||||||
from twisted.web.client import BrowserLikePolicyForHTTPS
|
from twisted.web.client import BrowserLikePolicyForHTTPS
|
||||||
from twisted.web.iweb import IPolicyForHTTPS
|
from twisted.web.iweb import IPolicyForHTTPS
|
||||||
from zope.interface.declarations import implementer
|
from zope.interface.declarations import implementer
|
||||||
from zope.interface.verify import verifyObject
|
|
||||||
|
|
||||||
from scrapy.core.downloader.tls import (
|
from scrapy.core.downloader.tls import (
|
||||||
_TWISTED_VERSION_MAP,
|
_TWISTED_VERSION_MAP,
|
||||||
|
|
@ -232,7 +231,6 @@ class _AcceptableProtocolsContextFactory:
|
||||||
# all of this with _ScrapyClientContextFactory.acceptableProtocols.
|
# all of this with _ScrapyClientContextFactory.acceptableProtocols.
|
||||||
|
|
||||||
def __init__(self, context_factory: Any, acceptable_protocols: list[bytes]):
|
def __init__(self, context_factory: Any, acceptable_protocols: list[bytes]):
|
||||||
verifyObject(IPolicyForHTTPS, context_factory)
|
|
||||||
self._wrapped_context_factory: Any = context_factory
|
self._wrapped_context_factory: Any = context_factory
|
||||||
self._acceptable_protocols: list[bytes] = acceptable_protocols
|
self._acceptable_protocols: list[bytes] = acceptable_protocols
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ from ._base_streaming import BaseStreamingDownloadHandler, _BaseResponseArgs
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
from httpcore import AsyncNetworkStream
|
from httpcore2 import AsyncNetworkStream
|
||||||
|
|
||||||
from scrapy import Request
|
from scrapy import Request
|
||||||
from scrapy.crawler import Crawler
|
from scrapy.crawler import Crawler
|
||||||
|
|
@ -39,8 +39,11 @@ if TYPE_CHECKING:
|
||||||
HAS_SOCKS = HAS_HTTP2 = False
|
HAS_SOCKS = HAS_HTTP2 = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import httpx
|
try:
|
||||||
except ImportError:
|
import httpx2 as httpx
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
import httpx # type: ignore[import-not-found,no-redef]
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
httpx = None # type: ignore[assignment]
|
httpx = None # type: ignore[assignment]
|
||||||
else:
|
else:
|
||||||
# a small hack to avoid importing these optional extras unconditionally
|
# a small hack to avoid importing these optional extras unconditionally
|
||||||
|
|
@ -84,7 +87,7 @@ class HttpxDownloadHandler(_Base):
|
||||||
self._enable_h2: bool = crawler.settings.getbool("HTTPX_HTTP2_ENABLED")
|
self._enable_h2: bool = crawler.settings.getbool("HTTPX_HTTP2_ENABLED")
|
||||||
if self._enable_h2 and not HAS_HTTP2: # pragma: no cover
|
if self._enable_h2 and not HAS_HTTP2: # pragma: no cover
|
||||||
raise NotConfigured(
|
raise NotConfigured(
|
||||||
f"HTTP/2 support in {type(self).__name__} requires the 'httpx[http2]' extra to be installed."
|
f"HTTP/2 support in {type(self).__name__} requires the 'httpx2[http2]' extra to be installed."
|
||||||
)
|
)
|
||||||
self._ssl_context: ssl.SSLContext = _make_ssl_context(crawler.settings)
|
self._ssl_context: ssl.SSLContext = _make_ssl_context(crawler.settings)
|
||||||
self._bind_host: str | None = self._get_bind_address_host()
|
self._bind_host: str | None = self._get_bind_address_host()
|
||||||
|
|
@ -96,7 +99,7 @@ class HttpxDownloadHandler(_Base):
|
||||||
)
|
)
|
||||||
|
|
||||||
self._default_client: httpx.AsyncClient = self._make_client()
|
self._default_client: httpx.AsyncClient = self._make_client()
|
||||||
# httpx doesn't support per-request proxies: https://github.com/encode/httpx/discussions/3183,
|
# httpx2 doesn't support per-request proxies: https://github.com/pydantic/httpx2/issues/818,
|
||||||
# so we keep a pool of clients per proxy URL. LRU eviction can be added here if needed.
|
# so we keep a pool of clients per proxy URL. LRU eviction can be added here if needed.
|
||||||
self._proxy_clients: dict[str, httpx.AsyncClient] = {}
|
self._proxy_clients: dict[str, httpx.AsyncClient] = {}
|
||||||
|
|
||||||
|
|
@ -104,7 +107,7 @@ class HttpxDownloadHandler(_Base):
|
||||||
def _check_deps_installed() -> None:
|
def _check_deps_installed() -> None:
|
||||||
if httpx is None: # pragma: no cover
|
if httpx is None: # pragma: no cover
|
||||||
raise NotConfigured(
|
raise NotConfigured(
|
||||||
"HttpxDownloadHandler requires the httpx library to be installed."
|
"HttpxDownloadHandler requires the httpx2 library to be installed."
|
||||||
)
|
)
|
||||||
|
|
||||||
def _make_client(self, proxy_url: str | None = None) -> httpx.AsyncClient:
|
def _make_client(self, proxy_url: str | None = None) -> httpx.AsyncClient:
|
||||||
|
|
@ -128,7 +131,7 @@ class HttpxDownloadHandler(_Base):
|
||||||
proxy=proxy,
|
proxy=proxy,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# https://github.com/encode/httpx/discussions/1566
|
# https://github.com/pydantic/httpx2/issues/368
|
||||||
for header_name in ("accept", "accept-encoding", "user-agent"):
|
for header_name in ("accept", "accept-encoding", "user-agent"):
|
||||||
client.headers.pop(header_name, None)
|
client.headers.pop(header_name, None)
|
||||||
return client
|
return client
|
||||||
|
|
@ -149,7 +152,7 @@ class HttpxDownloadHandler(_Base):
|
||||||
proxy = self._extract_proxy_url_with_creds(request)
|
proxy = self._extract_proxy_url_with_creds(request)
|
||||||
if proxy and proxy.startswith("socks") and not HAS_SOCKS: # pragma: no cover
|
if proxy and proxy.startswith("socks") and not HAS_SOCKS: # pragma: no cover
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"SOCKS proxy support in {type(self).__name__} requires the 'httpx[socks]' extra to be installed."
|
f"SOCKS proxy support in {type(self).__name__} requires the 'httpx2[socks]' extra to be installed."
|
||||||
)
|
)
|
||||||
client = self._get_client(proxy)
|
client = self._get_client(proxy)
|
||||||
headers = self._request_headers(request).to_tuple_list()
|
headers = self._request_headers(request).to_tuple_list()
|
||||||
|
|
@ -175,7 +178,7 @@ class HttpxDownloadHandler(_Base):
|
||||||
raise DownloadConnectionRefusedError(str(e)) from e
|
raise DownloadConnectionRefusedError(str(e)) from e
|
||||||
except httpx.ProxyError as e:
|
except httpx.ProxyError as e:
|
||||||
raise DownloadConnectionRefusedError(str(e)) from e
|
raise DownloadConnectionRefusedError(str(e)) from e
|
||||||
except DOWNLOAD_FAILED_EXCEPTIONS as e:
|
except DOWNLOAD_FAILED_EXCEPTIONS as e: # pylint: disable=catching-non-exception
|
||||||
raise DownloadFailedError(str(e)) from e
|
raise DownloadFailedError(str(e)) from e
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -218,7 +221,7 @@ class HttpxDownloadHandler(_Base):
|
||||||
def _log_tls_info(self, response: httpx.Response, request: Request) -> None:
|
def _log_tls_info(self, response: httpx.Response, request: Request) -> None:
|
||||||
network_stream: AsyncNetworkStream = response.extensions["network_stream"]
|
network_stream: AsyncNetworkStream = response.extensions["network_stream"]
|
||||||
extra_ssl_object = network_stream.get_extra_info("ssl_object")
|
extra_ssl_object = network_stream.get_extra_info("ssl_object")
|
||||||
if isinstance(extra_ssl_object, ssl.SSLObject):
|
if isinstance(extra_ssl_object, ssl.SSLObject): # pragma: no branch
|
||||||
_log_sslobj_debug_info(extra_ssl_object)
|
_log_sslobj_debug_info(extra_ssl_object)
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -126,5 +126,4 @@ class FTPDownloadHandler(BaseDownloadHandler):
|
||||||
headers = {"local filename": protocol.filename or b"", "size": protocol.size}
|
headers = {"local filename": protocol.filename or b"", "size": protocol.size}
|
||||||
body = protocol.filename or protocol.body.read()
|
body = protocol.filename or protocol.body.read()
|
||||||
respcls = responsetypes.from_args(url=request.url, body=body)
|
respcls = responsetypes.from_args(url=request.url, body=body)
|
||||||
# hints for Headers-related types may need to be fixed to not use AnyStr
|
return respcls(url=request.url, status=200, body=body, headers=headers)
|
||||||
return respcls(url=request.url, status=200, body=body, headers=headers) # type: ignore[arg-type]
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
from scrapy.core.downloader.handlers.base import BaseDownloadHandler
|
from scrapy.core.downloader.handlers.base import BaseDownloadHandler
|
||||||
from scrapy.exceptions import NotConfigured
|
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
|
||||||
from scrapy.utils.boto import is_botocore_available
|
from scrapy.utils.boto import is_botocore_available
|
||||||
from scrapy.utils.httpobj import urlparse_cached
|
from scrapy.utils.httpobj import urlparse_cached
|
||||||
from scrapy.utils.misc import build_from_crawler, load_object
|
from scrapy.utils.misc import build_from_crawler, load_object
|
||||||
|
|
@ -49,7 +50,16 @@ class S3DownloadHandler(BaseDownloadHandler):
|
||||||
|
|
||||||
async def download_request(self, request: Request) -> Response:
|
async def download_request(self, request: Request) -> Response:
|
||||||
p = urlparse_cached(request)
|
p = urlparse_cached(request)
|
||||||
scheme = "http" if request.meta.get("is_secure") is False else "https"
|
if request.meta.get("is_secure") is False:
|
||||||
|
warnings.warn(
|
||||||
|
"Passing is_secure=False for s3:// requests is deprecated."
|
||||||
|
" In future Scrapy releases this flag will be ignored.",
|
||||||
|
ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
scheme = "http"
|
||||||
|
else:
|
||||||
|
scheme = "https"
|
||||||
bucket = p.hostname
|
bucket = p.hostname
|
||||||
path = p.path + "?" + p.query if p.query else p.path
|
path = p.path + "?" + p.query if p.query else p.path
|
||||||
url = f"{scheme}://{bucket}.s3.amazonaws.com{path}"
|
url = f"{scheme}://{bucket}.s3.amazonaws.com{path}"
|
||||||
|
|
|
||||||
|
|
@ -470,16 +470,17 @@ class ExecutionEngine:
|
||||||
"""
|
"""
|
||||||
if self.spider is None:
|
if self.spider is None:
|
||||||
raise RuntimeError(f"No open spider to crawl: {request}")
|
raise RuntimeError(f"No open spider to crawl: {request}")
|
||||||
try:
|
while True:
|
||||||
response_or_request = await maybe_deferred_to_future(
|
try:
|
||||||
self._download(request)
|
response_or_request = await maybe_deferred_to_future(
|
||||||
)
|
self._download(request)
|
||||||
finally:
|
)
|
||||||
assert self._slot is not None
|
finally:
|
||||||
self._slot.remove_request(request)
|
assert self._slot is not None
|
||||||
if isinstance(response_or_request, Request):
|
self._slot.remove_request(request)
|
||||||
return await self.download_async(response_or_request)
|
if not isinstance(response_or_request, Request):
|
||||||
return response_or_request
|
return response_or_request
|
||||||
|
request = response_or_request
|
||||||
|
|
||||||
@inlineCallbacks
|
@inlineCallbacks
|
||||||
def _download(
|
def _download(
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,7 @@ class Stream:
|
||||||
0, self.metadata["remaining_content_length"]
|
0, self.metadata["remaining_content_length"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# End the stream if no more data needs to be send
|
# End the stream if no more data needs to be sent
|
||||||
if self.metadata["remaining_content_length"] == 0:
|
if self.metadata["remaining_content_length"] == 0:
|
||||||
self._protocol.conn.end_stream(self.stream_id)
|
self._protocol.conn.end_stream(self.stream_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,11 +289,11 @@ class Scheduler(BaseScheduler):
|
||||||
|
|
||||||
:param dqclass: A class to be used as persistent request queue.
|
:param dqclass: A class to be used as persistent request queue.
|
||||||
The value for the :setting:`SCHEDULER_DISK_QUEUE` setting is used by default.
|
The value for the :setting:`SCHEDULER_DISK_QUEUE` setting is used by default.
|
||||||
:type dqclass: class
|
:type dqclass: type
|
||||||
|
|
||||||
:param mqclass: A class to be used as non-persistent request queue.
|
:param mqclass: A class to be used as non-persistent request queue.
|
||||||
The value for the :setting:`SCHEDULER_MEMORY_QUEUE` setting is used by default.
|
The value for the :setting:`SCHEDULER_MEMORY_QUEUE` setting is used by default.
|
||||||
:type mqclass: class
|
:type mqclass: type
|
||||||
|
|
||||||
:param logunser: A boolean that indicates whether or not unserializable requests should be logged.
|
:param logunser: A boolean that indicates whether or not unserializable requests should be logged.
|
||||||
The value for the :setting:`SCHEDULER_DEBUG` setting is used by default.
|
The value for the :setting:`SCHEDULER_DEBUG` setting is used by default.
|
||||||
|
|
@ -306,7 +306,7 @@ class Scheduler(BaseScheduler):
|
||||||
|
|
||||||
:param pqclass: A class to be used as priority queue for requests.
|
:param pqclass: A class to be used as priority queue for requests.
|
||||||
The value for the :setting:`SCHEDULER_PRIORITY_QUEUE` setting is used by default.
|
The value for the :setting:`SCHEDULER_PRIORITY_QUEUE` setting is used by default.
|
||||||
:type pqclass: class
|
:type pqclass: type
|
||||||
|
|
||||||
:param crawler: The crawler object corresponding to the current crawl.
|
:param crawler: The crawler object corresponding to the current crawl.
|
||||||
:type crawler: :class:`scrapy.crawler.Crawler`
|
:type crawler: :class:`scrapy.crawler.Crawler`
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,11 @@ from scrapy.utils.reactor import (
|
||||||
verify_installed_asyncio_event_loop,
|
verify_installed_asyncio_event_loop,
|
||||||
verify_installed_reactor,
|
verify_installed_reactor,
|
||||||
)
|
)
|
||||||
from scrapy.utils.reactorless import install_reactor_import_hook
|
from scrapy.utils.reactorless import (
|
||||||
|
ReactorImportHook,
|
||||||
|
install_reactor_import_hook,
|
||||||
|
uninstall_reactor_import_hook,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Awaitable, Generator, Iterable
|
from collections.abc import Awaitable, Generator, Iterable
|
||||||
|
|
@ -837,6 +841,7 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
|
||||||
super().__init__(settings, install_root_handler)
|
super().__init__(settings, install_root_handler)
|
||||||
logger.debug("Using AsyncCrawlerProcess")
|
logger.debug("Using AsyncCrawlerProcess")
|
||||||
self._reactorless_loop: asyncio.AbstractEventLoop | None = None
|
self._reactorless_loop: asyncio.AbstractEventLoop | None = None
|
||||||
|
self._reactor_import_hook: ReactorImportHook | None = None
|
||||||
# We want the asyncio event loop to be installed early, so that it's
|
# We want the asyncio event loop to be installed early, so that it's
|
||||||
# always the correct one. And as we do that, we can also install the
|
# always the correct one. And as we do that, we can also install the
|
||||||
# reactor here.
|
# reactor here.
|
||||||
|
|
@ -849,7 +854,7 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
|
||||||
"TWISTED_REACTOR_ENABLED is False but a Twisted reactor is installed."
|
"TWISTED_REACTOR_ENABLED is False but a Twisted reactor is installed."
|
||||||
)
|
)
|
||||||
self._reactorless_loop = set_asyncio_event_loop(loop_path)
|
self._reactorless_loop = set_asyncio_event_loop(loop_path)
|
||||||
install_reactor_import_hook()
|
self._reactor_import_hook = install_reactor_import_hook()
|
||||||
elif is_reactor_installed():
|
elif is_reactor_installed():
|
||||||
# The user could install a reactor before this class is instantiated.
|
# The user could install a reactor before this class is instantiated.
|
||||||
# We need to make sure the reactor is the correct one and the loop
|
# We need to make sure the reactor is the correct one and the loop
|
||||||
|
|
@ -979,6 +984,10 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
|
||||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||||
loop.run_until_complete(loop.shutdown_default_executor())
|
loop.run_until_complete(loop.shutdown_default_executor())
|
||||||
finally:
|
finally:
|
||||||
|
# loop.close() can raise, so we uninstall the hook first
|
||||||
|
if self._reactor_import_hook: # pragma: no branch
|
||||||
|
uninstall_reactor_import_hook(self._reactor_import_hook)
|
||||||
|
self._reactor_import_hook = None
|
||||||
self._reactorless_main_task = None
|
self._reactorless_main_task = None
|
||||||
asyncio.set_event_loop(None)
|
asyncio.set_event_loop(None)
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
@ -28,6 +29,9 @@ if TYPE_CHECKING:
|
||||||
from scrapy.statscollectors import StatsCollector
|
from scrapy.statscollectors import StatsCollector
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HttpCacheMiddleware:
|
class HttpCacheMiddleware:
|
||||||
DOWNLOAD_EXCEPTIONS = (
|
DOWNLOAD_EXCEPTIONS = (
|
||||||
ConnectionDone,
|
ConnectionDone,
|
||||||
|
|
@ -77,9 +81,20 @@ class HttpCacheMiddleware:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Look for cached response and check if expired
|
# Look for cached response and check if expired
|
||||||
cachedresponse: Response | None = self.storage.retrieve_response(
|
cachedresponse: Response | None
|
||||||
self.crawler.spider, request
|
try:
|
||||||
)
|
cachedresponse = self.storage.retrieve_response(
|
||||||
|
self.crawler.spider, request
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.stats.inc_value("httpcache/retrieve_error")
|
||||||
|
logger.warning(
|
||||||
|
f"Could not read the cache entry for {request}, treating it as a "
|
||||||
|
f"cache miss.",
|
||||||
|
exc_info=True,
|
||||||
|
extra={"spider": self.crawler.spider},
|
||||||
|
)
|
||||||
|
cachedresponse = None
|
||||||
if cachedresponse is None:
|
if cachedresponse is None:
|
||||||
self.stats.inc_value("httpcache/miss")
|
self.stats.inc_value("httpcache/miss")
|
||||||
if self.ignore_missing:
|
if self.ignore_missing:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
from importlib.util import find_spec
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
@ -51,11 +52,7 @@ else:
|
||||||
else:
|
else:
|
||||||
ACCEPTED_ENCODINGS.append(b"br")
|
ACCEPTED_ENCODINGS.append(b"br")
|
||||||
|
|
||||||
try:
|
if find_spec("zstandard") is not None:
|
||||||
import zstandard # noqa: F401
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
ACCEPTED_ENCODINGS.append(b"zstd")
|
ACCEPTED_ENCODINGS.append(b"zstd")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -123,12 +120,14 @@ class HttpCompressionMiddleware:
|
||||||
response.body, content_encoding, max_size
|
response.body, content_encoding, max_size
|
||||||
)
|
)
|
||||||
except _DecompressionMaxSizeExceeded as e:
|
except _DecompressionMaxSizeExceeded as e:
|
||||||
raise IgnoreRequest(
|
msg = (
|
||||||
f"Ignored response {response} because its body "
|
f"Ignored response {response} because its body "
|
||||||
f"({len(response.body)} B compressed, "
|
f"({len(response.body)} B compressed, "
|
||||||
f"{e.decompressed_size} B decompressed so far) exceeded "
|
f"{e.decompressed_size} B decompressed so far) exceeded "
|
||||||
f"DOWNLOAD_MAXSIZE ({max_size} B) during decompression."
|
f"DOWNLOAD_MAXSIZE ({max_size} B) during decompression."
|
||||||
) from e
|
)
|
||||||
|
logger.warning(msg)
|
||||||
|
raise IgnoreRequest(msg) from e
|
||||||
if len(response.body) < warn_size <= len(decoded_body):
|
if len(response.body) < warn_size <= len(decoded_body):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{response} body size after decompression "
|
f"{response} body size after decompression "
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class OffsiteMiddleware:
|
||||||
)
|
)
|
||||||
self.stats.inc_value("offsite/domains")
|
self.stats.inc_value("offsite/domains")
|
||||||
self.stats.inc_value("offsite/filtered")
|
self.stats.inc_value("offsite/filtered")
|
||||||
raise IgnoreRequest
|
raise IgnoreRequest(f"Filtered offsite request to {domain!r}")
|
||||||
|
|
||||||
def should_follow(self, request: Request, spider: Spider) -> bool:
|
def should_follow(self, request: Request, spider: Spider) -> bool:
|
||||||
regex = self.host_regex
|
regex = self.host_regex
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,15 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class NotConfigured(Exception):
|
class NotConfigured(Exception):
|
||||||
"""Indicates a missing configuration situation"""
|
"""Raised by a :ref:`component <topics-components>` from its ``__init__()``
|
||||||
|
or :meth:`from_crawler` method to indicate that it will remain disabled.
|
||||||
|
|
||||||
|
Only the following components can be disabled this way:
|
||||||
|
|
||||||
|
- :ref:`Downloader middlewares <topics-downloader-middleware>`
|
||||||
|
- :ref:`Extensions <topics-extensions>`
|
||||||
|
- :ref:`Item pipelines <topics-item-pipeline>`
|
||||||
|
- :ref:`Spider middlewares <topics-spider-middleware>`"""
|
||||||
|
|
||||||
|
|
||||||
class _InvalidOutput(TypeError):
|
class _InvalidOutput(TypeError):
|
||||||
|
|
@ -30,15 +38,37 @@ class _InvalidOutput(TypeError):
|
||||||
|
|
||||||
|
|
||||||
class IgnoreRequest(Exception):
|
class IgnoreRequest(Exception):
|
||||||
"""Indicates a decision was made not to process a request"""
|
"""Raised to indicate that a request should be ignored.
|
||||||
|
|
||||||
|
A :ref:`downloader middleware <topics-downloader-middleware>` can raise it
|
||||||
|
from its
|
||||||
|
:meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_request`
|
||||||
|
or
|
||||||
|
:meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_response`
|
||||||
|
method to drop a request, and a :signal:`request_scheduled` signal handler
|
||||||
|
can raise it to drop a request before it reaches the
|
||||||
|
:ref:`scheduler <topics-scheduler>`."""
|
||||||
|
|
||||||
|
|
||||||
class DontCloseSpider(Exception):
|
class DontCloseSpider(Exception):
|
||||||
"""Request the spider not to be closed yet"""
|
"""Raised in a :signal:`spider_idle` signal handler to prevent the spider
|
||||||
|
from being closed."""
|
||||||
|
|
||||||
|
|
||||||
class CloseSpider(Exception):
|
class CloseSpider(Exception):
|
||||||
"""Raise this from callbacks to request the spider to be closed"""
|
"""Raised from a :ref:`spider callback <topics-spiders>` to request the
|
||||||
|
spider to be closed/stopped.
|
||||||
|
|
||||||
|
*reason* is a string with the reason for closing.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def parse_page(self, response):
|
||||||
|
if "Bandwidth exceeded" in response.text:
|
||||||
|
raise CloseSpider("bandwidth_exceeded")
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, reason: str = "cancelled"):
|
def __init__(self, reason: str = "cancelled"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -46,10 +76,27 @@ class CloseSpider(Exception):
|
||||||
|
|
||||||
|
|
||||||
class StopDownload(Exception):
|
class StopDownload(Exception):
|
||||||
"""
|
"""Raised from a :class:`~scrapy.signals.bytes_received` or
|
||||||
Stop the download of the body for a given response.
|
:class:`~scrapy.signals.headers_received` signal handler to :ref:`stop the
|
||||||
The 'fail' boolean parameter indicates whether or not the resulting partial response
|
download <topics-stop-response-download>` of the response body.
|
||||||
should be handled by the request errback. Note that 'fail' is a keyword-only argument.
|
|
||||||
|
The ``fail`` boolean parameter controls which method will handle the
|
||||||
|
resulting response:
|
||||||
|
|
||||||
|
* If ``fail=True`` (default), the request errback is called. The response
|
||||||
|
object is available as the ``response`` attribute of the ``StopDownload``
|
||||||
|
exception, which is in turn stored as the ``value`` attribute of the
|
||||||
|
received :class:`~twisted.python.failure.Failure` object. This means that
|
||||||
|
in an errback defined as ``def errback(self, failure)``, the response can
|
||||||
|
be accessed though ``failure.value.response``.
|
||||||
|
|
||||||
|
* If ``fail=False``, the request callback is called instead.
|
||||||
|
|
||||||
|
In both cases, the response could have its body truncated: the body contains
|
||||||
|
all bytes received up until the exception is raised, including the bytes
|
||||||
|
received in the signal handler that raises the exception. Also, the response
|
||||||
|
object is marked with ``"download_stopped"`` in its
|
||||||
|
:attr:`~scrapy.http.Response.flags` attribute.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response: Response | None
|
response: Response | None
|
||||||
|
|
@ -91,7 +138,8 @@ class UnsupportedURLSchemeError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class DropItem(Exception):
|
class DropItem(Exception):
|
||||||
"""Drop item from the item pipeline"""
|
"""Raised from the :meth:`process_item` method of an :ref:`item pipeline
|
||||||
|
<topics-item-pipeline>` to stop the processing of an item."""
|
||||||
|
|
||||||
def __init__(self, message: str, log_level: str | None = None):
|
def __init__(self, message: str, log_level: str | None = None):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
@ -99,7 +147,14 @@ class DropItem(Exception):
|
||||||
|
|
||||||
|
|
||||||
class NotSupported(Exception):
|
class NotSupported(Exception):
|
||||||
"""Indicates a feature or method is not supported"""
|
"""Raised to indicate that a requested feature is not supported.
|
||||||
|
|
||||||
|
For example, Scrapy raises it when text-parsing shortcuts such as
|
||||||
|
:meth:`response.css() <scrapy.http.TextResponse.css>` or
|
||||||
|
:meth:`response.xpath() <scrapy.http.TextResponse.xpath>` are used on a
|
||||||
|
:class:`~scrapy.http.Response` whose content is not text, or when sending a
|
||||||
|
request whose URL scheme has no matching :ref:`download handler
|
||||||
|
<topics-download-handlers>`."""
|
||||||
|
|
||||||
|
|
||||||
# Commands
|
# Commands
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ Item Exporters are used to export/serialize items into different formats.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
|
import logging
|
||||||
import marshal
|
import marshal
|
||||||
import pickle
|
import pickle
|
||||||
import pprint
|
import pprint
|
||||||
|
|
@ -24,6 +25,8 @@ from scrapy.utils.serialize import ScrapyJSONEncoder
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseItemExporter",
|
"BaseItemExporter",
|
||||||
"CsvItemExporter",
|
"CsvItemExporter",
|
||||||
|
|
@ -254,6 +257,8 @@ class CsvItemExporter(BaseItemExporter):
|
||||||
self.csv_writer = csv.writer(self.stream, **self._kwargs)
|
self.csv_writer = csv.writer(self.stream, **self._kwargs)
|
||||||
self._headers_not_written = True
|
self._headers_not_written = True
|
||||||
self._join_multivalued = join_multivalued
|
self._join_multivalued = join_multivalued
|
||||||
|
self._autodetected_fields = False
|
||||||
|
self._data_loss_warned = False
|
||||||
|
|
||||||
def serialize_field(
|
def serialize_field(
|
||||||
self, field: Mapping[str, Any] | Field, name: str, value: Any
|
self, field: Mapping[str, Any] | Field, name: str, value: Any
|
||||||
|
|
@ -274,6 +279,22 @@ class CsvItemExporter(BaseItemExporter):
|
||||||
self._headers_not_written = False
|
self._headers_not_written = False
|
||||||
self._write_headers_and_set_fields_to_export(item)
|
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)
|
fields = self._get_serialized_fields(item, default_value="", include_empty=True)
|
||||||
values = list(self._build_row(x for _, x in fields))
|
values = list(self._build_row(x for _, x in fields))
|
||||||
self.csv_writer.writerow(values)
|
self.csv_writer.writerow(values)
|
||||||
|
|
@ -293,6 +314,7 @@ class CsvItemExporter(BaseItemExporter):
|
||||||
if not self.fields_to_export:
|
if not self.fields_to_export:
|
||||||
# use declared field names, or keys if the item is a dict
|
# use declared field names, or keys if the item is a dict
|
||||||
self.fields_to_export = ItemAdapter(item).field_names()
|
self.fields_to_export = ItemAdapter(item).field_names()
|
||||||
|
self._autodetected_fields = True
|
||||||
fields: Iterable[str]
|
fields: Iterable[str]
|
||||||
if isinstance(self.fields_to_export, Mapping):
|
if isinstance(self.fields_to_export, Mapping):
|
||||||
fields = self.fields_to_export.values()
|
fields = self.fields_to_export.values()
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path, PureWindowsPath
|
from pathlib import Path, PureWindowsPath
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
@ -22,7 +22,7 @@ from urllib.parse import unquote, urlparse
|
||||||
|
|
||||||
from twisted.internet.defer import Deferred, DeferredList
|
from twisted.internet.defer import Deferred, DeferredList
|
||||||
from w3lib.url import file_uri_to_path
|
from w3lib.url import file_uri_to_path
|
||||||
from zope.interface import Interface, implementer
|
from zope.interface import Interface
|
||||||
|
|
||||||
from scrapy import Spider, signals
|
from scrapy import Spider, signals
|
||||||
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
|
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
|
||||||
|
|
@ -47,6 +47,33 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Printf-style placeholders (e.g. %(time)s) used to build feed URIs. Any other
|
||||||
|
# percent character in a URI (e.g. percent-encoding such as %20 or %23) must be
|
||||||
|
# treated as a literal rather than as the start of a placeholder.
|
||||||
|
_FEED_URI_PLACEHOLDER_RE = re.compile(
|
||||||
|
r"%\([^)]+\)[-+ #0]*(?:\d+|\*)?(?:\.(?:\d+|\*))?[diouxXeEfFgGcrsa]"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_uri_params(uri_template: str, uri_params: dict[str, Any]) -> str:
|
||||||
|
"""Return *uri_template* with its ``%(...)s`` placeholders replaced using
|
||||||
|
*uri_params*, leaving any other percent character untouched.
|
||||||
|
|
||||||
|
This allows feed URIs to contain percent-encoded characters (e.g. ``%20``
|
||||||
|
in a path with spaces or ``%23`` in FTP credentials) without them being
|
||||||
|
misinterpreted as printf-style formatting directives.
|
||||||
|
"""
|
||||||
|
parts: list[str] = []
|
||||||
|
last = 0
|
||||||
|
for match in _FEED_URI_PLACEHOLDER_RE.finditer(uri_template):
|
||||||
|
parts.append(uri_template[last : match.start()].replace("%", "%%"))
|
||||||
|
parts.append(match.group(0))
|
||||||
|
last = match.end()
|
||||||
|
parts.append(uri_template[last:].replace("%", "%%"))
|
||||||
|
return "".join(parts) % uri_params
|
||||||
|
|
||||||
|
|
||||||
UriParamsCallableT: TypeAlias = Callable[
|
UriParamsCallableT: TypeAlias = Callable[
|
||||||
[dict[str, Any], Spider], dict[str, Any] | None
|
[dict[str, Any], Spider], dict[str, Any] | None
|
||||||
]
|
]
|
||||||
|
|
@ -88,25 +115,18 @@ class ItemFilter:
|
||||||
return True # accept all items by default
|
return True # accept all items by default
|
||||||
|
|
||||||
|
|
||||||
class IFeedStorage(Interface): # type: ignore[misc]
|
class _IFeedStorage(Interface): # type: ignore[misc] # pragma: no cover
|
||||||
"""Interface that all Feed Storages must implement"""
|
|
||||||
|
|
||||||
# pylint: disable=no-self-argument
|
# pylint: disable=no-self-argument
|
||||||
|
|
||||||
def __init__(uri, *, feed_options=None): # type: ignore[no-untyped-def] # pylint: disable=super-init-not-called
|
def __init__(uri, *, feed_options=None): ... # type: ignore[no-untyped-def] # pylint: disable=super-init-not-called
|
||||||
"""Initialize the storage with the parameters given in the URI and the
|
|
||||||
feed-specific options (see :setting:`FEEDS`)"""
|
|
||||||
|
|
||||||
def open(spider): # type: ignore[no-untyped-def]
|
def open(spider): ... # type: ignore[no-untyped-def]
|
||||||
"""Open the storage for the given spider. It must return a file-like
|
|
||||||
object that will be used for the exporters"""
|
|
||||||
|
|
||||||
def store(file): # type: ignore[no-untyped-def]
|
def store(file): ... # type: ignore[no-untyped-def]
|
||||||
"""Store the given file stream"""
|
|
||||||
|
|
||||||
|
|
||||||
class FeedStorageProtocol(Protocol):
|
class FeedStorageProtocol(Protocol):
|
||||||
"""Reimplementation of ``IFeedStorage`` that can be used in type hints."""
|
"""Protocol that all Feed Storages must follow."""
|
||||||
|
|
||||||
def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None):
|
def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None):
|
||||||
"""Initialize the storage with the parameters given in the URI and the
|
"""Initialize the storage with the parameters given in the URI and the
|
||||||
|
|
@ -120,7 +140,6 @@ class FeedStorageProtocol(Protocol):
|
||||||
"""Store the given file stream"""
|
"""Store the given file stream"""
|
||||||
|
|
||||||
|
|
||||||
@implementer(IFeedStorage)
|
|
||||||
class BlockingFeedStorage(ABC):
|
class BlockingFeedStorage(ABC):
|
||||||
def open(self, spider: Spider) -> IO[bytes]:
|
def open(self, spider: Spider) -> IO[bytes]:
|
||||||
path = spider.crawler.settings["FEED_TEMPDIR"]
|
path = spider.crawler.settings["FEED_TEMPDIR"]
|
||||||
|
|
@ -137,7 +156,6 @@ class BlockingFeedStorage(ABC):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@implementer(IFeedStorage)
|
|
||||||
class StdoutFeedStorage:
|
class StdoutFeedStorage:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -164,7 +182,6 @@ class StdoutFeedStorage:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@implementer(IFeedStorage)
|
|
||||||
class FileFeedStorage:
|
class FileFeedStorage:
|
||||||
def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None):
|
def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None):
|
||||||
self.path: str = file_uri_to_path(uri) if uri.startswith("file:") else uri
|
self.path: str = file_uri_to_path(uri) if uri.startswith("file:") else uri
|
||||||
|
|
@ -458,7 +475,7 @@ class FeedExporter:
|
||||||
self.feeds = {}
|
self.feeds = {}
|
||||||
self.slots: list[FeedSlot] = []
|
self.slots: list[FeedSlot] = []
|
||||||
self.filters: dict[str, ItemFilter] = {}
|
self.filters: dict[str, ItemFilter] = {}
|
||||||
self._pending_close_coros: list[Coroutine[Any, Any, None]] = []
|
self._pending_close_tasks: list[asyncio.Task[None] | Deferred[None]] = []
|
||||||
|
|
||||||
if not self.settings["FEEDS"] and not self.settings["FEED_URI"]:
|
if not self.settings["FEEDS"] and not self.settings["FEED_URI"]:
|
||||||
raise NotConfigured
|
raise NotConfigured
|
||||||
|
|
@ -473,7 +490,7 @@ class FeedExporter:
|
||||||
)
|
)
|
||||||
uri = self.settings["FEED_URI"]
|
uri = self.settings["FEED_URI"]
|
||||||
# handle pathlib.Path objects
|
# handle pathlib.Path objects
|
||||||
uri = str(uri) if not isinstance(uri, Path) else uri.absolute().as_uri()
|
uri = str(uri.absolute()) if isinstance(uri, Path) else str(uri)
|
||||||
feed_options = {"format": self.settings["FEED_FORMAT"]}
|
feed_options = {"format": self.settings["FEED_FORMAT"]}
|
||||||
self.feeds[uri] = feed_complete_default_values_from_settings(
|
self.feeds[uri] = feed_complete_default_values_from_settings(
|
||||||
feed_options, self.settings
|
feed_options, self.settings
|
||||||
|
|
@ -485,9 +502,9 @@ class FeedExporter:
|
||||||
for settings_uri, feed_options in self.settings.getdict("FEEDS").items():
|
for settings_uri, feed_options in self.settings.getdict("FEEDS").items():
|
||||||
# handle pathlib.Path objects
|
# handle pathlib.Path objects
|
||||||
uri = (
|
uri = (
|
||||||
str(settings_uri)
|
str(settings_uri.absolute())
|
||||||
if not isinstance(settings_uri, Path)
|
if isinstance(settings_uri, Path)
|
||||||
else settings_uri.absolute().as_uri()
|
else str(settings_uri)
|
||||||
)
|
)
|
||||||
self.feeds[uri] = feed_complete_default_values_from_settings(
|
self.feeds[uri] = feed_complete_default_values_from_settings(
|
||||||
feed_options, self.settings
|
feed_options, self.settings
|
||||||
|
|
@ -514,7 +531,7 @@ class FeedExporter:
|
||||||
self.slots.append(
|
self.slots.append(
|
||||||
self._start_new_batch(
|
self._start_new_batch(
|
||||||
batch_id=1,
|
batch_id=1,
|
||||||
uri=uri % uri_params,
|
uri=apply_uri_params(uri, uri_params),
|
||||||
feed_options=feed_options,
|
feed_options=feed_options,
|
||||||
spider=spider,
|
spider=spider,
|
||||||
uri_template=uri,
|
uri_template=uri,
|
||||||
|
|
@ -522,23 +539,44 @@ class FeedExporter:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def close_spider(self, spider: Spider) -> None:
|
async def close_spider(self, spider: Spider) -> None:
|
||||||
self._pending_close_coros.extend(
|
for slot in self.slots:
|
||||||
self._close_slot(slot, spider) for slot in self.slots
|
self._schedule_slot_close(slot, spider)
|
||||||
)
|
|
||||||
|
|
||||||
if self._pending_close_coros:
|
if self._pending_close_tasks:
|
||||||
if is_asyncio_available():
|
if is_asyncio_available():
|
||||||
await asyncio.wait(
|
await asyncio.wait(
|
||||||
[asyncio.create_task(coro) for coro in self._pending_close_coros]
|
cast("list[asyncio.Task[None]]", list(self._pending_close_tasks))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await DeferredList(
|
await DeferredList(
|
||||||
deferred_from_coro(coro) for coro in self._pending_close_coros
|
cast("list[Deferred[None]]", list(self._pending_close_tasks))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send FEED_EXPORTER_CLOSED signal
|
# Send FEED_EXPORTER_CLOSED signal
|
||||||
await self.crawler.signals.send_catch_log_async(signals.feed_exporter_closed)
|
await self.crawler.signals.send_catch_log_async(signals.feed_exporter_closed)
|
||||||
|
|
||||||
|
def _schedule_slot_close(
|
||||||
|
self, slot: FeedSlot, spider: Spider
|
||||||
|
) -> asyncio.Task[None] | Deferred[None]:
|
||||||
|
"""Start closing the slot without waiting for it to finish, keeping
|
||||||
|
track of the pending work so that it can be awaited in
|
||||||
|
:meth:`close_spider` if it hasn't finished by then."""
|
||||||
|
aw: asyncio.Task[None] | Deferred[None]
|
||||||
|
coro = self._close_slot(slot, spider)
|
||||||
|
if is_asyncio_available():
|
||||||
|
aw = asyncio.create_task(coro)
|
||||||
|
self._pending_close_tasks.append(aw)
|
||||||
|
aw.add_done_callback(self._pending_close_tasks.remove)
|
||||||
|
else:
|
||||||
|
aw = deferred_from_coro(coro)
|
||||||
|
self._pending_close_tasks.append(aw)
|
||||||
|
aw.addBoth(self._untrack_pending_close_task, aw)
|
||||||
|
return aw
|
||||||
|
|
||||||
|
def _untrack_pending_close_task(self, result: Any, aw: Deferred[None]) -> Any:
|
||||||
|
self._pending_close_tasks.remove(aw)
|
||||||
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_file(slot_: FeedSlot) -> IO[bytes]:
|
def _get_file(slot_: FeedSlot) -> IO[bytes]:
|
||||||
assert slot_.file
|
assert slot_.file
|
||||||
|
|
@ -635,11 +673,11 @@ class FeedExporter:
|
||||||
uri_params = self._get_uri_params(
|
uri_params = self._get_uri_params(
|
||||||
spider, self.feeds[slot.uri_template]["uri_params"], slot
|
spider, self.feeds[slot.uri_template]["uri_params"], slot
|
||||||
)
|
)
|
||||||
self._pending_close_coros.append(self._close_slot(slot, spider))
|
self._schedule_slot_close(slot, spider)
|
||||||
slots.append(
|
slots.append(
|
||||||
self._start_new_batch(
|
self._start_new_batch(
|
||||||
batch_id=slot.batch_id + 1,
|
batch_id=slot.batch_id + 1,
|
||||||
uri=slot.uri_template % uri_params,
|
uri=apply_uri_params(slot.uri_template, uri_params),
|
||||||
feed_options=self.feeds[slot.uri_template],
|
feed_options=self.feeds[slot.uri_template],
|
||||||
spider=spider,
|
spider=spider,
|
||||||
uri_template=slot.uri_template,
|
uri_template=slot.uri_template,
|
||||||
|
|
@ -731,3 +769,14 @@ class FeedExporter:
|
||||||
feed_options.get("item_filter", ItemFilter)
|
feed_options.get("item_filter", ItemFilter)
|
||||||
)
|
)
|
||||||
return item_filter_class(feed_options)
|
return item_filter_class(feed_options)
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> Any: # pragma: no cover
|
||||||
|
if name == "IFeedStorage":
|
||||||
|
warnings.warn(
|
||||||
|
"scrapy.extensions.feedexport.IFeedStorage is deprecated.",
|
||||||
|
ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return _IFeedStorage
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import TYPE_CHECKING, Any, AnyStr, TypeAlias, cast
|
from typing import TYPE_CHECKING, Any, TypeAlias, cast
|
||||||
|
|
||||||
from w3lib.http import headers_dict_to_raw
|
from w3lib.http import headers_dict_to_raw
|
||||||
|
|
||||||
|
|
@ -25,14 +25,20 @@ class Headers(CaselessDict):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
seq: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,
|
seq: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None = None,
|
||||||
encoding: str = "utf-8",
|
encoding: str = "utf-8",
|
||||||
):
|
):
|
||||||
self.encoding: str = encoding
|
self.encoding: str = encoding
|
||||||
super().__init__(seq)
|
super().__init__(seq)
|
||||||
|
|
||||||
def update( # type: ignore[override]
|
def update( # type: ignore[override]
|
||||||
self, seq: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]]
|
self,
|
||||||
|
seq: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
seq = seq.items() if isinstance(seq, Mapping) else seq
|
seq = seq.items() if isinstance(seq, Mapping) else seq
|
||||||
iseq: dict[bytes, list[bytes]] = {}
|
iseq: dict[bytes, list[bytes]] = {}
|
||||||
|
|
@ -40,7 +46,7 @@ class Headers(CaselessDict):
|
||||||
iseq.setdefault(self.normkey(k), []).extend(self.normvalue(v))
|
iseq.setdefault(self.normkey(k), []).extend(self.normvalue(v))
|
||||||
super().update(iseq)
|
super().update(iseq)
|
||||||
|
|
||||||
def normkey(self, key: AnyStr) -> bytes: # type: ignore[override]
|
def normkey(self, key: str | bytes) -> bytes:
|
||||||
"""Normalize key to bytes"""
|
"""Normalize key to bytes"""
|
||||||
return self._tobytes(key.title())
|
return self._tobytes(key.title())
|
||||||
|
|
||||||
|
|
@ -67,19 +73,19 @@ class Headers(CaselessDict):
|
||||||
return str(x).encode(self.encoding)
|
return str(x).encode(self.encoding)
|
||||||
raise TypeError(f"Unsupported value type: {type(x)}")
|
raise TypeError(f"Unsupported value type: {type(x)}")
|
||||||
|
|
||||||
def __getitem__(self, key: AnyStr) -> bytes | None:
|
def __getitem__(self, key: str | bytes) -> bytes | None:
|
||||||
try:
|
try:
|
||||||
return cast("list[bytes]", super().__getitem__(key))[-1]
|
return cast("list[bytes]", super().__getitem__(key))[-1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(self, key: AnyStr, def_val: Any = None) -> bytes | None:
|
def get(self, key: str | bytes, def_val: Any = None) -> bytes | None:
|
||||||
try:
|
try:
|
||||||
return cast("list[bytes]", super().get(key, def_val))[-1]
|
return cast("list[bytes]", super().get(key, def_val))[-1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getlist(self, key: AnyStr, def_val: Any = None) -> list[bytes]:
|
def getlist(self, key: str | bytes, def_val: Any = None) -> list[bytes]:
|
||||||
try:
|
try:
|
||||||
return cast("list[bytes]", super().__getitem__(key))
|
return cast("list[bytes]", super().__getitem__(key))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
@ -87,15 +93,15 @@ class Headers(CaselessDict):
|
||||||
return self.normvalue(def_val)
|
return self.normvalue(def_val)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def setlist(self, key: AnyStr, list_: Iterable[_RawValue]) -> None:
|
def setlist(self, key: str | bytes, list_: Iterable[_RawValue]) -> None:
|
||||||
self[key] = list_
|
self[key] = list_
|
||||||
|
|
||||||
def setlistdefault(
|
def setlistdefault(
|
||||||
self, key: AnyStr, default_list: Iterable[_RawValue] = ()
|
self, key: str | bytes, default_list: Iterable[_RawValue] = ()
|
||||||
) -> Any:
|
) -> Any:
|
||||||
return self.setdefault(key, default_list)
|
return self.setdefault(key, default_list)
|
||||||
|
|
||||||
def appendlist(self, key: AnyStr, value: Iterable[_RawValue]) -> None:
|
def appendlist(self, key: str | bytes, value: Iterable[_RawValue]) -> None:
|
||||||
lst = self.getlist(key)
|
lst = self.getlist(key)
|
||||||
lst.extend(self.normvalue(value))
|
lst.extend(self.normvalue(value))
|
||||||
self[key] = lst
|
self[key] = lst
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import inspect
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
AnyStr,
|
|
||||||
Concatenate,
|
Concatenate,
|
||||||
NoReturn,
|
NoReturn,
|
||||||
TypeAlias,
|
TypeAlias,
|
||||||
|
|
@ -125,7 +124,10 @@ class Request(object_ref):
|
||||||
url: str,
|
url: str,
|
||||||
callback: CallbackT | None = None,
|
callback: CallbackT | None = None,
|
||||||
method: str = "GET",
|
method: str = "GET",
|
||||||
headers: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,
|
headers: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None = None,
|
||||||
body: bytes | str | None = None,
|
body: bytes | str | None = None,
|
||||||
cookies: CookiesT | None = None,
|
cookies: CookiesT | None = None,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: dict[str, Any] | None = None,
|
||||||
|
|
@ -310,7 +312,11 @@ class Request(object_ref):
|
||||||
|
|
||||||
@headers.setter
|
@headers.setter
|
||||||
def headers(
|
def headers(
|
||||||
self, value: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None
|
self,
|
||||||
|
value: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(value, Headers):
|
if isinstance(value, Headers):
|
||||||
self._headers = value
|
self._headers = value
|
||||||
|
|
@ -381,6 +387,20 @@ class Request(object_ref):
|
||||||
request_kwargs.update(kwargs)
|
request_kwargs.update(kwargs)
|
||||||
return cls(**request_kwargs)
|
return cls(**request_kwargs)
|
||||||
|
|
||||||
|
def to_curl(self) -> str:
|
||||||
|
"""Return a string with a `cURL <https://curl.se/>`_ command equivalent
|
||||||
|
to this request.
|
||||||
|
|
||||||
|
Inverse of :meth:`from_curl`. See also
|
||||||
|
:func:`scrapy.utils.request.request_to_curl`.
|
||||||
|
|
||||||
|
.. versionadded:: VERSION
|
||||||
|
"""
|
||||||
|
# Imported here to avoid a circular import.
|
||||||
|
from scrapy.utils.request import request_to_curl # noqa: PLC0415
|
||||||
|
|
||||||
|
return request_to_curl(self)
|
||||||
|
|
||||||
def to_dict(self, *, spider: scrapy.Spider | None = None) -> dict[str, Any]:
|
def to_dict(self, *, spider: scrapy.Spider | None = None) -> dict[str, Any]:
|
||||||
"""Return a dictionary containing the Request's data.
|
"""Return a dictionary containing the Request's data.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ See documentation in docs/topics/request-response.rst
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, AnyStr, TypeVar, overload
|
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from scrapy.exceptions import NotSupported
|
from scrapy.exceptions import NotSupported
|
||||||
|
|
@ -72,7 +72,10 @@ class Response(object_ref):
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
status: int = 200,
|
status: int = 200,
|
||||||
headers: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,
|
headers: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None = None,
|
||||||
body: bytes = b"",
|
body: bytes = b"",
|
||||||
flags: list[str] | None = None,
|
flags: list[str] | None = None,
|
||||||
request: Request | None = None,
|
request: Request | None = None,
|
||||||
|
|
@ -145,7 +148,11 @@ class Response(object_ref):
|
||||||
|
|
||||||
@headers.setter
|
@headers.setter
|
||||||
def headers(
|
def headers(
|
||||||
self, value: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None
|
self,
|
||||||
|
value: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(value, Headers):
|
if isinstance(value, Headers):
|
||||||
self._headers = value
|
self._headers = value
|
||||||
|
|
@ -222,7 +229,10 @@ class Response(object_ref):
|
||||||
url: str | Link,
|
url: str | Link,
|
||||||
callback: CallbackT | None = None,
|
callback: CallbackT | None = None,
|
||||||
method: str = "GET",
|
method: str = "GET",
|
||||||
headers: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,
|
headers: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None = None,
|
||||||
body: bytes | str | None = None,
|
body: bytes | str | None = None,
|
||||||
cookies: CookiesT | None = None,
|
cookies: CookiesT | None = None,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: dict[str, Any] | None = None,
|
||||||
|
|
@ -272,7 +282,10 @@ class Response(object_ref):
|
||||||
urls: Iterable[str | Link],
|
urls: Iterable[str | Link],
|
||||||
callback: CallbackT | None = None,
|
callback: CallbackT | None = None,
|
||||||
method: str = "GET",
|
method: str = "GET",
|
||||||
headers: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,
|
headers: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None = None,
|
||||||
body: bytes | str | None = None,
|
body: bytes | str | None = None,
|
||||||
cookies: CookiesT | None = None,
|
cookies: CookiesT | None = None,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: dict[str, Any] | None = None,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import TYPE_CHECKING, Any, AnyStr, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import parsel
|
import parsel
|
||||||
|
|
@ -170,7 +170,10 @@ class TextResponse(Response):
|
||||||
url: str | Link | parsel.Selector,
|
url: str | Link | parsel.Selector,
|
||||||
callback: CallbackT | None = None,
|
callback: CallbackT | None = None,
|
||||||
method: str = "GET",
|
method: str = "GET",
|
||||||
headers: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,
|
headers: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None = None,
|
||||||
body: bytes | str | None = None,
|
body: bytes | str | None = None,
|
||||||
cookies: CookiesT | None = None,
|
cookies: CookiesT | None = None,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: dict[str, Any] | None = None,
|
||||||
|
|
@ -223,7 +226,10 @@ class TextResponse(Response):
|
||||||
urls: Iterable[str | Link] | parsel.SelectorList[Any] | None = None,
|
urls: Iterable[str | Link] | parsel.SelectorList[Any] | None = None,
|
||||||
callback: CallbackT | None = None,
|
callback: CallbackT | None = None,
|
||||||
method: str = "GET",
|
method: str = "GET",
|
||||||
headers: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,
|
headers: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None = None,
|
||||||
body: bytes | str | None = None,
|
body: bytes | str | None = None,
|
||||||
cookies: CookiesT | None = None,
|
cookies: CookiesT | None = None,
|
||||||
meta: dict[str, Any] | None = None,
|
meta: dict[str, Any] | None = None,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
|
# pragma: no file cover
|
||||||
# pylint: disable=no-method-argument,no-self-argument
|
# pylint: disable=no-method-argument,no-self-argument
|
||||||
|
import warnings
|
||||||
|
|
||||||
from zope.interface import Interface
|
from zope.interface import Interface
|
||||||
|
|
||||||
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"The scrapy.interfaces module is deprecated.",
|
||||||
|
ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ISpiderLoader(Interface):
|
class ISpiderLoader(Interface):
|
||||||
def from_settings(settings):
|
def from_settings(settings): ...
|
||||||
"""Return an instance of the class for the given settings"""
|
|
||||||
|
|
||||||
def load(spider_name):
|
def load(spider_name): ...
|
||||||
"""Return the Spider class for the given spider name. If the spider
|
|
||||||
name is not found, it must raise a KeyError."""
|
|
||||||
|
|
||||||
def list():
|
def list(): ...
|
||||||
"""Return a list with the names of all spiders available in the
|
|
||||||
project"""
|
|
||||||
|
|
||||||
def find_by_request(request):
|
def find_by_request(request): ...
|
||||||
"""Return the list of spiders names that can handle the given request"""
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
Mail sending helpers
|
Mail sending helpers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# pragma: no file cover
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,15 @@ from twisted.internet.defer import Deferred, maybeDeferred
|
||||||
from scrapy.exceptions import IgnoreRequest, NotConfigured, ScrapyDeprecationWarning
|
from scrapy.exceptions import IgnoreRequest, NotConfigured, ScrapyDeprecationWarning
|
||||||
from scrapy.http import Request, Response
|
from scrapy.http import Request, Response
|
||||||
from scrapy.http.request import NO_CALLBACK
|
from scrapy.http.request import NO_CALLBACK
|
||||||
from scrapy.pipelines.media import FileInfo, FileInfoOrError, MediaPipeline
|
from scrapy.pipelines.media import (
|
||||||
|
FileException as FileException, # noqa: PLC0414 # re-exported for backward compatibility
|
||||||
|
)
|
||||||
|
from scrapy.pipelines.media import (
|
||||||
|
FileInfo,
|
||||||
|
FileInfoOrError,
|
||||||
|
MediaPipeline,
|
||||||
|
_MediaRequestFiltered,
|
||||||
|
)
|
||||||
from scrapy.utils.asyncio import run_in_thread
|
from scrapy.utils.asyncio import run_in_thread
|
||||||
from scrapy.utils.boto import is_botocore_available
|
from scrapy.utils.boto import is_botocore_available
|
||||||
from scrapy.utils.datatypes import CaseInsensitiveDict
|
from scrapy.utils.datatypes import CaseInsensitiveDict
|
||||||
|
|
@ -75,10 +83,6 @@ def _md5sum(file: IO[bytes]) -> str:
|
||||||
return m.hexdigest()
|
return m.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
class FileException(Exception):
|
|
||||||
"""General media error exception"""
|
|
||||||
|
|
||||||
|
|
||||||
class StatInfo(TypedDict, total=False):
|
class StatInfo(TypedDict, total=False):
|
||||||
checksum: str
|
checksum: str
|
||||||
last_modified: float
|
last_modified: float
|
||||||
|
|
@ -300,13 +304,13 @@ class GCSFilesStore:
|
||||||
)
|
)
|
||||||
if "storage.objects.get" not in permissions:
|
if "storage.objects.get" not in permissions:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"No 'storage.objects.get' permission for GSC bucket %(bucket)s. "
|
"No 'storage.objects.get' permission for GCS bucket %(bucket)s. "
|
||||||
"Checking if files are up to date will be impossible. Files will be downloaded every time.",
|
"Checking if files are up to date will be impossible. Files will be downloaded every time.",
|
||||||
{"bucket": bucket},
|
{"bucket": bucket},
|
||||||
)
|
)
|
||||||
if "storage.objects.create" not in permissions:
|
if "storage.objects.create" not in permissions:
|
||||||
logger.error(
|
logger.error(
|
||||||
"No 'storage.objects.create' permission for GSC bucket %(bucket)s. Saving files will be impossible!",
|
"No 'storage.objects.create' permission for GCS bucket %(bucket)s. Saving files will be impossible!",
|
||||||
{"bucket": bucket},
|
{"bucket": bucket},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -597,20 +601,20 @@ class FilesPipeline(MediaPipeline):
|
||||||
def media_failed(
|
def media_failed(
|
||||||
self, failure: Failure, request: Request, info: MediaPipeline.SpiderInfo
|
self, failure: Failure, request: Request, info: MediaPipeline.SpiderInfo
|
||||||
) -> NoReturn:
|
) -> NoReturn:
|
||||||
if not isinstance(failure.value, IgnoreRequest):
|
referer = referer_str(request)
|
||||||
referer = referer_str(request)
|
if isinstance(failure.value, IgnoreRequest):
|
||||||
logger.warning(
|
logger.debug(
|
||||||
"File (unknown-error): Error downloading %(medianame)s from "
|
f"File (filtered): Not downloading {self.MEDIA_NAME} from "
|
||||||
"%(request)s referred in <%(referer)s>: %(exception)s",
|
f"{request} referred in <{referer}>: {failure.value}",
|
||||||
{
|
|
||||||
"medianame": self.MEDIA_NAME,
|
|
||||||
"request": request,
|
|
||||||
"referer": referer,
|
|
||||||
"exception": failure.value,
|
|
||||||
},
|
|
||||||
extra={"spider": info.spider},
|
extra={"spider": info.spider},
|
||||||
)
|
)
|
||||||
|
raise _MediaRequestFiltered(str(failure.value)) from failure.value
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"File (unknown-error): Error downloading {self.MEDIA_NAME} from "
|
||||||
|
f"{request} referred in <{referer}>: {failure.value}",
|
||||||
|
extra={"spider": info.spider},
|
||||||
|
)
|
||||||
raise FileException
|
raise FileException
|
||||||
|
|
||||||
async def media_downloaded(
|
async def media_downloaded(
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,25 @@ FileInfoOrError: TypeAlias = (
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FileException(Exception):
|
||||||
|
"""General media error exception"""
|
||||||
|
|
||||||
|
|
||||||
|
class _MediaRequestFiltered(FileException):
|
||||||
|
"""Raised internally by media pipelines when a media request is filtered
|
||||||
|
out (e.g. as an offsite request) instead of being downloaded.
|
||||||
|
|
||||||
|
It is a subclass of :exc:`FileException` for backward compatibility, but
|
||||||
|
unlike an actual download error it is logged at the ``DEBUG`` level and
|
||||||
|
without a traceback, since filtering a request is expected behavior rather
|
||||||
|
than an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _media_request_filtered(failure: Failure) -> bool:
|
||||||
|
return isinstance(failure.value, _MediaRequestFiltered)
|
||||||
|
|
||||||
|
|
||||||
class MediaPipeline(ABC):
|
class MediaPipeline(ABC):
|
||||||
LOG_FAILED_RESULTS: bool = True
|
LOG_FAILED_RESULTS: bool = True
|
||||||
|
|
||||||
|
|
@ -193,7 +212,8 @@ class MediaPipeline(ABC):
|
||||||
result = await self._check_media_to_download(request, info, item=item)
|
result = await self._check_media_to_download(request, info, item=item)
|
||||||
except Exception:
|
except Exception:
|
||||||
result = Failure()
|
result = Failure()
|
||||||
logger.exception(result)
|
if not _media_request_filtered(result):
|
||||||
|
logger.exception(result)
|
||||||
self._cache_result_and_execute_waiters(result, fp, info)
|
self._cache_result_and_execute_waiters(result, fp, info)
|
||||||
return await maybe_deferred_to_future(wad) # it must return wad at last
|
return await maybe_deferred_to_future(wad) # it must return wad at last
|
||||||
|
|
||||||
|
|
@ -304,6 +324,8 @@ class MediaPipeline(ABC):
|
||||||
for ok, value in results:
|
for ok, value in results:
|
||||||
if not ok:
|
if not ok:
|
||||||
assert isinstance(value, Failure)
|
assert isinstance(value, Failure)
|
||||||
|
if _media_request_filtered(value):
|
||||||
|
continue
|
||||||
logger.error(
|
logger.error(
|
||||||
"%(class)s found errors processing %(item)s",
|
"%(class)s found errors processing %(item)s",
|
||||||
{"class": self.__class__.__name__, "item": item},
|
{"class": self.__class__.__name__, "item": item},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import attr
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.internet.base import ReactorBase, ThreadedResolver
|
from twisted.internet.base import ReactorBase, ThreadedResolver
|
||||||
from twisted.internet.interfaces import (
|
from twisted.internet.interfaces import (
|
||||||
|
|
@ -70,6 +71,12 @@ class CachingThreadedResolver(ThreadedResolver):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _address_with_port(address: IAddress, port: int) -> IAddress:
|
||||||
|
if getattr(address, "port", port) == port:
|
||||||
|
return address
|
||||||
|
return attr.evolve(address, port=port)
|
||||||
|
|
||||||
|
|
||||||
@implementer(IHostResolution)
|
@implementer(IHostResolution)
|
||||||
class HostResolution:
|
class HostResolution:
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
|
|
@ -97,7 +104,11 @@ class _CachingResolutionReceiver:
|
||||||
def resolutionComplete(self) -> None:
|
def resolutionComplete(self) -> None:
|
||||||
self.resolutionReceiver.resolutionComplete()
|
self.resolutionReceiver.resolutionComplete()
|
||||||
if self.addresses:
|
if self.addresses:
|
||||||
dnscache[self.hostName] = self.addresses
|
# Name resolution does not depend on the port, so cache entries are
|
||||||
|
# kept port-agnostic and the requested port is set on cache hits.
|
||||||
|
dnscache[self.hostName] = [
|
||||||
|
_address_with_port(address, 0) for address in self.addresses
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@implementer(IHostnameResolver)
|
@implementer(IHostnameResolver)
|
||||||
|
|
@ -142,7 +153,7 @@ class CachingHostnameResolver:
|
||||||
transportSemantics,
|
transportSemantics,
|
||||||
)
|
)
|
||||||
resolutionReceiver.resolutionBegan(HostResolution(hostName))
|
resolutionReceiver.resolutionBegan(HostResolution(hostName))
|
||||||
for addr in addresses:
|
for address in addresses:
|
||||||
resolutionReceiver.addressResolved(addr)
|
resolutionReceiver.addressResolved(_address_with_port(address, portNumber))
|
||||||
resolutionReceiver.resolutionComplete()
|
resolutionReceiver.resolutionComplete()
|
||||||
return resolutionReceiver
|
return resolutionReceiver
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ if TYPE_CHECKING:
|
||||||
# typing.Self requires Python 3.11
|
# typing.Self requires Python 3.11
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
_SettingsInput: TypeAlias = SupportsItems[str, Any] | str | None
|
_SettingsInput: TypeAlias = (
|
||||||
|
SupportsItems[str, Any] | Iterable[tuple[str, Any]] | str | None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
SETTINGS_PRIORITIES: dict[str, int] = {
|
SETTINGS_PRIORITIES: dict[str, int] = {
|
||||||
|
|
@ -288,10 +290,10 @@ class BaseSettings(MutableMapping[str, Any]):
|
||||||
- ``['one', 'two']`` if set to ``'["one", "two"]'`` or ``'one,two'``
|
- ``['one', 'two']`` if set to ``'["one", "two"]'`` or ``'one,two'``
|
||||||
|
|
||||||
:param name: the setting name
|
:param name: the setting name
|
||||||
:type name: string
|
:type name: str
|
||||||
|
|
||||||
:param default: the value to return if no setting is found
|
:param default: the value to return if no setting is found
|
||||||
:type default: any
|
:type default: object
|
||||||
"""
|
"""
|
||||||
value = self.get(name, default)
|
value = self.get(name, default)
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|
@ -560,7 +562,7 @@ class BaseSettings(MutableMapping[str, Any]):
|
||||||
if key.isupper():
|
if key.isupper():
|
||||||
self.set(key, getattr(module, key), priority)
|
self.set(key, getattr(module, key), priority)
|
||||||
|
|
||||||
# BaseSettings.update() doesn't support all inputs that MutableMapping.update() supports
|
# BaseSettings.update() doesn't support kwargs input like MutableMapping.update().
|
||||||
def update(self, values: _SettingsInput, priority: int | str = "project") -> None: # type: ignore[override]
|
def update(self, values: _SettingsInput, priority: int | str = "project") -> None: # type: ignore[override]
|
||||||
"""
|
"""
|
||||||
Store key/value pairs with a given priority.
|
Store key/value pairs with a given priority.
|
||||||
|
|
@ -577,7 +579,7 @@ class BaseSettings(MutableMapping[str, Any]):
|
||||||
command.
|
command.
|
||||||
|
|
||||||
:param values: the settings names and values
|
:param values: the settings names and values
|
||||||
:type values: dict or string or :class:`~scrapy.settings.BaseSettings`
|
:type values: dict, iterable, string or :class:`~scrapy.settings.BaseSettings`
|
||||||
|
|
||||||
:param priority: the priority of the settings. Should be a key of
|
:param priority: the priority of the settings. Should be a key of
|
||||||
:attr:`~scrapy.settings.SETTINGS_PRIORITIES` or an integer
|
:attr:`~scrapy.settings.SETTINGS_PRIORITIES` or an integer
|
||||||
|
|
@ -591,7 +593,12 @@ class BaseSettings(MutableMapping[str, Any]):
|
||||||
for name, value in values.items():
|
for name, value in values.items():
|
||||||
self.set(name, value, cast("int", values.getpriority(name)))
|
self.set(name, value, cast("int", values.getpriority(name)))
|
||||||
else:
|
else:
|
||||||
for name, value in values.items():
|
items: Iterable[tuple[str, Any]]
|
||||||
|
if hasattr(values, "items"):
|
||||||
|
items = cast("SupportsItems[str, Any]", values).items()
|
||||||
|
else:
|
||||||
|
items = values
|
||||||
|
for name, value in items:
|
||||||
self.set(name, value, priority)
|
self.set(name, value, priority)
|
||||||
|
|
||||||
def delete(self, name: str, priority: int | str = "project") -> None:
|
def delete(self, name: str, priority: int | str = "project") -> None:
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,6 @@ __all__ = [
|
||||||
"MAIL_TLS",
|
"MAIL_TLS",
|
||||||
"MAIL_USER",
|
"MAIL_USER",
|
||||||
"MEMDEBUG_ENABLED",
|
"MEMDEBUG_ENABLED",
|
||||||
"MEMDEBUG_NOTIFY",
|
|
||||||
"MEMUSAGE_CHECK_INTERVAL_SECONDS",
|
"MEMUSAGE_CHECK_INTERVAL_SECONDS",
|
||||||
"MEMUSAGE_ENABLED",
|
"MEMUSAGE_ENABLED",
|
||||||
"MEMUSAGE_LIMIT_MB",
|
"MEMUSAGE_LIMIT_MB",
|
||||||
|
|
@ -240,7 +239,7 @@ BOT_NAME = "scrapybot"
|
||||||
CLOSESPIDER_ERRORCOUNT = 0
|
CLOSESPIDER_ERRORCOUNT = 0
|
||||||
CLOSESPIDER_ITEMCOUNT = 0
|
CLOSESPIDER_ITEMCOUNT = 0
|
||||||
CLOSESPIDER_PAGECOUNT = 0
|
CLOSESPIDER_PAGECOUNT = 0
|
||||||
CLOSESPIDER_TIMEOUT = 0
|
CLOSESPIDER_TIMEOUT = 0.0
|
||||||
CLOSESPIDER_PAGECOUNT_NO_ITEM = 0
|
CLOSESPIDER_PAGECOUNT_NO_ITEM = 0
|
||||||
CLOSESPIDER_TIMEOUT_NO_ITEM = 0
|
CLOSESPIDER_TIMEOUT_NO_ITEM = 0
|
||||||
|
|
||||||
|
|
@ -470,7 +469,6 @@ MAIL_SSL = False
|
||||||
MAIL_TLS = False
|
MAIL_TLS = False
|
||||||
|
|
||||||
MEMDEBUG_ENABLED = False # enable memory debugging
|
MEMDEBUG_ENABLED = False # enable memory debugging
|
||||||
MEMDEBUG_NOTIFY = [] # send memory debugging report by mail at engine shutdown
|
|
||||||
|
|
||||||
MEMUSAGE_ENABLED = True
|
MEMUSAGE_ENABLED = True
|
||||||
MEMUSAGE_CHECK_INTERVAL_SECONDS = 60.0
|
MEMUSAGE_CHECK_INTERVAL_SECONDS = 60.0
|
||||||
|
|
@ -556,6 +554,7 @@ SPIDER_MIDDLEWARES_BASE = {
|
||||||
"scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
|
"scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
|
||||||
"scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
|
"scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
|
||||||
"scrapy.spidermiddlewares.depth.DepthMiddleware": 900,
|
"scrapy.spidermiddlewares.depth.DepthMiddleware": 900,
|
||||||
|
"scrapy.spidermiddlewares.metacopy.MetaCopyDetectionMiddleware": 1000,
|
||||||
# Spider side
|
# Spider side
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@ import warnings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING, Protocol, cast
|
from typing import TYPE_CHECKING, Protocol, cast
|
||||||
|
|
||||||
from zope.interface import implementer
|
# working around https://github.com/sphinx-doc/sphinx/issues/10400
|
||||||
from zope.interface.verify import verifyClass
|
from scrapy import Request, Spider # noqa: TC001
|
||||||
|
|
||||||
from scrapy.interfaces import ISpiderLoader
|
|
||||||
from scrapy.utils.misc import load_object, walk_modules_iter
|
from scrapy.utils.misc import load_object, walk_modules_iter
|
||||||
from scrapy.utils.spider import iter_spider_classes
|
from scrapy.utils.spider import iter_spider_classes
|
||||||
|
|
||||||
|
|
@ -18,7 +16,6 @@ if TYPE_CHECKING:
|
||||||
# typing.Self requires Python 3.11
|
# typing.Self requires Python 3.11
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from scrapy import Request, Spider
|
|
||||||
from scrapy.settings import BaseSettings
|
from scrapy.settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -26,28 +23,31 @@ def get_spider_loader(settings: BaseSettings) -> SpiderLoaderProtocol:
|
||||||
"""Get SpiderLoader instance from settings"""
|
"""Get SpiderLoader instance from settings"""
|
||||||
cls_path = settings.get("SPIDER_LOADER_CLASS")
|
cls_path = settings.get("SPIDER_LOADER_CLASS")
|
||||||
loader_cls = load_object(cls_path)
|
loader_cls = load_object(cls_path)
|
||||||
verifyClass(ISpiderLoader, loader_cls)
|
|
||||||
return cast("SpiderLoaderProtocol", loader_cls.from_settings(settings.frozencopy()))
|
return cast("SpiderLoaderProtocol", loader_cls.from_settings(settings.frozencopy()))
|
||||||
|
|
||||||
|
|
||||||
class SpiderLoaderProtocol(Protocol):
|
class SpiderLoaderProtocol(Protocol):
|
||||||
|
"""Protocol for spider loader implementations.
|
||||||
|
|
||||||
|
See :setting:`SPIDER_LOADER_CLASS`.
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_settings(cls, settings: BaseSettings) -> Self:
|
def from_settings(cls, settings: BaseSettings) -> Self:
|
||||||
"""Return an instance of the class for the given settings"""
|
"""Return an instance of the class for the given settings."""
|
||||||
|
|
||||||
def load(self, spider_name: str) -> type[Spider]:
|
def load(self, spider_name: str) -> type[Spider]:
|
||||||
"""Return the Spider class for the given spider name. If the spider
|
"""Return the spider class for the given spider name. If the spider
|
||||||
name is not found, it must raise a KeyError."""
|
name is not found, it must raise a :exc:`KeyError`."""
|
||||||
|
|
||||||
def list(self) -> list[str]:
|
def list(self) -> list[str]:
|
||||||
"""Return a list with the names of all spiders available in the
|
"""Return a list with the names of all spiders available in the
|
||||||
project"""
|
project."""
|
||||||
|
|
||||||
def find_by_request(self, request: Request) -> __builtins__.list[str]:
|
def find_by_request(self, request: Request) -> __builtins__.list[str]:
|
||||||
"""Return the list of spiders names that can handle the given request"""
|
"""Return the list of spiders names that can handle the given request."""
|
||||||
|
|
||||||
|
|
||||||
@implementer(ISpiderLoader)
|
|
||||||
class SpiderLoader:
|
class SpiderLoader:
|
||||||
"""
|
"""
|
||||||
SpiderLoader is a class which locates and loads spiders
|
SpiderLoader is a class which locates and loads spiders
|
||||||
|
|
@ -106,12 +106,18 @@ class SpiderLoader:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_settings(cls, settings: BaseSettings) -> Self:
|
def from_settings(cls, settings: BaseSettings) -> Self:
|
||||||
|
"""Create an instance of the class.
|
||||||
|
|
||||||
|
It's called with the current project settings, and it loads the spiders
|
||||||
|
found recursively in the modules of the :setting:`SPIDER_MODULES`
|
||||||
|
setting.
|
||||||
|
"""
|
||||||
return cls(settings)
|
return cls(settings)
|
||||||
|
|
||||||
def load(self, spider_name: str) -> type[Spider]:
|
def load(self, spider_name: str) -> type[Spider]:
|
||||||
"""
|
"""Return the spider class for the given spider name.
|
||||||
Return the Spider class for the given spider name. If the spider
|
|
||||||
name is not found, raise a KeyError.
|
If the spider name is not found, raise a :exc:`KeyError`.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self._spiders[spider_name]
|
return self._spiders[spider_name]
|
||||||
|
|
@ -121,19 +127,19 @@ class SpiderLoader:
|
||||||
def find_by_request(self, request: Request) -> list[str]:
|
def find_by_request(self, request: Request) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Return the list of spider names that can handle the given request.
|
Return the list of spider names that can handle the given request.
|
||||||
|
|
||||||
|
It will try to match the request's url against the domains of
|
||||||
|
the spiders.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
name for name, cls in self._spiders.items() if cls.handles_request(request)
|
name for name, cls in self._spiders.items() if cls.handles_request(request)
|
||||||
]
|
]
|
||||||
|
|
||||||
def list(self) -> list[str]:
|
def list(self) -> list[str]:
|
||||||
"""
|
"""Return a list with the names of all spiders available in the project."""
|
||||||
Return a list with the names of all spiders available in the project.
|
|
||||||
"""
|
|
||||||
return list(self._spiders.keys())
|
return list(self._spiders.keys())
|
||||||
|
|
||||||
|
|
||||||
@implementer(ISpiderLoader)
|
|
||||||
class DummySpiderLoader:
|
class DummySpiderLoader:
|
||||||
"""A dummy spider loader that does not load any spiders."""
|
"""A dummy spider loader that does not load any spiders."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ class BaseSpiderMiddleware:
|
||||||
|
|
||||||
@_warn_spider_arg
|
@_warn_spider_arg
|
||||||
def process_spider_output(
|
def process_spider_output(
|
||||||
self, response: Response, result: Iterable[Any], spider: Spider | None = None
|
self,
|
||||||
|
response: Response | None,
|
||||||
|
result: Iterable[Any],
|
||||||
|
spider: Spider | None = None,
|
||||||
) -> Iterable[Any]:
|
) -> Iterable[Any]:
|
||||||
for o in result:
|
for o in result:
|
||||||
if (o := self._get_processed(o, response)) is not None:
|
if (o := self._get_processed(o, response)) is not None:
|
||||||
|
|
@ -57,7 +60,7 @@ class BaseSpiderMiddleware:
|
||||||
@_warn_spider_arg
|
@_warn_spider_arg
|
||||||
async def process_spider_output_async(
|
async def process_spider_output_async(
|
||||||
self,
|
self,
|
||||||
response: Response,
|
response: Response | None,
|
||||||
result: AsyncIterator[Any],
|
result: AsyncIterator[Any],
|
||||||
spider: Spider | None = None,
|
spider: Spider | None = None,
|
||||||
) -> AsyncIterator[Any]:
|
) -> AsyncIterator[Any]:
|
||||||
|
|
|
||||||
|
|
@ -55,19 +55,24 @@ class DepthMiddleware(BaseSpiderMiddleware):
|
||||||
|
|
||||||
@_warn_spider_arg
|
@_warn_spider_arg
|
||||||
def process_spider_output(
|
def process_spider_output(
|
||||||
self, response: Response, result: Iterable[Any], spider: Spider | None = None
|
self,
|
||||||
|
response: Response | None,
|
||||||
|
result: Iterable[Any],
|
||||||
|
spider: Spider | None = None,
|
||||||
) -> Iterable[Any]:
|
) -> Iterable[Any]:
|
||||||
self._init_depth(response)
|
if response is not None:
|
||||||
|
self._init_depth(response)
|
||||||
yield from super().process_spider_output(response, result)
|
yield from super().process_spider_output(response, result)
|
||||||
|
|
||||||
@_warn_spider_arg
|
@_warn_spider_arg
|
||||||
async def process_spider_output_async(
|
async def process_spider_output_async(
|
||||||
self,
|
self,
|
||||||
response: Response,
|
response: Response | None,
|
||||||
result: AsyncIterator[Any],
|
result: AsyncIterator[Any],
|
||||||
spider: Spider | None = None,
|
spider: Spider | None = None,
|
||||||
) -> AsyncIterator[Any]:
|
) -> AsyncIterator[Any]:
|
||||||
self._init_depth(response)
|
if response is not None:
|
||||||
|
self._init_depth(response)
|
||||||
async for o in super().process_spider_output_async(response, result):
|
async for o in super().process_spider_output_async(response, result):
|
||||||
yield o
|
yield o
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from scrapy.spidermiddlewares.base import BaseSpiderMiddleware
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from scrapy.crawler import Crawler
|
||||||
|
from scrapy.http import Request, Response
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MetaCopyDetectionMiddleware(BaseSpiderMiddleware):
|
||||||
|
"""Warn when a spider yields a request with internal meta keys that should
|
||||||
|
not be copied from response.meta, or when two requests share the same meta
|
||||||
|
dict object.
|
||||||
|
|
||||||
|
Each warning is emitted at most once per crawl.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_INTERNAL_KEYS: frozenset[str] = frozenset(
|
||||||
|
{
|
||||||
|
"_auth_proxy",
|
||||||
|
"_dont_cache",
|
||||||
|
"_scheme_proxy",
|
||||||
|
"download_latency",
|
||||||
|
"redirect_reasons",
|
||||||
|
"redirect_times",
|
||||||
|
"redirect_ttl",
|
||||||
|
"redirect_urls",
|
||||||
|
"retry_times",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, crawler: Crawler) -> None:
|
||||||
|
super().__init__(crawler)
|
||||||
|
skip = frozenset(crawler.settings.getlist("META_COPY_WARN_SKIP_KEYS", []))
|
||||||
|
self._keys: frozenset[str] = self._INTERNAL_KEYS - skip
|
||||||
|
self._warned: bool = False
|
||||||
|
|
||||||
|
def get_processed_request(
|
||||||
|
self, request: Request, response: Response | None
|
||||||
|
) -> Request | None:
|
||||||
|
if response is None:
|
||||||
|
return request
|
||||||
|
|
||||||
|
if not self._warned:
|
||||||
|
found = self._keys & request.meta.keys()
|
||||||
|
if found:
|
||||||
|
spider_name = type(self.crawler.spider).__name__
|
||||||
|
logger.warning(
|
||||||
|
f"{spider_name} yielded a request containing internal "
|
||||||
|
f"meta keys that were likely copied from response.meta "
|
||||||
|
f"and should not be forwarded to new requests: "
|
||||||
|
f"{sorted(found)}. See the MetaCopyDetectionMiddleware "
|
||||||
|
f"documentation for more information. Source response: "
|
||||||
|
f"{response}, target request: {request}",
|
||||||
|
extra={"spider": self.crawler.spider},
|
||||||
|
)
|
||||||
|
self._warned = True
|
||||||
|
|
||||||
|
return request
|
||||||
|
|
@ -7,9 +7,11 @@ See documentation in docs/topics/spiders.rst
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
from scrapy import signals
|
from scrapy import signals
|
||||||
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||||
from scrapy.http import Request, Response
|
from scrapy.http import Request, Response
|
||||||
from scrapy.utils.trackref import object_ref
|
from scrapy.utils.trackref import object_ref
|
||||||
from scrapy.utils.url import url_is_from_spider
|
from scrapy.utils.url import url_is_from_spider
|
||||||
|
|
@ -66,6 +68,11 @@ class Spider(object_ref):
|
||||||
can use it directly (e.g. Spider.logger.info('msg')) or use any other
|
can use it directly (e.g. Spider.logger.info('msg')) or use any other
|
||||||
Python logger too.
|
Python logger too.
|
||||||
"""
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"Spider.log() is deprecated, use methods of Spider.logger instead.",
|
||||||
|
ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
self.logger.log(level, message, **kw)
|
self.logger.log(level, message, **kw)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from scrapy.exceptions import NotConfigured, NotSupported
|
from scrapy.exceptions import NotSupported
|
||||||
from scrapy.http import Response, TextResponse
|
from scrapy.http import Response, TextResponse
|
||||||
from scrapy.selector import Selector
|
from scrapy.selector import Selector
|
||||||
from scrapy.spiders import Spider
|
from scrapy.spiders import Spider
|
||||||
|
|
@ -76,11 +76,6 @@ class XMLFeedSpider(Spider):
|
||||||
yield from self.process_results(response, ret)
|
yield from self.process_results(response, ret)
|
||||||
|
|
||||||
def _parse(self, response: Response, **kwargs: Any) -> Any:
|
def _parse(self, response: Response, **kwargs: Any) -> Any:
|
||||||
if not hasattr(self, "parse_node"):
|
|
||||||
raise NotConfigured(
|
|
||||||
"You must define parse_node method in order to scrape this XML feed"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.adapt_response(response)
|
response = self.adapt_response(response)
|
||||||
nodes: Iterable[Selector]
|
nodes: Iterable[Selector]
|
||||||
if self.iterator == "iternodes":
|
if self.iterator == "iternodes":
|
||||||
|
|
@ -158,9 +153,5 @@ class CSVFeedSpider(Spider):
|
||||||
yield from self.process_results(response, ret)
|
yield from self.process_results(response, ret)
|
||||||
|
|
||||||
def _parse(self, response: Response, **kwargs: Any) -> Any:
|
def _parse(self, response: Response, **kwargs: Any) -> Any:
|
||||||
if not hasattr(self, "parse_row"):
|
|
||||||
raise NotConfigured(
|
|
||||||
"You must define parse_row method in order to scrape this CSV feed"
|
|
||||||
)
|
|
||||||
response = self.adapt_response(response)
|
response = self.adapt_response(response)
|
||||||
return self.parse_rows(response)
|
return self.parse_rows(response)
|
||||||
|
|
|
||||||
|
|
@ -101,9 +101,21 @@ class StatsCollector:
|
||||||
|
|
||||||
|
|
||||||
class MemoryStatsCollector(StatsCollector):
|
class MemoryStatsCollector(StatsCollector):
|
||||||
|
"""A simple stats collector that keeps the stats of the last scraping run
|
||||||
|
(for each spider) in memory, after they're closed. The stats can be
|
||||||
|
accessed through the :attr:`spider_stats` attribute, which is a dict keyed
|
||||||
|
by spider name.
|
||||||
|
|
||||||
|
This is the default stats collector used in Scrapy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
spider_stats: dict[str, StatsT]
|
||||||
|
"""A dict of dicts (keyed by spider name) containing the stats of the last
|
||||||
|
scraping run for each spider."""
|
||||||
|
|
||||||
def __init__(self, crawler: Crawler):
|
def __init__(self, crawler: Crawler):
|
||||||
super().__init__(crawler)
|
super().__init__(crawler)
|
||||||
self.spider_stats: dict[str, StatsT] = {}
|
self.spider_stats = {}
|
||||||
|
|
||||||
def _persist_stats(self, stats: StatsT) -> None:
|
def _persist_stats(self, stats: StatsT) -> None:
|
||||||
if self._crawler.spider:
|
if self._crawler.spider:
|
||||||
|
|
@ -111,6 +123,13 @@ class MemoryStatsCollector(StatsCollector):
|
||||||
|
|
||||||
|
|
||||||
class DummyStatsCollector(StatsCollector):
|
class DummyStatsCollector(StatsCollector):
|
||||||
|
"""A stats collector which does nothing but is very efficient (because it
|
||||||
|
does nothing). This stats collector can be set via the
|
||||||
|
:setting:`STATS_CLASS` setting, to disable stats collection in order to
|
||||||
|
improve performance. However, the performance penalty of stats collection
|
||||||
|
is usually marginal compared to other Scrapy workload like parsing pages.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_value(
|
def get_value(
|
||||||
self, key: str, default: Any = None, spider: Spider | None = None
|
self, key: str, default: Any = None, spider: Spider | None = None
|
||||||
) -> Any:
|
) -> Any:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
"""Boto/botocore helpers"""
|
"""Boto/botocore helpers"""
|
||||||
|
|
||||||
|
from importlib.util import find_spec
|
||||||
|
|
||||||
|
|
||||||
def is_botocore_available() -> bool:
|
def is_botocore_available() -> bool:
|
||||||
try:
|
return find_spec("botocore") is not None
|
||||||
import botocore # noqa: F401,PLC0415
|
|
||||||
|
|
||||||
return True
|
|
||||||
except ImportError:
|
|
||||||
return False
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ def _embed_ptpython_shell(
|
||||||
namespace: dict[str, Any] | None = None, banner: str = ""
|
namespace: dict[str, Any] | None = None, banner: str = ""
|
||||||
) -> EmbedFuncT:
|
) -> EmbedFuncT:
|
||||||
"""Start a ptpython shell"""
|
"""Start a ptpython shell"""
|
||||||
import ptpython.repl # noqa: PLC0415 # pylint: disable=import-error
|
import ptpython.repl # noqa: PLC0415
|
||||||
|
|
||||||
@wraps(_embed_ptpython_shell)
|
@wraps(_embed_ptpython_shell)
|
||||||
def wrapper(namespace: dict[str, Any] = namespace or {}, banner: str = "") -> None:
|
def wrapper(namespace: dict[str, Any] = namespace or {}, banner: str = "") -> None:
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ class DataAction(argparse.Action):
|
||||||
) -> None:
|
) -> None:
|
||||||
value = str(values)
|
value = str(values)
|
||||||
value = value.removeprefix("$")
|
value = value.removeprefix("$")
|
||||||
|
# curl merges repeated -d/--data/--data-raw options into a single body
|
||||||
|
# joined with "&"; mirror that instead of keeping only the last one.
|
||||||
|
previous = getattr(namespace, self.dest, None)
|
||||||
|
if previous is not None:
|
||||||
|
value = f"{previous}&{value}"
|
||||||
setattr(namespace, self.dest, value)
|
setattr(namespace, self.dest, value)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ import warnings
|
||||||
import weakref
|
import weakref
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import TYPE_CHECKING, Any, AnyStr, TypeVar, cast
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||||
|
|
||||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Container, Iterable
|
||||||
|
|
||||||
# typing.Self requires Python 3.11
|
# typing.Self requires Python 3.11
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
@ -44,22 +44,25 @@ class CaselessDict(dict): # type: ignore[type-arg]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
seq: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]] | None = None,
|
seq: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]]
|
||||||
|
| None = None,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if seq:
|
if seq:
|
||||||
self.update(seq)
|
self.update(seq)
|
||||||
|
|
||||||
def __getitem__(self, key: AnyStr) -> Any:
|
def __getitem__(self, key: str | bytes) -> Any:
|
||||||
return dict.__getitem__(self, self.normkey(key))
|
return dict.__getitem__(self, self.normkey(key))
|
||||||
|
|
||||||
def __setitem__(self, key: AnyStr, value: Any) -> None:
|
def __setitem__(self, key: str | bytes, value: Any) -> None:
|
||||||
dict.__setitem__(self, self.normkey(key), self.normvalue(value))
|
dict.__setitem__(self, self.normkey(key), self.normvalue(value))
|
||||||
|
|
||||||
def __delitem__(self, key: AnyStr) -> None:
|
def __delitem__(self, key: str | bytes) -> None:
|
||||||
dict.__delitem__(self, self.normkey(key))
|
dict.__delitem__(self, self.normkey(key))
|
||||||
|
|
||||||
def __contains__(self, key: AnyStr) -> bool: # type: ignore[override]
|
def __contains__(self, key: str | bytes) -> bool: # type: ignore[override]
|
||||||
return dict.__contains__(self, self.normkey(key))
|
return dict.__contains__(self, self.normkey(key))
|
||||||
|
|
||||||
has_key = __contains__
|
has_key = __contains__
|
||||||
|
|
@ -69,7 +72,7 @@ class CaselessDict(dict): # type: ignore[type-arg]
|
||||||
|
|
||||||
copy = __copy__
|
copy = __copy__
|
||||||
|
|
||||||
def normkey(self, key: AnyStr) -> AnyStr:
|
def normkey(self, key: str | bytes) -> str | bytes:
|
||||||
"""Method to normalize dictionary key access"""
|
"""Method to normalize dictionary key access"""
|
||||||
return key.lower()
|
return key.lower()
|
||||||
|
|
||||||
|
|
@ -77,23 +80,28 @@ class CaselessDict(dict): # type: ignore[type-arg]
|
||||||
"""Method to normalize values prior to be set"""
|
"""Method to normalize values prior to be set"""
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def get(self, key: AnyStr, def_val: Any = None) -> Any:
|
def get(self, key: str | bytes, def_val: Any = None) -> Any:
|
||||||
return dict.get(self, self.normkey(key), self.normvalue(def_val))
|
return dict.get(self, self.normkey(key), self.normvalue(def_val))
|
||||||
|
|
||||||
def setdefault(self, key: AnyStr, def_val: Any = None) -> Any:
|
def setdefault(self, key: str | bytes, def_val: Any = None) -> Any:
|
||||||
return dict.setdefault(self, self.normkey(key), self.normvalue(def_val))
|
return dict.setdefault(self, self.normkey(key), self.normvalue(def_val))
|
||||||
|
|
||||||
# doesn't fully implement MutableMapping.update()
|
# doesn't fully implement MutableMapping.update()
|
||||||
def update(self, seq: Mapping[AnyStr, Any] | Iterable[tuple[AnyStr, Any]]) -> None: # type: ignore[override]
|
def update( # type: ignore[override]
|
||||||
|
self,
|
||||||
|
seq: Mapping[str, Any]
|
||||||
|
| Mapping[bytes, Any]
|
||||||
|
| Iterable[tuple[str | bytes, Any]],
|
||||||
|
) -> None:
|
||||||
seq = seq.items() if isinstance(seq, Mapping) else seq
|
seq = seq.items() if isinstance(seq, Mapping) else seq
|
||||||
iseq = ((self.normkey(k), self.normvalue(v)) for k, v in seq)
|
iseq = ((self.normkey(k), self.normvalue(v)) for k, v in seq)
|
||||||
super().update(iseq)
|
super().update(iseq)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromkeys(cls, keys: Iterable[AnyStr], value: Any = None) -> Self: # type: ignore[override]
|
def fromkeys(cls, keys: Iterable[str | bytes], value: Any = None) -> Self: # type: ignore[override]
|
||||||
return cls((k, value) for k in keys) # type: ignore[misc]
|
return cls((k, value) for k in keys)
|
||||||
|
|
||||||
def pop(self, key: AnyStr, *args: Any) -> Any:
|
def pop(self, key: str | bytes, *args: Any) -> Any:
|
||||||
return dict.pop(self, self.normkey(key), *args)
|
return dict.pop(self, self.normkey(key), *args)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -132,6 +140,22 @@ class CaseInsensitiveDict(collections.UserDict[str | bytes, Any]):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<{self.__class__.__name__}: {super().__repr__()}>"
|
return f"<{self.__class__.__name__}: {super().__repr__()}>"
|
||||||
|
|
||||||
|
# UserDict.copy() shallow-copies the instance, which would share self._keys
|
||||||
|
# between the copy and the original.
|
||||||
|
def __copy__(self) -> Self:
|
||||||
|
new = self.__class__()
|
||||||
|
new.data = self.data.copy()
|
||||||
|
new._keys = self._keys.copy()
|
||||||
|
return new
|
||||||
|
|
||||||
|
copy = __copy__
|
||||||
|
|
||||||
|
# UserDict.__ior__ updates self.data directly, which would leave self._keys
|
||||||
|
# out of date.
|
||||||
|
def __ior__(self, other: Any) -> Self: # type: ignore[override,misc]
|
||||||
|
self.update(other)
|
||||||
|
return self
|
||||||
|
|
||||||
def _normkey(self, key: str | bytes) -> str | bytes:
|
def _normkey(self, key: str | bytes) -> str | bytes:
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
@ -189,8 +213,8 @@ class LocalWeakReferencedCache(weakref.WeakKeyDictionary[_KT, _VT | None]):
|
||||||
class SequenceExclude:
|
class SequenceExclude:
|
||||||
"""Object to test if an item is NOT within some sequence."""
|
"""Object to test if an item is NOT within some sequence."""
|
||||||
|
|
||||||
def __init__(self, seq: Sequence[Any]):
|
def __init__(self, seq: Container[Any]):
|
||||||
self.seq: Sequence[Any] = seq
|
self.seq: Container[Any] = seq
|
||||||
|
|
||||||
def __contains__(self, item: Any) -> bool:
|
def __contains__(self, item: Any) -> bool:
|
||||||
return item not in self.seq
|
return item not in self.seq
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,19 @@ _T = TypeVar("_T")
|
||||||
_P = ParamSpec("_P")
|
_P = ParamSpec("_P")
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def deprecated(use_instead: Callable[_P, _T]) -> Callable[_P, _T]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
def deprecated(
|
def deprecated(
|
||||||
use_instead: Any = None,
|
use_instead: str | None = None,
|
||||||
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]:
|
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ...
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated(
|
||||||
|
use_instead: Callable[_P, _T] | str | None = None,
|
||||||
|
) -> Callable[_P, _T] | Callable[[Callable[_P, _T]], Callable[_P, _T]]:
|
||||||
"""This is a decorator which can be used to mark functions
|
"""This is a decorator which can be used to mark functions
|
||||||
as deprecated. It will result in a warning being emitted
|
as deprecated. It will result in a warning being emitted
|
||||||
when the function is used."""
|
when the function is used."""
|
||||||
|
|
@ -38,8 +48,9 @@ def deprecated(
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
if callable(use_instead):
|
if callable(use_instead):
|
||||||
deco = deco(use_instead)
|
func = use_instead
|
||||||
use_instead = None
|
use_instead = None
|
||||||
|
return deco(func)
|
||||||
return deco
|
return deco
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,15 @@ _urlparse_cache: WeakKeyDictionary[Request | Response, ParseResult] = (
|
||||||
|
|
||||||
|
|
||||||
def urlparse_cached(request_or_response: Request | Response) -> ParseResult:
|
def urlparse_cached(request_or_response: Request | Response) -> ParseResult:
|
||||||
"""Return urlparse.urlparse caching the result, where the argument can be a
|
"""Return the result of parsing the URL of *request_or_response*, a
|
||||||
Request or Response object
|
:class:`~scrapy.Request` or :class:`~scrapy.http.Response` object, with
|
||||||
|
:func:`urllib.parse.urlparse`.
|
||||||
|
|
||||||
|
The result is cached, using a :class:`weakref.WeakKeyDictionary` keyed on
|
||||||
|
*request_or_response*, so that the URL of a given object is parsed only
|
||||||
|
once. Prefer this function over calling :func:`urllib.parse.urlparse` on
|
||||||
|
``request_or_response.url`` directly when the same URL may be parsed more
|
||||||
|
than once.
|
||||||
"""
|
"""
|
||||||
if request_or_response not in _urlparse_cache:
|
if request_or_response not in _urlparse_cache:
|
||||||
_urlparse_cache[request_or_response] = urlparse(request_or_response.url)
|
_urlparse_cache[request_or_response] = urlparse(request_or_response.url)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
@ -12,7 +11,6 @@ from lxml import etree
|
||||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||||
from scrapy.http import Response, TextResponse
|
from scrapy.http import Response, TextResponse
|
||||||
from scrapy.selector import Selector
|
from scrapy.selector import Selector
|
||||||
from scrapy.utils.python import re_rsearch
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable, Iterator
|
from collections.abc import Callable, Iterator
|
||||||
|
|
@ -20,64 +18,6 @@ if TYPE_CHECKING:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def xmliter(obj: Response | str | bytes, nodename: str) -> Iterator[Selector]:
|
|
||||||
"""Return a iterator of Selector's over all nodes of a XML document,
|
|
||||||
given the name of the node to iterate. Useful for parsing XML feeds.
|
|
||||||
|
|
||||||
obj can be:
|
|
||||||
- a Response object
|
|
||||||
- a unicode string
|
|
||||||
- a string encoded as utf-8
|
|
||||||
"""
|
|
||||||
warn(
|
|
||||||
(
|
|
||||||
"xmliter is deprecated and its use strongly discouraged because "
|
|
||||||
"it is vulnerable to ReDoS attacks. Use xmliter_lxml instead. See "
|
|
||||||
"https://github.com/scrapy/scrapy/security/advisories/GHSA-cc65-xxvf-f7r9"
|
|
||||||
),
|
|
||||||
ScrapyDeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
nodename_patt = re.escape(nodename)
|
|
||||||
|
|
||||||
DOCUMENT_HEADER_RE = re.compile(r"<\?xml[^>]+>\s*", re.DOTALL)
|
|
||||||
HEADER_END_RE = re.compile(rf"<\s*/{nodename_patt}\s*>", re.DOTALL)
|
|
||||||
END_TAG_RE = re.compile(r"<\s*/([^\s>]+)\s*>", re.DOTALL)
|
|
||||||
NAMESPACE_RE = re.compile(r"((xmlns[:A-Za-z]*)=[^>\s]+)", re.DOTALL)
|
|
||||||
text = _body_or_str(obj)
|
|
||||||
|
|
||||||
document_header_match = re.search(DOCUMENT_HEADER_RE, text)
|
|
||||||
document_header = (
|
|
||||||
document_header_match.group().strip() if document_header_match else ""
|
|
||||||
)
|
|
||||||
header_end_idx = re_rsearch(HEADER_END_RE, text)
|
|
||||||
header_end = text[header_end_idx[1] :].strip() if header_end_idx else ""
|
|
||||||
namespaces: dict[str, str] = {}
|
|
||||||
if header_end:
|
|
||||||
for tagname in reversed(re.findall(END_TAG_RE, header_end)):
|
|
||||||
assert header_end_idx
|
|
||||||
tag = re.search(
|
|
||||||
rf"<\s*{tagname}.*?xmlns[:=][^>]*>",
|
|
||||||
text[: header_end_idx[1]],
|
|
||||||
re.DOTALL,
|
|
||||||
)
|
|
||||||
if tag:
|
|
||||||
for x in re.findall(NAMESPACE_RE, tag.group()):
|
|
||||||
namespaces[x[1]] = x[0]
|
|
||||||
|
|
||||||
r = re.compile(rf"<{nodename_patt}[\s>].*?</{nodename_patt}>", re.DOTALL)
|
|
||||||
for match in r.finditer(text):
|
|
||||||
nodetext = (
|
|
||||||
document_header
|
|
||||||
+ match.group().replace(
|
|
||||||
nodename, f"{nodename} {' '.join(namespaces.values())}", 1
|
|
||||||
)
|
|
||||||
+ header_end
|
|
||||||
)
|
|
||||||
yield Selector(text=nodetext, type="xml")
|
|
||||||
|
|
||||||
|
|
||||||
def xmliter_lxml(
|
def xmliter_lxml(
|
||||||
obj: Response | str | bytes,
|
obj: Response | str | bytes,
|
||||||
nodename: str,
|
nodename: str,
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ def _chunk_iter(text: str, chunk_size: int) -> Iterable[tuple[str, int]]:
|
||||||
|
|
||||||
def re_rsearch(
|
def re_rsearch(
|
||||||
pattern: str | Pattern[str], text: str, chunk_size: int = 1024
|
pattern: str | Pattern[str], text: str, chunk_size: int = 1024
|
||||||
) -> tuple[int, int] | None:
|
) -> tuple[int, int] | None: # pragma: no cover
|
||||||
"""
|
"""
|
||||||
This function does a reverse search in a text using a regular expression
|
This function does a reverse search in a text using a regular expression
|
||||||
given in the attribute 'pattern'.
|
given in the attribute 'pattern'.
|
||||||
|
|
@ -127,6 +127,12 @@ def re_rsearch(
|
||||||
the start position of the match, and the ending (regarding the entire text).
|
the start position of the match, and the ending (regarding the entire text).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"re_rsearch() is deprecated and will be removed in a future Scrapy version.",
|
||||||
|
category=ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(pattern, str):
|
if isinstance(pattern, str):
|
||||||
pattern = re.compile(pattern)
|
pattern = re.compile(pattern)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import sys
|
import sys
|
||||||
from importlib.abc import MetaPathFinder
|
from importlib.abc import MetaPathFinder
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
@ -47,7 +48,19 @@ class ReactorImportHook(MetaPathFinder):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def install_reactor_import_hook() -> None:
|
def install_reactor_import_hook() -> ReactorImportHook:
|
||||||
"""Prevent importing :mod:`twisted.internet.reactor`."""
|
"""Prevent importing :mod:`twisted.internet.reactor`.
|
||||||
|
|
||||||
sys.meta_path.insert(0, ReactorImportHook())
|
The hook is returned and can later be uninstalled with
|
||||||
|
:func:`uninstall_reactor_import_hook()`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
hook = ReactorImportHook()
|
||||||
|
sys.meta_path.insert(0, hook)
|
||||||
|
return hook
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall_reactor_import_hook(hook: ReactorImportHook) -> None:
|
||||||
|
"""Uninstall the hook installed with :func:`install_reactor_import_hook()`."""
|
||||||
|
with contextlib.suppress(ValueError):
|
||||||
|
sys.meta_path.remove(hook)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ live_refs: defaultdict[type, WeakKeyDictionary[object, float]] = defaultdict(
|
||||||
|
|
||||||
|
|
||||||
class object_ref:
|
class object_ref:
|
||||||
"""Inherit from this class to a keep a record of live instances"""
|
"""Inherit from this class if you want to track live instances with the
|
||||||
|
``trackref`` module."""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
@ -60,12 +61,19 @@ def format_live_refs(ignore: Any = NoneType) -> str:
|
||||||
|
|
||||||
|
|
||||||
def print_live_refs(*a: Any, **kw: Any) -> None:
|
def print_live_refs(*a: Any, **kw: Any) -> None:
|
||||||
"""Print tracked objects"""
|
"""Print a report of live references, grouped by class name.
|
||||||
|
|
||||||
|
:param ignore: if given, all objects from the specified class (or tuple of
|
||||||
|
classes) will be ignored.
|
||||||
|
:type ignore: type or tuple
|
||||||
|
"""
|
||||||
print(format_live_refs(*a, **kw))
|
print(format_live_refs(*a, **kw))
|
||||||
|
|
||||||
|
|
||||||
def get_oldest(class_name: str) -> Any:
|
def get_oldest(class_name: str) -> Any:
|
||||||
"""Get the oldest object for a specific class name"""
|
"""Return the oldest object 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."""
|
||||||
for cls, wdict in live_refs.items():
|
for cls, wdict in live_refs.items():
|
||||||
if cls.__name__ == class_name:
|
if cls.__name__ == class_name:
|
||||||
if not wdict:
|
if not wdict:
|
||||||
|
|
@ -75,7 +83,9 @@ def get_oldest(class_name: str) -> Any:
|
||||||
|
|
||||||
|
|
||||||
def iter_all(class_name: str) -> Iterable[Any]:
|
def iter_all(class_name: str) -> Iterable[Any]:
|
||||||
"""Iterate over all objects of the same class by its 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."""
|
||||||
for cls, wdict in live_refs.items():
|
for cls, wdict in live_refs.items():
|
||||||
if cls.__name__ == class_name:
|
if cls.__name__ == class_name:
|
||||||
return wdict.keys()
|
return wdict.keys()
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from scrapy.utils.defer import deferred_from_coro
|
||||||
|
|
||||||
|
|
||||||
class UppercasePipeline:
|
class UppercasePipeline:
|
||||||
async def _open_spider(self, spider):
|
async def _open_spider(self, spider: Spider) -> None:
|
||||||
spider.logger.info("async pipeline opened!")
|
spider.logger.info("async pipeline opened!")
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from scrapy.crawler import AsyncCrawlerProcess
|
||||||
|
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
asyncioreactor.install(asyncio.get_event_loop())
|
asyncioreactor.install()
|
||||||
|
|
||||||
|
|
||||||
class NoRequestsSpider(scrapy.Spider):
|
class NoRequestsSpider(scrapy.Spider):
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class CachingHostnameResolverSpider(scrapy.Spider):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "caching_hostname_resolver_spider"
|
name = "caching_hostname_resolver_spider"
|
||||||
|
url: str
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
yield scrapy.Request(self.url)
|
yield scrapy.Request(self.url)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import scrapy
|
||||||
|
from scrapy.crawler import AsyncCrawlerProcess
|
||||||
|
from scrapy.utils.reactorless import ReactorImportHook
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def hook_count() -> int:
|
||||||
|
return sum(1 for finder in sys.meta_path if isinstance(finder, ReactorImportHook))
|
||||||
|
|
||||||
|
|
||||||
|
class NoRequestsSpider(scrapy.Spider):
|
||||||
|
name = "no_request"
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
self.logger.info(f"Hooks during run: {hook_count()}")
|
||||||
|
return
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
for _ in range(2):
|
||||||
|
process = AsyncCrawlerProcess(settings={"TWISTED_REACTOR_ENABLED": False})
|
||||||
|
process.crawl(NoRequestsSpider)
|
||||||
|
process.start()
|
||||||
|
|
||||||
|
logger.info(f"Hooks after runs: {hook_count()}")
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import scrapy
|
||||||
|
from scrapy.crawler import AsyncCrawlerProcess
|
||||||
|
from scrapy.utils.reactorless import ReactorImportHook
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NoRequestsSpider(scrapy.Spider):
|
||||||
|
name = "no_request"
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
return
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
process = AsyncCrawlerProcess(settings={"TWISTED_REACTOR_ENABLED": False})
|
||||||
|
|
||||||
|
process.crawl(NoRequestsSpider)
|
||||||
|
process.start()
|
||||||
|
|
||||||
|
hook_count = sum(1 for finder in sys.meta_path if isinstance(finder, ReactorImportHook))
|
||||||
|
logger.info(f"Hooks in sys.meta_path after start(): {hook_count}")
|
||||||
|
|
||||||
|
import twisted.internet.reactor # noqa: E402,F401,TID253
|
||||||
|
|
||||||
|
logger.info("Reactor imported after start()")
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import scrapy
|
||||||
|
from scrapy.crawler import AsyncCrawlerProcess
|
||||||
|
|
||||||
|
|
||||||
|
class NoRequestsSpider(scrapy.Spider):
|
||||||
|
name = "no_request"
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
return
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
process = AsyncCrawlerProcess(settings={"TWISTED_REACTOR_ENABLED": False})
|
||||||
|
process.crawl(NoRequestsSpider)
|
||||||
|
process.start()
|
||||||
|
|
||||||
|
process2 = AsyncCrawlerProcess()
|
||||||
|
process2.crawl(NoRequestsSpider)
|
||||||
|
process2.start()
|
||||||
|
|
@ -9,7 +9,7 @@ from scrapy.utils.defer import deferred_from_coro
|
||||||
|
|
||||||
|
|
||||||
class UppercasePipeline:
|
class UppercasePipeline:
|
||||||
async def _open_spider(self, spider):
|
async def _open_spider(self, spider: Spider) -> None:
|
||||||
spider.logger.info("async pipeline opened!")
|
spider.logger.info("async pipeline opened!")
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class CachingHostnameResolverSpider(scrapy.Spider):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "caching_hostname_resolver_spider"
|
name = "caching_hostname_resolver_spider"
|
||||||
|
url: str
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
yield scrapy.Request(self.url)
|
yield scrapy.Request(self.url)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue