Compare commits

...

92 Commits

Author SHA1 Message Date
Andrey Rakhmatullin e80f94fe8a
Tests for extensions.debug and extensions.memdebug. (#7749)
* Tests for extensions.debug and extensions.memdebug.

* Use a unique class in test_crawl_sets_stats.

* Don't assume a single log message.
2026-07-20 15:21:28 +02:00
Madan kumar 7faf20c6b5
Merge repeated curl -d/--data options in Request.from_curl() (#7728)
curl merges repeated -d/--data/--data-raw options into a single request
body joined with "&" (e.g. `curl -d a=1 -d b=2` sends `a=1&b=2`). scrapy's
DataAction kept only the last value, silently dropping the earlier ones, so
`Request.from_curl("curl -d a=1 -d b=2 ...")` produced a body of just `b=2`.
Accumulate the values with "&" to match curl. -H/--header and -b/--cookie
already accumulate via action="append"; -d was the only repeatable data flag
that didn't.
2026-07-20 15:18:55 +02:00
Andrey Rakhmatullin a591d15c04
Deprecate Spider.log(). (#7739) 2026-07-20 14:59:55 +02:00
Andrey Rakhmatullin 11d7a05a6f
Deprecate is_secure=False for s3://. (#7738) 2026-07-20 14:59:11 +02:00
Andrey Rakhmatullin d8ba1571e7
Assorted docs fixes, part 2. (#7725)
* Assorted docs fixes, part 2.

* Second pass.

* Address feedback.
2026-07-14 00:49:44 +05:00
Adrian b3670369b8
Solve the feed Path issue (#7674)
* Solve the feed Path issue

* Address additional, related issues
2026-07-13 22:46:41 +05:00
Andrey Rakhmatullin c9446931a8
Use external mitmproxy. (#7720) 2026-07-09 17:02:05 +02:00
Shaquon Kelley 9d9950df69
Fix silent data loss in CsvItemExporter (closes #4002) (#7651)
Adds state tracking to CsvItemExporter to detect when auto-detected fields drop data on items. Injects a warning instructing users to configure FEED_EXPORT_FIELDS. Includes unit tests. Corrects the false positive logic in stalled PR #7613.
2026-07-08 17:49:29 +05:00
Andrey Rakhmatullin 61f99f2df1
More granular untyped defs checking in tests. (#7712) 2026-07-08 10:05:45 +02:00
Andrey Rakhmatullin bdf3067935
Assorted docs fixes, part 1. (#7710) 2026-07-07 18:39:34 +05:00
Adrian c5ec881f1d
Update tools (#7724) 2026-07-07 17:27:26 +05:00
Adrian Chaves feb692f552 Bump version: 2.16.0 → 2.17.0 2026-07-07 12:26:52 +02:00
Adrian 9523e1ec8c
Merge commit from fork 2026-07-07 12:25:36 +02:00
Andrey Rakhmatullin 5ccc8dbe8a
Release notes for 2.17.0 (#7723)
* Cover 2.17.0 in the release notes up to dd10cb8.

* Fix a copypasted issue number.

* Address minor issues

* Minor fixes

* Cover additional deprecations introduced while improving test coverage

* versionadded/versionchanged improvements

* Add a versionadded to the give_up_log_level docstring

---------

Co-authored-by: Adrian Chaves <adrian@zyte.com>
2026-07-07 14:57:42 +05:00
Adrian dd10cb8e9a
LxmlLinkExtractor: add deny_attrs and deny_tags (#7679) 2026-07-05 16:46:22 +05:00
Fat-Coder-CN 870803b7fb
fix-utf16-response-test-on-big-endian-systems (#7508) 2026-07-01 12:16:01 +05:00
Adrian 361f689df7
Improve test coverage for crawler.py (#7682)
* Improve test coverage for crawler.py

* Silence mypy warnings

* Improve test coverage for crawler.py
2026-07-01 11:53:07 +05:00
Andrey Rakhmatullin fc5216f156
Clarify/cleanup Selector.type (#7704) 2026-07-01 08:50:32 +02:00
Adrian a6d6a48aa6
Keep Item fields in definition order (#7694) 2026-06-30 16:26:22 +02:00
Andrey Rakhmatullin 00098cb596
Assorted docstring fixes (#7698)
* Smaller fixes.

* Add more code blocks in docstrings.

* Queue stuff.

* Response stuff.

* Round 2.

* Address feedback.
2026-06-30 18:27:32 +05:00
Gaurav Yadav deb7e2861e
Fix _get_tag_name() crash for non-string elem.tag (#7686) (#7687)
* Fix _get_tag_name() crash for non-string elem.tag (#7686)

* test: improve non-string tag test accuracy and add direct unit test

* test: use minimal payload for non-string tag test

* test: address Adrian+syncrain PR feedback

- remove first docstring line (Adrian: unnecessary)
- replace weak isinstance assert with no-op call
- keep Cython function mention (Adrian: wording is great)

* test: replace silent call with assert results == [] per Adrian

* chore: drop accidental pyproject.toml and uv.lock changes
2026-06-30 17:49:47 +05:00
Adrian 6ad8a043ca
Fix genspider --editor (#7683) 2026-06-30 12:05:42 +02:00
Andrey Rakhmatullin 52147017b4
Cleanup cookie handling in request_to_curl() (#7684)
* Adjust CookiesT.

* Drop list of plain cookie dicts support from request_to_curl().

* Extract _decode_cookie().

* Unify logging.

* Extract _to_verbose_cookies().

* Sync and type hint _cookie_to_set_cookie_value() in tests.

* Add tests for bytes in Request.cookies.

* Type hint assertCookieValEqual().
2026-06-29 21:49:09 +05:00
tanishqtayade 6591cb756c
Fix to LocalCache with limit=0 still stores items rather than disabling the cache (#7663)
* Fix cell-var-from-loop in _send_catch_log_deferred

Replace lambda capturing receiver by reference with a default
argument to capture it by value, fixing a potential bug where
all deferred callbacks could reference the last receiver in the
loop instead of their respective receivers.

Removes the pylint disable comment and TODO that were suppressing
this issue.

* chore: trigger CI rerun for mypy network error

* Fix mypy error: pass receiver via addBoth args instead of lambda default

* style: apply pre-commit ruff formatting

* fix: LocalCache with limit=0 incorrectly stores items

When limit=0 is passed to LocalCache (e.g. when DNSCACHE_ENABLED=False),
the condition 'if self.limit' evaluates to False due to Python's truthiness
rules, causing items to be stored despite the cache being disabled.

This leads to an unbounded memory leak during long crawls when DNS caching
is explicitly disabled.

Fix changes the condition to 'if self.limit is not None' and adds an early
return when limit=0 to correctly handle the disabled cache case.

* test: add resolver-level regression tests for DNSCACHE_ENABLED=False

Add two regression tests that verify DNS results are not stored in
dnscache when DNSCACHE_ENABLED=False (cache_size=0):

- test_caching_hostname_resolver_dnscache_disabled_rejects_storage:
  verifies _CachingResolutionReceiver does not write to dnscache
  when CachingHostnameResolver is initialized with cache_size=0

- test_caching_threaded_resolver_dnscache_disabled_rejects_storage:
  verifies dnscache rejects storage at the LocalCache level
  when limit=0

* test: drop misleading threaded resolver test

it was writing directly to dnscache, not actually going through
CachingThreadedResolver at all. the threaded resolver already has
its own if dnscache.limit: guard so the bug doesnt affect it anyway.

keeping only the hostname resolver test which covers the actual
bug path through _CachingResolutionReceiver

* test: clean up comments in resolver test

* test: remove unnecessary comment
2026-06-29 21:37:06 +05:00
Javier 4b2b56f384
Update on Request objects (#7286) 2026-06-29 15:34:12 +02:00
Adrian 9559cbee1e
Solve timing issues with HTTP cache tests? (#7692) 2026-06-29 17:21:20 +05:00
greymoth 185d6b9a20
Fix request_to_curl() corrupting dict cookies with bytes keys/values (#7675)
* Fix request_to_curl() corrupting dict cookies with bytes keys/values

PR #7603 made the list-cookie branch of request_to_curl() bytes-safe via
_cookie_value_to_unicode(), but left the sibling dict-cookie branch using
raw f-string interpolation. A dict cookie with bytes keys/values (a
supported and common form, e.g. Request(url, cookies={b"k": b"v"})) was
rendered as --cookie 'b'k'=b'v'' instead of --cookie 'k=v', producing a
broken curl command.

Route the dict branch through the same _cookie_value_to_unicode() helper,
mirroring the list branch. Add a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: apply _cookie_value_to_unicode to list-branch cookie key/value

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:53:32 +05:00
Andrey Rakhmatullin 4b40d2d06a
Close various garbage collectible resources (mostly in tests) (#7644)
* Set TELNETCONSOLE_ENABLED=False in get_crawler().

* Make _get_console_and_portal() a context manager.

* Allow PYTHONTRACEMALLOC in tox.

* Close the event loop in test_custom_asyncio_loop_enabled_false().

* Close the handler in _uninstall_scrapy_root_handler().

* Close queues in test_squeues.py.

* Close empty queues in init_prios().

* Close PopenSpawn stdin and stdout explicitly.

* Silence the unclosed socket warning.

* Implement more methods in CustomStatsCollector.

* Link the Twisted issue.

* Close the connection on download_maxsize.

* Fix typing.

* Add a comment about CustomStatsCollector.

* Restore the coverage.

* Properly close fixture queues.

* More robust file closing in feed storages.

* Close the file in TestMarshalItemExporter.test_nonstring_types_item().

* Cleanup temporary file handling in test_exporters.py.

* Close the file in test_stats_file_failed().

* Use an explicit TextIOWrapper in XmlItemExporter.
2026-06-26 23:05:08 +05:00
Adrian cf5607f8bc
Undeprecated the basic FormRequest API (#7671) 2026-06-26 17:35:46 +02:00
Adrian edc353c975
Improve test coverage for shell/ (#7680)
* Improve test coverage for shell/

* Address test issues

* Make the shell config test more reliable on Windows
2026-06-26 19:40:36 +05:00
Adrian 1b940a75ac
Improve the item pipeline docs (#7676) 2026-06-26 16:18:22 +05:00
Adrian 7ab404c725
Add a documentation page about security (#7678) 2026-06-26 11:58:37 +02:00
Adrian 0ccddb4f61
Improve test coverage for contracts (#7677) 2026-06-26 11:47:30 +02:00
Adrian 0007676e8d
Improve test coverage for http/ (#7672)
* Improve test coverage for http/

* Use isinstance() instead of type()
2026-06-26 12:21:12 +05:00
Sriniketh24 d8d7de2339
Fix FTPDownloadHandler not closing FTP connection after download (#7667) 2026-06-26 04:26:59 +02:00
Adrian 74e6b61071
Make DOWNLOADER_CLIENT_TLS_CIPHERS=None enable Twisted defaults (#7665)
* Make DOWNLOADER_CLIENT_TLS_CIPHERS=None enable Twisted defaults

* Remove pointless comment

* Remove unnecessary mypy comments
2026-06-25 16:44:51 +05:00
Adrian c690eac770
Document “logging settings” as special settings (#7668) 2026-06-25 16:34:37 +05:00
Adrian 65e8954a06
Improve test coverage for spider middlewares (#7664) 2026-06-25 15:35:25 +05:00
Adrian dd4549e6f9
Improve test coverage for downloader middlewares (#7655)
* Improve test coverage for downloader middlewares

* Improve coverage further
2026-06-25 01:48:28 +05:00
Adrian b78ab3d6c8
Improve test coverage for settings/ (#7654) 2026-06-23 14:47:21 +05:00
smellslikeml fb3455304d
Add content-based image filtering example (#4954) 2026-06-23 09:04:19 +02:00
tanishqtayade f5a62a293f
Fix cell-var-from-loop bug in _send_catch_log_deferred (#7649) 2026-06-23 09:04:01 +02:00
Adrian f605defefc
Document scrapy-lint, remove start_url check (#7627)
* Document scrapy-lint, remove start_url check

* Remove the offsite URL check in favor of scrapy-lint
2026-06-22 20:12:16 +05:00
Adrian 7499d17e28
Improve test coverage for responsetypes.py (#7646)
* Improve test coverage for responsetypes.py

* Solve typing issues
2026-06-22 20:10:38 +05:00
Adrian b6596de317
Do not ignore CrawlerProcess settings (#7647)
* Do not ignore CrawlerProcess settings

* Update test_crawlerrunner_accepts_crawler
2026-06-22 20:10:10 +05:00
Shashank S. Khasare 75f05d4e80
Add test coverage for scrapy.utils.decorators (#7645) 2026-06-22 08:50:11 +02:00
Andrey Rakhmatullin c9f952c258
Refactor and improve catching warnings in tests. (#7643) 2026-06-19 21:04:34 +02:00
Adrian 6393858c7e
Improve coverage resolver (#7642)
* Improve test coverage for resolver.py

* Make the Twisted code more readable
2026-06-19 23:26:54 +05:00
Adrian d2842a205c
Improve coverage statscollectors (#7641) 2026-06-19 16:59:25 +02:00
Adrian 7b3f88f8ab
Improve test coverage for pqueues.py (#7640) 2026-06-19 18:28:32 +05:00
Adrian 699c93f6b2
Complete test coverage for linkextractors (#7639)
* Complete test coverage for linkextractors

* pylint: disable use-implicit-booleaness-not-comparison
2026-06-19 14:22:03 +05:00
Andrey Rakhmatullin 3f3cb885ed
Address warnings in tests. (#7637) 2026-06-19 06:55:02 +02:00
Vishal Gawade e5e48883b5
Deprecate ScrapyCommand.help() (#7633)
Co-authored-by: Vishal Gawade <vishalgw@amazon.com>
2026-06-19 06:49:32 +02:00
Lions-1 abbf3b95fc
tests: integration coverage for memusage extension (#7017) 2026-06-19 06:41:26 +02:00
Andrey Rakhmatullin fada8be1db
Fix Proxy-Authorization handling in BaseStreamingDownloadHandler (#7630) 2026-06-17 11:56:22 +02:00
JSap0914 b7824db573
Fix rel_has_nofollow to be case-insensitive per HTML spec (#7632)
The HTML specification states that link type keywords like 'nofollow'
are ASCII case-insensitive. Sites using rel="NoFollow" or rel="NOFOLLOW"
were incorrectly treated as follow links, causing Scrapy to crawl pages
it should skip.

Fix: add .lower() before the token split so all casing variants of
'nofollow' are correctly recognized.

Co-authored-by: JSap0914 <JSap0914@users.noreply.github.com>
2026-06-17 14:48:43 +05:00
Kushal Gupta 0a4a92e843
Fix image store ACL settings for S3 and GCS (#7614)
* Fix image store ACL settings for S3 and GCS

* Fix typing issues in ImagesPipeline ACL settings

* Fix typing issues in ImagesPipeline ACL settings

* Call parent _update_stores in ImagesPipeline
2026-06-16 00:24:44 +05:00
Adrian e74647572d
Test SignalManager.disconnect_all() (#7625) 2026-06-15 14:18:29 +05:00
Syncrain cfef12392a
Fix request_to_curl handling of verbose cookies dictionaries (#7603) 2026-06-15 08:46:30 +02:00
Andrey Rakhmatullin 4f241b73be
Fix deprecation warnings with pytest 9.1. (#7621) 2026-06-15 08:45:26 +02:00
Andrey Rakhmatullin 9893a7fac6
Fix deprecation warnings with pyOpenSSL 26.3.0. (#7619) 2026-06-15 08:44:33 +02:00
Nishita Matlani f63a3aff25
Fix strip_url() removing default port from password. (#7605) 2026-06-15 08:41:55 +02:00
Andrey Rakhmatullin 3a36955261
Assorted test fixes (#7616) 2026-06-15 08:39:58 +02:00
Adrian af30cfea12
Complete coverage for addons.py (#7612) 2026-06-12 16:15:36 +02:00
Adrian 983e6c1182
Link.__eq__: return NotImplemented instead of raising NotImplementedError (#7611) 2026-06-12 14:56:15 +02:00
Adrian b08ed1cf05
Add coverage for DummySpiderLoader.find_by_request() (#7608) 2026-06-12 13:13:17 +02:00
Adrian 7afc875081
Add coverage for Item.__delitem__() (#7610) 2026-06-12 15:32:38 +05:00
Adrian a8ffdcf851
Implement HTTP Auth settings and request metadata keys (#7590)
* Implement HTTP Auth settings and request metadata keys

* Fix doc-tests

* Require the domain to be set when using settings
2026-06-11 15:56:53 +05:00
Adrian c99f6e2209
Document the SPIDER_MODULES hack to lower startup time and memory usage (#7600) 2026-06-11 12:10:49 +02:00
Labib Bin Salam e0a7de7213
Document cb_kwargs/meta deep-copy when JOBDIR is set (#7573)
When JOBDIR is enabled, requests are serialized to disk with pickle, so
the objects stored in a request's cb_kwargs and meta are deep-copied on
the round trip. Callbacks then receive copies rather than the original
objects, which is easy to miss and can silently break code that relies on
sharing mutable state. Add a note to the request serialization section of
the jobs docs and a cross-referenced caution to the Request.cb_kwargs
attribute docs.

Closes #6120
2026-06-11 14:40:15 +05:00
Adrian e7f229f5b2
Clarify the first-class-citizen nature of asyncio support (#7599) 2026-06-11 10:51:46 +02:00
Andrey Rakhmatullin 4cb049cb15
Small docs fixes. (#7598) 2026-06-11 10:51:03 +02:00
msn ad4549673b
Remove set_asyncio_event_loop call from Shell._schedule (#7594)
Co-authored-by: Mayuresh <mayuresh@Mayureshs-MacBook-Air.local>
2026-06-11 10:45:25 +02:00
Syncrain ba28630c98
Validate reversed telnet console port ranges (#7593) 2026-06-11 12:47:00 +05:00
Adrian 2d0a898e2d
tox.ini: pinned, typing → min, mypy (#7595) 2026-06-11 12:10:48 +05:00
msn 93a627ba1c
Prevent scrapy shell from starting spider requests (#7557)
* Prevent scrapy shell from starting spider requests

* Run pre-commit fixes

* Update shell architecture notes

---------

Co-authored-by: Mayuresh <mayuresh@Mayureshs-MacBook-Air.local>
2026-06-11 00:59:03 +05:00
Andrey Rakhmatullin cd25ece58a
Fix the check for sync ITEM_PROCESSOR methods. (#7589)
* Fix the check for sync ITEM_PROCESSOR methods.

* Typo.
2026-06-11 00:22:30 +05:00
Andrey Rakhmatullin beb6c51c17
Update default_settings, deprecate CRAWLSPIDER_FOLLOW_LINKS. (#7592) 2026-06-11 00:22:09 +05:00
Andrey Rakhmatullin d59f9b644a
Replace _SettingsKeyT with str. (#7586) 2026-06-10 11:12:53 +02:00
Andrey Rakhmatullin ddafb37a7c
Assorted test fixes (#7585)
* Fix the mitmproxy junitxml file name.

* Fix TestAsyncCrawlerRunnerHasSpider regression.

* Update split out file refs.

* Update the test_counter_handler() docstring.
2026-06-10 11:01:06 +02:00
Andrey Rakhmatullin 4e956bd2de
Add support for HTTP/2 and for SOCKS proxies to HttpxDownloadHandler, improve handler docs (#7575)
* Add support for HTTP/2 and SOCKS proxies to HttpxDownloadHandler.

* Update the docs.

* Trim the tables.

* Restore lost wording.

* Handlers docs improvements and fixes.
2026-06-09 01:10:44 +05:00
Adnan Awan d2290c35c2
Allow configuring the log level of the retry give-up message (#7567)
* Allow configuring the log level of the retry give-up message

The "Gave up retrying ..." message was always logged at ERROR, which
inflates the log_count/ERROR stat even when giving up on a request is
expected (e.g. broad crawls hitting dead hosts).

Add a RETRY_GIVE_UP_LOG_LEVEL setting, a give_up_log_level argument to
get_retry_request(), and a give_up_log_level request meta key to override
it per request. The value accepts a level name ("WARNING") or number
(logging.WARNING). The default ("ERROR") preserves the previous behaviour.

Fixes #5297, fixes #4622

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address Adrian's review feedback: simplify and reorganize docs

- Move give_up_log_level reqmeta section before max_retry_times (alphabetical order)
- Simplify give_up_log_level section in request-response.rst (brief, links to setting)
- Simplify RETRY_GIVE_UP_LOG_LEVEL setting docs in downloader-middleware.rst
- Change 'When initialized' to 'When set' in max_retry_times section
- Docs now follow pattern of linking to complementary setting/meta key rather than duplicating information

Per Adrian's feedback: keep docs concise and cross-link setting ↔ meta key

* Address Adrian's code review feedback on RETRY_GIVE_UP_LOG_LEVEL feature

- Fix alphabetical ordering of give_up_log_level in request-response.rst
- Remove circular references: change 'see X for details' to 'see also X' pattern
- Simplify docstring for give_up_log_level parameter (4 lines → 2 lines)
- Update test domains from www.scrapytest.org to example.com

* Apply suggestion from @AdrianAtZyte

* Minor changes

* Fix example formatting.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Adrian <adrian@zyte.com>
Co-authored-by: Andrey Rakhmatullin <wrar@wrar.name>
2026-06-08 18:44:30 +05:00
Andrey Rakhmatullin d9e2f5fbf7
Add settings for TLS min/max version as a replacement for the TLS method (#6546) 2026-06-08 11:54:10 +02:00
Omkar Kabde 5149e2c679
docs: switch `scrapy.Item` examples to dataclasses (#7513)
* docs: switch `scrapy.Item` examples to dataclasses

* make serializer doc generic

* use modern type hints

* docs/spiders: switch TestItem consumer snippets to attribute access

Since the TestItem migration to @dataclass, the existing
item["id"] = ... assignments would raise TypeError on copy-paste.
Switch to item.id = ... to match the new dataclass declaration.

The snippets sit under .. skip: next so docs-tests still pass either
way, but the change keeps the examples runnable for readers.
2026-06-08 12:40:28 +05:00
Andrey Rakhmatullin 58af57a3ea
Work around coverage slowness on Python 3.14. (#7574) 2026-06-05 15:34:02 +02:00
Adrian b2d8b06be6
Support sending requests with "unsafe" URLs (#7473) 2026-06-05 11:51:03 +02:00
Ayush Singh 13c1c1faf8
Close GCS feed temp files after upload (#7546) 2026-06-04 20:11:50 +05:00
Ethan Kuo df2f3d708e
fix open_in_browser() logic issue plus add new tests (#7506) 2026-06-04 16:24:50 +02:00
Andrey Rakhmatullin fed75a6c76
Refactor test_crawl.py and test_crawler.py. (#7566) 2026-06-03 14:48:12 +02:00
Andrey Rakhmatullin 90deebe75e
Convert tests that fail with testfixtures 12.0.0 (#7545) 2026-06-03 09:16:24 +02:00
Fardin Alizadeh 44406806f8
fix typos (#7564) 2026-06-02 11:04:38 +02:00
SpiliosDmk 4a16550859
DOC -> Add missing documentation for CloseSpider & CoreStats (#7421)
* DOC -> Add missing documentation for CloseSpider & CoreStats

* Apply the suggestion from the PR review.

---------

Co-authored-by: Andrey Rakhmatullin <wrar@wrar.name>
2026-05-20 13:27:24 +05:00
265 changed files with 7883 additions and 2924 deletions

View File

@ -22,10 +22,10 @@ jobs:
TOXENV: pylint TOXENV: pylint
- python-version: "3.10" - python-version: "3.10"
env: env:
TOXENV: typing TOXENV: mypy
- python-version: "3.10" - python-version: "3.10"
env: env:
TOXENV: typing-tests TOXENV: mypy-tests
# Keep in sync with pyproject.toml tool.sphinx-scrapy.python-version. # Keep in sync with pyproject.toml tool.sphinx-scrapy.python-version.
- python-version: "3.14" - python-version: "3.14"
env: env:

View File

@ -45,26 +45,26 @@ jobs:
env: env:
TOXENV: pypy3 TOXENV: pypy3
# pinned deps # min deps
- python-version: "3.10.19" - python-version: "3.10.19"
env: env:
TOXENV: pinned TOXENV: min
- python-version: "3.10.19" - python-version: "3.10.19"
env: env:
TOXENV: default-reactor-pinned TOXENV: min-default-reactor
- python-version: "3.10.19" - python-version: "3.10.19"
env: env:
TOXENV: no-reactor-pinned TOXENV: min-no-reactor
# pinned due to https://github.com/pypy/pypy/issues/5388 # pinned due to https://github.com/pypy/pypy/issues/5388
- python-version: pypy3.11-7.3.20 - python-version: pypy3.11-7.3.20
env: env:
TOXENV: pypy3-pinned TOXENV: min-pypy3
- python-version: "3.10.19" - python-version: "3.10.19"
env: env:
TOXENV: extra-deps-pinned TOXENV: min-extra-deps
- python-version: "3.10.19" - python-version: "3.10.19"
env: env:
TOXENV: botocore-pinned TOXENV: min-botocore
- python-version: "3.14" - python-version: "3.14"
env: env:
@ -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
@ -92,11 +89,14 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install system libraries - name: Install system libraries
if: contains(matrix.python-version, 'pypy') || contains(matrix.env.TOXENV, 'pinned') if: contains(matrix.python-version, 'pypy') || contains(matrix.env.TOXENV, 'min')
run: | run: |
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: |

View File

@ -41,13 +41,13 @@ jobs:
env: env:
TOXENV: no-reactor TOXENV: no-reactor
# pinned deps # min deps
- python-version: "3.10.11" - python-version: "3.10.11"
env: env:
TOXENV: pinned TOXENV: min
- python-version: "3.10.11" - python-version: "3.10.11"
env: env:
TOXENV: extra-deps-pinned TOXENV: min-extra-deps
- python-version: "3.14" - python-version: "3.14"
env: env:

View File

@ -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:
@ -27,6 +27,6 @@ repos:
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/scrapy/sphinx-scrapy - repo: https://github.com/scrapy/sphinx-scrapy
rev: 0.8.6 rev: 0.8.8
hooks: hooks:
- id: sphinx-scrapy - id: sphinx-scrapy

View File

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

View File

@ -11,7 +11,7 @@ from scrapy.utils.reactor import set_asyncio_event_loop_policy
from scrapy.utils.reactorless import install_reactor_import_hook from 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
@ -24,13 +24,13 @@ def _py_files(folder):
collect_ignore = [ collect_ignore = [
# may need extra deps # may need extra deps
"docs/_ext", "docs/_ext",
# contains scripts to be run by tests/test_crawler.py::AsyncCrawlerProcessSubprocess # contains scripts to be run by tests/test_crawler_subprocess.py::AsyncCrawlerProcessSubprocess
*_py_files("tests/AsyncCrawlerProcess"), *_py_files("tests/AsyncCrawlerProcess"),
# contains scripts to be run by tests/test_crawler.py::AsyncCrawlerRunnerSubprocess # contains scripts to be run by tests/test_crawler_subprocess.py::AsyncCrawlerRunnerSubprocess
*_py_files("tests/AsyncCrawlerRunner"), *_py_files("tests/AsyncCrawlerRunner"),
# contains scripts to be run by tests/test_crawler.py::CrawlerProcessSubprocess # contains scripts to be run by tests/test_crawler_subprocess.py::CrawlerProcessSubprocess
*_py_files("tests/CrawlerProcess"), *_py_files("tests/CrawlerProcess"),
# contains scripts to be run by tests/test_crawler.py::CrawlerRunnerSubprocess # contains scripts to be run by tests/test_crawler_subprocess.py::CrawlerRunnerSubprocess
*_py_files("tests/CrawlerRunner"), *_py_files("tests/CrawlerRunner"),
] ]
@ -74,27 +74,19 @@ def mockserver() -> Generator[MockServer]:
@pytest.fixture # function scope because it modifies os.environ @pytest.fixture # function scope because it modifies os.environ
def mitm_proxy_server(monkeypatch: pytest.MonkeyPatch) -> Generator[MitmProxy]: def proxy_server(
proxy = MitmProxy() request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
) -> Generator[str]:
kind = request.param
proxy = MitmProxy(mode="socks5" if kind == "socks5" else None)
url = proxy.start() url = proxy.start()
if kind == "https":
url = url.replace("http://", "https://")
monkeypatch.setenv("http_proxy", url) monkeypatch.setenv("http_proxy", url)
monkeypatch.setenv("https_proxy", url) monkeypatch.setenv("https_proxy", url)
try: try:
yield proxy yield kind
finally:
proxy.stop()
@pytest.fixture # function scope because it modifies os.environ
def mitm_proxy_server_https(monkeypatch: pytest.MonkeyPatch) -> Generator[MitmProxy]:
proxy = MitmProxy()
url = proxy.start().replace("http://", "https://")
monkeypatch.setenv("http_proxy", url)
monkeypatch.setenv("https_proxy", url)
try:
yield proxy
finally: finally:
proxy.stop() proxy.stop()
@ -135,7 +127,6 @@ def pytest_runtest_setup(item):
"uvloop", "uvloop",
"botocore", "botocore",
"boto3", "boto3",
"mitmproxy",
] ]
for module in optional_deps: for module in optional_deps:
@ -145,6 +136,9 @@ def pytest_runtest_setup(item):
except ImportError: except ImportError:
pytest.skip(f"{module} is not installed") pytest.skip(f"{module} is not installed")
if item.get_closest_marker("requires_mitmproxy") and mitmdump_cmd() is None:
pytest.skip("mitmdump is not available")
# Generate localhost certificate files, needed by some tests # Generate localhost certificate files, needed by some tests
generate_keys() generate_keys()

View File

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

View File

@ -1,6 +1,6 @@
{% extends "!layout.html" %} {% extends "!layout.html" %}
{# Overriden to include a link to scrapy.org, not just to the docs root #} {# Overridden to include a link to scrapy.org, not just to the docs root #}
{%- block sidebartitle %} {%- block sidebartitle %}
{# the logo helper function was removed in Sphinx 6 and deprecated since Sphinx 4 #} {# the logo helper function was removed in Sphinx 6 and deprecated since Sphinx 4 #}

View File

@ -158,6 +158,7 @@ scrapy_intersphinx_enable = [
"itemloaders", "itemloaders",
"parsel", "parsel",
"pytest", "pytest",
"scrapy-lint",
"sphinx", "sphinx",
"tox", "tox",
"twisted", "twisted",

View File

@ -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/

View File

@ -82,10 +82,18 @@ to steal from us!
Does Scrapy work with HTTP proxies? Does Scrapy work with HTTP proxies?
----------------------------------- -----------------------------------
Yes. Support for HTTP proxies is provided (since Scrapy 0.8) through the HTTP Yes. Support for HTTP proxies is provided through the HTTP Proxy downloader
Proxy downloader middleware. See middleware. See
:class:`~scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware`. :class:`~scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware`.
Does Scrapy work with SOCKS proxies?
------------------------------------
Yes, when using
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`. See
:class:`~scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware` and the
handler documentation.
How can I scrape an item with attributes in different pages? How can I scrape an item with attributes in different pages?
------------------------------------------------------------ ------------------------------------------------------------
@ -277,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
@ -352,15 +361,19 @@ 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?
----------------------------------- -----------------------------------
Yes, by setting :setting:`TWISTED_DNS_RESOLVER` to ``scrapy.resolver.CachingHostnameResolver``. Yes, but when using
:class:`~scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler` or
:class:`~scrapy.core.downloader.handlers.http2.H2DownloadHandler` you need to
set :setting:`TWISTED_DNS_RESOLVER` to ``scrapy.resolver.CachingHostnameResolver``.
Note that by doing so, you lose the ability to set a specific timeout for DNS requests Note that by doing so, you lose the ability to set a specific timeout for DNS requests
(the value of the :setting:`DNS_TIMEOUT` setting is ignored). (the value of the :setting:`DNS_TIMEOUT` setting is ignored).
@ -371,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:
@ -398,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

View File

@ -24,7 +24,7 @@ Having trouble? We'd like to help!
* Ask or search questions in `StackOverflow using the scrapy tag`_. * Ask or search questions in `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.
@ -151,6 +151,7 @@ Solving specific problems
topics/debug topics/debug
topics/contracts topics/contracts
topics/practices topics/practices
topics/security
topics/broad-crawls topics/broad-crawls
topics/developer-tools topics/developer-tools
topics/dynamic-content topics/dynamic-content
@ -175,6 +176,10 @@ Solving specific problems
:doc:`topics/practices` :doc:`topics/practices`
Get familiar with some Scrapy common practices. Get familiar with some Scrapy common practices.
:doc:`topics/security`
Understand the security implications of Scrapy defaults and how to harden
them.
:doc:`topics/broad-crawls` :doc:`topics/broad-crawls`
Tune Scrapy for crawling a lot domains in parallel. Tune Scrapy for crawling a lot domains in parallel.

View File

@ -230,8 +230,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:

View File

@ -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:

View File

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

View File

@ -5,4 +5,4 @@ sphinx
sphinx-notfound-page sphinx-notfound-page
sphinx-rtd-theme sphinx-rtd-theme
sphinx-rtd-dark-mode sphinx-rtd-dark-mode
sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@0.8.6 sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@0.8.8

View File

@ -153,7 +153,7 @@ sphinx-rtd-theme==3.1.0
# via # via
# -r docs/requirements.in # -r docs/requirements.in
# sphinx-rtd-dark-mode # sphinx-rtd-dark-mode
sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@b1d55db4d16a5425fc68576d63519bbfe26dd9c0 sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@c0b2ac815afc3cb8857d575cecb5d55c05e6b737
# via -r docs/requirements.in # via -r docs/requirements.in
sphinx-sitemap==2.9.0 sphinx-sitemap==2.9.0
# via sphinx-scrapy # via sphinx-scrapy

View File

@ -168,7 +168,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"

View File

@ -4,19 +4,27 @@
asyncio asyncio
======= =======
Scrapy has partial support for :mod:`asyncio`. After you :ref:`install the Scrapy supports :mod:`asyncio` natively. New projects created with
asyncio reactor <install-asyncio>`, you may use :mod:`asyncio` and :command:`startproject` have asyncio enabled by default, and you can use
:mod:`asyncio`-powered libraries in any :doc:`coroutine <coroutines>`. :mod:`asyncio` and :mod:`asyncio`-powered libraries in any :doc:`coroutine
<coroutines>`.
The rest of this page covers advanced topics. If you are starting a new project,
no additional setup is needed.
.. _install-asyncio: .. _install-asyncio:
Installing the asyncio reactor Configuring the asyncio reactor
============================== ===============================
To enable :mod:`asyncio` support, your :setting:`TWISTED_REACTOR` setting needs New projects generated with :command:`startproject` have the asyncio
to be set to ``'twisted.internet.asyncioreactor.AsyncioSelectorReactor'``, reactor configured by default. No manual setup is needed.
which is the default value.
The :setting:`TWISTED_REACTOR` setting controls which Twisted reactor Scrapy
uses. Its default value is
``'twisted.internet.asyncioreactor.AsyncioSelectorReactor'``, which enables
:mod:`asyncio` support.
If you are using :class:`~scrapy.crawler.AsyncCrawlerRunner` or If you are using :class:`~scrapy.crawler.AsyncCrawlerRunner` or
:class:`~scrapy.crawler.CrawlerRunner`, you also need to :class:`~scrapy.crawler.CrawlerRunner`, you also need to
@ -97,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
@ -182,7 +193,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)
@ -307,8 +318,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

View File

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

View File

@ -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

View File

@ -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
@ -267,6 +269,6 @@ You can also send multiple requests in parallel:
responses = await asyncio.gather(*tasks) responses = await asyncio.gather(*tasks)
yield { yield {
"h1": response.css("h1::text").get(), "h1": response.css("h1::text").get(),
"price": responses[0][1].css(".price::text").get(), "price": responses[0].css(".price::text").get(),
"price2": responses[1][1].css(".color::text").get(), "color": responses[1].css(".color::text").get(),
} }

View File

@ -39,6 +39,10 @@ for additional schemes and to replace or disable default ones:
"sftp": "my.download_handlers.SftpHandler", "sftp": "my.download_handlers.SftpHandler",
} }
.. seealso:: :ref:`security-unencrypted-protocols` and
:ref:`security-local-resources`, for the security implications of the
default ``http``, ``ftp``, ``file`` and ``data`` handlers.
Replacing HTTP(S) download handlers Replacing HTTP(S) download handlers
----------------------------------- -----------------------------------
@ -85,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.
@ -130,44 +134,39 @@ these exceptions.
.. _download-handlers-ref: .. _download-handlers-ref:
Built-in download handlers reference Built-in HTTP download handlers reference
==================================== =========================================
DataURIDownloadHandler Scrapy ships several handlers for HTTP and HTTPS requests. While all of them
---------------------- support basic features, they may differ in support of specific Scrapy features
and settings and HTTP protocol features. See the documentation of specific
handlers and specific settings for more information. Additionally, as the
underlying HTTP client implementations differ between handlers, the behavior of
specific websites may be different when doing the same Scrapy requests but
using different handlers.
.. autoclass:: scrapy.core.downloader.handlers.datauri.DataURIDownloadHandler Here is a comparison of some features of the built-in HTTP handlers, see the
individual handler docs for more differences:
| Supported scheme: ``data``. ================== ================= ===================== ====================
| Lazy: no. Feature H2DownloadHandler HTTP11DownloadHandler HttpxDownloadHandler
================== ================= ===================== ====================
Requires asyncio No No Yes
Requires a reactor Yes Yes No
HTTP/1.1 No Yes Yes
HTTP/2 Yes No Yes
TLS implementation ``cryptography`` ``cryptography`` Stdlib ``ssl``
HTTP proxies No Yes Yes
SOCKS proxies No No Yes
================== ================= ===================== ====================
This handler supports RFC 2397 ``data:content/type;base64,`` data URIs. You can find additional HTTP download handlers in the
scrapy-download-handlers-incubator_ package. This package is made by the Scrapy
developers and contains experimental handlers that may be included in some
later Scrapy version but can already be used. Please refer to the documentation
of this package for more information.
FileDownloadHandler .. _scrapy-download-handlers-incubator: https://github.com/scrapy-plugins/scrapy-download-handlers-incubator
-------------------
.. autoclass:: scrapy.core.downloader.handlers.file.FileDownloadHandler
| Supported scheme: ``file``.
| Lazy: no.
This handler supports ``file:///path`` local file URIs. It doesn't
support remote files.
FTPDownloadHandler
------------------
.. autoclass:: scrapy.core.downloader.handlers.ftp.FTPDownloadHandler
| Supported scheme: ``ftp``.
| Lazy: no.
This handler supports ``ftp://host/path`` FTP URIs.
It's implemented using :mod:`twisted.protocols.ftp`.
.. note::
This handler is not supported when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
.. _twisted-http2-handler: .. _twisted-http2-handler:
@ -177,7 +176,9 @@ H2DownloadHandler
.. autoclass:: scrapy.core.downloader.handlers.http2.H2DownloadHandler .. autoclass:: scrapy.core.downloader.handlers.http2.H2DownloadHandler
| Supported scheme: ``https``. | Supported scheme: ``https``.
| Lazy: yes. | :ref:`Lazy <lazy-download-handlers>`: yes.
| :ref:`Requires asyncio support <using-asyncio>`: no.
| :ref:`Requires a Twisted reactor <asyncio-without-reactor>`: yes.
This handler supports ``https://host/path`` URLs and uses the HTTP/2 protocol This handler supports ``https://host/path`` URLs and uses the HTTP/2 protocol
for them. for them.
@ -196,63 +197,96 @@ If you want to use this handler you need to replace the default one for the
"https": "scrapy.core.downloader.handlers.http2.H2DownloadHandler", "https": "scrapy.core.downloader.handlers.http2.H2DownloadHandler",
} }
Features and limitations
^^^^^^^^^^^^^^^^^^^^^^^^
.. warning:: .. warning::
This handler is experimental, and not yet recommended for production This handler is experimental, and not yet recommended for production
environments. Future Scrapy versions may introduce related changes without environments. Future Scrapy versions may introduce related changes without
a deprecation period or warning. a deprecation period or warning.
.. note:: =========================== ================================================
HTTP proxies No (not implemented)
SOCKS proxies No (not supported by the library)
HTTP/2 Yes
``response.certificate`` :class:`twisted.internet.ssl.Certificate` object
Per-request ``bindaddress`` Yes
TLS implementation ``pyOpenSSL``/``cryptography``
=========================== ================================================
Known limitations of the HTTP/2 implementation in this handler include: Other limitations:
- No support for proxies. - No support for HTTP/1.1.
- No support for HTTP/2 Cleartext (h2c), since no major browser supports - IPv6 support requires setting :setting:`TWISTED_DNS_RESOLVER`
HTTP/2 unencrypted (refer `http2 faq`_). to ``scrapy.resolver.CachingHostnameResolver``.
- No setting to specify a maximum `frame size`_ larger than the default - No support for the :signal:`bytes_received` and :signal:`headers_received`
value, 16384. Connections to servers that send a larger frame will signals.
fail.
- No support for `server pushes`_, which are ignored. Known limitations of the HTTP/2 support:
- No support for the :signal:`bytes_received` and - No support for HTTP/2 Cleartext (h2c), since no major browser supports
:signal:`headers_received` signals. HTTP/2 unencrypted (refer `http2 faq`_).
- No setting to specify a maximum `frame size`_ larger than the default
value, 16384. Connections to servers that send a larger frame will fail.
- No support for `server pushes`_, which are ignored.
.. _frame size: https://datatracker.ietf.org/doc/html/rfc7540#section-4.2 .. _frame size: https://datatracker.ietf.org/doc/html/rfc7540#section-4.2
.. _http2 faq: https://http2.github.io/faq/#does-http2-require-encryption .. _http2 faq: https://http2.github.io/faq/#does-http2-require-encryption
.. _server pushes: https://datatracker.ietf.org/doc/html/rfc7540#section-8.2 .. _server pushes: https://datatracker.ietf.org/doc/html/rfc7540#section-8.2
.. note::
This handler is not supported when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
HTTP11DownloadHandler HTTP11DownloadHandler
--------------------- ---------------------
.. autoclass:: scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler .. autoclass:: scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler
| Supported schemes: ``http``, ``https``. | Supported schemes: ``http``, ``https``.
| Lazy: no. | :ref:`Lazy <lazy-download-handlers>`: no.
| :ref:`Requires asyncio support <using-asyncio>`: no.
| :ref:`Requires a Twisted reactor <asyncio-without-reactor>`: yes.
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 protocol for them. uses the HTTP/1.1 protocol for them.
It's implemented using :mod:`twisted.web.client`. It's implemented using :mod:`twisted.web.client`.
.. note:: Features and limitations
This handler is not supported when :setting:`TWISTED_REACTOR_ENABLED` is ``False``. ^^^^^^^^^^^^^^^^^^^^^^^^
=========================== ================================================
HTTP proxies Yes
SOCKS proxies No (not supported by the library)
HTTP/2 No (implemented as a separate handler)
``response.certificate`` :class:`twisted.internet.ssl.Certificate` object
Per-request ``bindaddress`` Yes
TLS implementation ``pyOpenSSL``/``cryptography``
=========================== ================================================
Other limitations:
- IPv6 support requires setting :setting:`TWISTED_DNS_RESOLVER`
to ``scrapy.resolver.CachingHostnameResolver``.
- HTTPS proxies to HTTPS destinations are not supported.
HttpxDownloadHandler HttpxDownloadHandler
-------------------- --------------------
.. versionadded:: 2.15.0
.. autoclass:: scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler .. autoclass:: scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler
| Supported schemes: ``http``, ``https``. | Supported schemes: ``http``, ``https``.
| Lazy: no. | :ref:`Lazy <lazy-download-handlers>`: no.
| :ref:`Requires asyncio support <using-asyncio>`: yes.
| :ref:`Requires a Twisted reactor <asyncio-without-reactor>`: no.
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 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 ``httpx`` library and needs it to be installed.
@ -266,30 +300,83 @@ If you want to use this handler you need to replace the default ones for the
"https": "scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler", "https": "scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler",
} }
Features and limitations
^^^^^^^^^^^^^^^^^^^^^^^^
.. warning:: .. warning::
This handler is experimental, and not yet recommended for production This handler is experimental, and not yet recommended for production
environments. Future Scrapy versions may introduce related changes without environments. Future Scrapy versions may introduce related changes without
a deprecation period or warning or even remove it altogether. a deprecation period or warning or even remove it altogether.
.. note:: =========================== =======================================
HTTP proxies Yes
SOCKS proxies Yes (SOCKS5; requires ``httpx[socks]``)
HTTP/2 Yes (requires ``httpx[http2]``)
``response.certificate`` DER bytes
Per-request ``bindaddress`` No (not supported by the library)
TLS implementation Standard library ``ssl``
=========================== =======================================
As this handler is based on a different HTTP client implementation compared Other limitations:
to :class:`~.HTTP11DownloadHandler`, it's expected that its behavior on
some websites may be different. Additionally, these are the Scrapy features
that are explicitly not supported when using it:
- Per-request bind address support (the :reqmeta:`bindaddress` meta key). - The handler creates a separate connection pool for each proxy URL (due to
The global :setting:`DOWNLOAD_BIND_ADDRESS` setting is supported but the limitations of ``httpx``) which may lead to higher resource usage when
port number, if specified, will be ignored. using proxy rotation.
- The :setting:`DOWNLOADER_CLIENT_TLS_METHOD` setting. .. setting:: HTTPX_HTTP2_ENABLED
- Settings specific to the Twisted networking or HTTP implementation, like HTTPX_HTTP2_ENABLED
:setting:`DNS_RESOLVER`. ^^^^^^^^^^^^^^^^^^^
- Using :ref:`non-asyncio reactors <disable-asyncio>` (``httpx`` requires .. versionadded:: 2.17.0
``asyncio``).
Default: ``False``
Whether to enable HTTP/2 support in this handler. The ``httpx[http2]`` extra
needs to be installed if you want to enable this setting.
Built-in non-HTTP download handlers reference
=============================================
DataURIDownloadHandler
----------------------
.. autoclass:: scrapy.core.downloader.handlers.datauri.DataURIDownloadHandler
| Supported scheme: ``data``.
| :ref:`Lazy <lazy-download-handlers>`: no.
| :ref:`Requires asyncio support <using-asyncio>`: no.
| :ref:`Requires a Twisted reactor <asyncio-without-reactor>`: no.
This handler supports RFC 2397 ``data:content/type;base64,`` data URIs.
FileDownloadHandler
-------------------
.. autoclass:: scrapy.core.downloader.handlers.file.FileDownloadHandler
| Supported scheme: ``file``.
| :ref:`Lazy <lazy-download-handlers>`: no.
| :ref:`Requires asyncio support <using-asyncio>`: no.
| :ref:`Requires a Twisted reactor <asyncio-without-reactor>`: no.
This handler supports ``file:///path`` local file URIs. It doesn't
support remote files.
FTPDownloadHandler
------------------
.. autoclass:: scrapy.core.downloader.handlers.ftp.FTPDownloadHandler
| Supported scheme: ``ftp``.
| :ref:`Lazy <lazy-download-handlers>`: no.
| :ref:`Requires asyncio support <using-asyncio>`: no.
| :ref:`Requires a Twisted reactor <asyncio-without-reactor>`: yes.
This handler supports ``ftp://host/path`` FTP URIs.
It's implemented using :mod:`twisted.protocols.ftp`.
S3DownloadHandler S3DownloadHandler
----------------- -----------------
@ -297,7 +384,9 @@ S3DownloadHandler
.. autoclass:: scrapy.core.downloader.handlers.s3.S3DownloadHandler .. autoclass:: scrapy.core.downloader.handlers.s3.S3DownloadHandler
| Supported scheme: ``s3``. | Supported scheme: ``s3``.
| Lazy: yes. | :ref:`Lazy <lazy-download-handlers>`: yes.
| :ref:`Requires asyncio support <using-asyncio>`: no.
| :ref:`Requires a Twisted reactor <asyncio-without-reactor>`: no.
This handler supports ``s3://bucket/path`` S3 URIs. This handler supports ``s3://bucket/path`` S3 URIs.

View File

@ -307,26 +307,15 @@ HttpAuthMiddleware
.. class:: HttpAuthMiddleware .. class:: HttpAuthMiddleware
This middleware authenticates all requests generated from certain spiders This middleware authenticates requests using `Basic access authentication`_
using `Basic access authentication`_ (aka. HTTP auth). (aka. HTTP auth).
To enable HTTP authentication for a spider, set the ``http_user`` and Use the :setting:`HTTPAUTH_USER`, :setting:`HTTPAUTH_PASS`, and
``http_pass`` spider attributes to the authentication data and the :setting:`HTTPAUTH_DOMAIN` settings to configure it. You can also override
``http_auth_domain`` spider attribute to the domain which requires this the credentials per request via :attr:`~scrapy.Request.meta` keys
authentication (its subdomains will be also handled in the same way). :reqmeta:`http_user`, :reqmeta:`http_pass`, and :reqmeta:`http_auth_domain`.
You can set ``http_auth_domain`` to ``None`` to enable the
authentication for all requests but you risk leaking your authentication
credentials to unrelated domains.
.. warning:: Example using settings (e.g. in :attr:`~scrapy.Spider.custom_settings`):
In previous Scrapy versions HttpAuthMiddleware sent the authentication
data with all requests, which is a security problem if the spider
makes requests to several different domains. Currently if the
``http_auth_domain`` attribute is not set, the middleware will use the
domain of the first request, which will work for some spiders but not
for others. In the future the middleware will produce an error instead.
Example:
.. code-block:: python .. code-block:: python
@ -334,13 +323,70 @@ HttpAuthMiddleware
class SomeIntranetSiteSpider(CrawlSpider): class SomeIntranetSiteSpider(CrawlSpider):
http_user = "someuser"
http_pass = "somepass"
http_auth_domain = "intranet.example.com"
name = "intranet.example.com" name = "intranet.example.com"
custom_settings = {
"HTTPAUTH_USER": "someuser",
"HTTPAUTH_PASS": "somepass",
"HTTPAUTH_DOMAIN": "intranet.example.com",
}
# .. rest of the spider code omitted ... # .. rest of the spider code omitted ...
Example using per-request meta:
.. code-block:: python
async def start(self):
yield Request(
"https://intranet.example.com/protected/",
meta={
"http_user": "someuser",
"http_pass": "somepass",
"http_auth_domain": "intranet.example.com",
},
)
.. setting:: HTTPAUTH_USER
HTTPAUTH_USER
~~~~~~~~~~~~~
.. versionadded:: 2.17.0
Default: ``""``
The username to use for HTTP basic authentication, applied to all requests
whose URL matches :setting:`HTTPAUTH_DOMAIN`.
.. setting:: HTTPAUTH_PASS
HTTPAUTH_PASS
~~~~~~~~~~~~~
.. versionadded:: 2.17.0
Default: ``""``
The password to use for HTTP basic authentication.
.. setting:: HTTPAUTH_DOMAIN
HTTPAUTH_DOMAIN
~~~~~~~~~~~~~~~
.. versionadded:: 2.17.0
Default: ``None``
The domain (and its subdomains) to which HTTP basic authentication credentials
are sent. Set to ``None`` to send credentials with all requests, but be aware
that this risks leaking credentials to unrelated domains.
This setting must be explicitly configured whenever :setting:`HTTPAUTH_USER`
or :setting:`HTTPAUTH_PASS` is set.
.. seealso:: :ref:`security-credential-leakage`
.. _Basic access authentication: https://en.wikipedia.org/wiki/Basic_access_authentication .. _Basic access authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
@ -459,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)
@ -501,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
@ -509,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
@ -545,8 +591,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
@ -745,8 +791,7 @@ HttpProxyMiddleware
Handling of this meta key needs to be implemented inside the :ref:`download Handling of this meta key needs to be implemented inside the :ref:`download
handler <topics-download-handlers>`, so it's not guaranteed to be supported handler <topics-download-handlers>`, so it's not guaranteed to be supported
by all 3rd-party handlers. It's currently unsupported by by all 3rd-party handlers. It's currently unsupported by
:class:`~scrapy.core.downloader.handlers.http2.H2DownloadHandler` and :class:`~scrapy.core.downloader.handlers.http2.H2DownloadHandler`.
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`.
.. note:: .. note::
@ -758,11 +803,17 @@ HttpProxyMiddleware
:class:`~scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler` :class:`~scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler`
supports HTTPS proxies only for HTTP destinations. supports HTTPS proxies only for HTTP destinations.
.. note::
If the download handler supports it, you can use a SOCKS proxy URL (e.g.
``socks5://username:password@some_proxy_server:port``).
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`
supports SOCKS proxies while other built-in handlers don't.
HttpProxyMiddleware settings HttpProxyMiddleware settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. setting:: HTTPPROXY_ENABLED .. setting:: HTTPPROXY_ENABLED
.. setting:: HTTPPROXY_AUTH_ENCODING
HTTPPROXY_ENABLED HTTPPROXY_ENABLED
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
@ -771,6 +822,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
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
@ -815,9 +868,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
------------------ ------------------
@ -932,7 +985,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.
@ -962,17 +1015,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
@ -1039,7 +1081,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',
] ]
@ -1053,6 +1095,23 @@ has been exceeded (see :setting:`RETRY_TIMES`). To learn about uncaught
exception propagation, see exception propagation, see
:meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_exception`. :meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_exception`.
.. setting:: RETRY_GIVE_UP_LOG_LEVEL
RETRY_GIVE_UP_LOG_LEVEL
^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 2.17.0
Default: ``"ERROR"``
:ref:`Logging level <levels>` used for the message logged when a request
exceeds its retries.
Can be a level name (e.g. ``"WARNING"``) or a number (e.g. ``logging.WARNING``
or ``30``).
See also: :reqmeta:`give_up_log_level`, :func:`get_retry_request`.
.. setting:: RETRY_PRIORITY_ADJUST .. setting:: RETRY_PRIORITY_ADJUST
RETRY_PRIORITY_ADJUST RETRY_PRIORITY_ADJUST

View File

@ -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`.

View File

@ -12,7 +12,8 @@ Exceptions
Built-in Exceptions reference Built-in Exceptions reference
============================= =============================
Here's a list of all exceptions included in Scrapy and their usage. Here's a list of all exceptions included in Scrapy and their usage, except for
the :ref:`download handler exceptions <download-handlers-exceptions>`.
CloseSpider CloseSpider
@ -31,7 +32,7 @@ For example:
.. code-block:: python .. code-block:: python
def parse_page(self, response): def parse_page(self, response):
if "Bandwidth exceeded" in response.body: if "Bandwidth exceeded" in response.text:
raise CloseSpider("bandwidth_exceeded") raise CloseSpider("bandwidth_exceeded")
DontCloseSpider DontCloseSpider
@ -71,7 +72,8 @@ remain disabled. Those components include:
- Downloader middlewares - Downloader middlewares
- Spider middlewares - Spider middlewares
The exception must be raised in the component's ``__init__`` method. The exception must be raised in the component's ``__init__()`` or
``from_crawler()`` method.
NotSupported NotSupported
------------ ------------

View File

@ -93,24 +93,25 @@ described next.
1. Declaring a serializer in the field 1. Declaring a serializer in the field
-------------------------------------- --------------------------------------
If you use :class:`~scrapy.Item` you can declare a serializer in the Every :ref:`item type <item-types>` except :class:`dict` lets you declare a
:ref:`field metadata <topics-items-fields>`. The serializer must be serializer in the :ref:`field metadata <topics-items-fields>`. The serializer
a callable which receives a value and returns its serialized form. must be a callable which receives a value and returns its serialized form.
Example: Example:
.. code-block:: python .. code-block:: python
import scrapy from dataclasses import dataclass, field
def serialize_price(value): def serialize_price(value):
return f"$ {str(value)}" return f"$ {str(value)}"
class Product(scrapy.Item): @dataclass
name = scrapy.Field() class Product:
price = scrapy.Field(serializer=serialize_price) name: str
price: float = field(metadata={"serializer": serialize_price})
2. Overriding the serialize_field() method 2. Overriding the serialize_field() method
@ -152,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
@ -238,7 +239,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
@ -327,7 +328,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
@ -341,14 +342,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.

View File

@ -136,7 +136,18 @@ Core Stats extension
Enable the collection of core statistics, provided the stats collection is Enable the collection of core statistics, provided the stats collection is
enabled (see :ref:`topics-stats`). enabled (see :ref:`topics-stats`).
.. _topics-extensions-ref-telnetconsole: The following stats are collected:
* ``start_time``: start date/time of the crawl (:class:`~datetime.datetime`).
* ``finish_time``: end date/time of the crawl (:class:`~datetime.datetime`).
* ``elapsed_time_seconds``: total crawl duration in seconds (:class:`float`).
* ``finish_reason``: the closing reason string (e.g. ``"finished"``,
``"closespider_timeout"``).
* ``item_scraped_count``: total number of items that passed all pipelines.
* ``item_dropped_count``: total number of items dropped by a pipeline.
* ``item_dropped_reasons_count/<ExceptionName>``: per-exception drop count
(e.g. ``item_dropped_reasons_count/DropItem``).
* ``response_received_count``: total number of HTTP responses received.
Log Count extension Log Count extension
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -146,6 +157,8 @@ Log Count extension
.. autoclass:: LogCount .. autoclass:: LogCount
.. _topics-extensions-ref-telnetconsole:
Telnet console extension Telnet console extension
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
@ -247,6 +260,7 @@ settings:
* :setting:`CLOSESPIDER_TIMEOUT_NO_ITEM` * :setting:`CLOSESPIDER_TIMEOUT_NO_ITEM`
* :setting:`CLOSESPIDER_ITEMCOUNT` * :setting:`CLOSESPIDER_ITEMCOUNT`
* :setting:`CLOSESPIDER_PAGECOUNT` * :setting:`CLOSESPIDER_PAGECOUNT`
* :setting:`CLOSESPIDER_PAGECOUNT_NO_ITEM`
* :setting:`CLOSESPIDER_ERRORCOUNT` * :setting:`CLOSESPIDER_ERRORCOUNT`
.. note:: .. note::
@ -260,12 +274,11 @@ settings:
CLOSESPIDER_TIMEOUT CLOSESPIDER_TIMEOUT
""""""""""""""""""" """""""""""""""""""
Default: ``0`` Default: ``0.0``
An integer which specifies a number of seconds. If the spider remains open for If the spider remains open for more than this number of seconds, it will be
more than that number of seconds, it will be automatically closed with the automatically closed with the reason ``closespider_timeout``. If zero (or non
reason ``closespider_timeout``. If zero (or non set), spiders won't be closed by set), spiders won't be closed by timeout.
timeout.
.. setting:: CLOSESPIDER_TIMEOUT_NO_ITEM .. setting:: CLOSESPIDER_TIMEOUT_NO_ITEM
@ -328,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
@ -405,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,
@ -450,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
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -143,6 +143,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 +166,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:
@ -528,10 +533,6 @@ safe numeric encoding (``\uXXXX`` sequences) for historic reasons.
Use ``"utf-8"`` if you want UTF-8 for JSON too. Use ``"utf-8"`` if you want UTF-8 for JSON too.
.. versionchanged:: 2.8
The :command:`startproject` command now sets this setting to
``"utf-8"`` in the generated ``settings.py`` file.
.. setting:: FEED_EXPORT_FIELDS .. setting:: FEED_EXPORT_FIELDS
FEED_EXPORT_FIELDS FEED_EXPORT_FIELDS
@ -619,6 +620,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",
} }
@ -760,8 +762,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:

View File

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

View File

@ -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):

View File

@ -104,6 +104,15 @@ If you wish to log the requests that couldn't be serialized, you can set the
:setting:`SCHEDULER_DEBUG` setting to ``True`` in the project's settings page. :setting:`SCHEDULER_DEBUG` setting to ``True`` in the project's settings page.
It is ``False`` by default. It is ``False`` by default.
.. note:: Because requests are serialized with :mod:`pickle`, the objects you
store on a request, such as the values of its
:attr:`~scrapy.Request.cb_kwargs` and :attr:`~scrapy.Request.meta`
dictionaries, are deep-copied when the request is written to and later read
back from the job directory. As a result, the callback receives a *copy* of
those objects rather than the original ones, and changes made to the copy are
not reflected in the original object. Keep this in mind if you rely on
sharing mutable state through ``cb_kwargs`` or ``meta``.
.. _job-dir-contents: .. _job-dir-contents:
Job directory contents Job directory contents
@ -142,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.

View File

@ -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`
@ -164,7 +164,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:
@ -187,7 +187,7 @@ Here are the functions available in the :mod:`~scrapy.utils.trackref` module.
Inherit from this class if you want to track live Inherit from this class if you want to track live
instances with the ``trackref`` module. instances with the ``trackref`` module.
.. function:: print_live_refs(class_name, ignore=NoneType) .. function:: print_live_refs(ignore=NoneType)
Print a report of live references, grouped by class name. Print a report of live references, grouped by class name.
@ -203,9 +203,9 @@ Here are the functions available in the :mod:`~scrapy.utils.trackref` module.
.. function:: iter_all(class_name) .. function:: iter_all(class_name)
Return an iterator over all objects alive with the given class name, or Return an iterator over all objects alive with the given class name. Use
``None`` if none is found. Use :func:`print_live_refs` first to get a list :func:`print_live_refs` first to get a list of all tracked live objects
of all tracked live objects per class name. per class name.
.. skip: end .. skip: end

View File

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

View File

@ -76,7 +76,7 @@ data that will be assigned to the ``name`` field later.
Afterwards, similar calls are used for ``price`` and ``stock`` fields 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
@ -102,14 +102,13 @@ One approach to overcome this is to define items using the
.. code-block:: python .. code-block:: python
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
@dataclass @dataclass
class InventoryItem: class InventoryItem:
name: Optional[str] = field(default=None) name: str | None = field(default=None)
price: Optional[float] = field(default=None) price: float | None = field(default=None)
stock: Optional[int] = field(default=None) stock: int | None = field(default=None)
.. _topics-loaders-processors: .. _topics-loaders-processors:
@ -228,7 +227,8 @@ metadata. Here is an example:
.. code-block:: python .. code-block:: python
import scrapy from dataclasses import dataclass, field
from itemloaders.processors import Join, MapCompose, TakeFirst from itemloaders.processors import Join, MapCompose, TakeFirst
from w3lib.html import remove_tags from w3lib.html import remove_tags
@ -238,14 +238,21 @@ metadata. Here is an example:
return value return value
class Product(scrapy.Item): @dataclass
name = scrapy.Field( class Product:
input_processor=MapCompose(remove_tags), name: str | None = field(
output_processor=Join(), default=None,
metadata={
"input_processor": MapCompose(remove_tags),
"output_processor": Join(),
},
) )
price = scrapy.Field( price: str | None = field(
input_processor=MapCompose(remove_tags, filter_price), default=None,
output_processor=TakeFirst(), metadata={
"input_processor": MapCompose(remove_tags, filter_price),
"output_processor": TakeFirst(),
},
) )
@ -257,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", ["&euro;", "<span>1000</span>"]) >>> il.add_value("price", ["&euro;", "<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
@ -266,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`.
@ -316,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

View File

@ -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.

View File

@ -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
@ -81,9 +80,6 @@ thumbnailing and normalizing images to JPEG/RGB format.
Enabling your Media Pipeline Enabling your Media Pipeline
============================ ============================
.. setting:: IMAGES_STORE
.. setting:: FILES_STORE
To enable your media pipeline you must first add it to your project To enable your media pipeline you must first add it to your project
:setting:`ITEM_PIPELINES` setting. :setting:`ITEM_PIPELINES` setting.
@ -102,6 +98,8 @@ For Files Pipeline, use:
.. note:: .. note::
You can also use both the Files and Images Pipeline at the same time. You can also use both the Files and Images Pipeline at the same time.
.. setting:: IMAGES_STORE
.. setting:: FILES_STORE
Then, configure the target storage setting to a valid value that will be used Then, configure the target storage setting to a valid value that will be used
for storing the downloaded images. Otherwise the pipeline will remain disabled, for storing the downloaded images. Otherwise the pipeline will remain disabled,
@ -337,17 +335,18 @@ respectively), the pipeline will put the results under the respective field
When using :ref:`item types <item-types>` for which fields are defined beforehand, When using :ref:`item types <item-types>` for which fields are defined beforehand,
you must define both the URLs field and the results field. For example, when you must define both the URLs field and the results field. For example, when
using the images pipeline, items must define both the ``image_urls`` and the using the images pipeline, items must define both the ``image_urls`` and the
``images`` field. For instance, using the :class:`~scrapy.Item` class: ``images`` field. For instance, using a dataclass:
.. code-block:: python .. code-block:: python
import scrapy from dataclasses import dataclass, field
class MyItem(scrapy.Item): @dataclass
class MyItem:
# ... other item fields ... # ... other item fields ...
image_urls = scrapy.Field() image_urls: list[str] = field(default_factory=list)
images = scrapy.Field() images: list[dict] = field(default_factory=list)
If you want to use another field name for the URLs key or for the results key, If you want to use another field name for the URLs key or for the results key,
it is also possible to override it. it is also possible to override it.
@ -371,11 +370,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
@ -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
@ -774,4 +771,28 @@ To enable your custom media pipeline component you must add its class import pat
ITEM_PIPELINES = {"myproject.pipelines.MyImagesPipeline": 300} ITEM_PIPELINES = {"myproject.pipelines.MyImagesPipeline": 300}
Content-based image filtering pipeline
--------------------------------------
This example overrides ``get_images()`` to filter images using a classifier,
such as a TensorFlow_ model. Override ``is_valid_image()`` with your
classification logic:
.. code-block:: python
from scrapy.pipelines.images import ImagesPipeline, ImageException
class ImageClassifierPipeline(ImagesPipeline):
def is_valid_image(self, image):
raise NotImplementedError
def get_images(self, response, request, info, *, item=None):
for path, image, buf in super().get_images(response, request, info, item=item):
if not self.is_valid_image(image):
raise ImageException("Image does not match criteria")
yield path, image, buf
.. _MD5 hash: https://en.wikipedia.org/wiki/MD5 .. _MD5 hash: https://en.wikipedia.org/wiki/MD5
.. _TensorFlow: https://tensorflow.org

View 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
@ -347,10 +349,10 @@ finishes before starting the next one:
install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor") install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
react(deferred_f_from_coro_f(crawl)) react(deferred_f_from_coro_f(crawl))
.. note:: When running multiple spiders in the same process, :ref:`reactor .. note:: When running multiple spiders in the same process, :ref:`logging
settings <reactor-settings>` should not have a different value per spider. settings <logging-settings>` and :ref:`reactor settings <reactor-settings>`
Also, :ref:`pre-crawler settings <pre-crawler-settings>` cannot be defined should not have a different value per spider, and :ref:`pre-crawler
per spider. settings <pre-crawler-settings>` cannot be defined per spider.
.. seealso:: :ref:`run-from-script`. .. seealso:: :ref:`run-from-script`.
@ -387,6 +389,26 @@ crawl::
curl http://scrapy2.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=2 curl http://scrapy2.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=2
curl http://scrapy3.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=3 curl http://scrapy3.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=3
.. _large-project-startup:
Reducing startup time in large projects
=======================================
When running a spider with ``scrapy crawl``, Scrapy loads all modules listed in
:setting:`SPIDER_MODULES` to find the target spider. In large projects with
many spiders, this can noticeably increase startup time and memory usage.
To avoid loading every spider module, override :setting:`SPIDER_MODULES` on the
command line to point only to the module that contains the spider you want to
run:
.. code-block:: shell
scrapy crawl myspider -s SPIDER_MODULES=myproject.spiders.myspider
Because :setting:`SPIDER_MODULES` is a list setting, you can include multiple
modules by separating them with commas.
.. _bans: .. _bans:
Avoiding getting banned Avoiding getting banned
@ -410,9 +432,9 @@ Here are some tips to keep in mind when dealing with these kinds of sites:
services like `ProxyMesh`_. An open source alternative is `scrapoxy`_, a services like `ProxyMesh`_. An open source alternative is `scrapoxy`_, a
super proxy that you can attach your own proxies to. super proxy that you can attach your own proxies to.
* for HTTPS websites, if blocking appears related to TLS behavior, consider * for HTTPS websites, if blocking appears related to TLS behavior, consider
adjusting the :setting:`DOWNLOADER_CLIENT_TLS_METHOD` setting, since some adjusting the :setting:`DOWNLOAD_TLS_MIN_VERSION` and
websites may respond differently depending on the TLS method used by the :setting:`DOWNLOAD_TLS_MAX_VERSION` settings, since some websites may respond
client. differently depending on the TLS method used by the client.
* use a ban avoidance service, such as `Zyte API`_, which provides a `Scrapy * use a ban avoidance service, such as `Zyte API`_, which provides a `Scrapy
plugin <https://github.com/scrapy-plugins/scrapy-zyte-api>`__ and additional plugin <https://github.com/scrapy-plugins/scrapy-zyte-api>`__ and additional
features, like `AI web scraping <https://www.zyte.com/ai-web-scraping/>`__ features, like `AI web scraping <https://www.zyte.com/ai-web-scraping/>`__
@ -420,6 +442,14 @@ Here are some tips to keep in mind when dealing with these kinds of sites:
If you are still unable to prevent your bot getting banned, consider contacting If you are still unable to prevent your bot getting banned, consider contacting
`commercial support`_. `commercial support`_.
.. _static-analysis:
Static analysis
===============
Consider using :doc:`scrapy-lint <scrapy-lint:index>`, a linter for Scrapy
projects that detects common mistakes and anti-patterns.
.. _Tor project: https://www.torproject.org/ .. _Tor project: https://www.torproject.org/
.. _commercial support: https://www.scrapy.org/companies .. _commercial support: https://www.scrapy.org/companies
.. _ProxyMesh: https://proxymesh.com/ .. _ProxyMesh: https://proxymesh.com/

View File

@ -63,7 +63,7 @@ Request objects
.. invisible-code-block: python .. invisible-code-block: python
from scrapy.http import Request from scrapy import Request
1. Using a dict: 1. Using a dict:
@ -117,6 +117,9 @@ Request objects
:param encoding: the encoding of this request (defaults to ``'utf-8'``). :param encoding: the encoding of this request (defaults to ``'utf-8'``).
This encoding will be used to percent-encode the URL and to convert the This encoding will be used to percent-encode the URL and to convert the
body to bytes (if given as a string). body to bytes (if given as a string).
To disable URL percent-encoding for a request, use the
:reqmeta:`verbatim_url` request meta key.
:type encoding: str :type encoding: str
:param priority: sets :attr:`priority`, defaults to ``0``. :param priority: sets :attr:`priority`, defaults to ``0``.
@ -136,9 +139,13 @@ Request objects
.. attribute:: Request.url .. attribute:: Request.url
A string containing the URL of this request. Keep in mind that this A string containing the URL of this request.
attribute contains the escaped URL, so it can differ from the URL passed in
the ``__init__()`` method. Keep in mind that this attribute contains the escaped URL, so it can
differ from the URL passed in the ``__init__()`` method.
If :reqmeta:`verbatim_url` is set to ``True``, the URL is kept as
passed to ``__init__()``.
This attribute is read-only. To change the URL of a Request use This attribute is read-only. To change the URL of a Request use
:meth:`replace`. :meth:`replace`.
@ -181,6 +188,13 @@ Request objects
``failure.request.cb_kwargs`` in the request's errback. For more information, ``failure.request.cb_kwargs`` in the request's errback. For more information,
see :ref:`errback-cb_kwargs`. see :ref:`errback-cb_kwargs`.
.. note:: When :setting:`JOBDIR` is set, requests are serialized to disk
with :mod:`pickle` (see :ref:`request-serialization`). As a result,
the callback receives a deep copy of any object stored in
``cb_kwargs``, so mutating such an object in the callback does not
affect the original. Avoid relying on shared mutable state passed
through ``cb_kwargs`` in that case.
.. attribute:: Request.meta .. attribute:: Request.meta
:value: {} :value: {}
@ -233,7 +247,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
@ -541,6 +555,11 @@ in your :meth:`fingerprint` method implementation:
.. autofunction:: scrapy.utils.request.fingerprint .. autofunction:: scrapy.utils.request.fingerprint
By default, request fingerprinting canonicalizes the request URL. If
:reqmeta:`verbatim_url` is set to ``True``, fingerprinting does not
canonicalize the URL, and the ``keep_fragments`` parameter is ignored (it is
effectively true).
For example, to take the value of a request header named ``X-ID`` into For example, to take the value of a request header named ``X-ID`` into
account: account:
@ -698,18 +717,24 @@ 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)
* ``ftp_user`` (See :setting:`FTP_USER` for more info) * ``ftp_user`` (See :setting:`FTP_USER` for more info)
* :reqmeta:`give_up_log_level`
* :reqmeta:`handle_httpstatus_all` * :reqmeta:`handle_httpstatus_all`
* :reqmeta:`handle_httpstatus_list` * :reqmeta:`handle_httpstatus_list`
* :reqmeta:`http_auth_domain`
* :reqmeta:`http_pass`
* :reqmeta:`http_user`
* :reqmeta:`is_start_request` * :reqmeta:`is_start_request`
* :reqmeta:`max_retry_times` * :reqmeta:`max_retry_times`
* :reqmeta:`proxy` * :reqmeta:`proxy`
* :reqmeta:`redirect_reasons` * :reqmeta:`redirect_reasons`
* :reqmeta:`redirect_urls` * :reqmeta:`redirect_urls`
* :reqmeta:`referrer_policy` * :reqmeta:`referrer_policy`
* :reqmeta:`verbatim_url`
.. reqmeta:: bindaddress .. reqmeta:: bindaddress
@ -777,15 +802,68 @@ download_fail_on_dataloss
Whether or not to fail on broken responses. See: Whether or not to fail on broken responses. See:
:setting:`DOWNLOAD_FAIL_ON_DATALOSS`. :setting:`DOWNLOAD_FAIL_ON_DATALOSS`.
.. reqmeta:: give_up_log_level
give_up_log_level
-----------------
.. versionadded:: 2.17.0
:ref:`Logging level <levels>` used for the message logged when a request
exceeds its retries. See :setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
.. reqmeta:: http_auth_domain
http_auth_domain
----------------
.. versionadded:: 2.17.0
Overrides :setting:`HTTPAUTH_DOMAIN` for this request.
.. reqmeta:: http_pass
http_pass
---------
.. versionadded:: 2.17.0
Overrides :setting:`HTTPAUTH_PASS` for this request.
.. reqmeta:: http_user
http_user
---------
.. versionadded:: 2.17.0
Overrides :setting:`HTTPAUTH_USER` for this request.
.. reqmeta:: max_retry_times .. reqmeta:: max_retry_times
max_retry_times max_retry_times
--------------- ---------------
The meta key is used set retry times per request. When initialized, the The meta key is used set retry times per request. When set, the
:reqmeta:`max_retry_times` meta key takes higher precedence over the :reqmeta:`max_retry_times` meta key takes higher precedence over the
:setting:`RETRY_TIMES` setting. :setting:`RETRY_TIMES` setting.
.. reqmeta:: verbatim_url
verbatim_url
------------
.. versionadded:: 2.17.0
Set this key to ``True`` to keep the request URL as passed to
:class:`~scrapy.Request`, without URL percent-encoding.
When this key is enabled, :func:`~scrapy.utils.request.fingerprint` does not
canonicalize the request URL, so requests whose URLs differ only in
characters that would otherwise be canonicalized get different fingerprints.
In this mode, the ``keep_fragments`` parameter is ignored, and it is
effectively true.
.. _topics-stop-response-download: .. _topics-stop-response-download:
@ -843,6 +921,11 @@ Request subclasses
Here is the list of built-in :class:`~scrapy.Request` subclasses. You can also subclass Here is the list of built-in :class:`~scrapy.Request` subclasses. You can also subclass
it to implement your own custom functionality. it to implement your own custom functionality.
FormRequest
-----------
.. autoclass:: scrapy.FormRequest
JsonRequest JsonRequest
----------- -----------
@ -942,7 +1025,7 @@ Response objects
A dictionary-like (:class:`scrapy.http.headers.Headers`) object which contains A dictionary-like (:class:`scrapy.http.headers.Headers`) object which contains
the response headers. Values can be accessed using the response headers. Values can be accessed using
:meth:`~scrapy.http.headers.Headers.get` to return the first 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::
@ -1003,7 +1086,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.
@ -1018,8 +1101,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
@ -1037,7 +1120,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

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

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

View File

@ -308,7 +308,7 @@ Examples:
* ``*::text`` selects all descendant text nodes of the current selector context: * ``*::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("")
... ...

View File

@ -361,6 +361,31 @@ All of these settings, except for :setting:`ASYNCIO_EVENT_LOOP`, are only used
when the Twisted reactor is used, i.e. when :setting:`TWISTED_REACTOR_ENABLED` when the Twisted reactor is used, i.e. when :setting:`TWISTED_REACTOR_ENABLED`
is ``True``. is ``True``.
.. _logging-settings:
Logging settings
----------------
**Logging settings** are settings that configure the global root logging
handler installed by :func:`~scrapy.utils.log.configure_logging`.
These settings can be defined from a spider. However, because only 1 root
logging handler is active per process, these settings cannot use a different
value per spider when :ref:`running multiple spiders in the same process
<run-multiple-spiders>`.
These settings are:
- :setting:`LOG_DATEFORMAT`
- :setting:`LOG_ENABLED`
- :setting:`LOG_ENCODING`
- :setting:`LOG_FILE`
- :setting:`LOG_FILE_APPEND`
- :setting:`LOG_FORMAT`
- :setting:`LOG_LEVEL`
- :setting:`LOG_SHORT_NAMES`
- :setting:`LOG_STDOUT`
.. _topics-settings-ref: .. _topics-settings-ref:
Built-in settings reference Built-in settings reference
@ -384,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
@ -394,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
@ -417,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
@ -446,41 +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.
.. setting:: BOT_NAME .. setting:: BOT_NAME
BOT_NAME BOT_NAME
@ -527,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
@ -566,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>`.
@ -660,7 +691,9 @@ 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>`.
.. setting:: DNSCACHE_SIZE .. setting:: DNSCACHE_SIZE
@ -671,22 +704,7 @@ Default: ``10000``
DNS in-memory cache size, see :setting:`DNSCACHE_ENABLED`. DNS in-memory cache size, see :setting:`DNSCACHE_ENABLED`.
.. setting:: TWISTED_DNS_RESOLVER .. note:: This is a :ref:`reactor setting <reactor-settings>`.
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``.
.. setting:: DNS_TIMEOUT .. setting:: DNS_TIMEOUT
@ -701,7 +719,9 @@ 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>`.
.. setting:: DOWNLOADER .. setting:: DOWNLOADER
@ -728,41 +748,74 @@ necessary to access certain HTTPS websites: for example, you may need to use
``'DEFAULT:!DH'`` for a website with weak DH parameters or enable a ``'DEFAULT:!DH'`` for a website with weak DH parameters or enable a
specific cipher that is not included in ``DEFAULT`` if a website requires it. specific cipher that is not included in ``DEFAULT`` if a website requires it.
Set this setting to ``None`` to use the default ciphers of the underlying TLS
implementation.
.. versionchanged:: 2.17.0
Added support for setting this to ``None``.
.. _OpenSSL cipher list format: https://docs.openssl.org/master/man1/openssl-ciphers/#cipher-list-format .. _OpenSSL cipher list format: https://docs.openssl.org/master/man1/openssl-ciphers/#cipher-list-format
.. note:: .. note::
Handling of this setting needs to be implemented inside the :ref:`download Handling of this setting needs to be implemented inside the :ref:`download
handler <topics-download-handlers>`, so it's not guaranteed to be supported handler <topics-download-handlers>`, so it's not guaranteed to be supported
by all 3rd-party handlers. It's currently unsupported by by all 3rd-party handlers.
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`.
.. setting:: DOWNLOADER_CLIENT_TLS_METHOD .. seealso:: :ref:`security-tls-protocols-ciphers`
DOWNLOADER_CLIENT_TLS_METHOD .. setting:: DOWNLOAD_TLS_MAX_VERSION
----------------------------
Default: ``'TLS'`` DOWNLOAD_TLS_MAX_VERSION
------------------------
Use this setting to customize the TLS/SSL method used by the HTTPS download .. versionadded:: 2.17.0
handler.
This setting must be one of these string values: Default: ``None``
- ``'TLS'``: maps to OpenSSL's ``TLS_method()`` (a.k.a ``SSLv23_method()``), Use this setting to change the maximum version of the TLS protocol allowed to
which allows protocol negotiation, starting from the highest supported be used by Scrapy.
by the platform; **default, recommended**
- ``'TLSv1.0'``: this value forces HTTPS connections to use TLS version 1.0 ; This setting must be either ``None``, in which case it doesn't affect the
set this if you want the behavior of Scrapy<1.1 version selection, or one of these string values:
- ``'TLSv1.1'``: forces TLS version 1.1
- ``'TLSv1.2'``: forces TLS version 1.2 - ``'TLSv1.0'``
- ``'TLSv1.1'``
- ``'TLSv1.2'``
- ``'TLSv1.3'``
The range of allowed TLS versions advertised by Scrapy when making TLS
connections will depend on the TLS implementation defaults and the values of
:setting:`DOWNLOAD_TLS_MIN_VERSION` and :setting:`DOWNLOAD_TLS_MAX_VERSION`.
It's possible to re-enable versions that are supported by the TLS
implementation but disabled by default by adjusting these settings, but it's
impossible to enable unsupported ones, such as any versions below 1.2 in many
modern environments.
.. note:: .. note::
Handling of this setting needs to be implemented inside the :ref:`download Handling of this setting needs to be implemented inside the :ref:`download
handler <topics-download-handlers>`, so it's not guaranteed to be supported handler <topics-download-handlers>`, so it's not guaranteed to be supported
by all 3rd-party handlers. It's currently unsupported by by all 3rd-party handlers. Additionally, the set of supported TLS versions
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`. depends on the TLS implementation being used by the handler.
.. seealso:: :ref:`security-tls-protocols-ciphers`
.. setting:: DOWNLOAD_TLS_MIN_VERSION
DOWNLOAD_TLS_MIN_VERSION
------------------------
.. versionadded:: 2.17.0
Default: ``None``
Use this setting to change the minimum version of the TLS protocol allowed to
be used by Scrapy.
See :setting:`DOWNLOAD_TLS_MAX_VERSION` for the details and limitations.
.. seealso:: :ref:`security-tls-protocols-ciphers`
.. setting:: DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING .. setting:: DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING
@ -868,9 +921,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
@ -924,6 +976,9 @@ enabled in your project.
See :setting:`DOWNLOAD_HANDLERS_BASE` for example format. See :setting:`DOWNLOAD_HANDLERS_BASE` for example format.
.. seealso:: :ref:`security-unencrypted-protocols` and
:ref:`security-local-resources`
.. setting:: DOWNLOAD_HANDLERS_BASE .. setting:: DOWNLOAD_HANDLERS_BASE
DOWNLOAD_HANDLERS_BASE DOWNLOAD_HANDLERS_BASE
@ -971,6 +1026,9 @@ handler (without replacement), place this in your ``settings.py``:
"ftp": None, "ftp": None,
} }
.. seealso:: :ref:`security-unencrypted-protocols` and
:ref:`security-local-resources`
.. setting:: DOWNLOAD_SLOTS .. setting:: DOWNLOAD_SLOTS
@ -1129,6 +1187,8 @@ when making a request and abort the request if the verification fails.
certificate problems are logged when this setting is set to ``False``) certificate problems are logged when this setting is set to ``False``)
depends on its implementation. depends on its implementation.
.. seealso:: :ref:`security-certificate-verification`
.. setting:: DUPEFILTER_CLASS .. setting:: DUPEFILTER_CLASS
DUPEFILTER_CLASS DUPEFILTER_CLASS
@ -1254,6 +1314,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,
@ -1276,6 +1337,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>`.
@ -1285,6 +1348,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>`_.
@ -1296,14 +1361,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>`.
@ -1425,6 +1495,8 @@ Default: ``True``
Whether to enable logging. Whether to enable logging.
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_ENCODING .. setting:: LOG_ENCODING
LOG_ENCODING LOG_ENCODING
@ -1434,6 +1506,8 @@ Default: ``'utf-8'``
The encoding to use for logging. The encoding to use for logging.
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_FILE .. setting:: LOG_FILE
LOG_FILE LOG_FILE
@ -1443,6 +1517,8 @@ Default: ``None``
File name to use for logging output. If ``None``, standard error will be used. File name to use for logging output. If ``None``, standard error will be used.
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_FILE_APPEND .. setting:: LOG_FILE_APPEND
LOG_FILE_APPEND LOG_FILE_APPEND
@ -1453,6 +1529,8 @@ Default: ``True``
If ``False``, the log file specified with :setting:`LOG_FILE` will be If ``False``, the log file specified with :setting:`LOG_FILE` will be
overwritten (discarding the output from previous runs, if any). overwritten (discarding the output from previous runs, if any).
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_FORMAT .. setting:: LOG_FORMAT
LOG_FORMAT LOG_FORMAT
@ -1464,6 +1542,8 @@ String for formatting log messages. Refer to the
:ref:`Python logging documentation <logrecord-attributes>` for the whole :ref:`Python logging documentation <logrecord-attributes>` for the whole
list of available placeholders. list of available placeholders.
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_DATEFORMAT .. setting:: LOG_DATEFORMAT
LOG_DATEFORMAT LOG_DATEFORMAT
@ -1476,6 +1556,8 @@ in :setting:`LOG_FORMAT`. Refer to the
:ref:`Python datetime documentation <strftime-strptime-behavior>` for the :ref:`Python datetime documentation <strftime-strptime-behavior>` for the
whole list of available directives. whole list of available directives.
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_FORMATTER .. setting:: LOG_FORMATTER
LOG_FORMATTER LOG_FORMATTER
@ -1495,6 +1577,8 @@ Default: ``'DEBUG'``
Minimum level to log. Available levels are: CRITICAL, ERROR, WARNING, Minimum level to log. Available levels are: CRITICAL, ERROR, WARNING,
INFO, DEBUG. For more info see :ref:`topics-logging`. INFO, DEBUG. For more info see :ref:`topics-logging`.
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_STDOUT .. setting:: LOG_STDOUT
LOG_STDOUT LOG_STDOUT
@ -1506,6 +1590,8 @@ If ``True``, all standard output (and error) of your process will be redirected
to the log. For example if you ``print('hello')`` it will appear in the Scrapy to the log. For example if you ``print('hello')`` it will appear in the Scrapy
log. log.
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_SHORT_NAMES .. setting:: LOG_SHORT_NAMES
LOG_SHORT_NAMES LOG_SHORT_NAMES
@ -1516,6 +1602,8 @@ Default: ``False``
If ``True``, the logs will just contain the root path. If it is set to ``False`` If ``True``, the logs will just contain the root path. If it is set to ``False``
then it displays the component responsible for the log output then it displays the component responsible for the log output
.. note:: This is a :ref:`logging setting <logging-settings>`.
.. setting:: LOG_VERSIONS .. setting:: LOG_VERSIONS
LOG_VERSIONS LOG_VERSIONS
@ -1535,6 +1623,8 @@ The following special items are also supported:
- ``Python`` - ``Python``
- ``pyOpenSSL``
.. setting:: LOGSTATS_INTERVAL .. setting:: LOGSTATS_INTERVAL
LOGSTATS_INTERVAL LOGSTATS_INTERVAL
@ -1662,7 +1752,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
@ -1678,6 +1771,8 @@ multi-purpose thread pool used by various Scrapy components. Threaded
DNS Resolver, BlockingFeedStorage, S3FilesStore just to name a few. Increase DNS Resolver, BlockingFeedStorage, S3FilesStore just to name a few. Increase
this value if you're experiencing problems with insufficient blocking IO. this value if you're experiencing problems with insufficient blocking IO.
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
.. setting:: REDIRECT_PRIORITY_ADJUST .. setting:: REDIRECT_PRIORITY_ADJUST
REDIRECT_PRIORITY_ADJUST REDIRECT_PRIORITY_ADJUST
@ -1721,7 +1816,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``
@ -1876,6 +1971,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,
} }
@ -1904,6 +2001,8 @@ Default: ``'scrapy.spiderloader.SpiderLoader'``
The class that will be used for loading spiders, which must implement the The class that will be used for loading spiders, which must implement the
:ref:`topics-api-spiderloader`. :ref:`topics-api-spiderloader`.
.. note:: This is a :ref:`pre-crawler setting <pre-crawler-settings>`.
.. setting:: SPIDER_LOADER_WARN_ONLY .. setting:: SPIDER_LOADER_WARN_ONLY
SPIDER_LOADER_WARN_ONLY SPIDER_LOADER_WARN_ONLY
@ -1916,6 +2015,8 @@ it will fail loudly if there is any ``ImportError`` or ``SyntaxError`` exception
But you can choose to silence this exception and turn it into a simple But you can choose to silence this exception and turn it into a simple
warning by setting ``SPIDER_LOADER_WARN_ONLY = True``. warning by setting ``SPIDER_LOADER_WARN_ONLY = True``.
.. note:: This is a :ref:`pre-crawler setting <pre-crawler-settings>`.
.. setting:: SPIDER_MIDDLEWARES .. setting:: SPIDER_MIDDLEWARES
SPIDER_MIDDLEWARES SPIDER_MIDDLEWARES
@ -1936,6 +2037,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,
@ -1961,6 +2063,8 @@ Example:
SPIDER_MODULES = ["mybot.spiders_prod", "mybot.spiders_dev"] SPIDER_MODULES = ["mybot.spiders_prod", "mybot.spiders_dev"]
.. note:: This is a :ref:`pre-crawler setting <pre-crawler-settings>`.
.. setting:: STATS_CLASS .. setting:: STATS_CLASS
STATS_CLASS STATS_CLASS
@ -1993,6 +2097,8 @@ Default: ``True`` (``False`` when :setting:`TWISTED_REACTOR_ENABLED` is ``False`
A boolean which specifies if the :ref:`telnet console <topics-telnetconsole>` A boolean which specifies if the :ref:`telnet console <topics-telnetconsole>`
will be enabled (provided its extension is also enabled). will be enabled (provided its extension is also enabled).
.. seealso:: :ref:`security-telnet`
.. setting:: TEMPLATES_DIR .. setting:: TEMPLATES_DIR
TEMPLATES_DIR TEMPLATES_DIR
@ -2007,6 +2113,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
@ -2032,7 +2157,7 @@ stopped) will not apply. This mode is currently experimental and may not be
suitable for production use. It may also not be supported by 3rd-party code. suitable for production use. It may also not be supported by 3rd-party code.
See :ref:`asyncio-without-reactor` for more information about this mode. See :ref:`asyncio-without-reactor` for more information about this mode.
.. note:: This setting can't be set :ref:`per-spider <spider-settings>`. .. note:: This is a :ref:`pre-crawler setting <pre-crawler-settings>`.
.. versionadded:: 2.15.0 .. versionadded:: 2.15.0
@ -2045,6 +2170,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
@ -2139,6 +2267,7 @@ current platform.
For additional information, see :doc:`core/howto/choosing-reactor`. For additional information, see :doc:`core/howto/choosing-reactor`.
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
.. setting:: URLLENGTH_LIMIT .. setting:: URLLENGTH_LIMIT
@ -2147,7 +2276,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.

View File

@ -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
@ -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

View File

@ -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
@ -355,6 +355,8 @@ Default: ``'scrapy.spidermiddlewares.referer.DefaultReferrerPolicy'``
using the special ``"referrer_policy"`` :ref:`Request.meta <topics-request-meta>` key, using the special ``"referrer_policy"`` :ref:`Request.meta <topics-request-meta>` key,
with the same acceptable values as for the ``REFERRER_POLICY`` setting. with the same acceptable values as for the ``REFERRER_POLICY`` setting.
.. seealso:: :ref:`security-credential-leakage`
Acceptable values for REFERRER_POLICY Acceptable values for REFERRER_POLICY
************************************* *************************************

View File

@ -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
@ -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
@ -354,11 +348,6 @@ Otherwise, you would cause iteration over a ``start_urls`` string
(a very common python pitfall) (a very common python pitfall)
resulting in each character being seen as a separate url. resulting in each character being seen as a separate url.
A valid use case is to set the http auth credentials
used by :class:`~scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware`::
scrapy crawl myspider -a http_user=myuser -a http_pass=mypassword
Spider arguments can also be passed through the Scrapyd ``schedule.json`` API. Spider arguments can also be passed through the Scrapyd ``schedule.json`` API.
See `Scrapyd documentation`_. See `Scrapyd documentation`_.
@ -457,13 +446,14 @@ with a ``TestItem`` declared in a ``myproject.items`` module:
.. code-block:: python .. code-block:: python
import scrapy from dataclasses import dataclass
class TestItem(scrapy.Item): @dataclass
id = scrapy.Field() class TestItem:
name = scrapy.Field() id: str | None = None
description = scrapy.Field() name: str | None = None
description: str | None = None
.. currentmodule:: scrapy.spiders .. currentmodule:: scrapy.spiders
@ -556,7 +546,6 @@ Let's now take a look at an example CrawlSpider with rules:
.. code-block:: python .. code-block:: python
import scrapy
from scrapy.spiders import CrawlSpider, Rule from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor from scrapy.linkextractors import LinkExtractor
@ -576,7 +565,7 @@ Let's now take a look at an example CrawlSpider with rules:
def parse_item(self, response): def parse_item(self, response):
self.logger.info("Hi, this is an item page! %s", response.url) self.logger.info("Hi, this is an item page! %s", response.url)
item = scrapy.Item() item = {}
item["id"] = response.xpath('//td[@id="item_id"]/text()').re(r"ID: (\d+)") item["id"] = response.xpath('//td[@id="item_id"]/text()').re(r"ID: (\d+)")
item["name"] = response.xpath('//td[@id="item_name"]/text()').get() item["name"] = response.xpath('//td[@id="item_name"]/text()').get()
item["description"] = response.xpath( item["description"] = response.xpath(
@ -598,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
------------- -------------
@ -619,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
@ -714,9 +703,9 @@ These spiders are pretty easy to use, let's have a look at one example:
) )
item = TestItem() item = TestItem()
item["id"] = node.xpath("@id").get() item.id = node.xpath("@id").get()
item["name"] = node.xpath("name").get() item.name = node.xpath("name").get()
item["description"] = node.xpath("description").get() item.description = node.xpath("description").get()
return item return item
Basically what we did up there was to create a spider that downloads a feed from Basically what we did up there was to create a spider that downloads a feed from
@ -778,9 +767,9 @@ Let's see an example similar to the previous one, but using a
self.logger.info("Hi, this is a row!: %r", row) self.logger.info("Hi, this is a row!: %r", row)
item = TestItem() item = TestItem()
item["id"] = row["id"] item.id = row["id"]
item["name"] = row["name"] item.name = row["name"]
item["description"] = row["description"] item.description = row["description"]
return item return item
@ -958,6 +947,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

View File

@ -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,13 +84,13 @@ 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
-------------------- --------------------
@ -102,7 +99,7 @@ MemoryStatsCollector
A simple stats collector that keeps the stats of the last scraping run (for 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 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 through the :attr:`spider_stats` attribute, which is a dict keyed by spider
domain name. name.
This is the default Stats Collector used in Scrapy. This is the default Stats Collector used in Scrapy.

View File

@ -29,14 +29,16 @@ disable it if you want. For more information about the extension itself see
.. note:: .. note::
This feature is not supported when :setting:`TWISTED_REACTOR_ENABLED` is ``False``. This feature is not supported when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
.. seealso:: :ref:`security-telnet`
.. highlight:: none .. highlight:: none
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...
@ -94,8 +96,6 @@ convenience:
+----------------+-------------------------------------------------------------------+ +----------------+-------------------------------------------------------------------+
| ``p`` | a shortcut to the :func:`pprint.pprint` function | | ``p`` | a shortcut to the :func:`pprint.pprint` function |
+----------------+-------------------------------------------------------------------+ +----------------+-------------------------------------------------------------------+
| ``hpy`` | for memory debugging (see :ref:`topics-leaks`) |
+----------------+-------------------------------------------------------------------+
Telnet console usage examples Telnet console usage examples
============================= =============================
@ -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()
@ -192,6 +192,8 @@ Default: ``'127.0.0.1'``
The interface the telnet console should listen on The interface the telnet console should listen on
.. seealso:: :ref:`security-telnet`
.. setting:: TELNETCONSOLE_USERNAME .. setting:: TELNETCONSOLE_USERNAME

View File

@ -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

View File

@ -94,7 +94,82 @@ 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_downloadermiddleware_stats",
"tests.test_downloaderslotssettings",
"tests.test_dupefilters",
"tests.test_engine_loop",
"tests.test_exporters",
"tests.test_extension_statsmailer",
"tests.test_extension_throttle",
"tests.test_feedexport",
"tests.test_feedexport_postprocess",
"tests.test_feedexport_storages",
"tests.test_feedexport_uri_params",
"tests.test_http2_client_protocol",
"tests.test_http_headers",
"tests.test_http_request",
"tests.test_http_request_form",
"tests.test_http_response",
"tests.test_http_response_text",
"tests.test_item",
"tests.test_link",
"tests.test_linkextractors",
"tests.test_loader",
"tests.test_logformatter",
"tests.test_logstats",
"tests.test_mail",
"tests.test_pipeline_crawl",
"tests.test_pipeline_files",
"tests.test_pipeline_images",
"tests.test_pipeline_media",
"tests.test_pipelines",
"tests.test_pqueues",
"tests.test_request_attribute_binding",
"tests.test_request_cb_kwargs",
"tests.test_request_dict",
"tests.test_request_left",
"tests.test_robotstxt_interface",
"tests.test_scheduler_base",
"tests.test_settings",
"tests.test_spider",
"tests.test_spider_crawl",
"tests.test_spidermiddleware_output_chain",
"tests.test_spidermiddleware_process_start",
"tests.test_spider_sitemap",
"tests.test_squeues",
"tests.test_squeues_request",
"tests.test_stats",
"tests.test_utils_datatypes",
"tests.test_utils_decorators",
"tests.test_utils_defer",
"tests.test_utils_deprecate",
"tests.test_utils_misc.test_return_with_argument_inside_generator",
"tests.test_utils_python",
"tests.test_utils_request",
]
check_untyped_defs = false check_untyped_defs = false
# Interface classes are hard to support # Interface classes are hard to support
@ -137,7 +212,7 @@ module = [
ignore_missing_imports = true ignore_missing_imports = true
[tool.bumpversion] [tool.bumpversion]
current_version = "2.16.0" current_version = "2.17.0"
commit = true commit = true
tag = true tag = true
tag_name = "{new_version}" tag_name = "{new_version}"
@ -157,6 +232,8 @@ parse = """(?P<major>0|[1-9]\\d*)\\.(?P<minor>0|[1-9]\\d*)"""
serialize = ["{major}.{minor}"] serialize = ["{major}.{minor}"]
[tool.coverage.run] [tool.coverage.run]
# sysmon, default on 3.14, is too slow: https://github.com/coveragepy/coveragepy/issues/2172
core = "ctrace"
branch = true branch = true
include = ["scrapy/*"] include = ["scrapy/*"]
omit = ["tests/*"] omit = ["tests/*"]
@ -225,8 +302,10 @@ disable = [
"too-many-positional-arguments", "too-many-positional-arguments",
"too-many-public-methods", "too-many-public-methods",
"too-many-return-statements", "too-many-return-statements",
"undefined-variable",
"unused-argument", "unused-argument",
"unused-variable", "unused-variable",
"use-implicit-booleaness-not-comparison",
"useless-import-alias", # used as a hint to mypy "useless-import-alias", # used as a hint to mypy
"useless-return", # https://github.com/pylint-dev/pylint/issues/6530 "useless-return", # https://github.com/pylint-dev/pylint/issues/6530
"wrong-import-position", "wrong-import-position",
@ -265,11 +344,13 @@ 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 = [
"ignore::DeprecationWarning:twisted.web.static", "ignore::DeprecationWarning:twisted.web.static",
# Twisted doesn't close failed sockets after CannotListenError: https://github.com/twisted/twisted/issues/6108
"ignore:Exception ignored in. <socket\\.socket.*laddr=..0\\.0\\.0\\.0., 0.:pytest.PytestUnraisableExceptionWarning",
] ]
[tool.ruff.lint] [tool.ruff.lint]

View File

@ -1 +1 @@
2.16.0 2.17.0

View File

@ -1,3 +1,4 @@
# pragma: no file cover
from scrapy.cmdline import execute from scrapy.cmdline import execute
if __name__ == "__main__": if __name__ == "__main__":

View File

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

View File

@ -16,6 +16,8 @@ from twisted.python import failure
from scrapy.exceptions import ScrapyDeprecationWarning, UsageError from scrapy.exceptions import ScrapyDeprecationWarning, UsageError
from scrapy.utils.conf import arglist_to_dict, feed_process_params_from_cli from scrapy.utils.conf import arglist_to_dict, feed_process_params_from_cli
from scrapy.utils.deprecate import method_is_overridden
from scrapy.utils.python import global_object_name
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterable from collections.abc import Iterable
@ -36,6 +38,14 @@ class ScrapyCommand(ABC):
def __init__(self) -> None: def __init__(self) -> None:
self.settings: Settings | None = None # set in scrapy.cmdline self.settings: Settings | None = None # set in scrapy.cmdline
if method_is_overridden(self.__class__, ScrapyCommand, "help"):
warnings.warn(
"The ScrapyCommand.help() method is deprecated and overriding "
f"it, as the {global_object_name(self.__class__)} class does, "
"has no effect; override long_desc() instead.",
ScrapyDeprecationWarning,
stacklevel=2,
)
def set_crawler(self, crawler: Crawler) -> None: # pragma: no cover def set_crawler(self, crawler: Crawler) -> None: # pragma: no cover
warnings.warn( warnings.warn(
@ -63,15 +73,16 @@ class ScrapyCommand(ABC):
def long_desc(self) -> str: def long_desc(self) -> str:
"""A long description of the command. Return short description when not """A long description of the command. Return short description when not
available. It cannot contain newlines since contents will be formatted available. It cannot contain newlines since contents will be formatted
by optparser which removes newlines and wraps text. by argparse which removes newlines and wraps text.
""" """
return self.short_desc() return self.short_desc()
def help(self) -> str: def help(self) -> str:
"""An extensive help for the command. It will be shown when using the warnings.warn(
"help" command. It can contain newlines since no post-formatting will "ScrapyCommand.help() is deprecated, use long_desc() instead.",
be applied to its contents. ScrapyDeprecationWarning,
""" stacklevel=2,
)
return self.long_desc() return self.long_desc()
def add_options(self, parser: argparse.ArgumentParser) -> None: def add_options(self, parser: argparse.ArgumentParser) -> None:

View File

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

View File

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

View File

@ -51,6 +51,8 @@ class Contract:
results.addSuccess(self.testcase_pre) results.addSuccess(self.testcase_pre)
cb_result = cb(response, **cb_kwargs) cb_result = cb(response, **cb_kwargs)
if isinstance(cb_result, (AsyncGenerator, CoroutineType)): if isinstance(cb_result, (AsyncGenerator, CoroutineType)):
if isinstance(cb_result, CoroutineType):
cb_result.close()
raise TypeError("Contracts don't support async callbacks") raise TypeError("Contracts don't support async callbacks")
return list(cast("Iterable[Any]", iterate_spider_output(cb_result))) return list(cast("Iterable[Any]", iterate_spider_output(cb_result)))
@ -67,6 +69,8 @@ class Contract:
def wrapper(response: Response, **cb_kwargs: Any) -> list[Any]: def wrapper(response: Response, **cb_kwargs: Any) -> list[Any]:
cb_result = cb(response, **cb_kwargs) cb_result = cb(response, **cb_kwargs)
if isinstance(cb_result, (AsyncGenerator, CoroutineType)): if isinstance(cb_result, (AsyncGenerator, CoroutineType)):
if isinstance(cb_result, CoroutineType):
cb_result.close()
raise TypeError("Contracts don't support async callbacks") raise TypeError("Contracts don't support async callbacks")
output = list(cast("Iterable[Any]", iterate_spider_output(cb_result))) output = list(cast("Iterable[Any]", iterate_spider_output(cb_result)))
try: try:

View File

@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any, cast
from OpenSSL import SSL from OpenSSL import SSL
from twisted.internet.ssl import ( from twisted.internet.ssl import (
AcceptableCiphers, AcceptableCiphers,
CertificateOptions, CertificateOptions,
TLSVersion,
optionsForClientTLS, optionsForClientTLS,
) )
from twisted.web.client import BrowserLikePolicyForHTTPS from twisted.web.client import BrowserLikePolicyForHTTPS
@ -16,19 +16,18 @@ from zope.interface.declarations import implementer
from zope.interface.verify import verifyObject from zope.interface.verify import verifyObject
from scrapy.core.downloader.tls import ( from scrapy.core.downloader.tls import (
DEFAULT_CIPHERS, _TWISTED_VERSION_MAP,
_openssl_methods,
_ScrapyClientTLSOptions, _ScrapyClientTLSOptions,
_ScrapyClientTLSOptions26, _ScrapyClientTLSOptions26,
openssl_methods,
) )
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils._deps_compat import TWISTED_TLS_NEW_IMPL from scrapy.utils._deps_compat import TWISTED_TLS_NEW_IMPL
from scrapy.utils.deprecate import create_deprecated_class from scrapy.utils.deprecate import create_deprecated_class
from scrapy.utils.misc import build_from_crawler, load_object from scrapy.utils.misc import build_from_crawler, load_object
from scrapy.utils.ssl import _get_cert_options_version_kwargs, _get_tls_version_limits
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Generator
from twisted.internet._sslverify import ClientTLSOptions from twisted.internet._sslverify import ClientTLSOptions
# typing.Self requires Python 3.11 # typing.Self requires Python 3.11
@ -38,54 +37,53 @@ if TYPE_CHECKING:
from scrapy.settings import BaseSettings from scrapy.settings import BaseSettings
@contextmanager
def _filter_method_warning() -> Generator[None]:
with warnings.catch_warnings():
# Twisted deprecation, https://github.com/scrapy/scrapy/issues/3288
warnings.filterwarnings(
"ignore",
message=r"Passing method to twisted\.internet\.ssl\.CertificateOptions",
category=DeprecationWarning,
)
yield
@implementer(IPolicyForHTTPS) @implementer(IPolicyForHTTPS)
class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS): class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
"""Non-peer-certificate verifying HTTPS context factory. """Non-peer-certificate verifying HTTPS context factory.
Default OpenSSL method is ``TLS_METHOD`` (also called ``SSLv23_METHOD``) Uses :setting:`DOWNLOADER_CLIENT_TLS_CIPHERS`,
which allows TLS protocol negotiation. :setting:`DOWNLOAD_TLS_MIN_VERSION` and :setting:`DOWNLOAD_TLS_MAX_VERSION`
to configure the :class:`~twisted.internet.ssl.CertificateOptions`
instance.
The purpose of this custom class is to provide a ``creatorForNetloc()`` The purpose of this custom class is to provide a ``creatorForNetloc()``
method that returns a ``_ScrapyClientTLSOptions`` instance configured based method that returns:
on TLS settings provided to the factory.
- a ``_ScrapyClientTLSOptions26`` or ``_ScrapyClientTLSOptions`` instance
configured based on TLS settings provided to the factory (when the
certificate verification is disabled);
- a result of ``optionsForClientTLS()`` called with those TLS settings
(when the certificate verification is enabled).
""" """
def __init__( def __init__(
self, self,
method: int = SSL.SSLv23_METHOD, # noqa: S503 method: int | None = SSL.SSLv23_METHOD, # noqa: S503
tls_verbose_logging: bool = False, tls_verbose_logging: bool = False,
tls_ciphers: str | None = None, tls_ciphers: str | None = None,
*args: Any, *args: Any,
verify_certificates: bool = False, verify_certificates: bool = False,
tls_min_version: TLSVersion | None = None,
tls_max_version: TLSVersion | None = None,
**kwargs: Any, **kwargs: Any,
): ):
super().__init__(*args, **kwargs) # type: ignore[no-untyped-call] super().__init__(*args, **kwargs) # type: ignore[no-untyped-call]
self._ssl_method: int = method self._ssl_method: int | None = method
self.tls_min_version: TLSVersion | None = tls_min_version
self.tls_max_version: TLSVersion | None = tls_max_version
self.tls_verbose_logging: bool = tls_verbose_logging # unused self.tls_verbose_logging: bool = tls_verbose_logging # unused
self.tls_ciphers: AcceptableCiphers self.tls_ciphers: AcceptableCiphers | None = (
if tls_ciphers: AcceptableCiphers.fromOpenSSLCipherString(tls_ciphers)
self.tls_ciphers = AcceptableCiphers.fromOpenSSLCipherString(tls_ciphers) if tls_ciphers
else: else None
self.tls_ciphers = DEFAULT_CIPHERS )
self._verify_certificates = verify_certificates self._verify_certificates = verify_certificates
@classmethod @classmethod
def from_crawler( def from_crawler(
cls, cls,
crawler: Crawler, crawler: Crawler,
method: int = SSL.SSLv23_METHOD, # noqa: S503 method: int | None = SSL.SSLv23_METHOD, # noqa: S503
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> Self: ) -> Self:
@ -93,12 +91,21 @@ class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
"DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING" "DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING"
) )
tls_ciphers: str | None = crawler.settings["DOWNLOADER_CLIENT_TLS_CIPHERS"] tls_ciphers: str | None = crawler.settings["DOWNLOADER_CLIENT_TLS_CIPHERS"]
# DOWNLOADER_CLIENT_TLS_METHOD reading and handling should be also moved here
# when the deprecated load_context_factory_from_settings() is removed
tls_min_ver, tls_max_ver = _get_tls_version_limits(
crawler.settings, _TWISTED_VERSION_MAP.__getitem__
)
if tls_min_ver or tls_max_ver:
method = None
verify_certificates = crawler.settings.getbool("DOWNLOAD_VERIFY_CERTIFICATES") verify_certificates = crawler.settings.getbool("DOWNLOAD_VERIFY_CERTIFICATES")
return cls( # type: ignore[misc] return cls( # type: ignore[misc]
*args, *args,
method=method, method=method,
tls_verbose_logging=tls_verbose_logging, tls_verbose_logging=tls_verbose_logging,
tls_ciphers=tls_ciphers, tls_ciphers=tls_ciphers,
tls_min_version=tls_min_ver,
tls_max_version=tls_max_ver,
verify_certificates=verify_certificates, verify_certificates=verify_certificates,
**kwargs, **kwargs,
) )
@ -108,12 +115,23 @@ class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
return self._get_cert_options() return self._get_cert_options()
def _get_cert_options(self) -> CertificateOptions: def _get_cert_options(self) -> CertificateOptions:
with _filter_method_warning(): return _ScrapyCertificateOptions(**self._get_cert_options_kwargs())
return _ScrapyCertificateOptions(
method=self._ssl_method, def _get_cert_options_kwargs(self) -> dict[str, Any]:
fixBrokenPeers=True, kwargs: dict[str, Any] = {
acceptableCiphers=self.tls_ciphers, "fixBrokenPeers": True,
"acceptableCiphers": self.tls_ciphers,
}
if self.tls_min_version or self.tls_max_version:
kwargs.update(
_get_cert_options_version_kwargs(
self.tls_min_version, self.tls_max_version
)
) )
# when ScrapyClientContextFactory is removed self._ssl_method can just be None by default
elif self._ssl_method != SSL.SSLv23_METHOD:
kwargs["method"] = self._ssl_method
return kwargs
# should be removed together with ScrapyClientContextFactory # should be removed together with ScrapyClientContextFactory
def getContext( def getContext(
@ -137,15 +155,10 @@ class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
self._get_context(), # type: ignore[arg-type] self._get_context(), # type: ignore[arg-type]
) )
# Otherwise use the normal Twisted function. # Otherwise use the normal Twisted function.
# Note that this doesn't use self._get_context(). return optionsForClientTLS( # type: ignore[no-any-return]
with _filter_method_warning(): hostname=hostname.decode("ascii"),
return optionsForClientTLS( # type: ignore[no-any-return] extraCertificateOptions=self._get_cert_options_kwargs(),
hostname=hostname.decode("ascii"), )
extraCertificateOptions={
"method": self._ssl_method,
"acceptableCiphers": self.tls_ciphers,
},
)
ScrapyClientContextFactory = create_deprecated_class( ScrapyClientContextFactory = create_deprecated_class(
@ -170,12 +183,6 @@ class BrowserLikeContextFactory(_ScrapyClientContextFactory):
:meth:`creatorForNetloc` is the same as :meth:`creatorForNetloc` is the same as
:class:`~twisted.web.client.BrowserLikePolicyForHTTPS` except this context :class:`~twisted.web.client.BrowserLikePolicyForHTTPS` except this context
factory allows setting the TLS/SSL method to use. factory allows setting the TLS/SSL method to use.
The default OpenSSL method is ``TLS_METHOD`` (also called
``SSLv23_METHOD``) which allows TLS protocol negotiation.
As this overrides the parent ``creatorForNetloc()`` method, only
``self._ssl_method`` is used from the parent class.
""" """
def __init__(self, *args: Any, **kwargs: Any): def __init__(self, *args: Any, **kwargs: Any):
@ -189,11 +196,10 @@ class BrowserLikeContextFactory(_ScrapyClientContextFactory):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def creatorForNetloc(self, hostname: bytes, port: int) -> ClientTLSOptions: def creatorForNetloc(self, hostname: bytes, port: int) -> ClientTLSOptions:
with _filter_method_warning(): return optionsForClientTLS( # type: ignore[no-any-return]
return optionsForClientTLS( # type: ignore[no-any-return] hostname=hostname.decode("ascii"),
hostname=hostname.decode("ascii"), extraCertificateOptions=self._get_cert_options_kwargs(),
extraCertificateOptions={"method": self._ssl_method}, )
)
@implementer(IPolicyForHTTPS) @implementer(IPolicyForHTTPS)
@ -268,6 +274,16 @@ def _load_context_factory_from_settings(crawler: Crawler) -> IPolicyForHTTPS:
Also passes values of other relevant settings to the factory class. Also passes values of other relevant settings to the factory class.
""" """
tls_method_setting: str = crawler.settings["DOWNLOADER_CLIENT_TLS_METHOD"]
if tls_method_setting != "TLS":
warnings.warn(
"Setting DOWNLOADER_CLIENT_TLS_METHOD to a non-default value is"
" deprecated, please use DOWNLOAD_TLS_MIN_VERSION and/or"
" DOWNLOAD_TLS_MAX_VERSION instead.",
ScrapyDeprecationWarning,
stacklevel=2,
)
tls_method = _openssl_methods[tls_method_setting]
if crawler.settings["DOWNLOADER_CLIENTCONTEXTFACTORY"] == "SENTINEL": if crawler.settings["DOWNLOADER_CLIENTCONTEXTFACTORY"] == "SENTINEL":
context_factory_cls = _ScrapyClientContextFactory context_factory_cls = _ScrapyClientContextFactory
else: # pragma: no cover else: # pragma: no cover
@ -279,13 +295,12 @@ def _load_context_factory_from_settings(crawler: Crawler) -> IPolicyForHTTPS:
context_factory_cls = load_object( context_factory_cls = load_object(
crawler.settings["DOWNLOADER_CLIENTCONTEXTFACTORY"] crawler.settings["DOWNLOADER_CLIENTCONTEXTFACTORY"]
) )
ssl_method = openssl_methods[crawler.settings.get("DOWNLOADER_CLIENT_TLS_METHOD")]
return cast( return cast(
"IPolicyForHTTPS", "IPolicyForHTTPS",
build_from_crawler( build_from_crawler(
context_factory_cls, context_factory_cls,
crawler, crawler,
method=ssl_method, method=tls_method,
), ),
) )

View File

@ -241,6 +241,16 @@ class BaseStreamingDownloadHandler(BaseHttpDownloadHandler, ABC, Generic[_Respon
body=response_body.getvalue(), body=response_body.getvalue(),
) )
@staticmethod
def _request_headers(request: Request) -> Headers:
"""Get a prepared copy of the request headers.
This removes the Proxy-Authorization header.
"""
headers = request.headers.copy()
headers.pop(b"Proxy-Authorization", None)
return headers
def _get_bind_address_host(self) -> str | None: def _get_bind_address_host(self) -> str | None:
"""Return the host portion of the bind address. """Return the host portion of the bind address.
@ -279,10 +289,8 @@ class BaseStreamingDownloadHandler(BaseHttpDownloadHandler, ABC, Generic[_Respon
if not proxy: if not proxy:
return None, None return None, None
proxy = add_http_if_no_scheme(proxy) proxy = add_http_if_no_scheme(proxy)
auth_header: list[bytes] | None = request.headers.pop( auth_header: bytes | None = request.headers.get(b"Proxy-Authorization")
b"Proxy-Authorization", None return proxy, auth_header.decode("ascii") if auth_header else None
)
return proxy, auth_header[0].decode("ascii") if auth_header else None
def _extract_proxy_url_with_creds(self, request: Request) -> str | None: def _extract_proxy_url_with_creds(self, request: Request) -> str | None:
"""Return the proxy URL with the userinfo added based on the """Return the proxy URL with the userinfo added based on the

View File

@ -5,6 +5,7 @@ from __future__ import annotations
import ipaddress import ipaddress
import ssl import ssl
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from socket import gaierror
from typing import TYPE_CHECKING, ClassVar from typing import TYPE_CHECKING, ClassVar
from scrapy.exceptions import ( from scrapy.exceptions import (
@ -17,6 +18,7 @@ from scrapy.exceptions import (
) )
from scrapy.http import Headers from scrapy.http import Headers
from scrapy.utils._download_handlers import NullCookieJar from scrapy.utils._download_handlers import NullCookieJar
from scrapy.utils.python import _iter_exc_causes
from scrapy.utils.ssl import ( from scrapy.utils.ssl import (
_log_sslobj_debug_info, _log_sslobj_debug_info,
_make_insecure_ssl_ctx, _make_insecure_ssl_ctx,
@ -34,10 +36,35 @@ if TYPE_CHECKING:
from scrapy.crawler import Crawler from scrapy.crawler import Crawler
HAS_SOCKS = HAS_HTTP2 = False
try: try:
import httpx import httpx
except ImportError: except ImportError:
httpx = None # type: ignore[assignment] httpx = None # type: ignore[assignment]
else:
# a small hack to avoid importing these optional extras unconditionally
DOWNLOAD_FAILED_EXCEPTIONS: tuple[type[BaseException], ...] = (
httpx.RequestError,
httpx.InvalidURL,
)
try:
import h2.exceptions
HAS_HTTP2 = True
DOWNLOAD_FAILED_EXCEPTIONS += (h2.exceptions.InvalidBodyLengthError,)
except ImportError: # pragma: no cover
pass
try:
import socksio.exceptions
HAS_SOCKS = True
DOWNLOAD_FAILED_EXCEPTIONS += (socksio.exceptions.ProtocolError,)
except ImportError: # pragma: no cover
pass
if TYPE_CHECKING: if TYPE_CHECKING:
@ -54,6 +81,11 @@ class HttpxDownloadHandler(_Base):
self._verify_certificates: bool = crawler.settings.getbool( self._verify_certificates: bool = crawler.settings.getbool(
"DOWNLOAD_VERIFY_CERTIFICATES" "DOWNLOAD_VERIFY_CERTIFICATES"
) )
self._enable_h2: bool = crawler.settings.getbool("HTTPX_HTTP2_ENABLED")
if self._enable_h2 and not HAS_HTTP2: # pragma: no cover
raise NotConfigured(
f"HTTP/2 support in {type(self).__name__} requires the 'httpx[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()
self._limits: httpx.Limits = httpx.Limits( self._limits: httpx.Limits = httpx.Limits(
@ -90,6 +122,7 @@ class HttpxDownloadHandler(_Base):
transport=httpx.AsyncHTTPTransport( transport=httpx.AsyncHTTPTransport(
verify=self._ssl_context, verify=self._ssl_context,
local_address=self._bind_host, local_address=self._bind_host,
http2=self._enable_h2,
limits=self._limits, limits=self._limits,
trust_env=False, trust_env=False,
proxy=proxy, proxy=proxy,
@ -113,13 +146,20 @@ class HttpxDownloadHandler(_Base):
async def _make_request( async def _make_request(
self, request: Request, timeout: float self, request: Request, timeout: float
) -> AsyncIterator[httpx.Response]: ) -> AsyncIterator[httpx.Response]:
client = self._get_client(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
raise ValueError(
f"SOCKS proxy support in {type(self).__name__} requires the 'httpx[socks]' extra to be installed."
)
client = self._get_client(proxy)
headers = self._request_headers(request).to_tuple_list()
try: try:
async with client.stream( async with client.stream(
request.method, request.method,
request.url, request.url,
content=request.body, content=request.body,
headers=request.headers.to_tuple_list(), headers=headers,
timeout=timeout, timeout=timeout,
) as response: ) as response:
yield response yield response
@ -130,18 +170,12 @@ class HttpxDownloadHandler(_Base):
except httpx.UnsupportedProtocol as e: except httpx.UnsupportedProtocol as e:
raise UnsupportedURLSchemeError(str(e)) from e raise UnsupportedURLSchemeError(str(e)) from e
except httpx.ConnectError as e: except httpx.ConnectError as e:
error_message = str(e) if any(isinstance(c, gaierror) for c in _iter_exc_causes(e)):
if ( raise CannotResolveHostError(str(e)) from e
"Name or service not known" in error_message
or "getaddrinfo failed" in error_message
or "nodename nor servname" in error_message
or "Temporary failure in name resolution" in error_message
):
raise CannotResolveHostError(error_message) from e
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 (httpx.NetworkError, httpx.RemoteProtocolError) as e: except DOWNLOAD_FAILED_EXCEPTIONS as e:
raise DownloadFailedError(str(e)) from e raise DownloadFailedError(str(e)) from e
@staticmethod @staticmethod

View File

@ -2,9 +2,9 @@
An asynchronous FTP file download handler for scrapy which somehow emulates an http response. An asynchronous FTP file download handler for scrapy which somehow emulates an http response.
FTP connection parameters are passed using the request meta field: FTP connection parameters are passed using the request meta field:
- ftp_user (required) - ftp_user (optional, falls back to FTP_USER)
- ftp_password (required) - ftp_password (optional, falls back to FTP_PASSWORD)
- ftp_passive (by default, enabled) sets FTP connection passive mode - ftp_passive (optional, falls back to FTP_PASSIVE_MODE) sets FTP connection passive mode
- ftp_local_filename - ftp_local_filename
- If not given, file data will come in the response.body, as a normal scrapy Response, - If not given, file data will come in the response.body, as a normal scrapy Response,
which will imply that the entire file will be on memory. which will imply that the entire file will be on memory.
@ -119,7 +119,10 @@ class FTPDownloadHandler(BaseDownloadHandler):
httpcode = self.CODE_MAPPING.get(ftpcode, self.CODE_MAPPING["default"]) httpcode = self.CODE_MAPPING.get(ftpcode, self.CODE_MAPPING["default"])
return Response(url=request.url, status=httpcode, body=message.encode()) return Response(url=request.url, status=httpcode, body=message.encode())
raise raise
protocol.close() finally:
protocol.close()
assert client.transport
client.transport.loseConnection()
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)

View File

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

View File

@ -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 = "https" if request.meta.get("is_secure") else "http" 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}"

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import warnings
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from OpenSSL import SSL from OpenSSL import SSL
@ -18,8 +19,9 @@ from service_identity.pyopenssl import (
verify_ip_address, verify_ip_address,
) )
from twisted.internet._sslverify import ClientTLSOptions from twisted.internet._sslverify import ClientTLSOptions
from twisted.internet.ssl import AcceptableCiphers from twisted.internet.ssl import AcceptableCiphers, TLSVersion
from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.deprecate import create_deprecated_class from scrapy.utils.deprecate import create_deprecated_class
if TYPE_CHECKING: if TYPE_CHECKING:
@ -32,17 +34,44 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
METHOD_TLS = "TLS" _openssl_methods: dict[str, int] = {
METHOD_TLSv10 = "TLSv1.0" "TLS": SSL.SSLv23_METHOD, # protocol negotiation (recommended)
METHOD_TLSv11 = "TLSv1.1" "TLSv1.0": SSL.TLSv1_METHOD, # TLS 1.0 only
METHOD_TLSv12 = "TLSv1.2" "TLSv1.1": SSL.TLSv1_1_METHOD, # TLS 1.1 only
"TLSv1.2": SSL.TLSv1_2_METHOD, # TLS 1.2 only
}
openssl_methods: dict[str, int] = { def __getattr__(name: str) -> Any:
METHOD_TLS: SSL.SSLv23_METHOD, # protocol negotiation (recommended) if name == "DEFAULT_CIPHERS":
METHOD_TLSv10: SSL.TLSv1_METHOD, # TLS 1.0 only warnings.warn(
METHOD_TLSv11: SSL.TLSv1_1_METHOD, # TLS 1.1 only "scrapy.core.downloader.tls.DEFAULT_CIPHERS is deprecated.",
METHOD_TLSv12: SSL.TLSv1_2_METHOD, # TLS 1.2 only ScrapyDeprecationWarning,
stacklevel=2,
)
return AcceptableCiphers.fromOpenSSLCipherString("DEFAULT")
deprecated = {
"METHOD_TLS": "TLS",
"METHOD_TLSv10": "TLSv1.0",
"METHOD_TLSv11": "TLSv1.1",
"METHOD_TLSv12": "TLSv1.2",
"openssl_methods": _openssl_methods,
}
if name in deprecated:
warnings.warn(
f"scrapy.core.downloader.tls.{name} is deprecated.",
ScrapyDeprecationWarning,
stacklevel=2,
)
return deprecated[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
_TWISTED_VERSION_MAP: dict[str, TLSVersion] = {
"TLSv1.0": TLSVersion.TLSv1_0,
"TLSv1.1": TLSVersion.TLSv1_1,
"TLSv1.2": TLSVersion.TLSv1_2,
"TLSv1.3": TLSVersion.TLSv1_3,
} }
@ -155,8 +184,3 @@ class _ScrapyClientTLSOptions26(ClientTLSOptions):
return True return True
return verifyCallback return verifyCallback
DEFAULT_CIPHERS: AcceptableCiphers = AcceptableCiphers.fromOpenSSLCipherString(
"DEFAULT"
)

View File

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

View File

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

View File

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

View File

@ -125,7 +125,7 @@ class Scraper:
def _check_deprecated_itemproc_method(self, method: str) -> None: def _check_deprecated_itemproc_method(self, method: str) -> None:
itemproc_cls = type(self.itemproc) itemproc_cls = type(self.itemproc)
if not hasattr(self.itemproc, "process_item_async"): if not hasattr(self.itemproc, f"{method}_async"):
warnings.warn( warnings.warn(
f"{global_object_name(itemproc_cls)} doesn't define a {method}_async() method," f"{global_object_name(itemproc_cls)} doesn't define a {method}_async() method,"
f" this is deprecated and the method will be required in future Scrapy versions.", f" this is deprecated and the method will be required in future Scrapy versions.",
@ -441,7 +441,7 @@ class Scraper:
self, output: Any, response: Response | Failure self, output: Any, response: Response | Failure
) -> Deferred[None]: ) -> Deferred[None]:
"""Process each Request/Item (given in the output parameter) returned """Process each Request/Item (given in the output parameter) returned
from the given spider. from the spider.
Items are sent to the item pipelines, requests are scheduled. Items are sent to the item pipelines, requests are scheduled.
""" """
@ -451,7 +451,7 @@ class Scraper:
self, output: Any, response: Response | Failure self, output: Any, response: Response | Failure
) -> None: ) -> None:
"""Process each Request/Item (given in the output parameter) returned """Process each Request/Item (given in the output parameter) returned
from the given spider. from the spider.
Items are sent to the item pipelines, requests are scheduled. Items are sent to the item pipelines, requests are scheduled.
""" """

View File

@ -363,9 +363,12 @@ class CrawlerRunnerBase(ABC):
""" """
Return a :class:`~scrapy.crawler.Crawler` object. Return a :class:`~scrapy.crawler.Crawler` object.
* If ``crawler_or_spidercls`` is a Crawler, it is returned as-is. * If ``crawler_or_spidercls`` is a Crawler, the runner's settings are
merged into it as defaults: for each setting, the runner's value
is applied only if the Crawler does not already have that setting at
an equal or higher priority. The Crawler is then returned.
* If ``crawler_or_spidercls`` is a Spider subclass, a new Crawler * If ``crawler_or_spidercls`` is a Spider subclass, a new Crawler
is constructed for it. is constructed for it using this runner's settings.
* If ``crawler_or_spidercls`` is a string, this function finds * If ``crawler_or_spidercls`` is a string, this function finds
a spider with this name in a Scrapy project (using spider loader), a spider with this name in a Scrapy project (using spider loader),
then creates a Crawler instance for it. then creates a Crawler instance for it.
@ -376,6 +379,7 @@ class CrawlerRunnerBase(ABC):
"it must be a spider class (or a Crawler object)" "it must be a spider class (or a Crawler object)"
) )
if isinstance(crawler_or_spidercls, Crawler): if isinstance(crawler_or_spidercls, Crawler):
crawler_or_spidercls.settings.update(self.settings)
return crawler_or_spidercls return crawler_or_spidercls
return self._create_crawler(crawler_or_spidercls) return self._create_crawler(crawler_or_spidercls)
@ -527,8 +531,8 @@ class AsyncCrawlerRunner(CrawlerRunnerBase):
""" """
Run a crawler with the provided arguments. Run a crawler with the provided arguments.
It will call the given Crawler's :meth:`~Crawler.crawl` method, while It will call the given Crawler's :meth:`~Crawler.crawl_async` method,
keeping track of it so it can be stopped later. while keeping track of it so it can be stopped later.
If ``crawler_or_spidercls`` isn't a :class:`~scrapy.crawler.Crawler` If ``crawler_or_spidercls`` isn't a :class:`~scrapy.crawler.Crawler`
instance, this method will try to create one using this parameter as instance, this method will try to create one using this parameter as
@ -769,7 +773,7 @@ class CrawlerProcess(CrawlerProcessBase, CrawlerRunner):
""" """
This method starts a :mod:`~twisted.internet.reactor`, adjusts its pool This method starts a :mod:`~twisted.internet.reactor`, adjusts its pool
size to :setting:`REACTOR_THREADPOOL_MAXSIZE`, and installs a DNS size to :setting:`REACTOR_THREADPOOL_MAXSIZE`, and installs a DNS
resolver based on :setting:`DNSCACHE_ENABLED`. resolver based on :setting:`TWISTED_DNS_RESOLVER`.
If ``stop_after_crawl`` is True, the reactor will be stopped after all If ``stop_after_crawl`` is True, the reactor will be stopped after all
crawlers have finished, using :meth:`join`. crawlers have finished, using :meth:`join`.
@ -871,10 +875,10 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
When using a reactor it adjusts its pool size to When using a reactor it adjusts its pool size to
:setting:`REACTOR_THREADPOOL_MAXSIZE` and installs a DNS resolver based :setting:`REACTOR_THREADPOOL_MAXSIZE` and installs a DNS resolver based
on :setting:`DNSCACHE_ENABLED`. on :setting:`TWISTED_DNS_RESOLVER`.
If ``stop_after_crawl`` is True, the reactor will be stopped after all If ``stop_after_crawl`` is True, the reactor/event loop will be stopped
crawlers have finished, using :meth:`join`. after all crawlers have finished, using :meth:`join`.
:param bool stop_after_crawl: stop or not the reactor when all :param bool stop_after_crawl: stop or not the reactor when all
crawlers have finished crawlers have finished

View File

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

View File

@ -6,11 +6,14 @@ See documentation in docs/topics/downloader-middleware.rst
from __future__ import annotations from __future__ import annotations
import warnings
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from w3lib.http import basic_auth_header from w3lib.http import basic_auth_header
from scrapy import Request, Spider, signals from scrapy import Request, Spider, signals
from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.settings import SETTINGS_PRIORITIES
from scrapy.utils.decorators import _warn_spider_arg from scrapy.utils.decorators import _warn_spider_arg
from scrapy.utils.url import url_is_from_any_domain from scrapy.utils.url import url_is_from_any_domain
@ -23,12 +26,28 @@ if TYPE_CHECKING:
class HttpAuthMiddleware: class HttpAuthMiddleware:
"""Set Basic HTTP Authorization header """Set Basic HTTP Authorization header."""
(http_user and http_pass spider class attributes)"""
def __init__(self) -> None:
self._auth: bytes | None = None
self._domain: str | None = None
@classmethod @classmethod
def from_crawler(cls, crawler: Crawler) -> Self: def from_crawler(cls, crawler: Crawler) -> Self:
o = cls() o = cls()
usr = crawler.settings.get("HTTPAUTH_USER", "")
pwd = crawler.settings.get("HTTPAUTH_PASS", "")
if usr or pwd:
domain_priority = crawler.settings.getpriority("HTTPAUTH_DOMAIN") or 0
if domain_priority <= SETTINGS_PRIORITIES["default"]:
raise ValueError(
"HTTPAUTH_DOMAIN must be set when HTTPAUTH_USER or HTTPAUTH_PASS "
"is configured. Set it to a domain (e.g. 'example.com') to restrict "
"credentials to that domain, or set it to None to send credentials "
"with all requests."
)
o._auth = basic_auth_header(usr, pwd)
o._domain = crawler.settings.get("HTTPAUTH_DOMAIN")
crawler.signals.connect(o.spider_opened, signal=signals.spider_opened) crawler.signals.connect(o.spider_opened, signal=signals.spider_opened)
return o return o
@ -36,18 +55,34 @@ class HttpAuthMiddleware:
usr = getattr(spider, "http_user", "") usr = getattr(spider, "http_user", "")
pwd = getattr(spider, "http_pass", "") pwd = getattr(spider, "http_pass", "")
if usr or pwd: if usr or pwd:
self.auth = basic_auth_header(usr, pwd) warnings.warn(
self.domain = spider.http_auth_domain # type: ignore[attr-defined] "Use the HTTPAUTH_USER, HTTPAUTH_PASS, and HTTPAUTH_DOMAIN settings "
"instead of the http_user, http_pass, and http_auth_domain spider "
"attributes. Support for the spider attributes will be removed in a "
"future version of Scrapy.",
category=ScrapyDeprecationWarning,
stacklevel=2,
)
self._auth = basic_auth_header(usr, pwd)
self._domain = spider.http_auth_domain # type: ignore[attr-defined]
@_warn_spider_arg @_warn_spider_arg
def process_request( def process_request(
self, request: Request, spider: Spider | None = None self, request: Request, spider: Spider | None = None
) -> Request | Response | None: ) -> Request | Response | None:
auth = getattr(self, "auth", None) if b"Authorization" in request.headers:
if ( return None
auth # Per-request meta overrides
and b"Authorization" not in request.headers usr = request.meta.get("http_user", "")
and (not self.domain or url_is_from_any_domain(request.url, [self.domain])) pwd = request.meta.get("http_pass", "")
if usr or pwd:
domain = request.meta.get("http_auth_domain")
if not domain or url_is_from_any_domain(request.url, [domain]):
request.headers[b"Authorization"] = basic_auth_header(usr, pwd)
return None
# Middleware-level auth
if self._auth and (
not self._domain or url_is_from_any_domain(request.url, [self._domain])
): ):
request.headers[b"Authorization"] = auth request.headers[b"Authorization"] = self._auth
return None return None

View File

@ -6,7 +6,7 @@ from logging import getLogger
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from scrapy import Request, Spider, signals from scrapy import Request, Spider, signals
from scrapy.exceptions import IgnoreRequest, NotConfigured from scrapy.exceptions import IgnoreRequest, NotConfigured, ScrapyDeprecationWarning
from scrapy.http import Response, TextResponse from scrapy.http import Response, TextResponse
from scrapy.responsetypes import responsetypes from scrapy.responsetypes import responsetypes
from scrapy.utils._compression import ( from scrapy.utils._compression import (
@ -60,7 +60,7 @@ else:
class HttpCompressionMiddleware: class HttpCompressionMiddleware:
"""This middleware allows compressed (gzip, deflate) traffic to be """This middleware allows compressed (gzip, deflate etc.) traffic to be
sent/received from websites""" sent/received from websites"""
def __init__( def __init__(
@ -70,6 +70,12 @@ class HttpCompressionMiddleware:
crawler: Crawler | None = None, crawler: Crawler | None = None,
): ):
if not crawler: if not crawler:
warnings.warn(
"Instantiating HttpCompressionMiddleware without a 'crawler' "
"argument is deprecated.",
category=ScrapyDeprecationWarning,
stacklevel=2,
)
self.stats = stats self.stats = stats
self._max_size = 1073741824 self._max_size = 1073741824
self._warn_size = 33554432 self._warn_size = 33554432
@ -108,49 +114,47 @@ class HttpCompressionMiddleware:
) -> Request | Response: ) -> Request | Response:
if request.method == "HEAD": if request.method == "HEAD":
return response return response
if isinstance(response, Response): content_encoding = response.headers.getlist("Content-Encoding")
content_encoding = response.headers.getlist("Content-Encoding") if content_encoding:
if content_encoding: max_size = request.meta.get("download_maxsize", self._max_size)
max_size = request.meta.get("download_maxsize", self._max_size) warn_size = request.meta.get("download_warnsize", self._warn_size)
warn_size = request.meta.get("download_warnsize", self._warn_size) try:
try: decoded_body, content_encoding = self._handle_encoding(
decoded_body, content_encoding = self._handle_encoding( response.body, content_encoding, max_size
response.body, content_encoding, max_size
)
except _DecompressionMaxSizeExceeded as e:
raise IgnoreRequest(
f"Ignored response {response} because its body "
f"({len(response.body)} B compressed, "
f"{e.decompressed_size} B decompressed so far) exceeded "
f"DOWNLOAD_MAXSIZE ({max_size} B) during decompression."
) from e
if len(response.body) < warn_size <= len(decoded_body):
logger.warning(
f"{response} body size after decompression "
f"({len(decoded_body)} B) is larger than the "
f"download warning size ({warn_size} B)."
)
if content_encoding:
self._warn_unknown_encoding(response, content_encoding)
response.headers["Content-Encoding"] = content_encoding
if self.stats:
self.stats.inc_value(
"httpcompression/response_bytes",
len(decoded_body),
)
self.stats.inc_value("httpcompression/response_count")
respcls = responsetypes.from_args(
headers=response.headers, url=response.url, body=decoded_body
) )
kwargs: dict[str, Any] = {"body": decoded_body} except _DecompressionMaxSizeExceeded as e:
if issubclass(respcls, TextResponse): raise IgnoreRequest(
# force recalculating the encoding until we make sure the f"Ignored response {response} because its body "
# responsetypes guessing is reliable f"({len(response.body)} B compressed, "
kwargs["encoding"] = None f"{e.decompressed_size} B decompressed so far) exceeded "
response = response.replace(cls=respcls, **kwargs) f"DOWNLOAD_MAXSIZE ({max_size} B) during decompression."
if not content_encoding: ) from e
del response.headers["Content-Encoding"] if len(response.body) < warn_size <= len(decoded_body):
logger.warning(
f"{response} body size after decompression "
f"({len(decoded_body)} B) is larger than the "
f"download warning size ({warn_size} B)."
)
if content_encoding:
self._warn_unknown_encoding(response, content_encoding)
response.headers["Content-Encoding"] = content_encoding
if self.stats:
self.stats.inc_value(
"httpcompression/response_bytes",
len(decoded_body),
)
self.stats.inc_value("httpcompression/response_count")
respcls = responsetypes.from_args(
headers=response.headers, url=response.url, body=decoded_body
)
kwargs: dict[str, Any] = {"body": decoded_body}
if issubclass(respcls, TextResponse):
# force recalculating the encoding until we make sure the
# responsetypes guessing is reliable
kwargs["encoding"] = None
response = response.replace(cls=respcls, **kwargs)
if not content_encoding:
del response.headers["Content-Encoding"]
return response return response
def _handle_encoding( def _handle_encoding(

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import logging import logging
import re import re
import warnings
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from scrapy import Request, Spider, signals from scrapy import Request, Spider, signals
@ -75,25 +74,10 @@ class OffsiteMiddleware:
allowed_domains = getattr(spider, "allowed_domains", None) allowed_domains = getattr(spider, "allowed_domains", None)
if not allowed_domains: if not allowed_domains:
return re.compile("") # allow all by default return re.compile("") # allow all by default
url_pattern = re.compile(r"^https?://.*$")
port_pattern = re.compile(r":\d+$")
domains = [] domains = []
for domain in allowed_domains: for domain in allowed_domains:
if domain is None: if domain is None:
continue continue
if url_pattern.match(domain): domains.append(re.escape(domain))
message = (
"allowed_domains accepts only domains, not URLs. "
f"Ignoring URL entry {domain} in allowed_domains."
)
warnings.warn(message, stacklevel=2)
elif port_pattern.search(domain):
message = (
"allowed_domains accepts only domains without ports. "
f"Ignoring entry {domain} in allowed_domains."
)
warnings.warn(message, stacklevel=2)
else:
domains.append(re.escape(domain))
regex = rf"^(.*\.)?({'|'.join(domains)})$" regex = rf"^(.*\.)?({'|'.join(domains)})$"
return re.compile(regex) return re.compile(regex)

View File

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

View File

@ -5,14 +5,11 @@ problems such as a connection timeout or HTTP 500 error.
You can change the behaviour of this middleware by modifying the scraping settings: You can change the behaviour of this middleware by modifying the scraping settings:
RETRY_TIMES - how many times to retry a failed page RETRY_TIMES - how many times to retry a failed page
RETRY_HTTP_CODES - which HTTP response codes to retry RETRY_HTTP_CODES - which HTTP response codes to retry
Failed pages are collected on the scraping process and rescheduled at the end,
once the spider has finished crawling all regular (non-failed) pages.
""" """
from __future__ import annotations from __future__ import annotations
from logging import Logger, getLogger from logging import Logger, getLevelName, getLogger
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from scrapy.exceptions import NotConfigured from scrapy.exceptions import NotConfigured
@ -43,6 +40,7 @@ def get_retry_request(
max_retry_times: int | None = None, max_retry_times: int | None = None,
priority_adjust: int | None = None, priority_adjust: int | None = None,
logger: Logger = retry_logger, logger: Logger = retry_logger,
give_up_log_level: int | str | None = None,
stats_base_key: str = "retry", stats_base_key: str = "retry",
) -> Request | None: ) -> Request | None:
""" """
@ -51,14 +49,16 @@ def get_retry_request(
exhausted. exhausted.
For example, in a :class:`~scrapy.Spider` callback, you could use it as For example, in a :class:`~scrapy.Spider` callback, you could use it as
follows:: follows:
.. code-block:: python
def parse(self, response): def parse(self, response):
if not response.text: if not response.text:
new_request_or_none = get_retry_request( new_request_or_none = get_retry_request(
response.request, response.request,
spider=self, spider=self,
reason='empty', reason="empty",
) )
return new_request_or_none return new_request_or_none
@ -67,8 +67,9 @@ def get_retry_request(
and :ref:`stats <topics-stats>`, and to provide extra logging context (see and :ref:`stats <topics-stats>`, and to provide extra logging context (see
:func:`logging.debug`). :func:`logging.debug`).
*reason* is a string or an :class:`Exception` object that indicates the *reason* is a string, an :class:`Exception` subclass or an
reason why the request needs to be retried. It is used to name retry stats. :class:`Exception` object that indicates the reason why the request needs
to be retried. It is used to name retry stats.
*max_retry_times* is a number that determines the maximum number of times *max_retry_times* is a number that determines the maximum number of times
that *request* can be retried. If not specified or ``None``, the number is that *request* can be retried. If not specified or ``None``, the number is
@ -82,6 +83,13 @@ def get_retry_request(
*logger* is the logging.Logger object to be used when logging messages *logger* is the logging.Logger object to be used when logging messages
*give_up_log_level* is the :ref:`logging level <levels>` used for the
message logged when a request exceeds its retries. See
:setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
.. versionadded:: 2.17.0
The *give_up_log_level* parameter.
*stats_base_key* is a string to be used as the base key for the *stats_base_key* is a string to be used as the base key for the
retry-related job stats retry-related job stats
""" """
@ -114,8 +122,16 @@ def get_retry_request(
stats.inc_value(f"{stats_base_key}/count") stats.inc_value(f"{stats_base_key}/count")
stats.inc_value(f"{stats_base_key}/reason_count/{reason}") stats.inc_value(f"{stats_base_key}/reason_count/{reason}")
return new_request return new_request
if give_up_log_level is None:
give_up_log_level = settings["RETRY_GIVE_UP_LOG_LEVEL"]
if isinstance(give_up_log_level, str):
level = getLevelName(give_up_log_level)
if not isinstance(level, int):
raise ValueError(f"Invalid give-up log level: {give_up_log_level!r}")
give_up_log_level = level
stats.inc_value(f"{stats_base_key}/max_reached") stats.inc_value(f"{stats_base_key}/max_reached")
logger.error( logger.log(
give_up_log_level,
"Gave up retrying %(request)s (failed %(retry_times)d times): %(reason)s", "Gave up retrying %(request)s (failed %(retry_times)d times): %(reason)s",
{"request": request, "retry_times": retry_times, "reason": reason}, {"request": request, "retry_times": retry_times, "reason": reason},
extra={"spider": spider}, extra={"spider": spider},
@ -132,6 +148,7 @@ class RetryMiddleware:
self.max_retry_times = settings.getint("RETRY_TIMES") self.max_retry_times = settings.getint("RETRY_TIMES")
self.retry_http_codes = {int(x) for x in settings.getlist("RETRY_HTTP_CODES")} self.retry_http_codes = {int(x) for x in settings.getlist("RETRY_HTTP_CODES")}
self.priority_adjust = settings.getint("RETRY_PRIORITY_ADJUST") self.priority_adjust = settings.getint("RETRY_PRIORITY_ADJUST")
self.give_up_log_level = settings["RETRY_GIVE_UP_LOG_LEVEL"]
self.exceptions_to_retry = tuple( self.exceptions_to_retry = tuple(
load_object(x) if isinstance(x, str) else x load_object(x) if isinstance(x, str) else x
for x in settings.getlist("RETRY_EXCEPTIONS") for x in settings.getlist("RETRY_EXCEPTIONS")
@ -175,6 +192,9 @@ class RetryMiddleware:
) -> Request | None: ) -> Request | None:
max_retry_times = request.meta.get("max_retry_times", self.max_retry_times) max_retry_times = request.meta.get("max_retry_times", self.max_retry_times)
priority_adjust = request.meta.get("priority_adjust", self.priority_adjust) priority_adjust = request.meta.get("priority_adjust", self.priority_adjust)
give_up_log_level = request.meta.get(
"give_up_log_level", self.give_up_log_level
)
assert self.crawler.spider assert self.crawler.spider
return get_retry_request( return get_retry_request(
request, request,
@ -182,4 +202,5 @@ class RetryMiddleware:
spider=self.crawler.spider, spider=self.crawler.spider,
max_retry_times=max_retry_times, max_retry_times=max_retry_times,
priority_adjust=priority_adjust, priority_adjust=priority_adjust,
give_up_log_level=give_up_log_level,
) )

View File

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

View File

@ -5,6 +5,7 @@ Item Exporters are used to export/serialize items into different formats.
from __future__ import annotations 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",
@ -171,7 +174,15 @@ class XmlItemExporter(BaseItemExporter):
super().__init__(**kwargs) super().__init__(**kwargs)
if not self.encoding: if not self.encoding:
self.encoding = "utf-8" self.encoding = "utf-8"
self.xg = XMLGenerator(file, encoding=self.encoding) # copied from xml.sax.saxutils._gettextwriter()
self.stream = TextIOWrapper(
file,
encoding=self.encoding,
errors="xmlcharrefreplace",
newline="\n",
write_through=True,
)
self.xg = XMLGenerator(self.stream, encoding=self.encoding)
def _beautify_newline(self, new_item: bool = False) -> None: def _beautify_newline(self, new_item: bool = False) -> None:
if self.indent is not None and (self.indent > 0 or new_item): if self.indent is not None and (self.indent > 0 or new_item):
@ -199,6 +210,7 @@ class XmlItemExporter(BaseItemExporter):
def finish_exporting(self) -> None: def finish_exporting(self) -> None:
self.xg.endElement(self.root_element) self.xg.endElement(self.root_element)
self.xg.endDocument() self.xg.endDocument()
self.stream.detach() # Avoid closing the wrapped file.
def _export_xml_field(self, name: str, serialized_value: Any, depth: int) -> None: def _export_xml_field(self, name: str, serialized_value: Any, depth: int) -> None:
self._beautify_indent(depth=depth) self._beautify_indent(depth=depth)
@ -245,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
@ -265,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)
@ -284,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()

View File

@ -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
] ]
@ -250,20 +277,22 @@ class S3FeedStorage(BlockingFeedStorage):
def _store_in_thread(self, file: IO[bytes]) -> None: def _store_in_thread(self, file: IO[bytes]) -> None:
file.seek(0) file.seek(0)
if self.acl: try:
self.s3_client.upload_fileobj( if self.acl:
Bucket=self.bucketname, self.s3_client.upload_fileobj(
Key=self.keyname, Bucket=self.bucketname,
Fileobj=file, Key=self.keyname,
ExtraArgs={"ACL": self.acl}, Fileobj=file,
) ExtraArgs={"ACL": self.acl},
else: )
self.s3_client.upload_fileobj( else:
Bucket=self.bucketname, self.s3_client.upload_fileobj(
Key=self.keyname, Bucket=self.bucketname,
Fileobj=file, Key=self.keyname,
) Fileobj=file,
file.close() )
finally:
file.close()
class GCSFeedStorage(BlockingFeedStorage): class GCSFeedStorage(BlockingFeedStorage):
@ -306,12 +335,15 @@ class GCSFeedStorage(BlockingFeedStorage):
def _store_in_thread(self, file: IO[bytes]) -> None: def _store_in_thread(self, file: IO[bytes]) -> None:
file.seek(0) file.seek(0)
from google.cloud.storage import Client # noqa: PLC0415 try:
from google.cloud.storage import Client # noqa: PLC0415
client = Client(project=self.project_id) client = Client(project=self.project_id)
bucket = client.get_bucket(self.bucket_name) bucket = client.get_bucket(self.bucket_name)
blob = bucket.blob(self.blob_name) blob = bucket.blob(self.blob_name)
blob.upload_from_file(file, predefined_acl=self.acl) blob.upload_from_file(file, predefined_acl=self.acl)
finally:
file.close()
class FTPFeedStorage(BlockingFeedStorage): class FTPFeedStorage(BlockingFeedStorage):
@ -468,7 +500,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
@ -480,9 +512,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
@ -509,7 +541,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,
@ -634,7 +666,7 @@ class FeedExporter:
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,

View File

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

View File

@ -113,7 +113,7 @@ class MemoryUsage:
{"memusage": mem}, {"memusage": mem},
extra={"crawler": self.crawler}, extra={"crawler": self.crawler},
) )
if self.notify_mails: if self.notify_mails: # pragma: no cover
subj = ( subj = (
f"{self.crawler.settings['BOT_NAME']} terminated: " f"{self.crawler.settings['BOT_NAME']} terminated: "
f"memory usage exceeded {mem}MiB at {socket.gethostname()}" f"memory usage exceeded {mem}MiB at {socket.gethostname()}"
@ -146,7 +146,7 @@ class MemoryUsage:
{"memusage": mem}, {"memusage": mem},
extra={"crawler": self.crawler}, extra={"crawler": self.crawler},
) )
if self.notify_mails: if self.notify_mails: # pragma: no cover
subj = ( subj = (
f"{self.crawler.settings['BOT_NAME']} warning: " f"{self.crawler.settings['BOT_NAME']} warning: "
f"memory usage reached {mem}MiB at {socket.gethostname()}" f"memory usage reached {mem}MiB at {socket.gethostname()}"
@ -155,7 +155,7 @@ class MemoryUsage:
self.crawler.stats.set_value("memusage/warning_notified", 1) self.crawler.stats.set_value("memusage/warning_notified", 1)
self.warned = True self.warned = True
def _send_report(self, rcpts: list[str], subject: str) -> None: def _send_report(self, rcpts: list[str], subject: str) -> None: # pragma: no cover
"""send notification mail with some additional useful info""" """send notification mail with some additional useful info"""
assert self.crawler.engine assert self.crawler.engine
assert self.crawler.stats assert self.crawler.stats

View File

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

View File

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

View File

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

View File

@ -51,7 +51,7 @@ class VerboseCookie(TypedDict):
secure: NotRequired[bool] secure: NotRequired[bool]
CookiesT: TypeAlias = dict[str, str] | list[VerboseCookie] CookiesT: TypeAlias = dict[str | bytes, str | bytes] | list[VerboseCookie]
RequestTypeVar = TypeVar("RequestTypeVar", bound="Request") RequestTypeVar = TypeVar("RequestTypeVar", bound="Request")
@ -138,6 +138,7 @@ class Request(object_ref):
) -> None: ) -> None:
self._encoding: str = encoding # this one has to be set first self._encoding: str = encoding # this one has to be set first
self.method: str = str(method).upper() self.method: str = str(method).upper()
self._meta: dict[str, Any] | None = dict(meta) if meta else None
self._set_url(url) self._set_url(url)
self._set_body(body) self._set_body(body)
if not isinstance(priority, int): if not isinstance(priority, int):
@ -232,7 +233,6 @@ class Request(object_ref):
#: default. See :meth:`~scrapy.Spider.start`. #: default. See :meth:`~scrapy.Spider.start`.
self.dont_filter: bool = dont_filter self.dont_filter: bool = dont_filter
self._meta: dict[str, Any] | None = dict(meta) if meta else None
self._cb_kwargs: dict[str, Any] | None = dict(cb_kwargs) if cb_kwargs else None self._cb_kwargs: dict[str, Any] | None = dict(cb_kwargs) if cb_kwargs else None
self._flags: list[str] | None = list(flags) if flags else None self._flags: list[str] | None = list(flags) if flags else None
@ -252,11 +252,17 @@ class Request(object_ref):
def url(self) -> str: def url(self) -> str:
return self._url return self._url
def _url_is_verbatim(self) -> bool:
return bool(self._meta and self._meta.get("verbatim_url"))
def _set_url(self, url: str) -> None: def _set_url(self, url: str) -> None:
if not isinstance(url, str): if not isinstance(url, str):
raise TypeError(f"Request url must be str, got {type(url).__name__}") raise TypeError(f"Request url must be str, got {type(url).__name__}")
self._url = safe_url_string(url, self.encoding) if self._url_is_verbatim():
self._url = url
else:
self._url = safe_url_string(url, self.encoding)
if ( if (
"://" not in self._url "://" not in self._url

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,9 @@ its documentation in: docs/topics/link-extractors.rst
class Link: class Link:
"""Link objects represent an extracted link by the LinkExtractor. """Link objects represent an extracted link by the LinkExtractor.
Using the anchor tag sample below to illustrate the parameters:: Using the anchor tag sample below to illustrate the parameters:
.. code-block:: html
<a href="https://example.com/nofollow.html#foo" rel="nofollow">Dont follow this one</a> <a href="https://example.com/nofollow.html#foo" rel="nofollow">Dont follow this one</a>
@ -39,7 +41,7 @@ class Link:
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if not isinstance(other, Link): if not isinstance(other, Link):
raise NotImplementedError return NotImplemented
return ( return (
self.url == other.url self.url == other.url
and self.text == other.text and self.text == other.text

View File

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

View File

@ -27,7 +27,7 @@ class ItemLoader(itemloaders.ItemLoader):
:param item: The item instance to populate using subsequent calls to :param item: The item instance to populate using subsequent calls to
:meth:`~ItemLoader.add_xpath`, :meth:`~ItemLoader.add_css`, :meth:`~ItemLoader.add_xpath`, :meth:`~ItemLoader.add_css`,
or :meth:`~ItemLoader.add_value`. or :meth:`~ItemLoader.add_value`.
:type item: scrapy.item.Item :type item: :ref:`item object <item-types>`
:param selector: The selector to extract data from, when using the :param selector: The selector to extract data from, when using the
:meth:`add_xpath`, :meth:`add_css`, :meth:`replace_xpath`, or :meth:`add_xpath`, :meth:`add_css`, :meth:`replace_xpath`, or

View File

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

View File

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

View File

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

View File

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

View File

@ -11,14 +11,20 @@ import hashlib
import warnings import warnings
from contextlib import suppress from contextlib import suppress
from io import BytesIO from io import BytesIO
from typing import TYPE_CHECKING, Any, ClassVar from typing import TYPE_CHECKING, Any, ClassVar, cast
from itemadapter import ItemAdapter from itemadapter import ItemAdapter
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning from scrapy.exceptions import 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.files import FileException, FilesPipeline, _md5sum from scrapy.pipelines.files import (
FileException,
FilesPipeline,
GCSFilesStore,
S3FilesStore,
_md5sum,
)
from scrapy.utils.defer import ensure_awaitable from scrapy.utils.defer import ensure_awaitable
from scrapy.utils.python import to_bytes from scrapy.utils.python import to_bytes
@ -33,6 +39,7 @@ if TYPE_CHECKING:
from scrapy.crawler import Crawler from scrapy.crawler import Crawler
from scrapy.pipelines.media import FileInfoOrError, MediaPipeline from scrapy.pipelines.media import FileInfoOrError, MediaPipeline
from scrapy.settings import BaseSettings
class ImageException(FileException): class ImageException(FileException):
@ -40,7 +47,7 @@ class ImageException(FileException):
class ImagesPipeline(FilesPipeline): class ImagesPipeline(FilesPipeline):
"""Abstract pipeline that implement the image thumbnail generation logic""" """Pipeline that implements the handling logic specific to images."""
MEDIA_NAME: str = "image" MEDIA_NAME: str = "image"
@ -126,6 +133,20 @@ class ImagesPipeline(FilesPipeline):
) -> str: ) -> str:
return await self.image_downloaded(response, request, info, item=item) return await self.image_downloaded(response, request, info, item=item)
@classmethod
def _update_stores(cls, settings: BaseSettings) -> None:
super()._update_stores(settings)
s3store: type[S3FilesStore] = cast(
"type[S3FilesStore]", cls.STORE_SCHEMES["s3"]
)
s3store.POLICY = settings["IMAGES_STORE_S3_ACL"]
gcs_store: type[GCSFilesStore] = cast(
"type[GCSFilesStore]", cls.STORE_SCHEMES["gs"]
)
gcs_store.POLICY = settings["IMAGES_STORE_GCS_ACL"] or None
async def image_downloaded( async def image_downloaded(
self, self,
response: Response, response: Response,

View File

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

View File

@ -75,7 +75,7 @@ class HostResolution:
def __init__(self, name: str): def __init__(self, name: str):
self.name: str = name self.name: str = name
def cancel(self) -> None: def cancel(self) -> None: # pragma: no cover
raise NotImplementedError raise NotImplementedError

View File

@ -40,18 +40,13 @@ class ResponseTypes:
self.classes: dict[str, type[Response]] = {} self.classes: dict[str, type[Response]] = {}
self.mimetypes: MimeTypes = MimeTypes() self.mimetypes: MimeTypes = MimeTypes()
mimedata = get_data("scrapy", "mime.types") mimedata = get_data("scrapy", "mime.types")
if not mimedata: assert mimedata is not None
raise ValueError(
"The mime.types file is not found in the Scrapy installation"
)
self.mimetypes.readfp(StringIO(mimedata.decode("utf8"))) self.mimetypes.readfp(StringIO(mimedata.decode("utf8")))
for mimetype, cls in self.CLASSES.items(): for mimetype, cls in self.CLASSES.items():
self.classes[mimetype] = load_object(cls) self.classes[mimetype] = load_object(cls)
def from_mimetype(self, mimetype: str) -> type[Response]: def from_mimetype(self, mimetype: str) -> type[Response]:
"""Return the most appropriate Response class for the given mimetype""" """Return the most appropriate Response class for the given mimetype"""
if mimetype is None:
return Response
if mimetype in self.classes: if mimetype in self.classes:
return self.classes[mimetype] return self.classes[mimetype]
basetype = f"{mimetype.split('/', maxsplit=1)[0]}/*" basetype = f"{mimetype.split('/', maxsplit=1)[0]}/*"

View File

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

View File

@ -16,10 +16,6 @@ from scrapy.utils.python import global_object_name
logger = getLogger(__name__) logger = getLogger(__name__)
# The key types are restricted in BaseSettings._get_key() to ones supported by JSON,
# see https://github.com/scrapy/scrapy/issues/5383.
_SettingsKey: TypeAlias = bool | float | int | str | None
if TYPE_CHECKING: if TYPE_CHECKING:
from types import ModuleType from types import ModuleType
@ -29,7 +25,7 @@ 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[_SettingsKey, Any] | str | None _SettingsInput: TypeAlias = SupportsItems[str, Any] | str | None
SETTINGS_PRIORITIES: dict[str, int] = { SETTINGS_PRIORITIES: dict[str, int] = {
@ -80,7 +76,7 @@ class SettingsAttribute:
return f"<SettingsAttribute value={self.value!r} priority={self.priority}>" return f"<SettingsAttribute value={self.value!r} priority={self.priority}>"
class BaseSettings(MutableMapping[_SettingsKey, Any]): class BaseSettings(MutableMapping[str, Any]):
""" """
Instances of this class behave like dictionaries, but store priorities Instances of this class behave like dictionaries, but store priorities
along with their ``(key, value)`` pairs, and can be frozen (i.e. marked along with their ``(key, value)`` pairs, and can be frozen (i.e. marked
@ -106,11 +102,11 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
def __init__(self, values: _SettingsInput = None, priority: int | str = "project"): def __init__(self, values: _SettingsInput = None, priority: int | str = "project"):
self.frozen: bool = False self.frozen: bool = False
self.attributes: dict[_SettingsKey, SettingsAttribute] = {} self.attributes: dict[str, SettingsAttribute] = {}
if values: if values:
self.update(values, priority) self.update(values, priority)
def __getitem__(self, opt_name: _SettingsKey) -> Any: def __getitem__(self, opt_name: str) -> Any:
if opt_name not in self: if opt_name not in self:
return None return None
return self.attributes[opt_name].value return self.attributes[opt_name].value
@ -118,7 +114,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
def __contains__(self, name: Any) -> bool: def __contains__(self, name: Any) -> bool:
return name in self.attributes return name in self.attributes
def add_to_list(self, name: _SettingsKey, item: Any) -> None: def add_to_list(self, name: str, item: Any) -> None:
"""Append *item* to the :class:`list` setting with the specified *name* """Append *item* to the :class:`list` setting with the specified *name*
if *item* is not already in that list. if *item* is not already in that list.
@ -129,7 +125,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
if item not in value: if item not in value:
self.set(name, [*value, item], self.getpriority(name) or 0) self.set(name, [*value, item], self.getpriority(name) or 0)
def remove_from_list(self, name: _SettingsKey, item: Any) -> None: def remove_from_list(self, name: str, item: Any) -> None:
"""Remove *item* from the :class:`list` setting with the specified """Remove *item* from the :class:`list` setting with the specified
*name*. *name*.
@ -143,7 +139,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
raise ValueError(f"{item!r} not found in the {name} setting ({value!r}).") raise ValueError(f"{item!r} not found in the {name} setting ({value!r}).")
self.set(name, [v for v in value if v != item], self.getpriority(name) or 0) self.set(name, [v for v in value if v != item], self.getpriority(name) or 0)
def get(self, name: _SettingsKey, default: Any = None) -> Any: # pylint: disable=arguments-renamed def get(self, name: str, default: Any = None) -> Any: # pylint: disable=arguments-renamed
""" """
Get a setting value without affecting its original type. Get a setting value without affecting its original type.
@ -172,7 +168,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
return self[name] if self[name] is not None else default return self[name] if self[name] is not None else default
def getbool(self, name: _SettingsKey, default: bool = False) -> bool: def getbool(self, name: str, default: bool = False) -> bool:
""" """
Get a setting value as a boolean. Get a setting value as a boolean.
@ -202,7 +198,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
"'True'/'False' and 'true'/'false'" "'True'/'False' and 'true'/'false'"
) from None ) from None
def getint(self, name: _SettingsKey, default: int = 0) -> int: def getint(self, name: str, default: int = 0) -> int:
""" """
Get a setting value as an int. Get a setting value as an int.
@ -214,7 +210,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
""" """
return int(self.get(name, default)) return int(self.get(name, default))
def getfloat(self, name: _SettingsKey, default: float = 0.0) -> float: def getfloat(self, name: str, default: float = 0.0) -> float:
""" """
Get a setting value as a float. Get a setting value as a float.
@ -226,9 +222,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
""" """
return float(self.get(name, default)) return float(self.get(name, default))
def getlist( def getlist(self, name: str, default: list[Any] | None = None) -> list[Any]:
self, name: _SettingsKey, default: list[Any] | None = None
) -> list[Any]:
""" """
Get a setting value as a list. If the setting original type is a list, Get a setting value as a list. If the setting original type is a list,
a copy of it will be returned. If it's a string it will be split by a copy of it will be returned. If it's a string it will be split by
@ -251,7 +245,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
return list(value) return list(value)
def getdict( def getdict(
self, name: _SettingsKey, default: dict[Any, Any] | None = None self, name: str, default: dict[Any, Any] | None = None
) -> dict[Any, Any]: ) -> dict[Any, Any]:
""" """
Get a setting value as a dictionary. If the setting original type is a Get a setting value as a dictionary. If the setting original type is a
@ -275,7 +269,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
def getdictorlist( def getdictorlist(
self, self,
name: _SettingsKey, name: str,
default: dict[Any, Any] | list[Any] | tuple[Any] | None = None, default: dict[Any, Any] | list[Any] | tuple[Any] | None = None,
) -> dict[Any, Any] | list[Any]: ) -> dict[Any, Any] | list[Any]:
"""Get a setting value as either a :class:`dict` or a :class:`list`. """Get a setting value as either a :class:`dict` or a :class:`list`.
@ -294,10 +288,10 @@ class BaseSettings(MutableMapping[_SettingsKey, 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:
@ -322,7 +316,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
) )
return copy.deepcopy(value) return copy.deepcopy(value)
def getwithbase(self, name: _SettingsKey) -> BaseSettings: def getwithbase(self, name: str) -> BaseSettings:
"""Get a composition of a dictionary-like setting and its ``_BASE`` """Get a composition of a dictionary-like setting and its ``_BASE``
counterpart. counterpart.
@ -341,7 +335,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
compbs.update(self[name]) compbs.update(self[name])
return compbs return compbs
def get_component_priority_dict_with_base(self, name: _SettingsKey) -> BaseSettings: def get_component_priority_dict_with_base(self, name: str) -> BaseSettings:
"""Get a composition of a component priority dictionary setting and """Get a composition of a component priority dictionary setting and
its ``_BASE`` counterpart. its ``_BASE`` counterpart.
@ -389,7 +383,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
{restore_key(k): v for k, v in result.items() if v is not None} {restore_key(k): v for k, v in result.items() if v is not None}
) )
def getpriority(self, name: _SettingsKey) -> int | None: def getpriority(self, name: str) -> int | None:
""" """
Return the current numerical priority value of a setting, or ``None`` if Return the current numerical priority value of a setting, or ``None`` if
the given ``name`` does not exist. the given ``name`` does not exist.
@ -414,7 +408,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
def replace_in_component_priority_dict( def replace_in_component_priority_dict(
self, self,
name: _SettingsKey, name: str,
old_cls: type, old_cls: type,
new_cls: type, new_cls: type,
priority: int | None = None, priority: int | None = None,
@ -453,18 +447,17 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
) )
self.set(name, component_priority_dict, priority=self.getpriority(name) or 0) self.set(name, component_priority_dict, priority=self.getpriority(name) or 0)
def __setitem__(self, name: _SettingsKey, value: Any) -> None: def __setitem__(self, name: str, value: Any) -> None:
self.set(name, value) self.set(name, value)
def set( def set(self, name: str, value: Any, priority: int | str = "project") -> None:
self, name: _SettingsKey, value: Any, priority: int | str = "project"
) -> None:
""" """
Store a key/value attribute with a given priority. Store a key/value attribute with a given priority.
Settings should be populated *before* configuring the Crawler object Settings should be populated *before* the Crawler object applies them
(through the :meth:`~scrapy.crawler.Crawler.configure` method), (in the :meth:`~scrapy.crawler.Crawler.crawl_async` or
otherwise they won't have any effect. :meth:`~scrapy.crawler.Crawler.crawl` method), otherwise they won't
have any effect.
:param name: the setting name :param name: the setting name
:type name: str :type name: str
@ -487,7 +480,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
self.attributes[name].set(value, priority) self.attributes[name].set(value, priority)
def set_in_component_priority_dict( def set_in_component_priority_dict(
self, name: _SettingsKey, cls: type, priority: int | None self, name: str, cls: type, priority: int | None
) -> None: ) -> None:
"""Set the *cls* component in the *name* :ref:`component priority """Set the *cls* component in the *name* :ref:`component priority
dictionary <component-priority-dictionaries>` setting with *priority*. dictionary <component-priority-dictionaries>` setting with *priority*.
@ -512,7 +505,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
def setdefault( # pylint: disable=arguments-renamed def setdefault( # pylint: disable=arguments-renamed
self, self,
name: _SettingsKey, name: str,
default: Any = None, default: Any = None,
priority: int | str = "project", priority: int | str = "project",
) -> Any: ) -> Any:
@ -523,7 +516,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
return self.attributes[name].value return self.attributes[name].value
def setdefault_in_component_priority_dict( def setdefault_in_component_priority_dict(
self, name: _SettingsKey, cls: type, priority: int | None self, name: str, cls: type, priority: int | None
) -> None: ) -> None:
"""Set the *cls* component in the *name* :ref:`component priority """Set the *cls* component in the *name* :ref:`component priority
dictionary <component-priority-dictionaries>` setting with *priority* dictionary <component-priority-dictionaries>` setting with *priority*
@ -592,7 +585,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
""" """
self._assert_mutability() self._assert_mutability()
if isinstance(values, str): if isinstance(values, str):
values = cast("dict[_SettingsKey, Any]", json.loads(values)) values = cast("dict[str, Any]", json.loads(values))
if values is not None: if values is not None:
if isinstance(values, BaseSettings): if isinstance(values, BaseSettings):
for name, value in values.items(): for name, value in values.items():
@ -601,7 +594,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
for name, value in values.items(): for name, value in values.items():
self.set(name, value, priority) self.set(name, value, priority)
def delete(self, name: _SettingsKey, priority: int | str = "project") -> None: def delete(self, name: str, priority: int | str = "project") -> None:
if name not in self: if name not in self:
raise KeyError(name) raise KeyError(name)
self._assert_mutability() self._assert_mutability()
@ -609,7 +602,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
if priority >= cast("int", self.getpriority(name)): if priority >= cast("int", self.getpriority(name)):
del self.attributes[name] del self.attributes[name]
def __delitem__(self, name: _SettingsKey) -> None: def __delitem__(self, name: str) -> None:
self._assert_mutability() self._assert_mutability()
del self.attributes[name] del self.attributes[name]
@ -621,7 +614,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
""" """
Make a deep copy of current settings. Make a deep copy of current settings.
This method returns a new instance of the :class:`Settings` class, This method returns a new instance of this class,
populated with the same values and their priorities. populated with the same values and their priorities.
Modifications to the new object won't be reflected on the original Modifications to the new object won't be reflected on the original
@ -649,31 +642,24 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
copy.freeze() copy.freeze()
return copy return copy
def __iter__(self) -> Iterator[_SettingsKey]: def __iter__(self) -> Iterator[str]:
return iter(self.attributes) return iter(self.attributes)
def __len__(self) -> int: def __len__(self) -> int:
return len(self.attributes) return len(self.attributes)
def _to_dict(self) -> dict[_SettingsKey, Any]: def _to_dict(self) -> dict[str, Any]:
return { return {
self._get_key(k): (v._to_dict() if isinstance(v, BaseSettings) else v) str(k): (v._to_dict() if isinstance(v, BaseSettings) else v)
for k, v in self.items() for k, v in self.items()
} }
def _get_key(self, key_value: Any) -> _SettingsKey: def copy_to_dict(self) -> dict[str, Any]:
return (
key_value
if isinstance(key_value, (bool, float, int, str, type(None)))
else str(key_value)
)
def copy_to_dict(self) -> dict[_SettingsKey, Any]:
""" """
Make a copy of current settings and convert to a dict. Make a copy of current settings and convert to a dict.
This method returns a new dict populated with the same values This method returns a new dict populated with the same values
and their priorities as the current settings. as the current settings.
Modifications to the returned dict won't be reflected on the original Modifications to the returned dict won't be reflected on the original
settings. settings.
@ -691,7 +677,7 @@ class BaseSettings(MutableMapping[_SettingsKey, Any]):
else: else:
p.text(pformat(self.copy_to_dict())) p.text(pformat(self.copy_to_dict()))
def pop(self, name: _SettingsKey, default: Any = __default) -> Any: # pylint: disable=arguments-renamed def pop(self, name: str, default: Any = __default) -> Any: # pylint: disable=arguments-renamed
try: try:
value = self.attributes[name].value value = self.attributes[name].value
except KeyError: except KeyError:
@ -735,7 +721,7 @@ def iter_default_settings() -> Iterable[tuple[str, Any]]:
def overridden_settings( def overridden_settings(
settings: Mapping[_SettingsKey, Any], settings: Mapping[str, Any],
) -> Iterable[tuple[str, Any]]: ) -> Iterable[tuple[str, Any]]:
"""Return an iterable of the settings that have been overridden""" """Return an iterable of the settings that have been overridden"""
for name, defvalue in iter_default_settings(): for name, defvalue in iter_default_settings():

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