mirror of https://github.com/scrapy/scrapy.git
Merge remote-tracking branch 'upstream/master' into cookiejar_to_spider2
# Conflicts: # scrapy/downloadermiddlewares/cookies.py
This commit is contained in:
commit
7648489522
|
|
@ -1,7 +0,0 @@
|
|||
skips:
|
||||
- B101 # assert_used, needed for mypy
|
||||
- B321 # ftplib, https://github.com/scrapy/scrapy/issues/4180
|
||||
- B402 # import_ftplib, https://github.com/scrapy/scrapy/issues/4180
|
||||
- B411 # import_xmlrpclib, https://github.com/PyCQA/bandit/issues/1082
|
||||
- B503 # ssl_with_bad_defaults
|
||||
exclude_dirs: ['tests']
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
[bumpversion]
|
||||
current_version = 2.11.2
|
||||
commit = True
|
||||
tag = True
|
||||
tag_name = {new_version}
|
||||
|
||||
[bumpversion:file:scrapy/VERSION]
|
||||
|
||||
[bumpversion:file:SECURITY.md]
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.x
|
||||
serialize = {major}.{minor}.x
|
||||
12
.coveragerc
12
.coveragerc
|
|
@ -1,12 +0,0 @@
|
|||
[run]
|
||||
branch = true
|
||||
include = scrapy/*
|
||||
omit =
|
||||
tests/*
|
||||
disable_warnings = include-ignored
|
||||
|
||||
[report]
|
||||
# https://github.com/nedbat/coveragepy/issues/831#issuecomment-517778185
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
82
.flake8
82
.flake8
|
|
@ -1,82 +0,0 @@
|
|||
[flake8]
|
||||
|
||||
max-line-length = 119
|
||||
extend-select = TC, TC1
|
||||
ignore =
|
||||
# black disagrees with flake8 about these
|
||||
E203, E501, E701, E704, W503
|
||||
|
||||
# Assigning to `os.environ` doesn't clear the environment.
|
||||
B003
|
||||
# Do not use mutable data structures for argument defaults.
|
||||
B006
|
||||
# Loop control variable not used within the loop body.
|
||||
B007
|
||||
# Do not perform function calls in argument defaults.
|
||||
B008
|
||||
# return/continue/break inside finally blocks cause exceptions to be
|
||||
# silenced.
|
||||
B012
|
||||
# Star-arg unpacking after a keyword argument is strongly discouraged
|
||||
B026
|
||||
# No explicit stacklevel argument found.
|
||||
B028
|
||||
|
||||
# docstring does contain unindexed parameters
|
||||
P102
|
||||
# other string does contain unindexed parameters
|
||||
P103
|
||||
|
||||
# Missing docstring in public module
|
||||
D100
|
||||
# Missing docstring in public class
|
||||
D101
|
||||
# Missing docstring in public method
|
||||
D102
|
||||
# Missing docstring in public function
|
||||
D103
|
||||
# Missing docstring in public package
|
||||
D104
|
||||
# Missing docstring in magic method
|
||||
D105
|
||||
# Missing docstring in public nested class
|
||||
D106
|
||||
# Missing docstring in __init__
|
||||
D107
|
||||
# One-line docstring should fit on one line with quotes
|
||||
D200
|
||||
# No blank lines allowed after function docstring
|
||||
D202
|
||||
# 1 blank line required between summary line and description
|
||||
D205
|
||||
# Multi-line docstring closing quotes should be on a separate line
|
||||
D209
|
||||
# First line should end with a period
|
||||
D400
|
||||
# First line should be in imperative mood; try rephrasing
|
||||
D401
|
||||
# First line should not be the function's "signature"
|
||||
D402
|
||||
# First word of the first line should be properly capitalized
|
||||
D403
|
||||
|
||||
# Annotation in typing.cast() should be a string literal
|
||||
TC006
|
||||
exclude =
|
||||
docs/conf.py
|
||||
|
||||
per-file-ignores =
|
||||
# Exclude files that are meant to provide top-level imports
|
||||
# E402: Module level import not at top of file
|
||||
# F401: Module imported but unused
|
||||
scrapy/__init__.py:E402
|
||||
scrapy/core/downloader/handlers/http.py:F401
|
||||
scrapy/http/__init__.py:F401
|
||||
scrapy/linkextractors/__init__.py:E402,F401
|
||||
scrapy/selector/__init__.py:F401
|
||||
scrapy/spiders/__init__.py:E402,F401
|
||||
tests/CrawlerRunner/change_reactor.py:E402
|
||||
|
||||
# Issues pending a review:
|
||||
scrapy/utils/url.py:F403,F405
|
||||
tests/test_loader.py:E741
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
name: Checks
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{ github.ref }}
|
||||
|
|
@ -12,7 +17,7 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- python-version: "3.12"
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: pylint
|
||||
- python-version: "3.9"
|
||||
|
|
@ -21,10 +26,10 @@ jobs:
|
|||
- python-version: "3.9"
|
||||
env:
|
||||
TOXENV: typing-tests
|
||||
- python-version: "3.12" # Keep in sync with .readthedocs.yml
|
||||
- python-version: "3.13" # Keep in sync with .readthedocs.yml
|
||||
env:
|
||||
TOXENV: docs
|
||||
- python-version: "3.12"
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: twinecheck
|
||||
|
||||
|
|
|
|||
|
|
@ -10,16 +10,20 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
publish:
|
||||
name: Upload release to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/Scrapy
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
- run: |
|
||||
pip install --upgrade build twine
|
||||
python-version: "3.13"
|
||||
- run: |
|
||||
python -m pip install --upgrade build
|
||||
python -m build
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.10.3
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
name: macOS
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{ github.ref }}
|
||||
|
|
@ -11,7 +16,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -27,4 +32,8 @@ jobs:
|
|||
tox -e py
|
||||
|
||||
- name: Upload coverage report
|
||||
run: bash <(curl -s https://codecov.io/bash)
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@v1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
name: Ubuntu
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{ github.ref }}
|
||||
|
|
@ -24,37 +29,43 @@ jobs:
|
|||
- python-version: "3.12"
|
||||
env:
|
||||
TOXENV: py
|
||||
- python-version: "3.12"
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: asyncio
|
||||
- python-version: pypy3.9
|
||||
TOXENV: py
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: default-reactor
|
||||
- python-version: pypy3.10
|
||||
env:
|
||||
TOXENV: pypy3
|
||||
- python-version: pypy3.10
|
||||
- python-version: pypy3.11
|
||||
env:
|
||||
TOXENV: pypy3
|
||||
|
||||
# pinned deps
|
||||
- python-version: 3.9.19
|
||||
- python-version: "3.9.21"
|
||||
env:
|
||||
TOXENV: pinned
|
||||
- python-version: 3.9.19
|
||||
- python-version: "3.9.21"
|
||||
env:
|
||||
TOXENV: asyncio-pinned
|
||||
- python-version: pypy3.9
|
||||
TOXENV: default-reactor-pinned
|
||||
- python-version: pypy3.10
|
||||
env:
|
||||
TOXENV: pypy3-pinned
|
||||
- python-version: 3.9.19
|
||||
- python-version: "3.9.21"
|
||||
env:
|
||||
TOXENV: extra-deps-pinned
|
||||
- python-version: 3.9.19
|
||||
- python-version: "3.9.21"
|
||||
env:
|
||||
TOXENV: botocore-pinned
|
||||
|
||||
- python-version: "3.12"
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: extra-deps
|
||||
- python-version: "3.12"
|
||||
- python-version: pypy3.11
|
||||
env:
|
||||
TOXENV: pypy3-extra-deps
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: botocore
|
||||
|
||||
|
|
@ -79,4 +90,8 @@ jobs:
|
|||
tox
|
||||
|
||||
- name: Upload coverage report
|
||||
run: bash <(curl -s https://codecov.io/bash)
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@v1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
name: Windows
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{ github.ref }}
|
||||
|
|
@ -14,7 +19,7 @@ jobs:
|
|||
include:
|
||||
- python-version: "3.9"
|
||||
env:
|
||||
TOXENV: windows-pinned
|
||||
TOXENV: py
|
||||
- python-version: "3.10"
|
||||
env:
|
||||
TOXENV: py
|
||||
|
|
@ -24,9 +29,24 @@ jobs:
|
|||
- python-version: "3.12"
|
||||
env:
|
||||
TOXENV: py
|
||||
- python-version: "3.12"
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: asyncio
|
||||
TOXENV: py
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: default-reactor
|
||||
|
||||
# pinned deps
|
||||
- python-version: "3.9.13"
|
||||
env:
|
||||
TOXENV: pinned
|
||||
- python-version: "3.9.13"
|
||||
env:
|
||||
TOXENV: extra-deps-pinned
|
||||
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: extra-deps
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -41,3 +61,10 @@ jobs:
|
|||
run: |
|
||||
pip install -U tox
|
||||
tox
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@v1
|
||||
|
|
|
|||
|
|
@ -5,16 +5,19 @@ _trial_temp*
|
|||
dropin.cache
|
||||
docs/build
|
||||
*egg-info
|
||||
.tox
|
||||
venv
|
||||
build
|
||||
dist
|
||||
.idea
|
||||
.tox/
|
||||
venv/
|
||||
.venv/
|
||||
build/
|
||||
dist/
|
||||
.idea/
|
||||
.vscode/
|
||||
htmlcov/
|
||||
.coverage
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.*
|
||||
*.junit.xml
|
||||
test-output.*
|
||||
.cache/
|
||||
.mypy_cache/
|
||||
|
|
@ -25,4 +28,4 @@ test-output.*
|
|||
Thumbs.db
|
||||
|
||||
# OSX miscellaneous
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
[settings]
|
||||
profile = black
|
||||
|
|
@ -1,36 +1,21 @@
|
|||
repos:
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.9
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.2
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [-r, -c, .bandit.yml]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
- flake8-comprehensions
|
||||
- flake8-debugger
|
||||
- flake8-docstrings
|
||||
- flake8-string-format
|
||||
- flake8-type-checking
|
||||
- repo: https://github.com/psf/black.git
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
- id: ruff-check
|
||||
args: [ --fix ]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/adamchainz/blacken-docs
|
||||
rev: 1.18.0
|
||||
rev: 1.19.1
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies:
|
||||
- black==24.4.2
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.18.0
|
||||
- black==25.1.0
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py39-plus]
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ sphinx:
|
|||
fail_on_warning: true
|
||||
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
# For available versions, see:
|
||||
# https://docs.readthedocs.io/en/stable/config-file/v2.html#build-tools-python
|
||||
python: "3.12" # Keep in sync with .github/workflows/checks.yml
|
||||
python: "3.13" # Keep in sync with .github/workflows/checks.yml
|
||||
|
||||
python:
|
||||
install:
|
||||
|
|
|
|||
23
MANIFEST.in
23
MANIFEST.in
|
|
@ -1,23 +0,0 @@
|
|||
include CODE_OF_CONDUCT.md
|
||||
include CONTRIBUTING.md
|
||||
include INSTALL.md
|
||||
include NEWS
|
||||
include SECURITY.md
|
||||
|
||||
include scrapy/VERSION
|
||||
include scrapy/mime.types
|
||||
include scrapy/py.typed
|
||||
|
||||
include codecov.yml
|
||||
include conftest.py
|
||||
include pytest.ini
|
||||
include tox.ini
|
||||
|
||||
recursive-include scrapy/templates *
|
||||
recursive-include docs *
|
||||
prune docs/build
|
||||
|
||||
recursive-include extras *
|
||||
recursive-include tests *
|
||||
|
||||
global-exclude __pycache__ *.py[cod]
|
||||
114
README.rst
114
README.rst
|
|
@ -1,114 +1,62 @@
|
|||
.. image:: https://scrapy.org/img/scrapylogo.png
|
||||
:target: https://scrapy.org/
|
||||
|
||||
======
|
||||
Scrapy
|
||||
======
|
||||
|logo|
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/Scrapy.svg
|
||||
:target: https://pypi.python.org/pypi/Scrapy
|
||||
.. |logo| image:: https://raw.githubusercontent.com/scrapy/scrapy/master/docs/_static/logo.svg
|
||||
:target: https://scrapy.org
|
||||
:alt: Scrapy
|
||||
:width: 480px
|
||||
|
||||
|version| |python_version| |ubuntu| |macos| |windows| |coverage| |conda| |deepwiki|
|
||||
|
||||
.. |version| image:: https://img.shields.io/pypi/v/Scrapy.svg
|
||||
:target: https://pypi.org/pypi/Scrapy
|
||||
:alt: PyPI Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/Scrapy.svg
|
||||
:target: https://pypi.python.org/pypi/Scrapy
|
||||
.. |python_version| image:: https://img.shields.io/pypi/pyversions/Scrapy.svg
|
||||
:target: https://pypi.org/pypi/Scrapy
|
||||
:alt: Supported Python Versions
|
||||
|
||||
.. image:: https://github.com/scrapy/scrapy/workflows/Ubuntu/badge.svg
|
||||
.. |ubuntu| image:: https://github.com/scrapy/scrapy/workflows/Ubuntu/badge.svg
|
||||
:target: https://github.com/scrapy/scrapy/actions?query=workflow%3AUbuntu
|
||||
:alt: Ubuntu
|
||||
|
||||
.. .. image:: https://github.com/scrapy/scrapy/workflows/macOS/badge.svg
|
||||
.. :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AmacOS
|
||||
.. :alt: macOS
|
||||
.. |macos| image:: https://github.com/scrapy/scrapy/workflows/macOS/badge.svg
|
||||
:target: https://github.com/scrapy/scrapy/actions?query=workflow%3AmacOS
|
||||
:alt: macOS
|
||||
|
||||
|
||||
.. image:: https://github.com/scrapy/scrapy/workflows/Windows/badge.svg
|
||||
.. |windows| image:: https://github.com/scrapy/scrapy/workflows/Windows/badge.svg
|
||||
:target: https://github.com/scrapy/scrapy/actions?query=workflow%3AWindows
|
||||
:alt: Windows
|
||||
|
||||
.. image:: https://img.shields.io/badge/wheel-yes-brightgreen.svg
|
||||
:target: https://pypi.python.org/pypi/Scrapy
|
||||
:alt: Wheel Status
|
||||
|
||||
.. image:: https://img.shields.io/codecov/c/github/scrapy/scrapy/master.svg
|
||||
.. |coverage| image:: https://img.shields.io/codecov/c/github/scrapy/scrapy/master.svg
|
||||
:target: https://codecov.io/github/scrapy/scrapy?branch=master
|
||||
:alt: Coverage report
|
||||
|
||||
.. image:: https://anaconda.org/conda-forge/scrapy/badges/version.svg
|
||||
.. |conda| image:: https://anaconda.org/conda-forge/scrapy/badges/version.svg
|
||||
:target: https://anaconda.org/conda-forge/scrapy
|
||||
:alt: Conda Version
|
||||
|
||||
.. |deepwiki| image:: https://deepwiki.com/badge.svg
|
||||
:target: https://deepwiki.com/scrapy/scrapy
|
||||
:alt: Ask DeepWiki
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
Scrapy is a BSD-licensed fast high-level web crawling and web scraping framework, used to
|
||||
crawl websites and extract structured data from their pages. It can be used for
|
||||
a wide range of purposes, from data mining to monitoring and automated testing.
|
||||
|
||||
Scrapy is maintained by Zyte_ (formerly Scrapinghub) and `many other
|
||||
contributors`_.
|
||||
Scrapy_ is a web scraping framework to extract structured data from websites.
|
||||
It is cross-platform, and requires Python 3.9+. It is maintained by Zyte_
|
||||
(formerly Scrapinghub) and `many other contributors`_.
|
||||
|
||||
.. _many other contributors: https://github.com/scrapy/scrapy/graphs/contributors
|
||||
.. _Scrapy: https://scrapy.org/
|
||||
.. _Zyte: https://www.zyte.com/
|
||||
|
||||
Check the Scrapy homepage at https://scrapy.org for more information,
|
||||
including a list of features.
|
||||
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
* Python 3.9+
|
||||
* Works on Linux, Windows, macOS, BSD
|
||||
|
||||
Install
|
||||
=======
|
||||
|
||||
The quick way:
|
||||
Install with:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pip install scrapy
|
||||
|
||||
See the install section in the documentation at
|
||||
https://docs.scrapy.org/en/latest/intro/install.html for more details.
|
||||
And follow the documentation_ to learn how to use it.
|
||||
|
||||
Documentation
|
||||
=============
|
||||
.. _documentation: https://docs.scrapy.org/en/latest/
|
||||
|
||||
Documentation is available online at https://docs.scrapy.org/ and in the ``docs``
|
||||
directory.
|
||||
If you wish to contribute, see Contributing_.
|
||||
|
||||
Releases
|
||||
========
|
||||
|
||||
You can check https://docs.scrapy.org/en/latest/news.html for the release notes.
|
||||
|
||||
Community (blog, twitter, mail list, IRC)
|
||||
=========================================
|
||||
|
||||
See https://scrapy.org/community/ for details.
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
See https://docs.scrapy.org/en/master/contributing.html for details.
|
||||
|
||||
Code of Conduct
|
||||
---------------
|
||||
|
||||
Please note that this project is released with a Contributor `Code of Conduct <https://github.com/scrapy/scrapy/blob/master/CODE_OF_CONDUCT.md>`_.
|
||||
|
||||
By participating in this project you agree to abide by its terms.
|
||||
Please report unacceptable behavior to opensource@zyte.com.
|
||||
|
||||
Companies using Scrapy
|
||||
======================
|
||||
|
||||
See https://scrapy.org/companies/ for a list.
|
||||
|
||||
Commercial Support
|
||||
==================
|
||||
|
||||
See https://scrapy.org/support/ for details.
|
||||
.. _Contributing: https://docs.scrapy.org/en/master/contributing.html
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.11.x | :white_check_mark: |
|
||||
| < 2.11.x | :x: |
|
||||
| 2.13.x | :white_check_mark: |
|
||||
| < 2.13.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
==============
|
||||
Scrapy artwork
|
||||
==============
|
||||
|
||||
This folder contains the Scrapy artwork resources such as logos and fonts.
|
||||
|
||||
scrapy-logo.jpg
|
||||
---------------
|
||||
|
||||
The main Scrapy logo, in JPEG format.
|
||||
|
||||
qlassik.zip
|
||||
-----------
|
||||
|
||||
The font used for the Scrapy logo. Homepage: https://www.dafont.com/qlassik.font
|
||||
|
||||
scrapy-blog.logo.xcf
|
||||
--------------------
|
||||
|
||||
The logo used in the Scrapy blog, in Gimp format.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
76
conftest.py
76
conftest.py
|
|
@ -1,10 +1,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from twisted.web.http import H2_ENABLED
|
||||
|
||||
from scrapy.utils.reactor import install_reactor
|
||||
from scrapy.utils.reactor import set_asyncio_event_loop_policy
|
||||
from tests.keys import generate_keys
|
||||
from tests.mockserver.http import MockServer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
|
||||
def _py_files(folder):
|
||||
|
|
@ -12,19 +19,28 @@ def _py_files(folder):
|
|||
|
||||
|
||||
collect_ignore = [
|
||||
# may need extra deps
|
||||
"docs/_ext",
|
||||
# not a test, but looks like a test
|
||||
"scrapy/utils/testproc.py",
|
||||
"scrapy/utils/testsite.py",
|
||||
"tests/ftpserver.py",
|
||||
"tests/mockserver.py",
|
||||
"tests/pipelines.py",
|
||||
"tests/spiders.py",
|
||||
# contains scripts to be run by tests/test_crawler.py::AsyncCrawlerProcessSubprocess
|
||||
*_py_files("tests/AsyncCrawlerProcess"),
|
||||
# contains scripts to be run by tests/test_crawler.py::AsyncCrawlerRunnerSubprocess
|
||||
*_py_files("tests/AsyncCrawlerRunner"),
|
||||
# contains scripts to be run by tests/test_crawler.py::CrawlerProcessSubprocess
|
||||
*_py_files("tests/CrawlerProcess"),
|
||||
# contains scripts to be run by tests/test_crawler.py::CrawlerRunnerSubprocess
|
||||
*_py_files("tests/CrawlerRunner"),
|
||||
]
|
||||
|
||||
with Path("tests/ignores.txt").open(encoding="utf-8") as reader:
|
||||
base_dir = Path(__file__).parent
|
||||
ignore_file_path = base_dir / "tests" / "ignores.txt"
|
||||
with ignore_file_path.open(encoding="utf-8") as reader:
|
||||
for line in reader:
|
||||
file_path = line.strip()
|
||||
if file_path and file_path[0] != "#":
|
||||
|
|
@ -39,27 +55,15 @@ if not H2_ENABLED:
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def chdir(tmpdir):
|
||||
"""Change to pytest-provided temporary directory"""
|
||||
tmpdir.chdir()
|
||||
@pytest.fixture(scope="session")
|
||||
def mockserver() -> Generator[MockServer]:
|
||||
with MockServer() as mockserver:
|
||||
yield mockserver
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--reactor",
|
||||
default="default",
|
||||
choices=["default", "asyncio"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def reactor_pytest(request):
|
||||
if not request.cls:
|
||||
# doctests
|
||||
return
|
||||
request.cls.reactor_pytest = request.config.getoption("--reactor")
|
||||
return request.cls.reactor_pytest
|
||||
@pytest.fixture(scope="session")
|
||||
def reactor_pytest(request) -> str:
|
||||
return request.config.getoption("--reactor")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
@ -82,16 +86,42 @@ def requires_uvloop(request):
|
|||
if not request.node.get_closest_marker("requires_uvloop"):
|
||||
return
|
||||
try:
|
||||
import uvloop
|
||||
import uvloop # noqa: PLC0415
|
||||
|
||||
del uvloop
|
||||
except ImportError:
|
||||
pytest.skip("uvloop is not installed")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def requires_botocore(request):
|
||||
if not request.node.get_closest_marker("requires_botocore"):
|
||||
return
|
||||
try:
|
||||
import botocore # noqa: PLC0415
|
||||
|
||||
del botocore
|
||||
except ImportError:
|
||||
pytest.skip("botocore is not installed")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def requires_boto3(request):
|
||||
if not request.node.get_closest_marker("requires_boto3"):
|
||||
return
|
||||
try:
|
||||
import boto3 # noqa: PLC0415
|
||||
|
||||
del boto3
|
||||
except ImportError:
|
||||
pytest.skip("boto3 is not installed")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.getoption("--reactor") == "asyncio":
|
||||
install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
|
||||
# Needed on Windows to switch from proactor to selector for Twisted reactor compatibility.
|
||||
# If we decide to run tests with both, we will need to add a new option and check it here.
|
||||
set_asyncio_event_loop_policy()
|
||||
|
||||
|
||||
# Generate localhost certificate files, needed by some tests
|
||||
|
|
|
|||
104
docs/Makefile
104
docs/Makefile
|
|
@ -1,96 +1,20 @@
|
|||
#
|
||||
# Makefile for Scrapy documentation [based on Python documentation Makefile]
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
PYTHON = python
|
||||
SPHINXOPTS =
|
||||
PAPER =
|
||||
SOURCES =
|
||||
SHELL = /usr/bin/env bash
|
||||
|
||||
ALLSPHINXOPTS = -b $(BUILDER) -d build/doctrees \
|
||||
-D latex_elements.papersize=$(PAPER) \
|
||||
$(SPHINXOPTS) . build/$(BUILDER) $(SOURCES)
|
||||
|
||||
.PHONY: help update build html htmlhelp clean
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " text to make plain text files"
|
||||
@echo " changes to make an overview over all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " watch build HTML docs, open in browser and watch for changes"
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
build-dirs:
|
||||
mkdir -p build/$(BUILDER) build/doctrees
|
||||
.PHONY: help Makefile
|
||||
|
||||
build: build-dirs
|
||||
sphinx-build $(ALLSPHINXOPTS)
|
||||
@echo
|
||||
|
||||
build-ignore-errors: build-dirs
|
||||
-sphinx-build $(ALLSPHINXOPTS)
|
||||
@echo
|
||||
|
||||
|
||||
html: BUILDER = html
|
||||
html: build
|
||||
@echo "Build finished. The HTML pages are in build/html."
|
||||
|
||||
htmlhelp: BUILDER = htmlhelp
|
||||
htmlhelp: build
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
"build/htmlhelp/pydoc.hhp project file."
|
||||
|
||||
latex: BUILDER = latex
|
||||
latex: build
|
||||
@echo "Build finished; the LaTeX files are in build/latex."
|
||||
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
|
||||
"run these through (pdf)latex."
|
||||
|
||||
text: BUILDER = text
|
||||
text: build
|
||||
@echo "Build finished; the text files are in build/text."
|
||||
|
||||
changes: BUILDER = changes
|
||||
changes: build
|
||||
@echo "The overview file is in build/changes."
|
||||
|
||||
linkcheck: BUILDER = linkcheck
|
||||
linkcheck: build
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in build/$(BUILDER)/output.txt"
|
||||
|
||||
linkfix: BUILDER = linkcheck
|
||||
linkfix: build-ignore-errors
|
||||
$(PYTHON) utils/linkfix.py
|
||||
@echo "Fixing redirecting links in docs has finished; check all " \
|
||||
"replacements before committing them"
|
||||
|
||||
doctest: BUILDER = doctest
|
||||
doctest: build
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in build/doctest/output.txt"
|
||||
|
||||
pydoc-topics: BUILDER = pydoc-topics
|
||||
pydoc-topics: build
|
||||
@echo "Building finished; now copy build/pydoc-topics/pydoc_topics.py " \
|
||||
"into the Lib/ directory"
|
||||
|
||||
coverage: BUILDER = coverage
|
||||
coverage: build
|
||||
|
||||
htmlview: html
|
||||
$(PYTHON) -c "import webbrowser; from pathlib import Path; \
|
||||
webbrowser.open(Path('build/html/index.html').resolve().as_uri())"
|
||||
|
||||
clean:
|
||||
-rm -rf build/*
|
||||
|
||||
watch: htmlview
|
||||
watchmedo shell-command -p '*.rst' -c 'make html' -R -D
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
|
|
|||
|
|
@ -1,62 +1,67 @@
|
|||
# pylint: disable=import-error
|
||||
from collections.abc import Sequence
|
||||
from operator import itemgetter
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.nodes import Element, General, Node, document
|
||||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.util.nodes import make_refnode
|
||||
|
||||
|
||||
class settingslist_node(nodes.General, nodes.Element):
|
||||
class SettingData(TypedDict):
|
||||
docname: str
|
||||
setting_name: str
|
||||
refid: str
|
||||
|
||||
|
||||
class SettingslistNode(General, Element):
|
||||
pass
|
||||
|
||||
|
||||
class SettingsListDirective(Directive):
|
||||
def run(self):
|
||||
return [settingslist_node("")]
|
||||
def run(self) -> Sequence[Node]:
|
||||
return [SettingslistNode()]
|
||||
|
||||
|
||||
def is_setting_index(node):
|
||||
if node.tagname == "index" and node["entries"]:
|
||||
def is_setting_index(node: Node) -> bool:
|
||||
if node.tagname == "index" and node["entries"]: # type: ignore[index,attr-defined]
|
||||
# index entries for setting directives look like:
|
||||
# [('pair', 'SETTING_NAME; setting', 'std:setting-SETTING_NAME', '')]
|
||||
entry_type, info, refid = node["entries"][0][:3]
|
||||
entry_type, info, refid = node["entries"][0][:3] # type: ignore[index]
|
||||
return entry_type == "pair" and info.endswith("; setting")
|
||||
return False
|
||||
|
||||
|
||||
def get_setting_target(node):
|
||||
# target nodes are placed next to the node in the doc tree
|
||||
return node.parent[node.parent.index(node) + 1]
|
||||
|
||||
|
||||
def get_setting_name_and_refid(node):
|
||||
def get_setting_name_and_refid(node: Node) -> tuple[str, str]:
|
||||
"""Extract setting name from directive index node"""
|
||||
entry_type, info, refid = node["entries"][0][:3]
|
||||
entry_type, info, refid = node["entries"][0][:3] # type: ignore[index]
|
||||
return info.replace("; setting", ""), refid
|
||||
|
||||
|
||||
def collect_scrapy_settings_refs(app, doctree):
|
||||
def collect_scrapy_settings_refs(app: Sphinx, doctree: document) -> None:
|
||||
env = app.builder.env
|
||||
|
||||
if not hasattr(env, "scrapy_all_settings"):
|
||||
env.scrapy_all_settings = []
|
||||
|
||||
for node in doctree.traverse(is_setting_index):
|
||||
targetnode = get_setting_target(node)
|
||||
assert isinstance(targetnode, nodes.target), "Next node is not a target"
|
||||
emptyList: list[SettingData] = []
|
||||
env.scrapy_all_settings = emptyList # type: ignore[attr-defined]
|
||||
|
||||
for node in doctree.findall(is_setting_index):
|
||||
setting_name, refid = get_setting_name_and_refid(node)
|
||||
|
||||
env.scrapy_all_settings.append(
|
||||
{
|
||||
"docname": env.docname,
|
||||
"setting_name": setting_name,
|
||||
"refid": refid,
|
||||
}
|
||||
env.scrapy_all_settings.append( # type: ignore[attr-defined]
|
||||
SettingData(
|
||||
docname=env.docname,
|
||||
setting_name=setting_name,
|
||||
refid=refid,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def make_setting_element(setting_data, app, fromdocname):
|
||||
def make_setting_element(
|
||||
setting_data: SettingData, app: Sphinx, fromdocname: str
|
||||
) -> Any:
|
||||
refnode = make_refnode(
|
||||
app.builder,
|
||||
fromdocname,
|
||||
|
|
@ -72,22 +77,56 @@ def make_setting_element(setting_data, app, fromdocname):
|
|||
return item
|
||||
|
||||
|
||||
def replace_settingslist_nodes(app, doctree, fromdocname):
|
||||
def replace_settingslist_nodes(
|
||||
app: Sphinx, doctree: document, fromdocname: str
|
||||
) -> None:
|
||||
env = app.builder.env
|
||||
|
||||
for node in doctree.traverse(settingslist_node):
|
||||
for node in doctree.findall(SettingslistNode):
|
||||
settings_list = nodes.bullet_list()
|
||||
settings_list.extend(
|
||||
[
|
||||
make_setting_element(d, app, fromdocname)
|
||||
for d in sorted(env.scrapy_all_settings, key=itemgetter("setting_name"))
|
||||
for d in sorted(env.scrapy_all_settings, key=itemgetter("setting_name")) # type: ignore[attr-defined]
|
||||
if fromdocname != d["docname"]
|
||||
]
|
||||
)
|
||||
node.replace_self(settings_list)
|
||||
|
||||
|
||||
def setup(app):
|
||||
def source_role(
|
||||
name, rawtext, text: str, lineno, inliner, options=None, content=None
|
||||
) -> tuple[list[Any], list[Any]]:
|
||||
ref = "https://github.com/scrapy/scrapy/blob/master/" + text
|
||||
node = nodes.reference(rawtext, text, refuri=ref, **options)
|
||||
return [node], []
|
||||
|
||||
|
||||
def issue_role(
|
||||
name, rawtext, text: str, lineno, inliner, options=None, content=None
|
||||
) -> tuple[list[Any], list[Any]]:
|
||||
ref = "https://github.com/scrapy/scrapy/issues/" + text
|
||||
node = nodes.reference(rawtext, "issue " + text, refuri=ref)
|
||||
return [node], []
|
||||
|
||||
|
||||
def commit_role(
|
||||
name, rawtext, text: str, lineno, inliner, options=None, content=None
|
||||
) -> tuple[list[Any], list[Any]]:
|
||||
ref = "https://github.com/scrapy/scrapy/commit/" + text
|
||||
node = nodes.reference(rawtext, "commit " + text, refuri=ref)
|
||||
return [node], []
|
||||
|
||||
|
||||
def rev_role(
|
||||
name, rawtext, text: str, lineno, inliner, options=None, content=None
|
||||
) -> tuple[list[Any], list[Any]]:
|
||||
ref = "http://hg.scrapy.org/scrapy/changeset/" + text
|
||||
node = nodes.reference(rawtext, "r" + text, refuri=ref)
|
||||
return [node], []
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> None:
|
||||
app.add_crossref_type(
|
||||
directivename="setting",
|
||||
rolename="setting",
|
||||
|
|
@ -113,36 +152,8 @@ def setup(app):
|
|||
app.add_role("issue", issue_role)
|
||||
app.add_role("rev", rev_role)
|
||||
|
||||
app.add_node(settingslist_node)
|
||||
app.add_node(SettingslistNode)
|
||||
app.add_directive("settingslist", SettingsListDirective)
|
||||
|
||||
app.connect("doctree-read", collect_scrapy_settings_refs)
|
||||
app.connect("doctree-resolved", replace_settingslist_nodes)
|
||||
|
||||
|
||||
def source_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
ref = "https://github.com/scrapy/scrapy/blob/master/" + text
|
||||
set_classes(options)
|
||||
node = nodes.reference(rawtext, text, refuri=ref, **options)
|
||||
return [node], []
|
||||
|
||||
|
||||
def issue_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
ref = "https://github.com/scrapy/scrapy/issues/" + text
|
||||
set_classes(options)
|
||||
node = nodes.reference(rawtext, "issue " + text, refuri=ref, **options)
|
||||
return [node], []
|
||||
|
||||
|
||||
def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
ref = "https://github.com/scrapy/scrapy/commit/" + text
|
||||
set_classes(options)
|
||||
node = nodes.reference(rawtext, "commit " + text, refuri=ref, **options)
|
||||
return [node], []
|
||||
|
||||
|
||||
def rev_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||
ref = "http://hg.scrapy.org/scrapy/changeset/" + text
|
||||
set_classes(options)
|
||||
node = nodes.reference(rawtext, "r" + text, refuri=ref, **options)
|
||||
return [node], []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
"""
|
||||
Must be included after 'sphinx.ext.autodoc'. Fixes unwanted 'alias of' behavior.
|
||||
https://github.com/sphinx-doc/sphinx/issues/4422
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error
|
||||
from sphinx.application import Sphinx
|
||||
|
||||
|
||||
def maybe_skip_member(app: Sphinx, what, name: str, obj, skip: bool, options) -> bool:
|
||||
if not skip:
|
||||
# autodocs was generating a text "alias of" for the following members
|
||||
return name in {"default_item_class", "default_selector_class"}
|
||||
return skip
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> None:
|
||||
app.connect("autodoc-skip-member", maybe_skip_member)
|
||||
|
|
@ -7,4 +7,50 @@
|
|||
}
|
||||
.rst-content dl p + ol, .rst-content dl p + ul {
|
||||
margin-top: -6px; /* Compensates margin-top: 12px of p */
|
||||
}
|
||||
}
|
||||
|
||||
/*override some styles in
|
||||
sphinx-rtd-dark-mode/static/dark_mode_css/general.css*/
|
||||
.theme-switcher {
|
||||
right: 0.4em !important;
|
||||
top: 0.6em !important;
|
||||
-webkit-box-shadow: 0px 3px 14px 4px rgba(0, 0, 0, 0.30) !important;
|
||||
box-shadow: 0px 3px 14px 4px rgba(0, 0, 0, 0.30) !important;
|
||||
height: 2em !important;
|
||||
width: 2em !important;
|
||||
}
|
||||
|
||||
/*place the toggle button for dark mode
|
||||
at the bottom right corner on small screens*/
|
||||
@media (max-width: 768px) {
|
||||
.theme-switcher {
|
||||
right: 0.4em !important;
|
||||
bottom: 2.6em !important;
|
||||
top: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*persist blue color at the top left used in
|
||||
default rtd theme*/
|
||||
html[data-theme="dark"] .wy-side-nav-search,
|
||||
html[data-theme="dark"] .wy-nav-top {
|
||||
background-color: #1d577d !important;
|
||||
}
|
||||
|
||||
/*all the styles below used to present
|
||||
API objects nicely in dark mode*/
|
||||
html[data-theme="dark"] .sig.sig-object {
|
||||
border-left-color: #3e4446 !important;
|
||||
background-color: #202325 !important
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .sig-name,
|
||||
html[data-theme="dark"] .sig-prename,
|
||||
html[data-theme="dark"] .property,
|
||||
html[data-theme="dark"] .sig-param,
|
||||
html[data-theme="dark"] .sig-paren,
|
||||
html[data-theme="dark"] .sig-return-icon,
|
||||
html[data-theme="dark"] .sig-return-typehint,
|
||||
html[data-theme="dark"] .optional {
|
||||
color: #e8e6e3 !important
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.5 KiB |
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "!layout.html" %}
|
||||
|
||||
{# Overriden to include a link to scrapy.org, not just to the docs root #}
|
||||
{%- block sidebartitle %}
|
||||
|
||||
{# the logo helper function was removed in Sphinx 6 and deprecated since Sphinx 4 #}
|
||||
{# the master_doc variable was renamed to root_doc in Sphinx 4 (master_doc still exists in later Sphinx versions) #}
|
||||
{%- set _logo_url = logo_url|default(pathto('_static/' + (logo or ""), 1)) %}
|
||||
{%- set _root_doc = root_doc|default(master_doc) %}
|
||||
<a href="https://scrapy.org">scrapy.org</a> / <a href="{{ pathto(_root_doc) }}">docs</a>
|
||||
|
||||
{%- if READTHEDOCS or DEBUG %}
|
||||
{%- if theme_version_selector or theme_language_selector %}
|
||||
<div class="switch-menus">
|
||||
<div class="version-switch"></div>
|
||||
<div class="language-switch"></div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- include "searchbox.html" %}
|
||||
|
||||
{%- endblock %}
|
||||
|
|
@ -16,13 +16,13 @@
|
|||
</div>
|
||||
<div class="col-md-4">
|
||||
<p>
|
||||
|
||||
|
||||
<a href="/login">Login</a>
|
||||
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
|
|
@ -34,16 +34,16 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" / >
|
||||
|
||||
<a class="tag" href="/tag/change/page/1/">change</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/thinking/page/1/">thinking</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/world/page/1/">world</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -54,12 +54,12 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="abilities,choices" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="abilities,choices" / >
|
||||
|
||||
<a class="tag" href="/tag/abilities/page/1/">abilities</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/choices/page/1/">choices</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -70,18 +70,18 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="inspirational,life,live,miracle,miracles" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="inspirational,life,live,miracle,miracles" / >
|
||||
|
||||
<a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/life/page/1/">life</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/live/page/1/">live</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/miracle/page/1/">miracle</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/miracles/page/1/">miracles</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -92,16 +92,16 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="aliteracy,books,classic,humor" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="aliteracy,books,classic,humor" / >
|
||||
|
||||
<a class="tag" href="/tag/aliteracy/page/1/">aliteracy</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/books/page/1/">books</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/classic/page/1/">classic</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/humor/page/1/">humor</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -112,12 +112,12 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="be-yourself,inspirational" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="be-yourself,inspirational" / >
|
||||
|
||||
<a class="tag" href="/tag/be-yourself/page/1/">be-yourself</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -128,14 +128,14 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="adulthood,success,value" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="adulthood,success,value" / >
|
||||
|
||||
<a class="tag" href="/tag/adulthood/page/1/">adulthood</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/success/page/1/">success</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/value/page/1/">value</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -146,12 +146,12 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="life,love" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="life,love" / >
|
||||
|
||||
<a class="tag" href="/tag/life/page/1/">life</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/love/page/1/">love</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -162,16 +162,16 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" / >
|
||||
|
||||
<a class="tag" href="/tag/edison/page/1/">edison</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/failure/page/1/">failure</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/paraphrased/page/1/">paraphrased</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -182,10 +182,10 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="misattributed-eleanor-roosevelt" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="misattributed-eleanor-roosevelt" / >
|
||||
|
||||
<a class="tag" href="/tag/misattributed-eleanor-roosevelt/page/1/">misattributed-eleanor-roosevelt</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -196,73 +196,73 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="humor,obvious,simile" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="humor,obvious,simile" / >
|
||||
|
||||
<a class="tag" href="/tag/humor/page/1/">humor</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/obvious/page/1/">obvious</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/simile/page/1/">simile</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pager">
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="next">
|
||||
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col-md-4 tags-box">
|
||||
|
||||
|
||||
<h2>Top Ten tags</h2>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 28px" href="/tag/love/">love</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 26px" href="/tag/inspirational/">inspirational</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 26px" href="/tag/life/">life</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 24px" href="/tag/humor/">humor</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 22px" href="/tag/books/">books</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 14px" href="/tag/reading/">reading</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 10px" href="/tag/friendship/">friendship</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 8px" href="/tag/friends/">friends</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 8px" href="/tag/truth/">truth</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 6px" href="/tag/simile/">simile</a>
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@
|
|||
</div>
|
||||
<div class="col-md-4">
|
||||
<p>
|
||||
|
||||
|
||||
<a href="/login">Login</a>
|
||||
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
|
|
@ -34,16 +34,16 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world" / >
|
||||
|
||||
<a class="tag" href="/tag/change/page/1/">change</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/thinking/page/1/">thinking</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/world/page/1/">world</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -54,12 +54,12 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="abilities,choices" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="abilities,choices" / >
|
||||
|
||||
<a class="tag" href="/tag/abilities/page/1/">abilities</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/choices/page/1/">choices</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -70,18 +70,18 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="inspirational,life,live,miracle,miracles" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="inspirational,life,live,miracle,miracles" / >
|
||||
|
||||
<a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/life/page/1/">life</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/live/page/1/">live</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/miracle/page/1/">miracle</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/miracles/page/1/">miracles</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -92,16 +92,16 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="aliteracy,books,classic,humor" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="aliteracy,books,classic,humor" / >
|
||||
|
||||
<a class="tag" href="/tag/aliteracy/page/1/">aliteracy</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/books/page/1/">books</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/classic/page/1/">classic</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/humor/page/1/">humor</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -112,12 +112,12 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="be-yourself,inspirational" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="be-yourself,inspirational" / >
|
||||
|
||||
<a class="tag" href="/tag/be-yourself/page/1/">be-yourself</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -128,14 +128,14 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="adulthood,success,value" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="adulthood,success,value" / >
|
||||
|
||||
<a class="tag" href="/tag/adulthood/page/1/">adulthood</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/success/page/1/">success</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/value/page/1/">value</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -146,12 +146,12 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="life,love" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="life,love" / >
|
||||
|
||||
<a class="tag" href="/tag/life/page/1/">life</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/love/page/1/">love</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -162,16 +162,16 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" / >
|
||||
|
||||
<a class="tag" href="/tag/edison/page/1/">edison</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/failure/page/1/">failure</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/paraphrased/page/1/">paraphrased</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -182,10 +182,10 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="misattributed-eleanor-roosevelt" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="misattributed-eleanor-roosevelt" / >
|
||||
|
||||
<a class="tag" href="/tag/misattributed-eleanor-roosevelt/page/1/">misattributed-eleanor-roosevelt</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -196,73 +196,73 @@
|
|||
</span>
|
||||
<div class="tags">
|
||||
Tags:
|
||||
<meta class="keywords" itemprop="keywords" content="humor,obvious,simile" / >
|
||||
|
||||
<meta class="keywords" itemprop="keywords" content="humor,obvious,simile" / >
|
||||
|
||||
<a class="tag" href="/tag/humor/page/1/">humor</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/obvious/page/1/">obvious</a>
|
||||
|
||||
|
||||
<a class="tag" href="/tag/simile/page/1/">simile</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pager">
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="next">
|
||||
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col-md-4 tags-box">
|
||||
|
||||
|
||||
<h2>Top Ten tags</h2>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 28px" href="/tag/love/">love</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 26px" href="/tag/inspirational/">inspirational</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 26px" href="/tag/life/">life</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 24px" href="/tag/humor/">humor</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 22px" href="/tag/books/">books</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 14px" href="/tag/reading/">reading</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 10px" href="/tag/friendship/">friendship</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 8px" href="/tag/friends/">friends</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 8px" href="/tag/truth/">truth</a>
|
||||
</span>
|
||||
|
||||
|
||||
<span class="tag-item">
|
||||
<a class="tag" style="font-size: 6px" href="/tag/simile/">simile</a>
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
244
docs/conf.py
244
docs/conf.py
|
|
@ -1,15 +1,11 @@
|
|||
# Scrapy documentation build configuration file, created by
|
||||
# sphinx-quickstart on Mon Nov 24 12:02:52 2008.
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# The contents of this file are pickled, so don't put values in the namespace
|
||||
# that aren't pickleable (module imports are okay, they're removed automatically).
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
# If your extensions are in another directory, add it here. If the directory
|
||||
|
|
@ -18,36 +14,30 @@ sys.path.append(str(Path(__file__).parent / "_ext"))
|
|||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
# General configuration
|
||||
# ---------------------
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = "Scrapy"
|
||||
project_copyright = "Scrapy developers"
|
||||
author = "Scrapy developers"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
"hoverxref.extension",
|
||||
"notfound.extension",
|
||||
"scrapydocs",
|
||||
"sphinx.ext.autodoc",
|
||||
"scrapyfixautodoc", # Must be after "sphinx.ext.autodoc"
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx_rtd_dark_mode",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = "Scrapy"
|
||||
copyright = "Scrapy developers"
|
||||
exclude_patterns = ["build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
|
@ -63,138 +53,26 @@ except ImportError:
|
|||
version = ""
|
||||
release = ""
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
language = "en"
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of documents that shouldn't be included in the build.
|
||||
# unused_docs = []
|
||||
|
||||
exclude_patterns = ["build"]
|
||||
|
||||
# List of directories, relative to source directory, that shouldn't be searched
|
||||
# for source files.
|
||||
exclude_trees = [".build"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# List of Sphinx warnings that will not be raised
|
||||
suppress_warnings = ["epub.unknown_project_files"]
|
||||
|
||||
|
||||
# Options for HTML output
|
||||
# -----------------------
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# Add path to the RTD explicitly to robustify builds (otherwise might
|
||||
# fail in a clean Debian build env)
|
||||
import sphinx_rtd_theme
|
||||
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# The style sheet to use for HTML and HTML Help pages. A file of that name
|
||||
# must exist either in Sphinx' static/ path, or in one of the custom paths
|
||||
# given in html_static_path.
|
||||
# html_style = 'scrapydoc.css'
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
html_last_updated_fmt = "%b %d, %Y"
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_use_modindex = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, the reST sources are included in the HTML build as _sources/<name>.
|
||||
html_copy_source = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = ''
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "Scrapydoc"
|
||||
|
||||
html_css_files = [
|
||||
"custom.css",
|
||||
]
|
||||
|
||||
# Set canonical URL from the Read the Docs Domain
|
||||
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "")
|
||||
|
||||
# Options for LaTeX output
|
||||
# ------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
# latex_paper_size = 'letter'
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# latex_font_size = '10pt'
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-latex-output
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, document class [howto/manual]).
|
||||
|
|
@ -202,38 +80,22 @@ latex_documents = [
|
|||
("index", "Scrapy.tex", "Scrapy Documentation", "Scrapy developers", "manual"),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
# -- Options for the linkcheck builder ---------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# latex_preamble = ''
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_use_modindex = True
|
||||
|
||||
|
||||
# Options for the linkcheck builder
|
||||
# ---------------------------------
|
||||
|
||||
# A list of regular expressions that match URIs that should not be checked when
|
||||
# doing a linkcheck build.
|
||||
linkcheck_ignore = [
|
||||
r"http://localhost:\d+",
|
||||
"http://hg.scrapy.org",
|
||||
"http://directory.google.com/",
|
||||
r"https://github.com/scrapy/scrapy/commit/\w+",
|
||||
r"https://github.com/scrapy/scrapy/issues/\d+",
|
||||
]
|
||||
|
||||
linkcheck_anchors_ignore_for_url = ["https://github.com/pyca/cryptography/issues/2692"]
|
||||
|
||||
# -- Options for the Coverage extension --------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/extensions/coverage.html#configuration
|
||||
|
||||
# Options for the Coverage extension
|
||||
# ----------------------------------
|
||||
coverage_ignore_pyobjects = [
|
||||
# Contract’s add_pre_hook and add_post_hook are not documented because
|
||||
# they should be transparent to contract developers, for whom pre_hook and
|
||||
|
|
@ -253,6 +115,10 @@ coverage_ignore_pyobjects = [
|
|||
# Base classes of downloader middlewares are implementation details that
|
||||
# are not meant for users.
|
||||
r"^scrapy\.downloadermiddlewares\.\w*?\.Base\w*?Middleware",
|
||||
# The interface methods of duplicate request filtering classes are already
|
||||
# covered in the interface documentation part of the DUPEFILTER_CLASS
|
||||
# setting documentation.
|
||||
r"^scrapy\.dupefilters\.[A-Z]\w*?\.(from_settings|request_seen|open|close|log)$",
|
||||
# Private exception used by the command-line interface implementation.
|
||||
r"^scrapy\.exceptions\.UsageError",
|
||||
# Methods of BaseItemExporter subclasses are only documented in
|
||||
|
|
@ -270,8 +136,8 @@ coverage_ignore_pyobjects = [
|
|||
]
|
||||
|
||||
|
||||
# Options for the InterSphinx extension
|
||||
# -------------------------------------
|
||||
# -- Options for the InterSphinx extension -----------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration
|
||||
|
||||
intersphinx_mapping = {
|
||||
"attrs": ("https://www.attrs.org/en/stable/", None),
|
||||
|
|
@ -279,6 +145,7 @@ intersphinx_mapping = {
|
|||
"cryptography": ("https://cryptography.io/en/latest/", None),
|
||||
"cssselect": ("https://cssselect.readthedocs.io/en/latest", None),
|
||||
"itemloaders": ("https://itemloaders.readthedocs.io/en/latest/", None),
|
||||
"parsel": ("https://parsel.readthedocs.io/en/latest/", None),
|
||||
"pytest": ("https://docs.pytest.org/en/latest", None),
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
"sphinx": ("https://www.sphinx-doc.org/en/master", None),
|
||||
|
|
@ -287,34 +154,7 @@ intersphinx_mapping = {
|
|||
"twistedapi": ("https://docs.twisted.org/en/stable/api/", None),
|
||||
"w3lib": ("https://w3lib.readthedocs.io/en/latest", None),
|
||||
}
|
||||
intersphinx_disabled_reftypes = []
|
||||
intersphinx_disabled_reftypes: Sequence[str] = []
|
||||
|
||||
|
||||
# Options for sphinx-hoverxref options
|
||||
# ------------------------------------
|
||||
|
||||
hoverxref_auto_ref = True
|
||||
hoverxref_role_types = {
|
||||
"class": "tooltip",
|
||||
"command": "tooltip",
|
||||
"confval": "tooltip",
|
||||
"hoverxref": "tooltip",
|
||||
"mod": "tooltip",
|
||||
"ref": "tooltip",
|
||||
"reqmeta": "tooltip",
|
||||
"setting": "tooltip",
|
||||
"signal": "tooltip",
|
||||
}
|
||||
hoverxref_roles = ["command", "reqmeta", "setting", "signal"]
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.connect("autodoc-skip-member", maybe_skip_member)
|
||||
|
||||
|
||||
def maybe_skip_member(app, what, name, obj, skip, options):
|
||||
if not skip:
|
||||
# autodocs was generating a text "alias of" for the following members
|
||||
# https://github.com/sphinx-doc/sphinx/issues/4422
|
||||
return name in {"default_item_class", "default_selector_class"}
|
||||
return skip
|
||||
# -- Other options ------------------------------------------------------------
|
||||
default_dark_mode = False
|
||||
|
|
|
|||
|
|
@ -6,8 +6,13 @@ Contributing to Scrapy
|
|||
|
||||
.. important::
|
||||
|
||||
Double check that you are reading the most recent version of this document at
|
||||
https://docs.scrapy.org/en/master/contributing.html
|
||||
Double check that you are reading the most recent version of this document
|
||||
at https://docs.scrapy.org/en/master/contributing.html
|
||||
|
||||
By participating in this project you agree to abide by the terms of our
|
||||
`Code of Conduct
|
||||
<https://github.com/scrapy/scrapy/blob/master/CODE_OF_CONDUCT.md>`_. Please
|
||||
report unacceptable behavior to opensource@zyte.com.
|
||||
|
||||
There are many ways to contribute to Scrapy. Here are some of them:
|
||||
|
||||
|
|
@ -74,18 +79,81 @@ guidelines when you're going to report a new bug.
|
|||
|
||||
.. _Minimal, Complete, and Verifiable example: https://stackoverflow.com/help/mcve
|
||||
|
||||
.. _find-work:
|
||||
|
||||
Finding work
|
||||
============
|
||||
|
||||
If you have decided to make a contribution to Scrapy, but you do not know what
|
||||
to contribute, you have a few options to find pending work:
|
||||
|
||||
- Check out the `contribution GitHub page`_, which lists open issues tagged
|
||||
as **good first issue**.
|
||||
|
||||
.. _contribution GitHub page: https://github.com/scrapy/scrapy/contribute
|
||||
|
||||
There are also `help wanted issues`_ but mind that some may require
|
||||
familiarity with the Scrapy code base. You can also target any other issue
|
||||
provided it is not tagged as **discuss**.
|
||||
|
||||
- If you enjoy writing documentation, there are `documentation issues`_ as
|
||||
well, but mind that some may require familiarity with the Scrapy code base
|
||||
as well.
|
||||
|
||||
.. _documentation issues: https://github.com/scrapy/scrapy/issues?q=is%3Aissue+is%3Aopen+label%3Adocs+
|
||||
|
||||
- If you enjoy :ref:`writing automated tests <write-tests>`, you can work on
|
||||
increasing our `test coverage`_.
|
||||
|
||||
- If you enjoy code cleanup, we welcome fixes for issues detected by our
|
||||
static analysis tools. See ``pyproject.toml`` for silenced issues that may
|
||||
need addressing.
|
||||
|
||||
Mind that some issues we do not aim to address at all, and usually include
|
||||
a comment on them explaining the reason; not to confuse with comments that
|
||||
state what the issue is about, for non-descriptive issue codes.
|
||||
|
||||
If you have found an issue, make sure you read the entire issue thread before
|
||||
you ask questions. That includes related issues and pull requests that show up
|
||||
in the issue thread when the issue is mentioned elsewhere.
|
||||
|
||||
We do not assign issues, and you do not need to announce that you are going to
|
||||
start working on an issue either. If you want to work on an issue, just go
|
||||
ahead and :ref:`write a patch for it <writing-patches>`.
|
||||
|
||||
Do not discard an issue simply because there is an open pull request for it.
|
||||
Check if open pull requests are active first. And even if some are active, if
|
||||
you think you can build a better implementation, feel free to create a pull
|
||||
request with your approach.
|
||||
|
||||
If you decide to work on something without an open issue, please:
|
||||
|
||||
- Do not create an issue to work on code coverage or code cleanup, create a
|
||||
pull request directly.
|
||||
|
||||
- Do not create both an issue and a pull request right away. Either open an
|
||||
issue first to get feedback on whether or not the issue is worth
|
||||
addressing, and create a pull request later only if the feedback from the
|
||||
team is positive, or create only a pull request, if you think a discussion
|
||||
will be easier over your code.
|
||||
|
||||
- Do not add docstrings for the sake of adding docstrings, or only to address
|
||||
silenced Ruff issues. We expect docstrings to exist only when they add
|
||||
something significant to readers, such as explaining something that is not
|
||||
easier to understand from reading the corresponding code, summarizing a
|
||||
long, hard-to-read implementation, providing context about calling code, or
|
||||
indicating purposely uncaught exceptions from called code.
|
||||
|
||||
- Do not add tests that use as much mocking as possible just to touch a given
|
||||
line of code and hence improve line coverage. While we do aim to maximize
|
||||
test coverage, tests should be written for real scenarios, with minimum
|
||||
mocking. We usually prefer end-to-end tests.
|
||||
|
||||
.. _writing-patches:
|
||||
|
||||
Writing patches
|
||||
===============
|
||||
|
||||
Scrapy has a list of `good first issues`_ and `help wanted issues`_ that you
|
||||
can work on. These issues are a great way to get started with contributing to
|
||||
Scrapy. If you're new to the codebase, you may want to focus on documentation
|
||||
or testing-related issues, as they are always useful and can help you get
|
||||
more familiar with the project. You can also check Scrapy's `test coverage`_
|
||||
to see which areas may benefit from more tests.
|
||||
|
||||
The better a patch is written, the higher the chances that it'll get accepted and the sooner it will be merged.
|
||||
|
||||
Well-written patches should:
|
||||
|
|
@ -131,6 +199,14 @@ Remember to explain what was fixed or the new functionality (what it is, why
|
|||
it's needed, etc). The more info you include, the easier will be for core
|
||||
developers to understand and accept your patch.
|
||||
|
||||
If your pull request aims to resolve an open issue, `link it accordingly
|
||||
<https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword>`__,
|
||||
e.g.:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
Resolves #123
|
||||
|
||||
You can also discuss the new functionality (or bug fix) before creating the
|
||||
patch, but it's always good to have a patch ready to illustrate your arguments
|
||||
and show that you have put some additional thought into the subject. A good
|
||||
|
|
@ -154,7 +230,7 @@ by running ``git fetch upstream pull/$PR_NUMBER/head:$BRANCH_NAME_TO_CREATE``
|
|||
(replace 'upstream' with a remote name for scrapy repository,
|
||||
``$PR_NUMBER`` with an ID of the pull request, and ``$BRANCH_NAME_TO_CREATE``
|
||||
with a name of the branch you want to create locally).
|
||||
See also: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/checking-out-pull-requests-locally#modifying-an-inactive-pull-request-locally.
|
||||
See also: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally#modifying-an-inactive-pull-request-locally.
|
||||
|
||||
When writing GitHub pull requests, try to keep titles short but descriptive.
|
||||
E.g. For bug #411: "Scrapy hangs if an exception raises in start_requests"
|
||||
|
|
@ -175,15 +251,15 @@ Coding style
|
|||
Please follow these coding conventions when writing code for inclusion in
|
||||
Scrapy:
|
||||
|
||||
* We use `black <https://black.readthedocs.io/en/stable/>`_ for code formatting.
|
||||
* We use `Ruff <https://docs.astral.sh/ruff/>`_ for code formatting.
|
||||
There is a hook in the pre-commit config
|
||||
that will automatically format your code before every commit. You can also
|
||||
run black manually with ``tox -e pre-commit``.
|
||||
run Ruff manually with ``tox -e pre-commit``.
|
||||
|
||||
* Don't put your name in the code you contribute; git provides enough
|
||||
metadata to identify author of the code.
|
||||
See https://help.github.com/en/github/using-git/setting-your-username-in-git for
|
||||
setup instructions.
|
||||
See https://docs.github.com/en/get-started/getting-started-with-git/setting-your-username-in-git
|
||||
for setup instructions.
|
||||
|
||||
.. _scrapy-pre-commit:
|
||||
|
||||
|
|
@ -242,6 +318,7 @@ Documentation about deprecated features must be removed as those features are
|
|||
deprecated, so that new readers do not run into it. New deprecations and
|
||||
deprecation removals are documented in the :ref:`release notes <news>`.
|
||||
|
||||
.. _write-tests:
|
||||
|
||||
Tests
|
||||
=====
|
||||
|
|
@ -317,9 +394,8 @@ And their unit-tests are in::
|
|||
.. _AUTHORS: https://github.com/scrapy/scrapy/blob/master/AUTHORS
|
||||
.. _tests/: https://github.com/scrapy/scrapy/tree/master/tests
|
||||
.. _open issues: https://github.com/scrapy/scrapy/issues
|
||||
.. _PEP 257: https://www.python.org/dev/peps/pep-0257/
|
||||
.. _pull request: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||
.. _PEP 257: https://peps.python.org/pep-0257/
|
||||
.. _pull request: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
|
||||
.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist
|
||||
.. _good first issues: https://github.com/scrapy/scrapy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%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
|
||||
|
|
|
|||
53
docs/faq.rst
53
docs/faq.rst
|
|
@ -23,7 +23,7 @@ comparing `jinja2`_ to `Django`_.
|
|||
|
||||
.. _BeautifulSoup: https://www.crummy.com/software/BeautifulSoup/
|
||||
.. _lxml: https://lxml.de/
|
||||
.. _jinja2: https://palletsprojects.com/p/jinja/
|
||||
.. _jinja2: https://palletsprojects.com/projects/jinja/
|
||||
.. _Django: https://www.djangoproject.com/
|
||||
|
||||
Can I use Scrapy with BeautifulSoup?
|
||||
|
|
@ -96,30 +96,13 @@ How can I simulate a user login in my spider?
|
|||
|
||||
See :ref:`topics-request-response-ref-request-userlogin`.
|
||||
|
||||
|
||||
.. _faq-bfo-dfo:
|
||||
|
||||
Does Scrapy crawl in breadth-first or depth-first order?
|
||||
--------------------------------------------------------
|
||||
|
||||
By default, Scrapy uses a `LIFO`_ queue for storing pending requests, which
|
||||
basically means that it crawls in `DFO order`_. This order is more convenient
|
||||
in most cases.
|
||||
|
||||
If you do want to crawl in true `BFO order`_, you can do it by
|
||||
setting the following settings:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
DEPTH_PRIORITY = 1
|
||||
SCHEDULER_DISK_QUEUE = "scrapy.squeues.PickleFifoDiskQueue"
|
||||
SCHEDULER_MEMORY_QUEUE = "scrapy.squeues.FifoMemoryQueue"
|
||||
|
||||
While pending requests are below the configured values of
|
||||
:setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` or
|
||||
:setting:`CONCURRENT_REQUESTS_PER_IP`, those requests are sent
|
||||
concurrently. As a result, the first few requests of a crawl rarely follow the
|
||||
desired order. Lowering those settings to ``1`` enforces the desired order, but
|
||||
it significantly slows down the crawl as a whole.
|
||||
:ref:`DFO by default, but other orders are possible <request-order>`.
|
||||
|
||||
|
||||
My Scrapy crawler has memory leaks. What can I do?
|
||||
|
|
@ -148,7 +131,7 @@ middleware with a :ref:`custom downloader middleware
|
|||
instead joining the strings in :attr:`~scrapy.Spider.allowed_domains` into
|
||||
a complex regular expression.
|
||||
|
||||
- If you can `meet the installation requirements`_, use pyre2_ instead of
|
||||
- If you can meet the installation requirements, use pyre2_ instead of
|
||||
Python’s re_ to compile your URL-filtering regular expression. See
|
||||
:issue:`1908`.
|
||||
|
||||
|
|
@ -166,9 +149,8 @@ See also `other suggestions at StackOverflow
|
|||
"myproject.middlewares.CustomOffsiteMiddleware": 50,
|
||||
}
|
||||
|
||||
.. _meet the installation requirements: https://github.com/andreasvc/pyre2#installation
|
||||
.. _pyre2: https://github.com/andreasvc/pyre2
|
||||
.. _re: https://docs.python.org/library/re.html
|
||||
.. _re: https://docs.python.org/3/library/re.html
|
||||
|
||||
Can I use Basic HTTP Authentication in my spiders?
|
||||
--------------------------------------------------
|
||||
|
|
@ -282,7 +264,7 @@ The ``__VIEWSTATE`` parameter is used in sites built with ASP.NET/VB.NET. For
|
|||
more info on how it works see `this page`_. Also, here's an `example spider`_
|
||||
which scrapes one of these sites.
|
||||
|
||||
.. _this page: https://metacpan.org/pod/release/ECARROLL/HTML-TreeBuilderX-ASP_NET-0.09/lib/HTML/TreeBuilderX/ASP_NET.pm
|
||||
.. _this page: https://metacpan.org/release/ECARROLL/HTML-TreeBuilderX-ASP_NET-0.09/view/lib/HTML/TreeBuilderX/ASP_NET.pm
|
||||
.. _example spider: https://github.com/AmbientLighter/rpn-fas/blob/master/fas/spiders/rnp.py
|
||||
|
||||
What's the best way to parse big XML/CSV data feeds?
|
||||
|
|
@ -362,16 +344,18 @@ method for this purpose. For example:
|
|||
|
||||
from copy import deepcopy
|
||||
|
||||
from itemadapter import is_item, ItemAdapter
|
||||
from itemadapter import ItemAdapter
|
||||
from scrapy import Request
|
||||
|
||||
|
||||
class MultiplyItemsMiddleware:
|
||||
def process_spider_output(self, response, result, spider):
|
||||
for item in result:
|
||||
if is_item(item):
|
||||
adapter = ItemAdapter(item)
|
||||
for _ in range(adapter["multiply_by"]):
|
||||
yield deepcopy(item)
|
||||
def process_spider_output(self, response, result):
|
||||
for item_or_request in result:
|
||||
if isinstance(item_or_request, Request):
|
||||
continue
|
||||
adapter = ItemAdapter(item)
|
||||
for _ in range(adapter["multiply_by"]):
|
||||
yield deepcopy(item)
|
||||
|
||||
Does Scrapy support IPv6 addresses?
|
||||
-----------------------------------
|
||||
|
|
@ -411,7 +395,7 @@ How can I make a blank request?
|
|||
-------------------------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
from scrapy import Request
|
||||
|
||||
|
||||
|
|
@ -432,9 +416,6 @@ See :issue:`2680`.
|
|||
|
||||
|
||||
.. _has been reported: https://github.com/scrapy/scrapy/issues/2905
|
||||
.. _Python standard library modules: https://docs.python.org/py-modindex.html
|
||||
.. _Python standard library modules: https://docs.python.org/3/py-modindex.html
|
||||
.. _Python package: https://pypi.org/
|
||||
.. _user agents: https://en.wikipedia.org/wiki/User_agent
|
||||
.. _LIFO: https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
|
||||
.. _DFO order: https://en.wikipedia.org/wiki/Depth-first_search
|
||||
.. _BFO order: https://en.wikipedia.org/wiki/Breadth-first_search
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Having trouble? We'd like to help!
|
|||
.. _StackOverflow using the scrapy tag: https://stackoverflow.com/tags/scrapy
|
||||
.. _#scrapy IRC channel: irc://irc.freenode.net/scrapy
|
||||
.. _issue tracker: https://github.com/scrapy/scrapy/issues
|
||||
.. _Scrapy Discord: https://discord.gg/mv3yErfpvq
|
||||
.. _Scrapy Discord: https://discord.com/invite/mv3yErfpvq
|
||||
|
||||
|
||||
First steps
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ Once you've installed `Anaconda`_ or `Miniconda`_, install Scrapy with::
|
|||
To install Scrapy on Windows using ``pip``:
|
||||
|
||||
.. warning::
|
||||
This installation method requires “Microsoft Visual C++” for installing some
|
||||
This installation method requires “Microsoft Visual C++” for installing some
|
||||
Scrapy dependencies, which demands significantly more disk space than Anaconda.
|
||||
|
||||
#. Download and execute `Microsoft C++ Build Tools`_ to install the Visual Studio Installer.
|
||||
|
|
@ -123,7 +123,7 @@ To install Scrapy on Windows using ``pip``:
|
|||
#. Check the installation details and make sure following packages are selected as optional components:
|
||||
|
||||
* **MSVC** (e.g MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.23) )
|
||||
|
||||
|
||||
* **Windows SDK** (e.g Windows 10 SDK (10.0.18362.0))
|
||||
|
||||
#. Install the Visual Studio Build Tools.
|
||||
|
|
@ -267,10 +267,10 @@ For details, see `Issue #2473 <https://github.com/scrapy/scrapy/issues/2473>`_.
|
|||
.. _lxml: https://lxml.de/index.html
|
||||
.. _parsel: https://pypi.org/project/parsel/
|
||||
.. _w3lib: https://pypi.org/project/w3lib/
|
||||
.. _twisted: https://twistedmatrix.com/trac/
|
||||
.. _twisted: https://twisted.org/
|
||||
.. _cryptography: https://cryptography.io/en/latest/
|
||||
.. _pyOpenSSL: https://pypi.org/project/pyOpenSSL/
|
||||
.. _setuptools: https://pypi.python.org/pypi/setuptools
|
||||
.. _setuptools: https://pypi.org/pypi/setuptools
|
||||
.. _homebrew: https://brew.sh/
|
||||
.. _zsh: https://www.zsh.org/
|
||||
.. _Anaconda: https://docs.anaconda.com/anaconda/
|
||||
|
|
|
|||
|
|
@ -152,6 +152,6 @@ interest!
|
|||
|
||||
.. _join the community: https://scrapy.org/community/
|
||||
.. _web scraping: https://en.wikipedia.org/wiki/Web_scraping
|
||||
.. _Amazon Associates Web Services: https://affiliate-program.amazon.com/gp/advertising/api/detail/main.html
|
||||
.. _Amazon Associates Web Services: https://affiliate-program.amazon.com/welcome/ecs
|
||||
.. _Amazon S3: https://aws.amazon.com/s3/
|
||||
.. _Sitemaps: https://www.sitemaps.org/index.html
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ This is the code for our first Spider. Save it in a file named
|
|||
class QuotesSpider(scrapy.Spider):
|
||||
name = "quotes"
|
||||
|
||||
def start_requests(self):
|
||||
async def start(self):
|
||||
urls = [
|
||||
"https://quotes.toscrape.com/page/1/",
|
||||
"https://quotes.toscrape.com/page/2/",
|
||||
|
|
@ -116,10 +116,10 @@ and defines some attributes and methods:
|
|||
unique within a project, that is, you can't set the same name for different
|
||||
Spiders.
|
||||
|
||||
* :meth:`~scrapy.Spider.start_requests`: must return an iterable of
|
||||
Requests (you can return a list of requests or write a generator function)
|
||||
which the Spider will begin to crawl from. Subsequent requests will be
|
||||
generated successively from these initial requests.
|
||||
* :meth:`~scrapy.Spider.start`: must be an asynchronous generator that
|
||||
yields requests (and, optionally, items) for the spider to start crawling.
|
||||
Subsequent requests will be generated successively from these initial
|
||||
requests.
|
||||
|
||||
* :meth:`~scrapy.Spider.parse`: a method that will be called to handle
|
||||
the response downloaded for each of the requests made. The response parameter
|
||||
|
|
@ -164,21 +164,22 @@ for the respective URLs, as our ``parse`` method instructs.
|
|||
What just happened under the hood?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Scrapy schedules the :class:`scrapy.Request <scrapy.Request>` objects
|
||||
returned by the ``start_requests`` method of the Spider. Upon receiving a
|
||||
response for each one, it instantiates :class:`~scrapy.http.Response` objects
|
||||
and calls the callback method associated with the request (in this case, the
|
||||
``parse`` method) passing the response as an argument.
|
||||
Scrapy sends the first :class:`scrapy.Request <scrapy.Request>` objects yielded
|
||||
by the :meth:`~scrapy.Spider.start` spider method. Upon receiving a
|
||||
response for each one, Scrapy calls the callback method associated with the
|
||||
request (in this case, the ``parse`` method) with a
|
||||
:class:`~scrapy.http.Response` object.
|
||||
|
||||
|
||||
A shortcut to the start_requests method
|
||||
---------------------------------------
|
||||
Instead of implementing a :meth:`~scrapy.Spider.start_requests` method
|
||||
that generates :class:`scrapy.Request <scrapy.Request>` objects from URLs,
|
||||
you can just define a :attr:`~scrapy.Spider.start_urls` class attribute
|
||||
with a list of URLs. This list will then be used by the default implementation
|
||||
of :meth:`~scrapy.Spider.start_requests` to create the initial requests
|
||||
for your spider.
|
||||
A shortcut to the ``start`` method
|
||||
----------------------------------
|
||||
|
||||
Instead of implementing a :meth:`~scrapy.Spider.start` method that yields
|
||||
:class:`~scrapy.Request` objects from URLs, you can define a
|
||||
:attr:`~scrapy.Spider.start_urls` class attribute with a list of URLs. This
|
||||
list will then be used by the default implementation of
|
||||
:meth:`~scrapy.Spider.start` to create the initial requests for your
|
||||
spider.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -292,7 +293,7 @@ As an alternative, you could've written:
|
|||
>>> response.css("title::text")[0].get()
|
||||
'Quotes to Scrape'
|
||||
|
||||
Accessing an index on a :class:`~scrapy.selector.SelectorList` instance will
|
||||
Accessing an index on a :class:`~scrapy.selector.SelectorList` instance will
|
||||
raise an :exc:`IndexError` exception if there are no results:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
|
@ -302,8 +303,8 @@ raise an :exc:`IndexError` exception if there are no results:
|
|||
...
|
||||
IndexError: list index out of range
|
||||
|
||||
You might want to use ``.get()`` directly on the
|
||||
:class:`~scrapy.selector.SelectorList` instance instead, which returns ``None``
|
||||
You might want to use ``.get()`` directly on the
|
||||
:class:`~scrapy.selector.SelectorList` instance instead, which returns ``None``
|
||||
if there are no results:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
|
@ -369,7 +370,7 @@ recommend `this tutorial to learn XPath through examples
|
|||
<http://zvon.org/comp/r/tut-XPath_1.html>`_, and `this tutorial to learn "how
|
||||
to think in XPath" <http://plasmasturm.org/log/xpath101/>`_.
|
||||
|
||||
.. _XPath: https://www.w3.org/TR/xpath/all/
|
||||
.. _XPath: https://www.w3.org/TR/xpath-10/
|
||||
.. _CSS: https://www.w3.org/TR/selectors
|
||||
|
||||
Extracting quotes and authors
|
||||
|
|
@ -541,7 +542,7 @@ for Item Pipelines has been set up for you when the project is created, in
|
|||
``tutorial/pipelines.py``. Though you don't need to implement any item
|
||||
pipelines if you just want to store the scraped items.
|
||||
|
||||
.. _JSON Lines: http://jsonlines.org
|
||||
.. _JSON Lines: https://jsonlines.org
|
||||
.. _JQ: https://stedolan.github.io/jq
|
||||
|
||||
|
||||
|
|
@ -794,7 +795,7 @@ with a specific tag, building the URL based on the argument:
|
|||
class QuotesSpider(scrapy.Spider):
|
||||
name = "quotes"
|
||||
|
||||
def start_requests(self):
|
||||
async def start(self):
|
||||
url = "https://quotes.toscrape.com/"
|
||||
tag = getattr(self, "tag", None)
|
||||
if tag is not None:
|
||||
|
|
|
|||
1494
docs/news.rst
1494
docs/news.rst
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +1,4 @@
|
|||
sphinx==6.2.1
|
||||
sphinx-hoverxref==1.3.0
|
||||
sphinx-notfound-page==1.0.0
|
||||
sphinx-rtd-theme==2.0.0
|
||||
sphinx==8.1.3
|
||||
sphinx-notfound-page==1.0.4
|
||||
sphinx-rtd-theme==3.0.2
|
||||
sphinx-rtd-dark-mode==1.3.0
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ This is an example where two add-ons are enabled in a project's
|
|||
Writing your own add-ons
|
||||
========================
|
||||
|
||||
Add-ons are Python classes that include the following method:
|
||||
Add-ons are :ref:`components <topics-components>` that include one or both of
|
||||
the following methods:
|
||||
|
||||
.. method:: update_settings(settings)
|
||||
|
||||
|
|
@ -45,19 +46,14 @@ Add-ons are Python classes that include the following method:
|
|||
:param settings: The settings object storing Scrapy/component configuration
|
||||
:type settings: :class:`~scrapy.settings.Settings`
|
||||
|
||||
They can also have the following method:
|
||||
.. classmethod:: update_pre_crawler_settings(cls, settings)
|
||||
|
||||
.. classmethod:: from_crawler(cls, crawler)
|
||||
:noindex:
|
||||
Use this class method instead of the :meth:`update_settings` method to
|
||||
update :ref:`pre-crawler settings <pre-crawler-settings>` whose value is
|
||||
used before the :class:`~scrapy.crawler.Crawler` object is created.
|
||||
|
||||
If present, this class method is called to create an add-on instance
|
||||
from a :class:`~scrapy.crawler.Crawler`. It must return a new instance
|
||||
of the add-on. The crawler object provides access to all Scrapy core
|
||||
components like settings and signals; it is a way for the add-on to access
|
||||
them and hook its functionality into Scrapy.
|
||||
|
||||
:param crawler: The crawler that uses this add-on
|
||||
:type crawler: :class:`~scrapy.crawler.Crawler`
|
||||
:param settings: The settings object storing Scrapy/component configuration
|
||||
:type settings: :class:`~scrapy.settings.BaseSettings`
|
||||
|
||||
The settings set by the add-on should use the ``addon`` priority (see
|
||||
:ref:`populating-settings` and :func:`scrapy.settings.BaseSettings.set`)::
|
||||
|
|
@ -67,15 +63,11 @@ The settings set by the add-on should use the ``addon`` priority (see
|
|||
settings.set("DNSCACHE_ENABLED", True, "addon")
|
||||
|
||||
This allows users to override these settings in the project or spider
|
||||
configuration. This is not possible with settings that are mutable objects,
|
||||
such as the dict that is a value of :setting:`ITEM_PIPELINES`. In these cases
|
||||
you can provide an add-on-specific setting that governs whether the add-on will
|
||||
modify :setting:`ITEM_PIPELINES`::
|
||||
configuration.
|
||||
|
||||
class MyAddon:
|
||||
def update_settings(self, settings):
|
||||
if settings.getbool("MYADDON_ENABLE_PIPELINE"):
|
||||
settings["ITEM_PIPELINES"]["path.to.mypipeline"] = 200
|
||||
When editing the value of a setting instead of overriding it entirely, it is
|
||||
usually best to leave its priority unchanged. For example, when editing a
|
||||
:ref:`component priority dictionary <component-priority-dictionaries>`.
|
||||
|
||||
If the ``update_settings`` method raises
|
||||
:exc:`scrapy.exceptions.NotConfigured`, the add-on will be skipped. This makes
|
||||
|
|
@ -118,12 +110,30 @@ Add-on examples
|
|||
|
||||
Set some basic configuration:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
from myproject.pipelines import MyPipeline
|
||||
|
||||
|
||||
class MyAddon:
|
||||
def update_settings(self, settings):
|
||||
settings["ITEM_PIPELINES"]["path.to.mypipeline"] = 200
|
||||
settings.set("DNSCACHE_ENABLED", True, "addon")
|
||||
settings.remove_from_list("METAREFRESH_IGNORE_TAGS", "noscript")
|
||||
settings.setdefault_in_component_priority_dict(
|
||||
"ITEM_PIPELINES", MyPipeline, 200
|
||||
)
|
||||
|
||||
.. _priority-dict-helpers:
|
||||
|
||||
.. tip:: When editing a :ref:`component priority dictionary
|
||||
<component-priority-dictionaries>` setting, like :setting:`ITEM_PIPELINES`,
|
||||
consider using setting methods like
|
||||
:meth:`~scrapy.settings.BaseSettings.replace_in_component_priority_dict`,
|
||||
:meth:`~scrapy.settings.BaseSettings.set_in_component_priority_dict`
|
||||
and
|
||||
:meth:`~scrapy.settings.BaseSettings.setdefault_in_component_priority_dict`
|
||||
to avoid mistakes.
|
||||
|
||||
Check dependencies:
|
||||
|
||||
|
|
@ -157,6 +167,7 @@ Use a fallback component:
|
|||
.. code-block:: python
|
||||
|
||||
from scrapy.core.downloader.handlers.http import HTTPDownloadHandler
|
||||
from scrapy.utils.misc import build_from_crawler
|
||||
|
||||
|
||||
FALLBACK_SETTING = "MY_FALLBACK_DOWNLOAD_HANDLER"
|
||||
|
|
@ -167,11 +178,7 @@ Use a fallback component:
|
|||
|
||||
def __init__(self, settings, crawler):
|
||||
dhcls = load_object(settings.get(FALLBACK_SETTING))
|
||||
self._fallback_handler = create_instance(
|
||||
dhcls,
|
||||
settings=None,
|
||||
crawler=crawler,
|
||||
)
|
||||
self._fallback_handler = build_from_crawler(dhcls, crawler)
|
||||
|
||||
def download_request(self, request, spider):
|
||||
if request.meta.get("my_params"):
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ extensions and middlewares.
|
|||
Crawler API
|
||||
===========
|
||||
|
||||
The main entry point to Scrapy API is the :class:`~scrapy.crawler.Crawler`
|
||||
object, passed to extensions through the ``from_crawler`` class method. This
|
||||
object provides access to all Scrapy core components, and it's the only way for
|
||||
extensions to access them and hook their functionality into Scrapy.
|
||||
The main entry point to the Scrapy API is the :class:`~scrapy.crawler.Crawler`
|
||||
object, which :ref:`components <topics-components>` can :ref:`get for
|
||||
initialization <from-crawler>`. It provides access to all Scrapy core
|
||||
components, and it is the only way for components to access them and hook their
|
||||
functionality into Scrapy.
|
||||
|
||||
.. module:: scrapy.crawler
|
||||
:synopsis: The Scrapy crawler
|
||||
|
|
@ -26,7 +27,9 @@ contains a dictionary of all available extensions and their order similar to
|
|||
how you :ref:`configure the downloader middlewares
|
||||
<topics-downloader-middleware-setting>`.
|
||||
|
||||
.. class:: Crawler(spidercls, settings)
|
||||
.. autoclass:: Crawler
|
||||
:members: get_addon, get_downloader_middleware, get_extension,
|
||||
get_item_pipeline, get_spider_middleware
|
||||
|
||||
The Crawler object must be instantiated with a
|
||||
:class:`scrapy.Spider` subclass and a
|
||||
|
|
@ -86,7 +89,7 @@ how you :ref:`configure the downloader middlewares
|
|||
The execution engine, which coordinates the core crawling logic
|
||||
between the scheduler, downloader and spiders.
|
||||
|
||||
Some extension may want to access the Scrapy engine, to inspect or
|
||||
Some extension may want to access the Scrapy engine, to inspect or
|
||||
modify the downloader and scheduler behaviour, although this is an
|
||||
advanced use and this API is not yet stable.
|
||||
|
||||
|
|
@ -96,19 +99,25 @@ how you :ref:`configure the downloader middlewares
|
|||
provided while constructing the crawler, and it is created after the
|
||||
arguments given in the :meth:`crawl` method.
|
||||
|
||||
.. method:: crawl(*args, **kwargs)
|
||||
.. automethod:: crawl_async
|
||||
|
||||
Starts the crawler by instantiating its spider class with the given
|
||||
``args`` and ``kwargs`` arguments, while setting the execution engine in
|
||||
motion. Should be called only once.
|
||||
.. automethod:: crawl
|
||||
|
||||
Returns a deferred that is fired when the crawl is finished.
|
||||
.. automethod:: stop_async
|
||||
|
||||
.. automethod:: stop
|
||||
|
||||
.. autoclass:: AsyncCrawlerRunner
|
||||
:members:
|
||||
|
||||
.. autoclass:: CrawlerRunner
|
||||
:members:
|
||||
|
||||
.. autoclass:: AsyncCrawlerProcess
|
||||
:show-inheritance:
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: CrawlerProcess
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
|
@ -204,6 +213,8 @@ SpiderLoader API
|
|||
:param request: queried request
|
||||
:type request: :class:`~scrapy.Request` instance
|
||||
|
||||
.. autoclass:: DummySpiderLoader
|
||||
|
||||
.. _topics-api-signals:
|
||||
|
||||
Signals API
|
||||
|
|
@ -269,11 +280,17 @@ class (which they all inherit from).
|
|||
The following methods are not part of the stats collection api but instead
|
||||
used when implementing custom stats collectors:
|
||||
|
||||
.. method:: open_spider(spider)
|
||||
.. method:: open_spider()
|
||||
|
||||
Open the given spider for stats collection.
|
||||
Open the spider for stats collection.
|
||||
|
||||
.. method:: close_spider(spider)
|
||||
.. method:: close_spider()
|
||||
|
||||
Close the given spider. After this is called, no more specific stats
|
||||
Close the spider. After this is called, no more specific stats
|
||||
can be accessed or collected.
|
||||
|
||||
Engine API
|
||||
==========
|
||||
|
||||
.. autoclass:: scrapy.core.engine.ExecutionEngine()
|
||||
:members: needs_backout
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ of the system, and triggering events when certain actions occur. See the
|
|||
Scheduler
|
||||
---------
|
||||
|
||||
The :ref:`scheduler <topics-scheduler>` receives requests from the engine and
|
||||
enqueues them for feeding them later (also to the engine) when the engine
|
||||
The :ref:`scheduler <topics-scheduler>` receives requests from the engine and
|
||||
enqueues them for feeding them later (also to the engine) when the engine
|
||||
requests them.
|
||||
|
||||
.. _component-downloader:
|
||||
|
|
@ -150,7 +150,7 @@ requests).
|
|||
Use a Spider middleware if you need to
|
||||
|
||||
* post-process output of spider callbacks - change/add/remove requests or items;
|
||||
* post-process start_requests;
|
||||
* post-process start requests or items;
|
||||
* handle spider exceptions;
|
||||
* call errback instead of callback for some of the requests based on response
|
||||
content.
|
||||
|
|
@ -168,9 +168,7 @@ For more information about asynchronous programming and Twisted see these
|
|||
links:
|
||||
|
||||
* :doc:`twisted:core/howto/defer-intro`
|
||||
* `Twisted - hello, asynchronous programming`_
|
||||
* `Twisted Introduction - Krondo`_
|
||||
|
||||
.. _Twisted: https://twistedmatrix.com/trac/
|
||||
.. _Twisted - hello, asynchronous programming: http://jessenoller.com/blog/2009/02/11/twisted-hello-asynchronous-programming/
|
||||
.. _Twisted Introduction - Krondo: http://krondo.com/an-introduction-to-asynchronous-programming-and-twisted/
|
||||
.. _Twisted: https://twisted.org/
|
||||
.. _Twisted Introduction - Krondo: https://krondo.com/an-introduction-to-asynchronous-programming-and-twisted/
|
||||
|
|
|
|||
|
|
@ -16,15 +16,20 @@ asyncio reactor <install-asyncio>`, you may use :mod:`asyncio` and
|
|||
Installing the asyncio reactor
|
||||
==============================
|
||||
|
||||
To enable :mod:`asyncio` support, set the :setting:`TWISTED_REACTOR` setting to
|
||||
``'twisted.internet.asyncioreactor.AsyncioSelectorReactor'``.
|
||||
To enable :mod:`asyncio` support, your :setting:`TWISTED_REACTOR` setting needs
|
||||
to be set to ``'twisted.internet.asyncioreactor.AsyncioSelectorReactor'``,
|
||||
which is the default value.
|
||||
|
||||
If you are using :class:`~scrapy.crawler.CrawlerRunner`, you also need to
|
||||
If you are using :class:`~scrapy.crawler.AsyncCrawlerRunner` or
|
||||
:class:`~scrapy.crawler.CrawlerRunner`, you also need to
|
||||
install the :class:`~twisted.internet.asyncioreactor.AsyncioSelectorReactor`
|
||||
reactor manually. You can do that using
|
||||
:func:`~scrapy.utils.reactor.install_reactor`::
|
||||
:func:`~scrapy.utils.reactor.install_reactor`:
|
||||
|
||||
install_reactor('twisted.internet.asyncioreactor.AsyncioSelectorReactor')
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
|
||||
|
||||
|
||||
.. _asyncio-preinstalled-reactor:
|
||||
|
|
@ -44,6 +49,7 @@ You can usually fix the issue by moving those offending module-level Twisted
|
|||
imports to the method or function definitions where they are used. For example,
|
||||
if you have something like:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
|
@ -68,24 +74,32 @@ those imports happen.
|
|||
|
||||
.. _asyncio-await-dfd:
|
||||
|
||||
Awaiting on Deferreds
|
||||
=====================
|
||||
Integrating Deferred code and asyncio code
|
||||
==========================================
|
||||
|
||||
When the asyncio reactor isn't installed, you can await on Deferreds in the
|
||||
coroutines directly. When it is installed, this is not possible anymore, due to
|
||||
specifics of the Scrapy coroutine integration (the coroutines are wrapped into
|
||||
:class:`asyncio.Future` objects, not into
|
||||
:class:`~twisted.internet.defer.Deferred` directly), and you need to wrap them into
|
||||
Futures. Scrapy provides two helpers for this:
|
||||
Coroutine functions can await on Deferreds by wrapping them into
|
||||
:class:`asyncio.Future` objects. Scrapy provides two helpers for this:
|
||||
|
||||
.. autofunction:: scrapy.utils.defer.deferred_to_future
|
||||
.. autofunction:: scrapy.utils.defer.maybe_deferred_to_future
|
||||
|
||||
.. tip:: If you don't need to support reactors other than the default
|
||||
:class:`~twisted.internet.asyncioreactor.AsyncioSelectorReactor`, you
|
||||
can use :func:`~scrapy.utils.defer.deferred_to_future`, otherwise you
|
||||
should use :func:`~scrapy.utils.defer.maybe_deferred_to_future`.
|
||||
|
||||
.. tip:: If you need to use these functions in code that aims to be compatible
|
||||
with lower versions of Scrapy that do not provide these functions,
|
||||
down to Scrapy 2.0 (earlier versions do not support
|
||||
:mod:`asyncio`), you can copy the implementation of these functions
|
||||
into your own code.
|
||||
|
||||
Coroutines and futures can be wrapped into Deferreds (for example, when a
|
||||
Scrapy API requires passing a Deferred to it) using the following helpers:
|
||||
|
||||
.. autofunction:: scrapy.utils.defer.deferred_from_coro
|
||||
.. autofunction:: scrapy.utils.defer.deferred_f_from_coro_f
|
||||
|
||||
|
||||
.. _enforce-asyncio-requirement:
|
||||
|
||||
|
|
@ -93,25 +107,28 @@ Enforcing asyncio as a requirement
|
|||
==================================
|
||||
|
||||
If you are writing a :ref:`component <topics-components>` that requires asyncio
|
||||
to work, use :func:`scrapy.utils.reactor.is_asyncio_reactor_installed` to
|
||||
to work, use :func:`scrapy.utils.asyncio.is_asyncio_available` to
|
||||
:ref:`enforce it as a requirement <enforce-component-requirements>`. For
|
||||
example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy.utils.reactor import is_asyncio_reactor_installed
|
||||
from scrapy.utils.asyncio import is_asyncio_available
|
||||
|
||||
|
||||
class MyComponent:
|
||||
def __init__(self):
|
||||
if not is_asyncio_reactor_installed():
|
||||
if not is_asyncio_available():
|
||||
raise ValueError(
|
||||
f"{MyComponent.__qualname__} requires the asyncio Twisted "
|
||||
f"reactor. Make sure you have it configured in the "
|
||||
f"{MyComponent.__qualname__} requires the asyncio support. "
|
||||
f"Make sure you have configured the asyncio reactor in the "
|
||||
f"TWISTED_REACTOR setting. See the asyncio documentation "
|
||||
f"of Scrapy for more information."
|
||||
)
|
||||
|
||||
.. autofunction:: scrapy.utils.asyncio.is_asyncio_available
|
||||
.. autofunction:: scrapy.utils.reactor.is_asyncio_reactor_installed
|
||||
|
||||
|
||||
.. _asyncio-windows:
|
||||
|
||||
|
|
@ -144,3 +161,18 @@ Using custom asyncio loops
|
|||
You can also use custom asyncio event loops with the asyncio reactor. Set the
|
||||
:setting:`ASYNCIO_EVENT_LOOP` setting to the import path of the desired event
|
||||
loop class to use it instead of the default asyncio event loop.
|
||||
|
||||
|
||||
.. _disable-asyncio:
|
||||
|
||||
Switching to a non-asyncio reactor
|
||||
==================================
|
||||
|
||||
If for some reason your code doesn't work with the asyncio reactor, you can use
|
||||
a different reactor by setting the :setting:`TWISTED_REACTOR` setting to its
|
||||
import path (e.g. ``'twisted.internet.epollreactor.EPollReactor'``) or to
|
||||
``None``, which will use the default reactor for your platform. If you are
|
||||
using :class:`~scrapy.crawler.AsyncCrawlerRunner` or
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` you also need to switch to their
|
||||
Deferred-based counterparts: :class:`~scrapy.crawler.CrawlerRunner` or
|
||||
:class:`~scrapy.crawler.CrawlerProcess` respectively.
|
||||
|
|
|
|||
|
|
@ -21,9 +21,14 @@ Design goals
|
|||
How it works
|
||||
============
|
||||
|
||||
AutoThrottle extension adjusts download delays dynamically to make spider send
|
||||
:setting:`AUTOTHROTTLE_TARGET_CONCURRENCY` concurrent requests on average
|
||||
to each remote website.
|
||||
Scrapy allows defining the concurrency and delay of different download slots,
|
||||
e.g. through the :setting:`DOWNLOAD_SLOTS` setting. By default requests are
|
||||
assigned to slots based on their URL domain, although it is possible to
|
||||
customize the download slot of any request.
|
||||
|
||||
The AutoThrottle extension adjusts the delay of each download slot dynamically,
|
||||
to make your spider send :setting:`AUTOTHROTTLE_TARGET_CONCURRENCY` concurrent
|
||||
requests on average to each remote website.
|
||||
|
||||
It uses download latency to compute the delays. The main idea is the
|
||||
following: if a server needs ``latency`` seconds to respond, a client
|
||||
|
|
@ -32,8 +37,7 @@ processed in parallel.
|
|||
|
||||
Instead of adjusting the delays one can just set a small fixed
|
||||
download delay and impose hard limits on concurrency using
|
||||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` or
|
||||
:setting:`CONCURRENT_REQUESTS_PER_IP` options. It will provide a similar
|
||||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN`. It will provide a similar
|
||||
effect, but there are some important differences:
|
||||
|
||||
* because the download delay is small there will be occasional bursts
|
||||
|
|
@ -47,18 +51,6 @@ effect, but there are some important differences:
|
|||
|
||||
AutoThrottle doesn't have these issues.
|
||||
|
||||
Disabling throttling on a downloader slot
|
||||
=========================================
|
||||
|
||||
It is possible to disable AutoThrottle for a specific download slot at run time
|
||||
by setting its ``throttle`` attribute to ``False``, e.g. using
|
||||
:setting:`DOWNLOAD_SLOTS`.
|
||||
|
||||
Note, however, that AutoThrottle still determines the starting delay of every
|
||||
slot by setting the ``download_delay`` attribute on the running spider. You
|
||||
might want to set a custom value for the ``delay`` attribute of the slot, e.g.
|
||||
using :setting:`DOWNLOAD_SLOTS`.
|
||||
|
||||
Throttling algorithm
|
||||
====================
|
||||
|
||||
|
|
@ -78,7 +70,6 @@ AutoThrottle algorithm adjusts download delays based on the following rules:
|
|||
.. note:: The AutoThrottle extension honours the standard Scrapy settings for
|
||||
concurrency and delay. This means that it will respect
|
||||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` and
|
||||
:setting:`CONCURRENT_REQUESTS_PER_IP` options and
|
||||
never set a download delay lower than :setting:`DOWNLOAD_DELAY`.
|
||||
|
||||
.. _download-latency:
|
||||
|
|
@ -92,6 +83,33 @@ callback, for example, and unable to attend downloads. However, these latencies
|
|||
should still give a reasonable estimate of how busy Scrapy (and ultimately, the
|
||||
server) is, and this extension builds on that premise.
|
||||
|
||||
.. reqmeta:: autothrottle_dont_adjust_delay
|
||||
|
||||
Prevent specific requests from triggering slot delay adjustments
|
||||
================================================================
|
||||
|
||||
AutoThrottle adjusts the delay of download slots based on the latencies of
|
||||
responses that belong to that download slot. The only exceptions are non-200
|
||||
responses, which are only taken into account to increase that delay, but
|
||||
ignored if they would decrease that delay.
|
||||
|
||||
You can also set the ``autothrottle_dont_adjust_delay`` request metadata key to
|
||||
``True`` in any request to prevent its response latency from impacting the
|
||||
delay of its download slot:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy import Request
|
||||
|
||||
Request("https://example.com", meta={"autothrottle_dont_adjust_delay": True})
|
||||
|
||||
Note, however, that AutoThrottle still determines the starting delay of every
|
||||
download slot by setting the ``download_delay`` attribute on the running
|
||||
spider. If you want AutoThrottle not to impact a download slot at all, in
|
||||
addition to setting this meta key in all requests that use that download slot,
|
||||
you might want to set a custom value for the ``delay`` attribute of that
|
||||
download slot, e.g. using :setting:`DOWNLOAD_SLOTS`.
|
||||
|
||||
Settings
|
||||
========
|
||||
|
||||
|
|
@ -103,7 +121,6 @@ The settings used to control the AutoThrottle extension are:
|
|||
* :setting:`AUTOTHROTTLE_TARGET_CONCURRENCY`
|
||||
* :setting:`AUTOTHROTTLE_DEBUG`
|
||||
* :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`
|
||||
* :setting:`CONCURRENT_REQUESTS_PER_IP`
|
||||
* :setting:`DOWNLOAD_DELAY`
|
||||
|
||||
For more information see :ref:`autothrottle-algorithm`.
|
||||
|
|
@ -151,12 +168,10 @@ a higher value (e.g. ``2.0``) to increase the throughput and the load on remote
|
|||
servers. A lower ``AUTOTHROTTLE_TARGET_CONCURRENCY`` value
|
||||
(e.g. ``0.5``) makes the crawler more conservative and polite.
|
||||
|
||||
Note that :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`
|
||||
and :setting:`CONCURRENT_REQUESTS_PER_IP` options are still respected
|
||||
Note that :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` is still respected
|
||||
when AutoThrottle extension is enabled. This means that if
|
||||
``AUTOTHROTTLE_TARGET_CONCURRENCY`` is set to a value higher than
|
||||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` or
|
||||
:setting:`CONCURRENT_REQUESTS_PER_IP`, the crawler won't reach this number
|
||||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN`, the crawler won't reach this number
|
||||
of concurrent requests.
|
||||
|
||||
At every given time point Scrapy can be sending more or less concurrent
|
||||
|
|
|
|||
|
|
@ -83,4 +83,4 @@ and how well it's written.
|
|||
|
||||
Use scrapy-bench_ for more complex benchmarking.
|
||||
|
||||
.. _scrapy-bench: https://github.com/scrapy/scrapy-bench
|
||||
.. _scrapy-bench: https://github.com/scrapy/scrapy-bench
|
||||
|
|
|
|||
|
|
@ -61,12 +61,7 @@ Increase concurrency
|
|||
|
||||
Concurrency is the number of requests that are processed in parallel. There is
|
||||
a global limit (:setting:`CONCURRENT_REQUESTS`) and an additional limit that
|
||||
can be set either per domain (:setting:`CONCURRENT_REQUESTS_PER_DOMAIN`) or per
|
||||
IP (:setting:`CONCURRENT_REQUESTS_PER_IP`).
|
||||
|
||||
.. note:: The scheduler priority queue :ref:`recommended for broad crawls
|
||||
<broad-crawls-scheduler-priority-queue>` does not support
|
||||
:setting:`CONCURRENT_REQUESTS_PER_IP`.
|
||||
can be set per domain (:setting:`CONCURRENT_REQUESTS_PER_DOMAIN`).
|
||||
|
||||
The default global concurrency limit in Scrapy is not suitable for crawling
|
||||
many different domains in parallel, so you will want to increase it. How much
|
||||
|
|
@ -182,32 +177,6 @@ To disable redirects use:
|
|||
|
||||
REDIRECT_ENABLED = False
|
||||
|
||||
Enable crawling of "Ajax Crawlable Pages"
|
||||
=========================================
|
||||
|
||||
Some pages (up to 1%, based on empirical data from year 2013) declare
|
||||
themselves as `ajax crawlable`_. This means they provide plain HTML
|
||||
version of content that is usually available only via AJAX.
|
||||
Pages can indicate it in two ways:
|
||||
|
||||
1) by using ``#!`` in URL - this is the default way;
|
||||
2) by using a special meta tag - this way is used on
|
||||
"main", "index" website pages.
|
||||
|
||||
Scrapy handles (1) automatically; to handle (2) enable
|
||||
:ref:`AjaxCrawlMiddleware <ajaxcrawl-middleware>`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
AJAXCRAWL_ENABLED = True
|
||||
|
||||
When doing broad crawls it's common to crawl a lot of "index" web pages;
|
||||
AjaxCrawlMiddleware helps to crawl them correctly.
|
||||
It is turned OFF by default because it has some performance overhead,
|
||||
and enabling it for focused crawls doesn't make much sense.
|
||||
|
||||
.. _ajax crawlable: https://developers.google.com/search/docs/ajax-crawling/docs/getting-started
|
||||
|
||||
.. _broad-crawls-bfo:
|
||||
|
||||
Crawl in BFO order
|
||||
|
|
|
|||
|
|
@ -278,8 +278,6 @@ Supported options:
|
|||
|
||||
* ``--overwrite-output FILE`` or ``-O FILE``: dump scraped items into FILE, overwriting any existing file. To define the output format, set a colon at the end of the output URI (i.e. ``-O FILE:FORMAT``)
|
||||
|
||||
* ``--output-format FORMAT`` or ``-t FORMAT``: deprecated way to define format to use for dumping items, does not work in combination with ``-O``
|
||||
|
||||
Usage examples::
|
||||
|
||||
$ scrapy crawl myspider
|
||||
|
|
@ -291,9 +289,6 @@ Usage examples::
|
|||
$ scrapy crawl -O myfile:json myspider
|
||||
[ ... myspider starts crawling and saves the result in myfile in json format overwriting the original content... ]
|
||||
|
||||
$ scrapy crawl -o myfile -t csv myspider
|
||||
[ ... myspider starts crawling and appends the result to the file myfile in csv format ... ]
|
||||
|
||||
.. command:: check
|
||||
|
||||
check
|
||||
|
|
@ -592,6 +587,44 @@ bench
|
|||
|
||||
Run a quick benchmark test. :ref:`benchmarking`.
|
||||
|
||||
.. _topics-commands-crawlerprocess:
|
||||
|
||||
Commands that run a crawl
|
||||
=========================
|
||||
|
||||
Many commands need to run a crawl of some kind, running either a user-provided
|
||||
spider or a special internal one:
|
||||
|
||||
* :command:`bench`
|
||||
* :command:`check`
|
||||
* :command:`crawl`
|
||||
* :command:`fetch`
|
||||
* :command:`parse`
|
||||
* :command:`runspider`
|
||||
* :command:`shell`
|
||||
* :command:`view`
|
||||
|
||||
They use an internal instance of :class:`scrapy.crawler.AsyncCrawlerProcess` or
|
||||
:class:`scrapy.crawler.CrawlerProcess` for this. In most cases this detail
|
||||
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.
|
||||
|
||||
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
|
||||
(``'twisted.internet.asyncioreactor.AsyncioSelectorReactor'``),
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` will be used, otherwise
|
||||
:class:`~scrapy.crawler.CrawlerProcess` will be used. The :ref:`spider settings
|
||||
<spider-settings>` are not taken into account when doing this, as they are
|
||||
loaded after this decision is made. This may cause an error if the
|
||||
project-level setting is set to :ref:`the asyncio reactor <install-asyncio>`
|
||||
(:ref:`explicitly <project-settings>` or :ref:`by using the Scrapy default
|
||||
<default-settings>`) and :ref:`the setting of the spider being run
|
||||
<spider-settings>` is set to :ref:`a different one <disable-asyncio>`, because
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` only supports the asyncio reactor.
|
||||
In this case you should set the :setting:`FORCE_CRAWLER_PROCESS` setting to
|
||||
``True`` (at the project level or via the command line) so that Scrapy uses
|
||||
:class:`~scrapy.crawler.CrawlerProcess` which supports all reactors.
|
||||
|
||||
Custom project commands
|
||||
=======================
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
Components
|
||||
==========
|
||||
|
||||
A Scrapy component is any class whose objects are created using
|
||||
:func:`scrapy.utils.misc.create_instance`.
|
||||
A Scrapy component is any class whose objects are built using
|
||||
:func:`~scrapy.utils.misc.build_from_crawler`.
|
||||
|
||||
That includes the classes that you may assign to the following settings:
|
||||
|
||||
- :setting:`ADDONS`
|
||||
|
||||
- :setting:`DNS_RESOLVER`
|
||||
|
||||
- :setting:`DOWNLOAD_HANDLERS`
|
||||
|
|
@ -35,16 +37,90 @@ That includes the classes that you may assign to the following settings:
|
|||
|
||||
- :setting:`SCHEDULER_PRIORITY_QUEUE`
|
||||
|
||||
- :setting:`SCHEDULER_START_DISK_QUEUE`
|
||||
|
||||
- :setting:`SCHEDULER_START_MEMORY_QUEUE`
|
||||
|
||||
- :setting:`SPIDER_MIDDLEWARES`
|
||||
|
||||
Third-party Scrapy components may also let you define additional Scrapy
|
||||
components, usually configurable through :ref:`settings <topics-settings>`, to
|
||||
modify their behavior.
|
||||
|
||||
.. _from-crawler:
|
||||
|
||||
Initializing from the crawler
|
||||
=============================
|
||||
|
||||
Any Scrapy component may optionally define the following class method:
|
||||
|
||||
.. classmethod:: from_crawler(cls, crawler: scrapy.crawler.Crawler, *args, **kwargs)
|
||||
|
||||
Return an instance of the component based on *crawler*.
|
||||
|
||||
*args* and *kwargs* are component-specific arguments that some components
|
||||
receive. However, most components do not get any arguments, and instead
|
||||
:ref:`use settings <component-settings>`.
|
||||
|
||||
If a component class defines this method, this class method is called to
|
||||
create any instance of the component.
|
||||
|
||||
The *crawler* object provides access to all Scrapy core components like
|
||||
:ref:`settings <topics-settings>` and :ref:`signals <topics-signals>`,
|
||||
allowing the component to access them and hook its functionality into
|
||||
Scrapy.
|
||||
|
||||
.. _component-settings:
|
||||
|
||||
Settings
|
||||
========
|
||||
|
||||
Components can be configured through :ref:`settings <topics-settings>`.
|
||||
|
||||
Components can read any setting from the
|
||||
:attr:`~scrapy.crawler.Crawler.settings` attribute of the
|
||||
:class:`~scrapy.crawler.Crawler` object they can :ref:`get for initialization
|
||||
<from-crawler>`. That includes both built-in and custom settings.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyExtension:
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler):
|
||||
settings = crawler.settings
|
||||
return cls(settings.getbool("LOG_ENABLED"))
|
||||
|
||||
def __init__(self, log_is_enabled=False):
|
||||
if log_is_enabled:
|
||||
print("log is enabled!")
|
||||
|
||||
Components do not need to declare their custom settings programmatically.
|
||||
However, they should document them, so that users know they exist and how to
|
||||
use them.
|
||||
|
||||
It is a good practice to prefix custom settings with the name of the component,
|
||||
to avoid collisions with custom settings of other existing (or future)
|
||||
components. For example, an extension called ``WarcCaching`` could prefix its
|
||||
custom settings with ``WARC_CACHING_``.
|
||||
|
||||
Another good practice, mainly for components meant for :ref:`component priority
|
||||
dictionaries <component-priority-dictionaries>`, is to provide a boolean setting
|
||||
called ``<PREFIX>_ENABLED`` (e.g. ``WARC_CACHING_ENABLED``) to allow toggling
|
||||
that component on and off without changing the component priority dictionary
|
||||
setting. You can usually check the value of such a setting during
|
||||
initialization, and if ``False``, raise
|
||||
:exc:`~scrapy.exceptions.NotConfigured`.
|
||||
|
||||
When choosing a name for a custom setting, it is also a good idea to have a
|
||||
look at the names of :ref:`built-in settings <topics-settings-ref>`, to try to
|
||||
maintain consistency with them.
|
||||
|
||||
.. _enforce-component-requirements:
|
||||
|
||||
Enforcing component requirements
|
||||
================================
|
||||
Enforcing requirements
|
||||
======================
|
||||
|
||||
Sometimes, your components may only be intended to work under certain
|
||||
conditions. For example, they may require a minimum version of Scrapy to work as
|
||||
|
|
@ -58,8 +134,8 @@ In the case of :ref:`downloader middlewares <topics-downloader-middleware>`,
|
|||
:ref:`extensions <topics-extensions>`, :ref:`item pipelines
|
||||
<topics-item-pipeline>`, and :ref:`spider middlewares
|
||||
<topics-spider-middleware>`, you should raise
|
||||
:exc:`scrapy.exceptions.NotConfigured`, passing a description of the issue as a
|
||||
parameter to the exception so that it is printed in the logs, for the user to
|
||||
:exc:`~scrapy.exceptions.NotConfigured`, passing a description of the issue as
|
||||
a parameter to the exception so that it is printed in the logs, for the user to
|
||||
see. For other components, feel free to raise whatever other exception feels
|
||||
right to you; for example, :exc:`RuntimeError` would make sense for a Scrapy
|
||||
version mismatch, while :exc:`ValueError` may be better if the issue is the
|
||||
|
|
@ -84,3 +160,15 @@ If your requirement is a minimum Scrapy version, you may use
|
|||
f"method of spider middlewares as an asynchronous "
|
||||
f"generator."
|
||||
)
|
||||
|
||||
API reference
|
||||
=============
|
||||
|
||||
The following function can be used to create an instance of a component class:
|
||||
|
||||
.. autofunction:: scrapy.utils.misc.build_from_crawler
|
||||
|
||||
The following function can also be useful when implementing a component, to
|
||||
report the import path of the component class, e.g. when reporting problems:
|
||||
|
||||
.. autofunction:: scrapy.utils.python.global_object_name
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ Coroutines
|
|||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Scrapy has :ref:`partial support <coroutine-support>` for the
|
||||
:ref:`coroutine syntax <async>`.
|
||||
Scrapy :ref:`supports <coroutine-support>` the :ref:`coroutine syntax <async>`
|
||||
(i.e. ``async def``).
|
||||
|
||||
|
||||
.. _coroutine-support:
|
||||
|
||||
|
|
@ -17,6 +18,11 @@ Supported callables
|
|||
The following callables may be defined as coroutines using ``async def``, and
|
||||
hence use coroutine syntax (e.g. ``await``, ``async for``, ``async with``):
|
||||
|
||||
- The :meth:`~scrapy.spiders.Spider.start` spider method, which *must* be
|
||||
defined as an :term:`asynchronous generator`.
|
||||
|
||||
.. versionadded:: 2.13
|
||||
|
||||
- :class:`~scrapy.Request` callbacks.
|
||||
|
||||
If you are using any custom or third-party :ref:`spider middleware
|
||||
|
|
@ -37,20 +43,134 @@ hence use coroutine syntax (e.g. ``await``, ``async for``, ``async with``):
|
|||
methods of
|
||||
:ref:`downloader middlewares <topics-downloader-middleware-custom>`.
|
||||
|
||||
- :ref:`Signal handlers that support deferreds <signal-deferred>`.
|
||||
|
||||
- The
|
||||
:meth:`~scrapy.spidermiddlewares.SpiderMiddleware.process_spider_output`
|
||||
method of :ref:`spider middlewares <topics-spider-middleware>`.
|
||||
|
||||
It must be defined as an :term:`asynchronous generator`. The input
|
||||
``result`` parameter is an :term:`asynchronous iterable`.
|
||||
If defined as a coroutine, it must be an :term:`asynchronous generator`.
|
||||
The input ``result`` parameter is an :term:`asynchronous iterable`.
|
||||
|
||||
See also :ref:`sync-async-spider-middleware` and
|
||||
:ref:`universal-spider-middleware`.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
- The :meth:`~scrapy.spidermiddlewares.SpiderMiddleware.process_start` method
|
||||
of :ref:`spider middlewares <custom-spider-middleware>`, which *must* be
|
||||
defined as an :term:`asynchronous generator`.
|
||||
|
||||
.. versionadded:: 2.13
|
||||
|
||||
- :ref:`Signal handlers that support deferreds <signal-deferred>`.
|
||||
|
||||
|
||||
.. _coroutine-deferred-apis:
|
||||
|
||||
Using Deferred-based APIs
|
||||
=========================
|
||||
|
||||
In addition to native coroutine APIs Scrapy has some APIs that return a
|
||||
:class:`~twisted.internet.defer.Deferred` object or take a user-supplied
|
||||
function that returns a :class:`~twisted.internet.defer.Deferred` object. These
|
||||
APIs are also asynchronous but don't yet support native ``async def`` syntax.
|
||||
In the future we plan to add support for the ``async def`` syntax to these APIs
|
||||
or replace them with other APIs where changing the existing ones is
|
||||
possible.
|
||||
|
||||
The following Scrapy methods return :class:`~twisted.internet.defer.Deferred`
|
||||
objects (this list is not complete as it only includes methods that we think
|
||||
may be useful for user code):
|
||||
|
||||
- :class:`scrapy.crawler.Crawler`:
|
||||
|
||||
- :meth:`~scrapy.crawler.Crawler.crawl`
|
||||
|
||||
- :meth:`~scrapy.crawler.Crawler.stop`
|
||||
|
||||
- :class:`scrapy.crawler.CrawlerRunner` (also inherited by
|
||||
:class:`scrapy.crawler.CrawlerProcess`):
|
||||
|
||||
- :meth:`~scrapy.crawler.CrawlerRunner.crawl`
|
||||
|
||||
- :meth:`~scrapy.crawler.CrawlerRunner.stop`
|
||||
|
||||
- :meth:`~scrapy.crawler.CrawlerRunner.join`
|
||||
|
||||
- :class:`scrapy.core.engine.ExecutionEngine`:
|
||||
|
||||
- :meth:`~scrapy.core.engine.ExecutionEngine.download`
|
||||
|
||||
- :class:`scrapy.signalmanager.SignalManager`:
|
||||
|
||||
- :meth:`~scrapy.signalmanager.SignalManager.send_catch_log_deferred`
|
||||
|
||||
- :class:`~scrapy.mail.MailSender`
|
||||
|
||||
- :meth:`~scrapy.mail.MailSender.send`
|
||||
|
||||
The following user-supplied methods can return
|
||||
:class:`~twisted.internet.defer.Deferred` objects (the methods that can also
|
||||
return coroutines are listed in :ref:`coroutine-support`):
|
||||
|
||||
- Custom download handlers (see :setting:`DOWNLOAD_HANDLERS`):
|
||||
|
||||
- ``download_request()``
|
||||
|
||||
- ``close()``
|
||||
|
||||
- Custom downloader implementations (see :setting:`DOWNLOADER`):
|
||||
|
||||
- ``fetch()``
|
||||
|
||||
- Custom scheduler implementations (see :setting:`SCHEDULER`):
|
||||
|
||||
- :meth:`~scrapy.core.scheduler.BaseScheduler.open`
|
||||
|
||||
- :meth:`~scrapy.core.scheduler.BaseScheduler.close`
|
||||
|
||||
- Custom dupefilters (see :setting:`DUPEFILTER_CLASS`):
|
||||
|
||||
- ``open()``
|
||||
|
||||
- ``close()``
|
||||
|
||||
- Custom feed storages (see :setting:`FEED_STORAGES`):
|
||||
|
||||
- ``store()``
|
||||
|
||||
- Subclasses of :class:`scrapy.pipelines.media.MediaPipeline`:
|
||||
|
||||
- ``media_to_download()``
|
||||
|
||||
- ``item_completed()``
|
||||
|
||||
- Custom storages used by subclasses of
|
||||
:class:`scrapy.pipelines.files.FilesPipeline`:
|
||||
|
||||
- ``persist_file()``
|
||||
|
||||
- ``stat_file()``
|
||||
|
||||
In most cases you can use these APIs in code that otherwise uses coroutines, by
|
||||
wrapping a :class:`~twisted.internet.defer.Deferred` object into a
|
||||
:class:`~asyncio.Future` object or vice versa. See :ref:`asyncio-await-dfd` for
|
||||
more information about this.
|
||||
|
||||
For example:
|
||||
|
||||
- The :meth:`ExecutionEngine.download()
|
||||
<scrapy.core.engine.ExecutionEngine.download>` method returns a
|
||||
:class:`~twisted.internet.defer.Deferred` object that fires with the
|
||||
downloaded response. You can use this object directly in Deferred-based
|
||||
code or convert it into a :class:`~asyncio.Future` object with
|
||||
:func:`~scrapy.utils.defer.maybe_deferred_to_future`.
|
||||
- A custom download handler needs to define a ``download_request()`` method
|
||||
that returns a :class:`~twisted.internet.defer.Deferred` object. You can
|
||||
write a method that works with Deferreds and returns one directly, or you
|
||||
can write a coroutine and convert it into a function that returns a
|
||||
Deferred with :func:`~scrapy.utils.defer.deferred_f_from_coro_f`.
|
||||
|
||||
|
||||
General usage
|
||||
=============
|
||||
|
||||
|
|
@ -71,7 +191,7 @@ shorter and cleaner:
|
|||
adapter["field"] = data
|
||||
return item
|
||||
|
||||
def process_item(self, item, spider):
|
||||
def process_item(self, item):
|
||||
adapter = ItemAdapter(item)
|
||||
dfd = db.get_some_data(adapter["id"])
|
||||
dfd.addCallback(self._update_item, item)
|
||||
|
|
@ -85,7 +205,7 @@ becomes:
|
|||
|
||||
|
||||
class DbPipeline:
|
||||
async def process_item(self, item, spider):
|
||||
async def process_item(self, item):
|
||||
adapter = ItemAdapter(item)
|
||||
adapter["field"] = await db.get_some_data(adapter["id"])
|
||||
return item
|
||||
|
|
@ -123,8 +243,9 @@ This means you can use many useful Python libraries providing such code:
|
|||
|
||||
Common use cases for asynchronous code include:
|
||||
|
||||
* requesting data from websites, databases and other services (in callbacks,
|
||||
pipelines and middlewares);
|
||||
* requesting data from websites, databases and other services (in
|
||||
:meth:`~scrapy.spiders.Spider.start`, callbacks, pipelines and
|
||||
middlewares);
|
||||
* storing data in databases (in pipelines and middlewares);
|
||||
* delaying the spider initialization until some external event (in the
|
||||
:signal:`spider_opened` handler);
|
||||
|
|
@ -145,7 +266,6 @@ within a spider callback:
|
|||
.. code-block:: python
|
||||
|
||||
from scrapy import Spider, Request
|
||||
from scrapy.utils.defer import maybe_deferred_to_future
|
||||
|
||||
|
||||
class SingleRequestSpider(Spider):
|
||||
|
|
@ -154,8 +274,9 @@ within a spider callback:
|
|||
|
||||
async def parse(self, response, **kwargs):
|
||||
additional_request = Request("https://example.org/price")
|
||||
deferred = self.crawler.engine.download(additional_request)
|
||||
additional_response = await maybe_deferred_to_future(deferred)
|
||||
additional_response = await self.crawler.engine.download_async(
|
||||
additional_request
|
||||
)
|
||||
yield {
|
||||
"h1": response.css("h1").get(),
|
||||
"price": additional_response.css("#price").get(),
|
||||
|
|
@ -165,9 +286,9 @@ You can also send multiple requests in parallel:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
from scrapy import Spider, Request
|
||||
from scrapy.utils.defer import maybe_deferred_to_future
|
||||
from twisted.internet.defer import DeferredList
|
||||
|
||||
|
||||
class MultipleRequestsSpider(Spider):
|
||||
|
|
@ -179,11 +300,11 @@ You can also send multiple requests in parallel:
|
|||
Request("https://example.com/price"),
|
||||
Request("https://example.com/color"),
|
||||
]
|
||||
deferreds = []
|
||||
tasks = []
|
||||
for r in additional_requests:
|
||||
deferred = self.crawler.engine.download(r)
|
||||
deferreds.append(deferred)
|
||||
responses = await maybe_deferred_to_future(DeferredList(deferreds))
|
||||
task = self.crawler.engine.download_async(r)
|
||||
tasks.append(task)
|
||||
responses = await asyncio.gather(*tasks)
|
||||
yield {
|
||||
"h1": response.css("h1::text").get(),
|
||||
"price": responses[0][1].css(".price::text").get(),
|
||||
|
|
@ -238,16 +359,52 @@ active spider middlewares must either have their ``process_spider_output``
|
|||
method defined as an asynchronous generator or :ref:`define a
|
||||
process_spider_output_async method <universal-spider-middleware>`.
|
||||
|
||||
.. note:: When using third-party spider middlewares that only define a
|
||||
synchronous ``process_spider_output`` method, consider
|
||||
:ref:`making them universal <universal-spider-middleware>` through
|
||||
:ref:`subclassing <tut-inheritance>`.
|
||||
.. _sync-async-spider-middleware-users:
|
||||
|
||||
For middleware users
|
||||
--------------------
|
||||
|
||||
If you have asynchronous callbacks or use asynchronous-only spider middlewares
|
||||
you should make sure the asynchronous-to-synchronous conversions
|
||||
:ref:`described above <sync-async-spider-middleware>` don't happen. To do this,
|
||||
make sure all spider middlewares you use support asynchronous spider output.
|
||||
Even if you don't have asynchronous callbacks and don't use asynchronous-only
|
||||
spider middlewares in your project, it's still a good idea to make sure all
|
||||
middlewares you use support asynchronous spider output, so that it will be easy
|
||||
to start using asynchronous callbacks in the future. Because of this, Scrapy
|
||||
logs a warning when it detects a synchronous-only spider middleware.
|
||||
|
||||
If you want to update middlewares you wrote, see the :ref:`following section
|
||||
<sync-async-spider-middleware-authors>`. If you have 3rd-party middlewares that
|
||||
aren't yet updated by their authors, you can :ref:`subclass <tut-inheritance>`
|
||||
them to make them :ref:`universal <universal-spider-middleware>` and use the
|
||||
subclasses in your projects.
|
||||
|
||||
.. _sync-async-spider-middleware-authors:
|
||||
|
||||
For middleware authors
|
||||
----------------------
|
||||
|
||||
If you have a spider middleware that defines a synchronous
|
||||
``process_spider_output`` method, you should update it to support asynchronous
|
||||
spider output for :ref:`better compatibility <sync-async-spider-middleware>`,
|
||||
even if you don't yet use it with asynchronous callbacks, especially if you
|
||||
publish this middleware for other people to use. You have two options for this:
|
||||
|
||||
1. Make the middleware asynchronous, by making the ``process_spider_output``
|
||||
method an :term:`asynchronous generator`.
|
||||
2. Make the middleware universal, as described in the :ref:`next section
|
||||
<universal-spider-middleware>`.
|
||||
|
||||
If your middleware won't be used in projects with synchronous-only middlewares,
|
||||
e.g. because it's an internal middleware and you know that all other
|
||||
middlewares in your projects are already updated, it's safe to choose the first
|
||||
option. Otherwise, it's better to choose the second option.
|
||||
|
||||
.. _universal-spider-middleware:
|
||||
|
||||
Universal spider middlewares
|
||||
============================
|
||||
----------------------------
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
|
|
@ -264,12 +421,12 @@ For example:
|
|||
.. code-block:: python
|
||||
|
||||
class UniversalSpiderMiddleware:
|
||||
def process_spider_output(self, response, result, spider):
|
||||
def process_spider_output(self, response, result):
|
||||
for r in result:
|
||||
# ... do something with r
|
||||
yield r
|
||||
|
||||
async def process_spider_output_async(self, response, result, spider):
|
||||
async def process_spider_output_async(self, response, result):
|
||||
async for r in result:
|
||||
# ... do something with r
|
||||
yield r
|
||||
|
|
@ -284,3 +441,9 @@ For example:
|
|||
feature will be removed, and all spider middlewares will be expected
|
||||
to define their ``process_spider_output`` method as an asynchronous
|
||||
generator.
|
||||
|
||||
Since 2.13.0, Scrapy provides a base class,
|
||||
:class:`~scrapy.spidermiddlewares.base.BaseSpiderMiddleware`, which implements
|
||||
the ``process_spider_output()`` and ``process_spider_output_async()`` methods,
|
||||
so instead of duplicating the processing code you can override the
|
||||
``get_processed_request()`` and/or the ``get_processed_item()`` method.
|
||||
|
|
|
|||
|
|
@ -54,6 +54,6 @@ just like ``scrapyd-deploy``.
|
|||
.. _scrapyd-client: https://github.com/scrapy/scrapyd-client
|
||||
.. _scrapyd-deploy documentation: https://scrapyd.readthedocs.io/en/latest/deploy.html
|
||||
.. _shub: https://shub.readthedocs.io/en/latest/
|
||||
.. _Zyte: https://zyte.com/
|
||||
.. _Zyte: https://www.zyte.com/
|
||||
.. _Zyte Scrapy Cloud: https://www.zyte.com/scrapy-cloud/
|
||||
.. _Zyte Scrapy Cloud documentation: https://docs.zyte.com/scrapy-cloud.html
|
||||
|
|
|
|||
|
|
@ -278,9 +278,9 @@ into our ``url``.
|
|||
|
||||
In more complex websites, it could be difficult to easily reproduce the
|
||||
requests, as we could need to add ``headers`` or ``cookies`` to make it work.
|
||||
In those cases you can export the requests in `cURL <https://curl.haxx.se/>`_
|
||||
In those cases you can export the requests in `cURL <https://curl.se/>`_
|
||||
format, by right-clicking on each of them in the network tool and using the
|
||||
:meth:`~scrapy.Request.from_curl()` method to generate an equivalent
|
||||
:meth:`~scrapy.Request.from_curl` method to generate an equivalent
|
||||
request:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
|
|||
|
|
@ -61,12 +61,8 @@ particular setting. See each middleware documentation for more info.
|
|||
Writing your own downloader middleware
|
||||
======================================
|
||||
|
||||
Each downloader middleware is a Python class that defines one or more of the
|
||||
methods defined below.
|
||||
|
||||
The main entry point is the ``from_crawler`` class method, which receives a
|
||||
:class:`~scrapy.crawler.Crawler` instance. The :class:`~scrapy.crawler.Crawler`
|
||||
object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
||||
Each downloader middleware is a :ref:`component <topics-components>` that
|
||||
defines one or more of these methods:
|
||||
|
||||
.. module:: scrapy.downloadermiddlewares
|
||||
|
||||
|
|
@ -74,13 +70,13 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
|||
|
||||
.. note:: Any of the downloader middleware methods may also return a deferred.
|
||||
|
||||
.. method:: process_request(request, spider)
|
||||
.. method:: process_request(request)
|
||||
|
||||
This method is called for each request that goes through the download
|
||||
middleware.
|
||||
|
||||
:meth:`process_request` should either: return ``None``, return a
|
||||
:class:`~scrapy.Response` object, return a :class:`~scrapy.http.Request`
|
||||
:class:`~scrapy.http.Response` object, return a :class:`~scrapy.Request`
|
||||
object, or raise :exc:`~scrapy.exceptions.IgnoreRequest`.
|
||||
|
||||
If it returns ``None``, Scrapy will continue processing this request, executing all
|
||||
|
|
@ -106,10 +102,7 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
|||
:param request: the request being processed
|
||||
:type request: :class:`~scrapy.Request` object
|
||||
|
||||
:param spider: the spider for which this request is intended
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. method:: process_response(request, response, spider)
|
||||
.. method:: process_response(request, response)
|
||||
|
||||
:meth:`process_response` should either: return a :class:`~scrapy.http.Response`
|
||||
object, return a :class:`~scrapy.Request` object or
|
||||
|
|
@ -133,10 +126,7 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
|||
:param response: the response being processed
|
||||
:type response: :class:`~scrapy.http.Response` object
|
||||
|
||||
:param spider: the spider for which this response is intended
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. method:: process_exception(request, exception, spider)
|
||||
.. method:: process_exception(request, exception)
|
||||
|
||||
Scrapy calls :meth:`process_exception` when a download handler
|
||||
or a :meth:`process_request` (from a downloader middleware) raises an
|
||||
|
|
@ -164,20 +154,6 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
|||
:param exception: the raised exception
|
||||
:type exception: an ``Exception`` object
|
||||
|
||||
:param spider: the spider for which this request is intended
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. method:: from_crawler(cls, crawler)
|
||||
|
||||
If present, this classmethod is called to create a middleware instance
|
||||
from a :class:`~scrapy.crawler.Crawler`. It must return a new instance
|
||||
of the middleware. Crawler object provides access to all Scrapy core
|
||||
components like settings and signals; it is a way for middleware to
|
||||
access them and hook its functionality into Scrapy.
|
||||
|
||||
:param crawler: crawler that uses this middleware
|
||||
:type crawler: :class:`~scrapy.crawler.Crawler` object
|
||||
|
||||
.. _topics-downloader-middleware-ref:
|
||||
|
||||
Built-in downloader middleware reference
|
||||
|
|
@ -849,6 +825,26 @@ HttpProxyMiddleware
|
|||
Keep in mind this value will take precedence over ``http_proxy``/``https_proxy``
|
||||
environment variables, and it will also ignore ``no_proxy`` environment variable.
|
||||
|
||||
HttpProxyMiddleware settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. setting:: HTTPPROXY_ENABLED
|
||||
.. setting:: HTTPPROXY_AUTH_ENCODING
|
||||
|
||||
HTTPPROXY_ENABLED
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Default: ``True``
|
||||
|
||||
Whether or not to enable the :class:`HttpProxyMiddleware`.
|
||||
|
||||
HTTPPROXY_AUTH_ENCODING
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Default: ``"latin-1"``
|
||||
|
||||
The default encoding for proxy authentication on :class:`HttpProxyMiddleware`.
|
||||
|
||||
OffsiteMiddleware
|
||||
-----------------
|
||||
|
||||
|
|
@ -883,9 +879,12 @@ OffsiteMiddleware
|
|||
:attr:`~scrapy.Spider.allowed_domains` attribute, or the
|
||||
attribute is empty, the offsite middleware will allow all requests.
|
||||
|
||||
If the request has the :attr:`~scrapy.Request.dont_filter` attribute
|
||||
set, the offsite middleware will allow the request even if its domain is not
|
||||
listed in allowed domains.
|
||||
.. reqmeta:: allow_offsite
|
||||
|
||||
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
|
||||
the OffsiteMiddleware will allow the request even if its domain is not listed
|
||||
in allowed domains.
|
||||
|
||||
RedirectMiddleware
|
||||
------------------
|
||||
|
|
@ -962,7 +961,7 @@ REDIRECT_MAX_TIMES
|
|||
Default: ``20``
|
||||
|
||||
The maximum number of redirections that will be followed for a single request.
|
||||
After this maximum, the request's response is returned as is.
|
||||
If maximum redirections are exceeded, the request is aborted and ignored.
|
||||
|
||||
MetaRefreshMiddleware
|
||||
---------------------
|
||||
|
|
@ -1012,10 +1011,6 @@ Meta tags within these tags are ignored.
|
|||
The default value of :setting:`METAREFRESH_IGNORE_TAGS` changed from
|
||||
``[]`` to ``["noscript"]``.
|
||||
|
||||
.. versionchanged:: VERSION
|
||||
The default value of :setting:`METAREFRESH_IGNORE_TAGS` changed from
|
||||
``[]`` to ``['noscript']``.
|
||||
|
||||
.. setting:: METAREFRESH_MAXDELAY
|
||||
|
||||
METAREFRESH_MAXDELAY
|
||||
|
|
@ -1191,7 +1186,7 @@ Parsers vary in several aspects:
|
|||
|
||||
* Support for wildcard matching
|
||||
|
||||
* Usage of `length based rule <https://developers.google.com/search/reference/robots_txt#order-of-precedence-for-group-member-lines>`_:
|
||||
* Usage of `length based rule <https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt#order-of-precedence-for-rules>`_:
|
||||
in particular for ``Allow`` and ``Disallow`` directives, where the most
|
||||
specific rule based on the length of the path trumps the less specific
|
||||
(shorter) rule
|
||||
|
|
@ -1209,7 +1204,7 @@ Based on `Protego <https://github.com/scrapy/protego>`_:
|
|||
* implemented in Python
|
||||
|
||||
* is compliant with `Google's Robots.txt Specification
|
||||
<https://developers.google.com/search/reference/robots_txt>`_
|
||||
<https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt>`_
|
||||
|
||||
* supports wildcard matching
|
||||
|
||||
|
|
@ -1244,7 +1239,7 @@ In order to use this parser, set:
|
|||
Robotexclusionrulesparser
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Based on `Robotexclusionrulesparser <http://nikitathespider.com/python/rerp/>`_:
|
||||
Based on `Robotexclusionrulesparser <https://pypi.org/project/robotexclusionrulesparser/>`_:
|
||||
|
||||
* implemented in Python
|
||||
|
||||
|
|
@ -1257,7 +1252,7 @@ Based on `Robotexclusionrulesparser <http://nikitathespider.com/python/rerp/>`_:
|
|||
|
||||
In order to use this parser:
|
||||
|
||||
* Install `Robotexclusionrulesparser <http://nikitathespider.com/python/rerp/>`_ by running
|
||||
* Install ``Robotexclusionrulesparser`` by running
|
||||
``pip install robotexclusionrulesparser``
|
||||
|
||||
* Set :setting:`ROBOTSTXT_PARSER` setting to
|
||||
|
|
@ -1307,59 +1302,4 @@ UserAgentMiddleware
|
|||
In order for a spider to override the default user agent, its ``user_agent``
|
||||
attribute must be set.
|
||||
|
||||
.. _ajaxcrawl-middleware:
|
||||
|
||||
AjaxCrawlMiddleware
|
||||
-------------------
|
||||
|
||||
.. module:: scrapy.downloadermiddlewares.ajaxcrawl
|
||||
|
||||
.. class:: AjaxCrawlMiddleware
|
||||
|
||||
Middleware that finds 'AJAX crawlable' page variants based
|
||||
on meta-fragment html tag. See
|
||||
https://developers.google.com/search/docs/ajax-crawling/docs/getting-started
|
||||
for more info.
|
||||
|
||||
.. note::
|
||||
|
||||
Scrapy finds 'AJAX crawlable' pages for URLs like
|
||||
``'http://example.com/!#foo=bar'`` even without this middleware.
|
||||
AjaxCrawlMiddleware is necessary when URL doesn't contain ``'!#'``.
|
||||
This is often a case for 'index' or 'main' website pages.
|
||||
|
||||
AjaxCrawlMiddleware Settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. setting:: AJAXCRAWL_ENABLED
|
||||
|
||||
AJAXCRAWL_ENABLED
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Whether the AjaxCrawlMiddleware will be enabled. You may want to
|
||||
enable it for :ref:`broad crawls <topics-broad-crawls>`.
|
||||
|
||||
HttpProxyMiddleware settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. setting:: HTTPPROXY_ENABLED
|
||||
.. setting:: HTTPPROXY_AUTH_ENCODING
|
||||
|
||||
HTTPPROXY_ENABLED
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Default: ``True``
|
||||
|
||||
Whether or not to enable the :class:`HttpProxyMiddleware`.
|
||||
|
||||
HTTPPROXY_AUTH_ENCODING
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Default: ``"latin-1"``
|
||||
|
||||
The default encoding for proxy authentication on :class:`HttpProxyMiddleware`.
|
||||
|
||||
|
||||
.. _DBM: https://en.wikipedia.org/wiki/Dbm
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from it.
|
|||
|
||||
If you fail to do that, and you can nonetheless access the desired data through
|
||||
the :ref:`DOM <topics-livedom>` from your web browser, see
|
||||
:ref:`topics-javascript-rendering`.
|
||||
:ref:`topics-headless-browsing`.
|
||||
|
||||
.. _topics-finding-data-source:
|
||||
|
||||
|
|
@ -85,9 +85,8 @@ It might be enough to yield a :class:`~scrapy.Request` with the same HTTP
|
|||
method and URL. However, you may also need to reproduce the body, headers and
|
||||
form parameters (see :class:`~scrapy.FormRequest`) of that request.
|
||||
|
||||
As all major browsers allow to export the requests in `cURL
|
||||
<https://curl.haxx.se/>`_ format, Scrapy incorporates the method
|
||||
:meth:`~scrapy.Request.from_curl()` to generate an equivalent
|
||||
As all major browsers allow to export the requests in curl_ format, Scrapy
|
||||
incorporates the method :meth:`~scrapy.Request.from_curl` to generate an equivalent
|
||||
:class:`~scrapy.Request` from a cURL command. To get more information
|
||||
visit :ref:`request from curl <requests-from-curl>` inside the network
|
||||
tool section.
|
||||
|
|
@ -98,7 +97,7 @@ it <topics-handling-response-formats>`.
|
|||
You can reproduce any request with Scrapy. However, some times reproducing all
|
||||
necessary requests may not seem efficient in developer time. If that is your
|
||||
case, and crawling speed is not a major concern for you, you can alternatively
|
||||
consider :ref:`JavaScript pre-rendering <topics-javascript-rendering>`.
|
||||
consider :ref:`using a headless browser <topics-headless-browsing>`.
|
||||
|
||||
If you get the expected response `sometimes`, but not always, the issue is
|
||||
probably not your request, but the target server. The target server might be
|
||||
|
|
@ -112,13 +111,16 @@ you may use `curl2scrapy <https://michael-shub.github.io/curl2scrapy/>`_.
|
|||
Handling different response formats
|
||||
===================================
|
||||
|
||||
.. skip: start
|
||||
|
||||
Once you have a response with the desired data, how you extract the desired
|
||||
data from it depends on the type of response:
|
||||
|
||||
- If the response is HTML, XML or JSON, use :ref:`selectors
|
||||
<topics-selectors>` as usual.
|
||||
|
||||
- If the response is JSON, use :func:`response.json()` to load the desired data:
|
||||
- If the response is JSON, use :func:`response.json()
|
||||
<scrapy.http.TextResponse.json>` to load the desired data:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -144,7 +146,7 @@ data from it depends on the type of response:
|
|||
|
||||
- If the response is an image or another format based on images (e.g. PDF),
|
||||
read the response as bytes from
|
||||
:attr:`response.body <scrapy.http.TextResponse.body>` and use an OCR
|
||||
:attr:`response.body <scrapy.http.Response.body>` and use an OCR
|
||||
solution to extract the desired data as text.
|
||||
|
||||
For example, you can use pytesseract_. To read a table from a PDF,
|
||||
|
|
@ -157,11 +159,15 @@ data from it depends on the type of response:
|
|||
Otherwise, you might need to convert the SVG code into a raster image, and
|
||||
:ref:`handle that raster image <topics-parsing-images>`.
|
||||
|
||||
.. skip: end
|
||||
|
||||
.. _topics-parsing-javascript:
|
||||
|
||||
Parsing JavaScript code
|
||||
=======================
|
||||
|
||||
.. skip: start
|
||||
|
||||
If the desired data is hardcoded in JavaScript, you first need to get the
|
||||
JavaScript code:
|
||||
|
||||
|
|
@ -220,9 +226,11 @@ data from it:
|
|||
>>> selector.css('var[name="data"]').get()
|
||||
'<var name="data"><object><property name="field"><string>value</string></property></object></var>'
|
||||
|
||||
.. _topics-javascript-rendering:
|
||||
.. skip: end
|
||||
|
||||
Pre-rendering JavaScript
|
||||
.. _topics-headless-browsing:
|
||||
|
||||
Using a headless browser
|
||||
========================
|
||||
|
||||
On webpages that fetch data from additional requests, reproducing those
|
||||
|
|
@ -232,35 +240,17 @@ network transfer.
|
|||
|
||||
However, sometimes it can be really hard to reproduce certain requests. Or you
|
||||
may need something that no request can give you, such as a screenshot of a
|
||||
webpage as seen in a web browser.
|
||||
webpage as seen in a web browser. In this case using a `headless browser`_ will
|
||||
help.
|
||||
|
||||
In these cases use the Splash_ JavaScript-rendering service, along with
|
||||
`scrapy-splash`_ for seamless integration.
|
||||
|
||||
Splash returns as HTML the :ref:`DOM <topics-livedom>` of a webpage, so that
|
||||
you can parse it with :ref:`selectors <topics-selectors>`. It provides great
|
||||
flexibility through configuration_ or scripting_.
|
||||
|
||||
If you need something beyond what Splash offers, such as interacting with the
|
||||
DOM on-the-fly from Python code instead of using a previously-written script,
|
||||
or handling multiple web browser windows, you might need to
|
||||
:ref:`use a headless browser <topics-headless-browsing>` instead.
|
||||
|
||||
.. _configuration: https://splash.readthedocs.io/en/stable/api.html
|
||||
.. _scripting: https://splash.readthedocs.io/en/stable/scripting-tutorial.html
|
||||
|
||||
.. _topics-headless-browsing:
|
||||
|
||||
Using a headless browser
|
||||
========================
|
||||
|
||||
A `headless browser`_ is a special web browser that provides an API for
|
||||
A headless browser is a special web browser that provides an API for
|
||||
automation. By installing the :ref:`asyncio reactor <install-asyncio>`,
|
||||
it is possible to integrate ``asyncio``-based libraries which handle headless browsers.
|
||||
|
||||
One such library is `playwright-python`_ (an official Python port of `playwright`_).
|
||||
The following is a simple snippet to illustrate its usage within a Scrapy spider:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
|
|
@ -287,9 +277,8 @@ We recommend using `scrapy-playwright`_ for a better integration.
|
|||
.. _AJAX: https://en.wikipedia.org/wiki/Ajax_%28programming%29
|
||||
.. _CSS: https://en.wikipedia.org/wiki/Cascading_Style_Sheets
|
||||
.. _JavaScript: https://en.wikipedia.org/wiki/JavaScript
|
||||
.. _Splash: https://github.com/scrapinghub/splash
|
||||
.. _chompjs: https://github.com/Nykakin/chompjs
|
||||
.. _curl: https://curl.haxx.se/
|
||||
.. _curl: https://curl.se/
|
||||
.. _headless browser: https://en.wikipedia.org/wiki/Headless_browser
|
||||
.. _js2xml: https://github.com/scrapinghub/js2xml
|
||||
.. _playwright-python: https://github.com/microsoft/playwright-python
|
||||
|
|
@ -297,7 +286,6 @@ We recommend using `scrapy-playwright`_ for a better integration.
|
|||
.. _pyppeteer: https://pyppeteer.github.io/pyppeteer/
|
||||
.. _pytesseract: https://github.com/madmaze/pytesseract
|
||||
.. _scrapy-playwright: https://github.com/scrapy-plugins/scrapy-playwright
|
||||
.. _scrapy-splash: https://github.com/scrapy-plugins/scrapy-splash
|
||||
.. _tabula-py: https://github.com/chezou/tabula-py
|
||||
.. _wget: https://www.gnu.org/software/wget/
|
||||
.. _wgrep: https://github.com/stav/wgrep
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ the standard ``__init__`` method:
|
|||
|
||||
mailer = MailSender()
|
||||
|
||||
Or you can instantiate it passing a Scrapy settings object, which will respect
|
||||
the :ref:`settings <topics-email-settings>`:
|
||||
Or you can instantiate it passing a :class:`scrapy.Crawler` instance, which
|
||||
will respect the :ref:`settings <topics-email-settings>`:
|
||||
|
||||
.. skip: start
|
||||
.. code-block:: python
|
||||
|
||||
mailer = MailSender.from_settings(settings)
|
||||
mailer = MailSender.from_crawler(crawler)
|
||||
|
||||
And here is how to use it to send an e-mail (without attachments):
|
||||
|
||||
|
|
@ -50,9 +50,9 @@ And here is how to use it to send an e-mail (without attachments):
|
|||
MailSender class reference
|
||||
==========================
|
||||
|
||||
MailSender is the preferred class to use for sending emails from Scrapy, as it
|
||||
uses :doc:`Twisted non-blocking IO <twisted:core/howto/defer-intro>`, like the
|
||||
rest of the framework.
|
||||
The MailSender :ref:`components <topics-components>` is the preferred class to
|
||||
use for sending emails from Scrapy, as it uses :doc:`Twisted non-blocking IO
|
||||
<twisted:core/howto/defer-intro>`, like the rest of the framework.
|
||||
|
||||
.. class:: MailSender(smtphost=None, mailfrom=None, smtpuser=None, smtppass=None, smtpport=None)
|
||||
|
||||
|
|
@ -81,14 +81,6 @@ rest of the framework.
|
|||
:param smtpssl: enforce using a secure SSL connection
|
||||
:type smtpssl: bool
|
||||
|
||||
.. classmethod:: from_settings(settings)
|
||||
|
||||
Instantiate using a Scrapy settings object, which will respect
|
||||
:ref:`these Scrapy settings <topics-email-settings>`.
|
||||
|
||||
:param settings: the e-mail recipients
|
||||
:type settings: :class:`scrapy.settings.Settings` object
|
||||
|
||||
.. method:: send(to, subject, body, cc=None, attachs=(), mimetype='text/plain', charset=None)
|
||||
|
||||
Send email to the given recipients.
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ response:
|
|||
In both cases, the response could have its body truncated: the body contains
|
||||
all bytes received up until the exception is raised, including the bytes
|
||||
received in the signal handler that raises the exception. Also, the response
|
||||
object is marked with ``"download_stopped"`` in its :attr:`Response.flags`
|
||||
object is marked with ``"download_stopped"`` in its :attr:`~scrapy.http.Response.flags`
|
||||
attribute.
|
||||
|
||||
.. note:: ``fail`` is a keyword-only parameter, i.e. raising
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ value of one of their fields:
|
|||
self.year_to_exporter[year] = (exporter, xml_file)
|
||||
return self.year_to_exporter[year][0]
|
||||
|
||||
def process_item(self, item, spider):
|
||||
def process_item(self, item):
|
||||
exporter = self._exporter_for_item(item)
|
||||
exporter.export_item(item)
|
||||
return item
|
||||
|
|
@ -116,10 +116,10 @@ Example:
|
|||
2. Overriding the serialize_field() method
|
||||
------------------------------------------
|
||||
|
||||
You can also override the :meth:`~BaseItemExporter.serialize_field()` method to
|
||||
You can also override the :meth:`~BaseItemExporter.serialize_field` method to
|
||||
customize how your field value will be exported.
|
||||
|
||||
Make sure you call the base class :meth:`~BaseItemExporter.serialize_field()` method
|
||||
Make sure you call the base class :meth:`~BaseItemExporter.serialize_field` method
|
||||
after your custom code.
|
||||
|
||||
Example:
|
||||
|
|
@ -224,7 +224,7 @@ BaseItemExporter
|
|||
.. [1] Not all exporters respect the specified field order.
|
||||
.. [2] When using :ref:`item objects <item-types>` that do not expose
|
||||
all their possible fields, exporters that do not support exporting
|
||||
a different subset of fields per item will only export the fields
|
||||
a different subset of fields per item will only export the fields
|
||||
found in the first item exported.
|
||||
|
||||
.. attribute:: export_empty_fields
|
||||
|
|
|
|||
|
|
@ -4,34 +4,21 @@
|
|||
Extensions
|
||||
==========
|
||||
|
||||
The extensions framework provides a mechanism for inserting your own
|
||||
custom functionality into Scrapy.
|
||||
Extensions are :ref:`components <topics-components>` that allow inserting your
|
||||
own custom functionality into Scrapy.
|
||||
|
||||
Extensions are just regular classes.
|
||||
Unlike other components, extensions do not have a specific role in Scrapy. They
|
||||
are “wildcard” components that can be used for anything that does not fit the
|
||||
role of any other type of component.
|
||||
|
||||
Extension settings
|
||||
==================
|
||||
Loading and activating extensions
|
||||
=================================
|
||||
|
||||
Extensions use the :ref:`Scrapy settings <topics-settings>` to manage their
|
||||
settings, just like any other Scrapy code.
|
||||
Extensions are loaded at startup by creating a single instance of the extension
|
||||
class per spider being run.
|
||||
|
||||
It is customary for extensions to prefix their settings with their own name, to
|
||||
avoid collision with existing (and future) extensions. For example, a
|
||||
hypothetical extension to handle `Google Sitemaps`_ would use settings like
|
||||
``GOOGLESITEMAP_ENABLED``, ``GOOGLESITEMAP_DEPTH``, and so on.
|
||||
|
||||
.. _Google Sitemaps: https://en.wikipedia.org/wiki/Sitemaps
|
||||
|
||||
Loading & activating extensions
|
||||
===============================
|
||||
|
||||
Extensions are loaded and activated at startup by instantiating a single
|
||||
instance of the extension class per spider being run. All the extension
|
||||
initialization code must be performed in the class ``__init__`` method.
|
||||
|
||||
To make an extension available, add it to the :setting:`EXTENSIONS` setting in
|
||||
your Scrapy settings. In :setting:`EXTENSIONS`, each extension is represented
|
||||
by a string: the full Python path to the extension's class name. For example:
|
||||
To enable an extension, add it to the :setting:`EXTENSIONS` setting. For
|
||||
example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -40,55 +27,24 @@ by a string: the full Python path to the extension's class name. For example:
|
|||
"scrapy.extensions.telnet.TelnetConsole": 500,
|
||||
}
|
||||
|
||||
|
||||
As you can see, the :setting:`EXTENSIONS` setting is a dict where the keys are
|
||||
the extension paths, and their values are the orders, which define the
|
||||
extension *loading* order. The :setting:`EXTENSIONS` setting is merged with the
|
||||
:setting:`EXTENSIONS_BASE` setting defined in Scrapy (and not meant to be
|
||||
overridden) and then sorted by order to get the final sorted list of enabled
|
||||
extensions.
|
||||
:setting:`EXTENSIONS` is merged with :setting:`EXTENSIONS_BASE` (not meant to
|
||||
be overridden), and the priorities in the resulting value determine the
|
||||
*loading* order.
|
||||
|
||||
As extensions typically do not depend on each other, their loading order is
|
||||
irrelevant in most cases. This is why the :setting:`EXTENSIONS_BASE` setting
|
||||
defines all extensions with the same order (``0``). However, this feature can
|
||||
be exploited if you need to add an extension which depends on other extensions
|
||||
already loaded.
|
||||
|
||||
Available, enabled and disabled extensions
|
||||
==========================================
|
||||
|
||||
Not all available extensions will be enabled. Some of them usually depend on a
|
||||
particular setting. For example, the HTTP Cache extension is available by default
|
||||
but disabled unless the :setting:`HTTPCACHE_ENABLED` setting is set.
|
||||
|
||||
Disabling an extension
|
||||
======================
|
||||
|
||||
In order to disable an extension that comes enabled by default (i.e. those
|
||||
included in the :setting:`EXTENSIONS_BASE` setting) you must set its order to
|
||||
``None``. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
EXTENSIONS = {
|
||||
"scrapy.extensions.corestats.CoreStats": None,
|
||||
}
|
||||
defines all extensions with the same order (``0``). However, you may need to
|
||||
carefully use priorities if you add an extension that depends on other
|
||||
extensions being already loaded.
|
||||
|
||||
Writing your own extension
|
||||
==========================
|
||||
|
||||
Each extension is a Python class. The main entry point for a Scrapy extension
|
||||
(this also includes middlewares and pipelines) is the ``from_crawler``
|
||||
class method which receives a ``Crawler`` instance. Through the Crawler object
|
||||
you can access settings, signals, stats, and also control the crawling behaviour.
|
||||
Each extension is a :ref:`component <topics-components>`.
|
||||
|
||||
Typically, extensions connect to :ref:`signals <topics-signals>` and perform
|
||||
tasks triggered by them.
|
||||
|
||||
Finally, if the ``from_crawler`` method raises the
|
||||
:exc:`~scrapy.exceptions.NotConfigured` exception, the extension will be
|
||||
disabled. Otherwise, the extension will be enabled.
|
||||
|
||||
Sample extension
|
||||
----------------
|
||||
|
||||
|
|
@ -243,6 +199,32 @@ An extension for debugging memory usage. It collects information about:
|
|||
To enable this extension, turn on the :setting:`MEMDEBUG_ENABLED` setting. The
|
||||
info will be stored in the stats.
|
||||
|
||||
.. _topics-extensions-ref-spiderstate:
|
||||
|
||||
Spider state extension
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. module:: scrapy.extensions.spiderstate
|
||||
:synopsis: Spider state extension
|
||||
|
||||
.. class:: SpiderState
|
||||
|
||||
Manages spider state data by loading it before a crawl and saving it after.
|
||||
|
||||
Give a value to the :setting:`JOBDIR` setting to enable this extension.
|
||||
When enabled, this extension manages the :attr:`~scrapy.Spider.state`
|
||||
attribute of your :class:`~scrapy.Spider` instance:
|
||||
|
||||
- When your spider closes (:signal:`spider_closed`), the contents of its
|
||||
:attr:`~scrapy.Spider.state` attribute are serialized into a file named
|
||||
``spider.state`` in the :setting:`JOBDIR` folder.
|
||||
- When your spider opens (:signal:`spider_opened`), if a previously-generated
|
||||
``spider.state`` file exists in the :setting:`JOBDIR` folder, it is loaded
|
||||
into the :attr:`~scrapy.Spider.state` attribute.
|
||||
|
||||
|
||||
For an example, see :ref:`topics-keeping-persistent-state-between-batches`.
|
||||
|
||||
Close spider extension
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
@ -265,8 +247,8 @@ settings:
|
|||
|
||||
.. note::
|
||||
|
||||
When a certain closing condition is met, requests which are
|
||||
currently in the downloader queue (up to :setting:`CONCURRENT_REQUESTS`
|
||||
When a certain closing condition is met, requests which are
|
||||
currently in the downloader queue (up to :setting:`CONCURRENT_REQUESTS`
|
||||
requests) are still processed.
|
||||
|
||||
.. setting:: CLOSESPIDER_TIMEOUT
|
||||
|
|
@ -520,8 +502,4 @@ Invokes a :doc:`Python debugger <library/pdb>` inside a running Scrapy process w
|
|||
signal is received. After the debugger is exited, the Scrapy process continues
|
||||
running normally.
|
||||
|
||||
For more info see `Debugging in Python`_.
|
||||
|
||||
This extension only works on POSIX-compliant platforms (i.e. not Windows).
|
||||
|
||||
.. _Debugging in Python: https://pythonconquerstheuniverse.wordpress.com/2009/09/10/debugging-in-python/
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ FTP supports two different connection modes: `active or passive
|
|||
mode by default. To use the active connection mode instead, set the
|
||||
:setting:`FEED_STORAGE_FTP_ACTIVE` setting to ``True``.
|
||||
|
||||
The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
|
||||
The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
|
||||
storage backend is: ``True``.
|
||||
|
||||
.. caution:: The value ``True`` in ``overwrite`` will cause you to lose the
|
||||
|
|
@ -213,7 +213,7 @@ passed through the following settings:
|
|||
- :setting:`AWS_SECRET_ACCESS_KEY`
|
||||
- :setting:`AWS_SESSION_TOKEN` (only needed for `temporary security credentials`_)
|
||||
|
||||
.. _temporary security credentials: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#temporary-access-keys
|
||||
.. _temporary security credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html
|
||||
|
||||
You can also define a custom ACL, custom endpoint, and region name for exported
|
||||
feeds using these settings:
|
||||
|
|
@ -222,7 +222,7 @@ feeds using these settings:
|
|||
- :setting:`AWS_ENDPOINT_URL`
|
||||
- :setting:`AWS_REGION_NAME`
|
||||
|
||||
The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
|
||||
The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
|
||||
storage backend is: ``True``.
|
||||
|
||||
.. caution:: The value ``True`` in ``overwrite`` will cause you to lose the
|
||||
|
|
@ -248,14 +248,14 @@ The feeds are stored on `Google Cloud Storage`_.
|
|||
|
||||
- Required external libraries: `google-cloud-storage`_.
|
||||
|
||||
For more information about authentication, please refer to `Google Cloud documentation <https://cloud.google.com/docs/authentication/production>`_.
|
||||
For more information about authentication, please refer to `Google Cloud documentation <https://cloud.google.com/docs/authentication>`_.
|
||||
|
||||
You can set a *Project ID* and *Access Control List (ACL)* through the following settings:
|
||||
|
||||
- :setting:`FEED_STORAGE_GCS_ACL`
|
||||
- :setting:`GCS_PROJECT_ID`
|
||||
|
||||
The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
|
||||
The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
|
||||
storage backend is: ``True``.
|
||||
|
||||
.. caution:: The value ``True`` in ``overwrite`` will cause you to lose the
|
||||
|
|
@ -516,8 +516,7 @@ as a fallback value if that key is not provided for a specific feed definition:
|
|||
.. note:: Some FTP servers may not support appending to files (the
|
||||
``APPE`` FTP command).
|
||||
|
||||
- :ref:`topics-feed-storage-s3`: ``True`` (appending `is not supported
|
||||
<https://forums.aws.amazon.com/message.jspa?messageID=540395>`_)
|
||||
- :ref:`topics-feed-storage-s3`: ``True`` (appending is not supported)
|
||||
|
||||
- :ref:`topics-feed-storage-gcs`: ``True`` (appending is not supported)
|
||||
|
||||
|
|
@ -540,18 +539,18 @@ as a fallback value if that key is not provided for a specific feed definition:
|
|||
FEED_EXPORT_ENCODING
|
||||
--------------------
|
||||
|
||||
Default: ``None``
|
||||
Default: ``"utf-8"`` (:ref:`fallback <default-settings>`: ``None``)
|
||||
|
||||
The encoding to be used for the feed.
|
||||
|
||||
If unset or set to ``None`` (default) it uses UTF-8 for everything except JSON output,
|
||||
which uses safe numeric encoding (``\uXXXX`` sequences) for historic reasons.
|
||||
If set to ``None``, it uses UTF-8 for everything except JSON output, which uses
|
||||
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.
|
||||
``"utf-8"`` in the generated ``settings.py`` file.
|
||||
|
||||
.. setting:: FEED_EXPORT_FIELDS
|
||||
|
||||
|
|
@ -588,8 +587,8 @@ FEED_STORE_EMPTY
|
|||
Default: ``True``
|
||||
|
||||
Whether to export empty feeds (i.e. feeds with no items).
|
||||
If ``False``, and there are no items to export, no new files are created and
|
||||
existing files are not modified, even if the :ref:`overwrite feed option
|
||||
If ``False``, and there are no items to export, no new files are created and
|
||||
existing files are not modified, even if the :ref:`overwrite feed option
|
||||
<feed-options>` is enabled.
|
||||
|
||||
.. setting:: FEED_STORAGES
|
||||
|
|
@ -816,5 +815,5 @@ source spider in the feed URI:
|
|||
.. _URIs: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
|
||||
.. _Amazon S3: https://aws.amazon.com/s3/
|
||||
.. _boto3: https://github.com/boto/boto3
|
||||
.. _Canned ACL: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
||||
.. _Canned ACL: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||
.. _Google Cloud Storage: https://cloud.google.com/storage/
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ Typical uses of item pipelines are:
|
|||
Writing your own item pipeline
|
||||
==============================
|
||||
|
||||
Each item pipeline component is a Python class that must implement the following method:
|
||||
Each item pipeline is a :ref:`component <topics-components>` that must
|
||||
implement the following method:
|
||||
|
||||
.. method:: process_item(self, item, spider)
|
||||
.. method:: process_item(self, item)
|
||||
|
||||
This method is called for every item pipeline component.
|
||||
|
||||
|
|
@ -41,36 +42,16 @@ Each item pipeline component is a Python class that must implement the following
|
|||
:param item: the scraped item
|
||||
:type item: :ref:`item object <item-types>`
|
||||
|
||||
:param spider: the spider which scraped the item
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
Additionally, they may also implement the following methods:
|
||||
|
||||
.. method:: open_spider(self, spider)
|
||||
.. method:: open_spider(self)
|
||||
|
||||
This method is called when the spider is opened.
|
||||
|
||||
:param spider: the spider which was opened
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. method:: close_spider(self, spider)
|
||||
.. method:: close_spider(self)
|
||||
|
||||
This method is called when the spider is closed.
|
||||
|
||||
:param spider: the spider which was closed
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. classmethod:: from_crawler(cls, crawler)
|
||||
|
||||
If present, this class method is called to create a pipeline instance
|
||||
from a :class:`~scrapy.crawler.Crawler`. It must return a new instance
|
||||
of the pipeline. Crawler object provides access to all Scrapy core
|
||||
components like settings and signals; it is a way for pipeline to
|
||||
access them and hook its functionality into Scrapy.
|
||||
|
||||
:param crawler: crawler that uses this pipeline
|
||||
:type crawler: :class:`~scrapy.crawler.Crawler` object
|
||||
|
||||
|
||||
Item pipeline example
|
||||
=====================
|
||||
|
|
@ -92,7 +73,7 @@ contain a price:
|
|||
class PricePipeline:
|
||||
vat_factor = 1.15
|
||||
|
||||
def process_item(self, item, spider):
|
||||
def process_item(self, item):
|
||||
adapter = ItemAdapter(item)
|
||||
if adapter.get("price"):
|
||||
if adapter.get("price_excludes_vat"):
|
||||
|
|
@ -117,13 +98,13 @@ format:
|
|||
|
||||
|
||||
class JsonWriterPipeline:
|
||||
def open_spider(self, spider):
|
||||
def open_spider(self):
|
||||
self.file = open("items.jsonl", "w")
|
||||
|
||||
def close_spider(self, spider):
|
||||
def close_spider(self):
|
||||
self.file.close()
|
||||
|
||||
def process_item(self, item, spider):
|
||||
def process_item(self, item):
|
||||
line = json.dumps(ItemAdapter(item).asdict()) + "\n"
|
||||
self.file.write(line)
|
||||
return item
|
||||
|
|
@ -139,8 +120,8 @@ In this example we'll write items to MongoDB_ using pymongo_.
|
|||
MongoDB address and database name are specified in Scrapy settings;
|
||||
MongoDB collection is named after item class.
|
||||
|
||||
The main point of this example is to show how to use :meth:`from_crawler`
|
||||
method and how to clean up the resources properly.
|
||||
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.
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
|
@ -163,19 +144,19 @@ method and how to clean up the resources properly.
|
|||
mongo_db=crawler.settings.get("MONGO_DATABASE", "items"),
|
||||
)
|
||||
|
||||
def open_spider(self, spider):
|
||||
def open_spider(self):
|
||||
self.client = pymongo.MongoClient(self.mongo_uri)
|
||||
self.db = self.client[self.mongo_db]
|
||||
|
||||
def close_spider(self, spider):
|
||||
def close_spider(self):
|
||||
self.client.close()
|
||||
|
||||
def process_item(self, item, spider):
|
||||
def process_item(self, item):
|
||||
self.db[self.collection_name].insert_one(ItemAdapter(item).asdict())
|
||||
return item
|
||||
|
||||
.. _MongoDB: https://www.mongodb.com/
|
||||
.. _pymongo: https://api.mongodb.com/python/current/
|
||||
.. _pymongo: https://pymongo.readthedocs.io/en/stable/
|
||||
|
||||
|
||||
.. _ScreenshotPipeline:
|
||||
|
|
@ -200,7 +181,6 @@ item.
|
|||
import scrapy
|
||||
from itemadapter import ItemAdapter
|
||||
from scrapy.http.request import NO_CALLBACK
|
||||
from scrapy.utils.defer import maybe_deferred_to_future
|
||||
|
||||
|
||||
class ScreenshotPipeline:
|
||||
|
|
@ -209,14 +189,19 @@ item.
|
|||
|
||||
SPLASH_URL = "http://localhost:8050/render.png?url={}"
|
||||
|
||||
async def process_item(self, item, spider):
|
||||
def __init__(crawler):
|
||||
self.crawler = crawler
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler):
|
||||
return cls(crawler)
|
||||
|
||||
async def process_item(self, item):
|
||||
adapter = ItemAdapter(item)
|
||||
encoded_item_url = quote(adapter["url"])
|
||||
screenshot_url = self.SPLASH_URL.format(encoded_item_url)
|
||||
request = scrapy.Request(screenshot_url, callback=NO_CALLBACK)
|
||||
response = await maybe_deferred_to_future(
|
||||
spider.crawler.engine.download(request)
|
||||
)
|
||||
response = await self.crawler.engine.download_async(request)
|
||||
|
||||
if response.status != 200:
|
||||
# Error happened, return item.
|
||||
|
|
@ -251,7 +236,7 @@ returns multiples items with the same id:
|
|||
def __init__(self):
|
||||
self.ids_seen = set()
|
||||
|
||||
def process_item(self, item, spider):
|
||||
def process_item(self, item):
|
||||
adapter = ItemAdapter(item)
|
||||
if adapter["id"] in self.ids_seen:
|
||||
raise DropItem(f"Item ID already seen: {adapter['id']}")
|
||||
|
|
|
|||
|
|
@ -42,39 +42,27 @@ Item objects
|
|||
:class:`Item` provides a :class:`dict`-like API plus additional features that
|
||||
make it the most feature-complete item type:
|
||||
|
||||
.. class:: scrapy.item.Item([arg])
|
||||
.. class:: scrapy.Item([arg])
|
||||
.. autoclass:: scrapy.Item
|
||||
:members: copy, deepcopy, fields
|
||||
:undoc-members:
|
||||
|
||||
:class:`Item` objects replicate the standard :class:`dict` API, including
|
||||
its ``__init__`` method.
|
||||
:class:`Item` objects replicate the standard :class:`dict` API, including
|
||||
its ``__init__`` method.
|
||||
|
||||
:class:`Item` allows the defining of field names, so that:
|
||||
:class:`Item` allows the defining of field names, so that:
|
||||
|
||||
- :class:`KeyError` is raised when using undefined field names (i.e.
|
||||
prevents typos going unnoticed)
|
||||
- :class:`KeyError` is raised when using undefined field names (i.e.
|
||||
prevents typos going unnoticed)
|
||||
|
||||
- :ref:`Item exporters <topics-exporters>` can export all fields by
|
||||
default even if the first scraped object does not have values for all
|
||||
of them
|
||||
- :ref:`Item exporters <topics-exporters>` can export all fields by
|
||||
default even if the first scraped object does not have values for all
|
||||
of them
|
||||
|
||||
:class:`Item` also allows the defining of field metadata, which can be used to
|
||||
:ref:`customize serialization <topics-exporters-field-serialization>`.
|
||||
:class:`Item` also allows the defining of field metadata, which can be used to
|
||||
:ref:`customize serialization <topics-exporters-field-serialization>`.
|
||||
|
||||
:mod:`trackref` tracks :class:`Item` objects to help find memory leaks
|
||||
(see :ref:`topics-leaks-trackrefs`).
|
||||
|
||||
:class:`Item` objects also provide the following additional API members:
|
||||
|
||||
.. automethod:: copy
|
||||
|
||||
.. automethod:: deepcopy
|
||||
|
||||
.. attribute:: fields
|
||||
|
||||
A dictionary containing *all declared fields* for this Item, not only
|
||||
those populated. The keys are the field names and the values are the
|
||||
:class:`Field` objects used in the :ref:`Item declaration
|
||||
<topics-items-declaring>`.
|
||||
:mod:`trackref` tracks :class:`Item` objects to help find memory leaks
|
||||
(see :ref:`topics-leaks-trackrefs`).
|
||||
|
||||
Example:
|
||||
|
||||
|
|
@ -205,10 +193,9 @@ documentation to see which metadata keys are used by each component.
|
|||
|
||||
It's important to note that the :class:`Field` objects used to declare the item
|
||||
do not stay assigned as class attributes. Instead, they can be accessed through
|
||||
the :attr:`Item.fields` attribute.
|
||||
the :attr:`~scrapy.Item.fields` attribute.
|
||||
|
||||
.. class:: scrapy.item.Field([arg])
|
||||
.. class:: scrapy.Field([arg])
|
||||
.. autoclass:: scrapy.Field
|
||||
|
||||
The :class:`Field` class is just an alias to the built-in :class:`dict` class and
|
||||
doesn't provide any extra functionality or attributes. In other words,
|
||||
|
|
@ -221,12 +208,14 @@ the :attr:`Item.fields` attribute.
|
|||
`attr.ib`_ for additional information.
|
||||
|
||||
.. _dataclasses.field: https://docs.python.org/3/library/dataclasses.html#dataclasses.field
|
||||
.. _attr.ib: https://www.attrs.org/en/stable/api.html#attr.ib
|
||||
.. _attr.ib: https://www.attrs.org/en/stable/api-attr.html#attr.ib
|
||||
|
||||
|
||||
Working with Item objects
|
||||
-------------------------
|
||||
|
||||
.. skip: start
|
||||
|
||||
Here are some examples of common tasks performed with items, using the
|
||||
``Product`` item :ref:`declared above <topics-items-declaring>`. You will
|
||||
notice the API is very similar to the :class:`dict` API.
|
||||
|
|
@ -388,6 +377,8 @@ appending more values, or changing existing values, like this:
|
|||
That adds (or replaces) the ``serializer`` metadata key for the ``name`` field,
|
||||
keeping all the previously existing metadata values.
|
||||
|
||||
.. skip: end
|
||||
|
||||
|
||||
.. _supporting-item-types:
|
||||
|
||||
|
|
@ -397,9 +388,8 @@ Supporting All Item Types
|
|||
In code that receives an item, such as methods of :ref:`item pipelines
|
||||
<topics-item-pipeline>` or :ref:`spider middlewares
|
||||
<topics-spider-middleware>`, it is a good practice to use the
|
||||
:class:`~itemadapter.ItemAdapter` class and the
|
||||
:func:`~itemadapter.is_item` function to write code that works for
|
||||
any supported item type.
|
||||
:class:`~itemadapter.ItemAdapter` class to write code that works for any
|
||||
supported item type.
|
||||
|
||||
Other classes related to items
|
||||
==============================
|
||||
|
|
|
|||
|
|
@ -46,9 +46,9 @@ Keeping persistent state between batches
|
|||
|
||||
Sometimes you'll want to keep some persistent spider state between pause/resume
|
||||
batches. You can use the ``spider.state`` attribute for that, which should be a
|
||||
dict. There's a built-in extension that takes care of serializing, storing and
|
||||
loading that attribute from the job directory, when the spider starts and
|
||||
stops.
|
||||
dict. There's :ref:`a built-in extension <topics-extensions-ref-spiderstate>`
|
||||
that takes care of serializing, storing and loading that attribute from the job
|
||||
directory, when the spider starts and stops.
|
||||
|
||||
Here's an example of a callback that uses the spider state (other spider code
|
||||
is omitted for brevity):
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ in control.
|
|||
Debugging memory leaks with ``trackref``
|
||||
========================================
|
||||
|
||||
.. skip: start
|
||||
|
||||
:mod:`trackref` is a module provided by Scrapy to debug the most common cases of
|
||||
memory leaks. It basically tracks the references to all live Request,
|
||||
Response, Item, Spider and Selector objects.
|
||||
|
|
@ -160,7 +162,7 @@ Too many spiders?
|
|||
-----------------
|
||||
|
||||
If your project has too many spiders executed in parallel,
|
||||
the output of :func:`prefs()` can be difficult to read.
|
||||
the output of :func:`prefs` can be difficult to read.
|
||||
For this reason, that function has a ``ignore`` argument which can be used to
|
||||
ignore a particular class (and all its subclasses). For
|
||||
example, this won't show any live references to spiders:
|
||||
|
|
@ -203,6 +205,8 @@ Here are the functions available in the :mod:`~scrapy.utils.trackref` module.
|
|||
``None`` if none is found. Use :func:`print_live_refs` first to get a list
|
||||
of all tracked live objects per class name.
|
||||
|
||||
.. skip: end
|
||||
|
||||
.. _topics-leaks-muppy:
|
||||
|
||||
Debugging memory leaks with muppy
|
||||
|
|
@ -226,6 +230,7 @@ If you use ``pip``, you can install muppy with the following command::
|
|||
Here's an example to view all Python objects available in
|
||||
the heap using muppy:
|
||||
|
||||
.. skip: start
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pympler import muppy
|
||||
|
|
@ -253,6 +258,8 @@ the heap using muppy:
|
|||
<class 'list | 446 | 58.52 KB
|
||||
<class 'int | 1425 | 43.20 KB
|
||||
|
||||
.. skip: end
|
||||
|
||||
For more info about muppy, refer to the `muppy documentation`_.
|
||||
|
||||
.. _muppy documentation: https://pythonhosted.org/Pympler/muppy.html
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ Here is a typical Item Loader usage in a :ref:`Spider <topics-spiders>`, using
|
|||
the :ref:`Product item <topics-items-declaring>` declared in the :ref:`Items
|
||||
chapter <topics-items>`:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy.loader import ItemLoader
|
||||
|
|
@ -130,6 +131,7 @@ assigned to the item.
|
|||
Let's see an example to illustrate how the input and output processors are
|
||||
called for a particular field (the same applies for any other field):
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
l = ItemLoader(Product(), some_selector)
|
||||
|
|
@ -250,6 +252,7 @@ metadata. Here is an example:
|
|||
)
|
||||
|
||||
|
||||
.. skip: start
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from scrapy.loader import ItemLoader
|
||||
|
|
@ -259,6 +262,8 @@ metadata. Here is an example:
|
|||
>>> il.load_item()
|
||||
{'name': 'Welcome to my website', 'price': '1000'}
|
||||
|
||||
.. skip: end
|
||||
|
||||
The precedence order, for both input and output processors, is as follows:
|
||||
|
||||
1. Item Loader field-specific attributes: ``field_in`` and ``field_out`` (most
|
||||
|
|
@ -294,6 +299,8 @@ the Item Loader that it's able to receive an Item Loader context, so the Item
|
|||
Loader passes the currently active context when calling it, and the processor
|
||||
function (``parse_length`` in this case) can thus use them.
|
||||
|
||||
.. skip: start
|
||||
|
||||
There are several ways to modify Item Loader context values:
|
||||
|
||||
1. By modifying the currently active Item Loader context
|
||||
|
|
@ -320,6 +327,8 @@ There are several ways to modify Item Loader context values:
|
|||
class ProductLoader(ItemLoader):
|
||||
length_out = MapCompose(parse_length, unit="cm")
|
||||
|
||||
.. skip: end
|
||||
|
||||
|
||||
ItemLoader objects
|
||||
==================
|
||||
|
|
@ -350,6 +359,7 @@ that you wish to extract.
|
|||
|
||||
Example:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
loader = ItemLoader(item=Item())
|
||||
|
|
@ -364,6 +374,7 @@ the footer selector.
|
|||
|
||||
Example:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
loader = ItemLoader(item=Item())
|
||||
|
|
@ -401,6 +412,7 @@ those dashes in the final product names.
|
|||
Here's how you can remove those dashes by reusing and extending the default
|
||||
Product Item Loader (``ProductLoader``):
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
from itemloaders.processors import MapCompose
|
||||
|
|
@ -418,6 +430,7 @@ Another case where extending Item Loaders can be very helpful is when you have
|
|||
multiple source formats, for example XML and HTML. In the XML version you may
|
||||
want to remove ``CDATA`` occurrences. Here's an example of how to do it:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
from itemloaders.processors import MapCompose
|
||||
|
|
|
|||
|
|
@ -266,9 +266,9 @@ e.g. in the spider's ``__init__`` method:
|
|||
If you run this spider again then INFO messages from
|
||||
``scrapy.spidermiddlewares.httperror`` logger will be gone.
|
||||
|
||||
You can also filter log records by :class:`~logging.LogRecord` data. For
|
||||
You can also filter log records by :class:`~logging.LogRecord` data. For
|
||||
example, you can filter log records by message content using a substring or
|
||||
a regular expression. Create a :class:`logging.Filter` subclass
|
||||
a regular expression. Create a :class:`logging.Filter` subclass
|
||||
and equip it with a regular expression pattern to
|
||||
filter out unwanted messages:
|
||||
|
||||
|
|
@ -284,8 +284,8 @@ filter out unwanted messages:
|
|||
if match:
|
||||
return False
|
||||
|
||||
A project-level filter may be attached to the root
|
||||
handler created by Scrapy, this is a wieldy way to
|
||||
A project-level filter may be attached to the root
|
||||
handler created by Scrapy, this is a wieldy way to
|
||||
filter all loggers in different parts of the project
|
||||
(middlewares, spider, etc.):
|
||||
|
||||
|
|
@ -301,7 +301,7 @@ filter all loggers in different parts of the project
|
|||
for handler in logging.root.handlers:
|
||||
handler.addFilter(ContentFilter())
|
||||
|
||||
Alternatively, you may choose a specific logger
|
||||
Alternatively, you may choose a specific logger
|
||||
and hide it without affecting other loggers:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ The advantage of using the :class:`ImagesPipeline` for image files is that you
|
|||
can configure some extra functions like generating thumbnails and filtering
|
||||
the images based on their size.
|
||||
|
||||
The Images Pipeline requires Pillow_ 7.1.0 or greater. It is used for
|
||||
The Images Pipeline requires Pillow_ 8.0.0 or greater. It is used for
|
||||
thumbnailing and normalizing images to JPEG/RGB format.
|
||||
|
||||
.. _Pillow: https://github.com/python-pillow/Pillow
|
||||
|
|
@ -261,7 +261,7 @@ policy:
|
|||
For more information, see `canned ACLs`_ in the Amazon S3 Developer Guide.
|
||||
|
||||
You can also use other S3-like storages. Storages like self-hosted `Minio`_ or
|
||||
`s3.scality`_. All you need to do is set endpoint option in you Scrapy
|
||||
`Zenko CloudServer`_. All you need to do is set endpoint option in you Scrapy
|
||||
settings:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -276,9 +276,9 @@ For self-hosting you also might feel the need not to use SSL and not to verify S
|
|||
AWS_VERIFY = False # or True (None by default)
|
||||
|
||||
.. _botocore: https://github.com/boto/botocore
|
||||
.. _canned ACLs: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
||||
.. _canned ACLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||
.. _Minio: https://github.com/minio/minio
|
||||
.. _s3.scality: https://s3.scality.com/
|
||||
.. _Zenko CloudServer: https://www.zenko.io/cloudserver/
|
||||
|
||||
|
||||
.. _media-pipeline-gcs:
|
||||
|
|
@ -303,7 +303,7 @@ For example, these are valid :setting:`IMAGES_STORE` and :setting:`GCS_PROJECT_I
|
|||
|
||||
For information about authentication, see this `documentation`_.
|
||||
|
||||
.. _documentation: https://cloud.google.com/docs/authentication/production
|
||||
.. _documentation: https://cloud.google.com/docs/authentication
|
||||
|
||||
You can modify the Access Control List (ACL) policy used for the stored files,
|
||||
which is defined by the :setting:`FILES_STORE_GCS_ACL` and
|
||||
|
|
@ -414,7 +414,7 @@ class name. E.g. given pipeline class called MyPipeline you can set setting key:
|
|||
|
||||
and pipeline class MyPipeline will have expiration time set to 180.
|
||||
|
||||
The last modified time from the file is used to determine the age of the file in days,
|
||||
The last modified time from the file is used to determine the age of the file in days,
|
||||
which is then compared to the set expiration time to determine if the file is expired.
|
||||
|
||||
.. _topics-images-thumbnails:
|
||||
|
|
@ -519,7 +519,7 @@ See here the methods that you can override in your custom Files Pipeline:
|
|||
|
||||
In addition to ``response``, this method receives the original
|
||||
:class:`request <scrapy.Request>`,
|
||||
:class:`info <scrapy.pipelines.media.MediaPipeline.SpiderInfo>` and
|
||||
:class:`info <scrapy.pipelines.media.MediaPipeline.SpiderInfo>` and
|
||||
:class:`item <scrapy.Item>`
|
||||
|
||||
You can override this method to customize the download path of each file.
|
||||
|
|
@ -541,9 +541,9 @@ See here the methods that you can override in your custom Files Pipeline:
|
|||
def file_path(self, request, response=None, info=None, *, item=None):
|
||||
return "files/" + PurePosixPath(urlparse_cached(request).path).name
|
||||
|
||||
Similarly, you can use the ``item`` to determine the file path based on some item
|
||||
Similarly, you can use the ``item`` to determine the file path based on some item
|
||||
property.
|
||||
|
||||
|
||||
By default the :meth:`file_path` method returns
|
||||
``full/<request URL hash>.<extension>``.
|
||||
|
||||
|
|
@ -677,7 +677,7 @@ See here the methods that you can override in your custom Images Pipeline:
|
|||
|
||||
In addition to ``response``, this method receives the original
|
||||
:class:`request <scrapy.Request>`,
|
||||
:class:`info <scrapy.pipelines.media.MediaPipeline.SpiderInfo>` and
|
||||
:class:`info <scrapy.pipelines.media.MediaPipeline.SpiderInfo>` and
|
||||
:class:`item <scrapy.Item>`
|
||||
|
||||
You can override this method to customize the download path of each file.
|
||||
|
|
@ -699,9 +699,9 @@ See here the methods that you can override in your custom Images Pipeline:
|
|||
def file_path(self, request, response=None, info=None, *, item=None):
|
||||
return "files/" + PurePosixPath(urlparse_cached(request).path).name
|
||||
|
||||
Similarly, you can use the ``item`` to determine the file path based on some item
|
||||
Similarly, you can use the ``item`` to determine the file path based on some item
|
||||
property.
|
||||
|
||||
|
||||
By default the :meth:`file_path` method returns
|
||||
``full/<request URL hash>.<extension>``.
|
||||
|
||||
|
|
|
|||
|
|
@ -21,16 +21,21 @@ Remember that Scrapy is built on top of the Twisted
|
|||
asynchronous networking library, so you need to run it inside the Twisted reactor.
|
||||
|
||||
The first utility you can use to run your spiders is
|
||||
:class:`scrapy.crawler.CrawlerProcess`. This class will start a Twisted reactor
|
||||
for you, configuring the logging and setting shutdown handlers. This class is
|
||||
the one used by all Scrapy commands.
|
||||
:class:`scrapy.crawler.AsyncCrawlerProcess` or
|
||||
:class:`scrapy.crawler.CrawlerProcess`. These classes will start a Twisted
|
||||
reactor for you, configuring the logging and setting shutdown handlers. These
|
||||
classes are the ones used by all Scrapy commands. They have similar
|
||||
functionality, differing in their asynchronous API style:
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` returns coroutines from its
|
||||
asynchronous methods while :class:`~scrapy.crawler.CrawlerProcess` returns
|
||||
:class:`~twisted.internet.defer.Deferred` objects.
|
||||
|
||||
Here's an example showing how to run a single spider with it.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
from scrapy.crawler import CrawlerProcess
|
||||
from scrapy.crawler import AsyncCrawlerProcess
|
||||
|
||||
|
||||
class MySpider(scrapy.Spider):
|
||||
|
|
@ -38,7 +43,7 @@ Here's an example showing how to run a single spider with it.
|
|||
...
|
||||
|
||||
|
||||
process = CrawlerProcess(
|
||||
process = AsyncCrawlerProcess(
|
||||
settings={
|
||||
"FEEDS": {
|
||||
"items.json": {"format": "json"},
|
||||
|
|
@ -49,96 +54,115 @@ Here's an example showing how to run a single spider with it.
|
|||
process.crawl(MySpider)
|
||||
process.start() # the script will block here until the crawling is finished
|
||||
|
||||
Define settings within dictionary in CrawlerProcess. Make sure to check :class:`~scrapy.crawler.CrawlerProcess`
|
||||
You can define :ref:`settings <topics-settings>` within the dictionary passed
|
||||
to :class:`~scrapy.crawler.AsyncCrawlerProcess`. Make sure to check the
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess`
|
||||
documentation to get acquainted with its usage details.
|
||||
|
||||
If you are inside a Scrapy project there are some additional helpers you can
|
||||
use to import those components within the project. You can automatically import
|
||||
your spiders passing their name to :class:`~scrapy.crawler.CrawlerProcess`, and
|
||||
use ``get_project_settings`` to get a :class:`~scrapy.settings.Settings`
|
||||
instance with your project settings.
|
||||
your spiders passing their name to
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess`, and use
|
||||
:func:`scrapy.utils.project.get_project_settings` to get a
|
||||
:class:`~scrapy.settings.Settings` instance with your project settings.
|
||||
|
||||
What follows is a working example of how to do that, using the `testspiders`_
|
||||
project as example.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy.crawler import CrawlerProcess
|
||||
from scrapy.crawler import AsyncCrawlerProcess
|
||||
from scrapy.utils.project import get_project_settings
|
||||
|
||||
process = CrawlerProcess(get_project_settings())
|
||||
process = AsyncCrawlerProcess(get_project_settings())
|
||||
|
||||
# 'followall' is the name of one of the spiders of the project.
|
||||
process.crawl("followall", domain="scrapy.org")
|
||||
process.start() # the script will block here until the crawling is finished
|
||||
|
||||
There's another Scrapy utility that provides more control over the crawling
|
||||
process: :class:`scrapy.crawler.CrawlerRunner`. This class is a thin wrapper
|
||||
that encapsulates some simple helpers to run multiple crawlers, but it won't
|
||||
start or interfere with existing reactors in any way.
|
||||
process: :class:`scrapy.crawler.AsyncCrawlerRunner` or
|
||||
:class:`scrapy.crawler.CrawlerRunner`. These classes are thin wrappers
|
||||
that encapsulate some simple helpers to run multiple crawlers, but they won't
|
||||
start or interfere with existing reactors in any way. Just like
|
||||
:class:`scrapy.crawler.AsyncCrawlerProcess` and
|
||||
:class:`scrapy.crawler.CrawlerProcess` they differ in their asynchronous API
|
||||
style.
|
||||
|
||||
Using this class the reactor should be explicitly run after scheduling your
|
||||
spiders. It's recommended you use :class:`~scrapy.crawler.CrawlerRunner`
|
||||
instead of :class:`~scrapy.crawler.CrawlerProcess` if your application is
|
||||
already using Twisted and you want to run Scrapy in the same reactor.
|
||||
When using these classes the reactor should be explicitly run after scheduling
|
||||
your spiders. It's recommended that you use
|
||||
:class:`~scrapy.crawler.AsyncCrawlerRunner` or
|
||||
:class:`~scrapy.crawler.CrawlerRunner` instead of
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` or
|
||||
:class:`~scrapy.crawler.CrawlerProcess` if your application is already using
|
||||
Twisted and you want to run Scrapy in the same reactor.
|
||||
|
||||
Note that you will also have to shutdown the Twisted reactor yourself after the
|
||||
spider is finished. This can be achieved by adding callbacks to the deferred
|
||||
returned by the :meth:`CrawlerRunner.crawl
|
||||
<scrapy.crawler.CrawlerRunner.crawl>` method.
|
||||
If you want to stop the reactor or run any other code right after the spider
|
||||
finishes you can do that after the task returned from
|
||||
:meth:`AsyncCrawlerRunner.crawl() <scrapy.crawler.AsyncCrawlerRunner.crawl>`
|
||||
completes (or the Deferred returned from :meth:`CrawlerRunner.crawl()
|
||||
<scrapy.crawler.CrawlerRunner.crawl>` fires). In the simplest case you can also
|
||||
use :func:`twisted.internet.task.react` to start and stop the reactor, though
|
||||
it may be easier to just use :class:`~scrapy.crawler.AsyncCrawlerProcess` or
|
||||
:class:`~scrapy.crawler.CrawlerProcess` instead.
|
||||
|
||||
Here's an example of its usage, along with a callback to manually stop the
|
||||
reactor after ``MySpider`` has finished running.
|
||||
Here's an example of using :class:`~scrapy.crawler.AsyncCrawlerRunner` together
|
||||
with simple reactor management code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
from scrapy.crawler import CrawlerRunner
|
||||
from scrapy.crawler import AsyncCrawlerRunner
|
||||
from scrapy.utils.defer import deferred_f_from_coro_f
|
||||
from scrapy.utils.log import configure_logging
|
||||
|
||||
|
||||
class MySpider(scrapy.Spider):
|
||||
# Your spider definition
|
||||
...
|
||||
|
||||
|
||||
configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
|
||||
runner = CrawlerRunner()
|
||||
|
||||
d = runner.crawl(MySpider)
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
d.addBoth(lambda _: reactor.stop())
|
||||
reactor.run() # the script will block here until the crawling is finished
|
||||
|
||||
Same example but using a non-default reactor, it's only necessary call
|
||||
``install_reactor`` if you are using ``CrawlerRunner`` since ``CrawlerProcess`` already does this automatically.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
from scrapy.crawler import CrawlerRunner
|
||||
from scrapy.utils.log import configure_logging
|
||||
|
||||
|
||||
class MySpider(scrapy.Spider):
|
||||
# Your spider definition
|
||||
...
|
||||
|
||||
|
||||
configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
|
||||
|
||||
from scrapy.utils.reactor import install_reactor
|
||||
from twisted.internet.task import react
|
||||
|
||||
|
||||
class MySpider(scrapy.Spider):
|
||||
# Your spider definition
|
||||
...
|
||||
|
||||
|
||||
async def crawl(_):
|
||||
configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
|
||||
runner = AsyncCrawlerRunner()
|
||||
await runner.crawl(MySpider) # completes when the spider finishes
|
||||
|
||||
|
||||
install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
|
||||
runner = CrawlerRunner()
|
||||
d = runner.crawl(MySpider)
|
||||
react(deferred_f_from_coro_f(crawl))
|
||||
|
||||
from twisted.internet import reactor
|
||||
Same example but using :class:`~scrapy.crawler.CrawlerRunner` and a
|
||||
different reactor (:class:`~scrapy.crawler.AsyncCrawlerRunner` only works
|
||||
with :class:`~twisted.internet.asyncioreactor.AsyncioSelectorReactor`):
|
||||
|
||||
d.addBoth(lambda _: reactor.stop())
|
||||
reactor.run() # the script will block here until the crawling is finished
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
from scrapy.crawler import CrawlerRunner
|
||||
from scrapy.utils.log import configure_logging
|
||||
from scrapy.utils.reactor import install_reactor
|
||||
from twisted.internet.task import react
|
||||
|
||||
|
||||
class MySpider(scrapy.Spider):
|
||||
custom_settings = {
|
||||
"TWISTED_REACTOR": "twisted.internet.epollreactor.EPollReactor",
|
||||
}
|
||||
# Your spider definition
|
||||
...
|
||||
|
||||
|
||||
def crawl(_):
|
||||
configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
|
||||
runner = CrawlerRunner()
|
||||
d = runner.crawl(MySpider)
|
||||
return d # this Deferred fires when the spider finishes
|
||||
|
||||
|
||||
install_reactor("twisted.internet.epollreactor.EPollReactor")
|
||||
react(crawl)
|
||||
|
||||
.. seealso:: :doc:`twisted:core/howto/reactor-basics`
|
||||
|
||||
|
|
@ -156,7 +180,7 @@ Here is an example that runs multiple spiders simultaneously:
|
|||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
from scrapy.crawler import CrawlerProcess
|
||||
from scrapy.crawler import AsyncCrawlerProcess
|
||||
from scrapy.utils.project import get_project_settings
|
||||
|
||||
|
||||
|
|
@ -171,19 +195,21 @@ Here is an example that runs multiple spiders simultaneously:
|
|||
|
||||
|
||||
settings = get_project_settings()
|
||||
process = CrawlerProcess(settings)
|
||||
process = AsyncCrawlerProcess(settings)
|
||||
process.crawl(MySpider1)
|
||||
process.crawl(MySpider2)
|
||||
process.start() # the script will block here until all crawling jobs are finished
|
||||
|
||||
Same example using :class:`~scrapy.crawler.CrawlerRunner`:
|
||||
Same example using :class:`~scrapy.crawler.AsyncCrawlerRunner`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
from scrapy.crawler import CrawlerRunner
|
||||
from scrapy.crawler import AsyncCrawlerRunner
|
||||
from scrapy.utils.defer import deferred_f_from_coro_f
|
||||
from scrapy.utils.log import configure_logging
|
||||
from scrapy.utils.project import get_project_settings
|
||||
from scrapy.utils.reactor import install_reactor
|
||||
from twisted.internet.task import react
|
||||
|
||||
|
||||
class MySpider1(scrapy.Spider):
|
||||
|
|
@ -196,27 +222,29 @@ Same example using :class:`~scrapy.crawler.CrawlerRunner`:
|
|||
...
|
||||
|
||||
|
||||
configure_logging()
|
||||
settings = get_project_settings()
|
||||
runner = CrawlerRunner(settings)
|
||||
runner.crawl(MySpider1)
|
||||
runner.crawl(MySpider2)
|
||||
d = runner.join()
|
||||
async def crawl(_):
|
||||
configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
|
||||
runner = AsyncCrawlerRunner()
|
||||
runner.crawl(MySpider1)
|
||||
runner.crawl(MySpider2)
|
||||
await runner.join() # completes when both spiders finish
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
d.addBoth(lambda _: reactor.stop())
|
||||
install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
|
||||
react(deferred_f_from_coro_f(crawl))
|
||||
|
||||
reactor.run() # the script will block here until all crawling jobs are finished
|
||||
|
||||
Same example but running the spiders sequentially by chaining the deferreds:
|
||||
Same example but running the spiders sequentially by awaiting until each one
|
||||
finishes before starting the next one:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from twisted.internet import defer
|
||||
from scrapy.crawler import CrawlerRunner
|
||||
import scrapy
|
||||
from scrapy.crawler import AsyncCrawlerRunner
|
||||
from scrapy.utils.defer import deferred_f_from_coro_f
|
||||
from scrapy.utils.log import configure_logging
|
||||
from scrapy.utils.project import get_project_settings
|
||||
from scrapy.utils.reactor import install_reactor
|
||||
from twisted.internet.task import react
|
||||
|
||||
|
||||
class MySpider1(scrapy.Spider):
|
||||
|
|
@ -229,41 +257,20 @@ Same example but running the spiders sequentially by chaining the deferreds:
|
|||
...
|
||||
|
||||
|
||||
settings = get_project_settings()
|
||||
configure_logging(settings)
|
||||
runner = CrawlerRunner(settings)
|
||||
async def crawl(_):
|
||||
configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
|
||||
runner = AsyncCrawlerRunner()
|
||||
await runner.crawl(MySpider1)
|
||||
await runner.crawl(MySpider2)
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def crawl():
|
||||
yield runner.crawl(MySpider1)
|
||||
yield runner.crawl(MySpider2)
|
||||
reactor.stop()
|
||||
install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
|
||||
react(deferred_f_from_coro_f(crawl))
|
||||
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
crawl()
|
||||
reactor.run() # the script will block here until the last crawl call is finished
|
||||
|
||||
Different spiders can set different values for the same setting, but when they
|
||||
run in the same process it may be impossible, by design or because of some
|
||||
limitations, to use these different values. What happens in practice is
|
||||
different for different settings:
|
||||
|
||||
* :setting:`SPIDER_LOADER_CLASS` and the ones used by its value
|
||||
(:setting:`SPIDER_MODULES`, :setting:`SPIDER_LOADER_WARN_ONLY` for the
|
||||
default one) cannot be read from the per-spider settings. These are applied
|
||||
when the :class:`~scrapy.crawler.CrawlerRunner` or
|
||||
:class:`~scrapy.crawler.CrawlerProcess` object is created.
|
||||
* For :setting:`TWISTED_REACTOR` and :setting:`ASYNCIO_EVENT_LOOP` the first
|
||||
available value is used, and if a spider requests a different reactor an
|
||||
exception will be raised. These are applied when the reactor is installed.
|
||||
* For :setting:`REACTOR_THREADPOOL_MAXSIZE`, :setting:`DNS_RESOLVER` and the
|
||||
ones used by the resolver (:setting:`DNSCACHE_ENABLED`,
|
||||
:setting:`DNSCACHE_SIZE`, :setting:`DNS_TIMEOUT` for ones included in Scrapy)
|
||||
the first available value is used. These are applied when the reactor is
|
||||
started.
|
||||
.. note:: When running multiple spiders in the same process, :ref:`reactor
|
||||
settings <reactor-settings>` should not have a different value per spider.
|
||||
Also, :ref:`pre-crawler settings <pre-crawler-settings>` cannot be defined
|
||||
per spider.
|
||||
|
||||
.. seealso:: :ref:`run-from-script`.
|
||||
|
||||
|
|
@ -323,7 +330,7 @@ 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
|
||||
super proxy that you can attach your own proxies to.
|
||||
* 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/>`__
|
||||
|
||||
If you are still unable to prevent your bot getting banned, consider contacting
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ Requests and Responses
|
|||
.. module:: scrapy.http
|
||||
:synopsis: Request and Response classes
|
||||
|
||||
Scrapy uses :class:`Request` and :class:`Response` objects for crawling web
|
||||
Scrapy uses :class:`~scrapy.Request` and :class:`Response` objects for crawling web
|
||||
sites.
|
||||
|
||||
Typically, :class:`Request` objects are generated in the spiders and pass
|
||||
Typically, :class:`~scrapy.Request` objects are generated in the spiders and pass
|
||||
across the system until they reach the Downloader, which executes the request
|
||||
and returns a :class:`Response` object which travels back to the spider that
|
||||
issued the request.
|
||||
|
||||
Both :class:`Request` and :class:`Response` classes have subclasses which add
|
||||
Both :class:`~scrapy.Request` and :class:`Response` classes have subclasses which add
|
||||
functionality not required in the base classes. These are described
|
||||
below in :ref:`topics-request-response-ref-request-subclasses` and
|
||||
:ref:`topics-request-response-ref-response-subclasses`.
|
||||
|
|
@ -24,35 +24,24 @@ below in :ref:`topics-request-response-ref-request-subclasses` and
|
|||
Request objects
|
||||
===============
|
||||
|
||||
.. autoclass:: Request
|
||||
.. autoclass:: scrapy.Request
|
||||
|
||||
:param url: the URL of this request
|
||||
|
||||
If the URL is invalid, a :exc:`ValueError` exception is raised.
|
||||
:type url: str
|
||||
|
||||
:param callback: the function that will be called with the response of this
|
||||
request (once it's downloaded) as its first parameter.
|
||||
:param callback: sets :attr:`callback`, defaults to ``None``.
|
||||
|
||||
In addition to a function, the following values are supported:
|
||||
|
||||
- ``None`` (default), which indicates that the spider's
|
||||
:meth:`~scrapy.Spider.parse` method must be used.
|
||||
|
||||
- :func:`~scrapy.http.request.NO_CALLBACK`
|
||||
|
||||
For more information, see
|
||||
:ref:`topics-request-response-ref-request-callback-arguments`.
|
||||
|
||||
.. note:: If exceptions are raised during processing, ``errback`` is
|
||||
called instead.
|
||||
|
||||
:type callback: collections.abc.Callable
|
||||
.. versionchanged:: 2.0
|
||||
The *callback* parameter is no longer required when the *errback*
|
||||
parameter is specified.
|
||||
:type callback: Callable[Concatenate[Response, ...], Any] | None
|
||||
|
||||
:param method: the HTTP method of this request. Defaults to ``'GET'``.
|
||||
:type method: str
|
||||
|
||||
:param meta: the initial values for the :attr:`Request.meta` attribute. If
|
||||
:param meta: the initial values for the :attr:`.Request.meta` attribute. If
|
||||
given, the dict passed in this parameter will be shallow copied.
|
||||
:type meta: dict
|
||||
|
||||
|
|
@ -67,10 +56,10 @@ Request objects
|
|||
(for single valued headers) or lists (for multi-valued headers). If
|
||||
``None`` is passed as value, the HTTP header will not be sent at all.
|
||||
|
||||
.. caution:: Cookies set via the ``Cookie`` header are not considered by the
|
||||
:ref:`cookies-mw`. If you need to set cookies for a request, use the
|
||||
:class:`Request.cookies <scrapy.Request>` parameter. This is a known
|
||||
current limitation that is being worked on.
|
||||
.. caution:: Cookies set via the ``Cookie`` header are not considered by the
|
||||
:ref:`cookies-mw`. If you need to set cookies for a request, use the
|
||||
``cookies`` argument. This is a known current limitation that is being
|
||||
worked on.
|
||||
|
||||
:type headers: dict
|
||||
|
||||
|
|
@ -124,7 +113,7 @@ Request objects
|
|||
|
||||
.. caution:: Cookies set via the ``Cookie`` header are not considered by the
|
||||
:ref:`cookies-mw`. If you need to set cookies for a request, use the
|
||||
:class:`Request.cookies <scrapy.Request>` parameter. This is a known
|
||||
:class:`scrapy.Request.cookies <scrapy.Request>` parameter. This is a known
|
||||
current limitation that is being worked on.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
|
|
@ -138,29 +127,18 @@ Request objects
|
|||
body to bytes (if given as a string).
|
||||
:type encoding: str
|
||||
|
||||
:param priority: the priority of this request (defaults to ``0``).
|
||||
The priority is used by the scheduler to define the order used to process
|
||||
requests. Requests with a higher priority value will execute earlier.
|
||||
Negative values are allowed in order to indicate relatively low-priority.
|
||||
:param priority: sets :attr:`priority`, defaults to ``0``.
|
||||
:type priority: int
|
||||
|
||||
:param dont_filter: indicates that this request should not be filtered by
|
||||
the scheduler. This is used when you want to perform an identical
|
||||
request multiple times, to ignore the duplicates filter. Use it with
|
||||
care, or you will get into crawling loops. Default to ``False``.
|
||||
:param dont_filter: sets :attr:`dont_filter`, defaults to ``False``.
|
||||
:type dont_filter: bool
|
||||
|
||||
:param errback: a function that will be called if any exception was
|
||||
raised while processing the request. This includes pages that failed
|
||||
with 404 HTTP errors and such. It receives a
|
||||
:exc:`~twisted.python.failure.Failure` as first parameter.
|
||||
For more information,
|
||||
see :ref:`topics-request-response-ref-errbacks` below.
|
||||
:param errback: sets :attr:`errback`, defaults to ``None``.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
The *callback* parameter is no longer required when the *errback*
|
||||
parameter is specified.
|
||||
:type errback: collections.abc.Callable
|
||||
.. versionchanged:: 2.0
|
||||
The *callback* parameter is no longer required when the *errback*
|
||||
parameter is specified.
|
||||
:type errback: Callable[[Failure], Any] | None
|
||||
|
||||
:param flags: Flags sent to the request, can be used for logging or similar purposes.
|
||||
:type flags: list
|
||||
|
|
@ -172,7 +150,7 @@ Request objects
|
|||
|
||||
A string containing the URL of this request. Keep in mind that this
|
||||
attribute contains the escaped URL, so it can differ from the URL passed in
|
||||
the ``__init__`` method.
|
||||
the ``__init__()`` method.
|
||||
|
||||
This attribute is read-only. To change the URL of a Request use
|
||||
:meth:`replace`.
|
||||
|
|
@ -184,7 +162,8 @@ Request objects
|
|||
|
||||
.. attribute:: Request.headers
|
||||
|
||||
A dictionary-like object which contains the request headers.
|
||||
A dictionary-like (:class:`scrapy.http.headers.Headers`) object which contains
|
||||
the request headers.
|
||||
|
||||
.. attribute:: Request.body
|
||||
|
||||
|
|
@ -193,6 +172,27 @@ Request objects
|
|||
This attribute is read-only. To change the body of a Request use
|
||||
:meth:`replace`.
|
||||
|
||||
.. autoattribute:: callback
|
||||
|
||||
.. autoattribute:: errback
|
||||
|
||||
.. autoattribute:: priority
|
||||
|
||||
.. attribute:: Request.cb_kwargs
|
||||
|
||||
A dictionary that contains arbitrary metadata for this request. Its contents
|
||||
will be passed to the Request's callback as keyword arguments. It is empty
|
||||
for new Requests, which means by default callbacks only get a
|
||||
:class:`~scrapy.http.Response` object as argument.
|
||||
|
||||
This dict is :doc:`shallow copied <library/copy>` when the request is
|
||||
cloned using the ``copy()`` or ``replace()`` methods, and can also be
|
||||
accessed, in your spider, from the ``response.cb_kwargs`` attribute.
|
||||
|
||||
In case of a failure to process the request, this dict can be accessed as
|
||||
``failure.request.cb_kwargs`` in the request's errback. For more information,
|
||||
see :ref:`errback-cb_kwargs`.
|
||||
|
||||
.. attribute:: Request.meta
|
||||
:value: {}
|
||||
|
||||
|
|
@ -236,20 +236,7 @@ Request objects
|
|||
Also mind that the :meth:`copy` and :meth:`replace` request methods
|
||||
:doc:`shallow-copy <library/copy>` request metadata.
|
||||
|
||||
.. attribute:: Request.cb_kwargs
|
||||
|
||||
A dictionary that contains arbitrary metadata for this request. Its contents
|
||||
will be passed to the Request's callback as keyword arguments. It is empty
|
||||
for new Requests, which means by default callbacks only get a :class:`Response`
|
||||
object as argument.
|
||||
|
||||
This dict is :doc:`shallow copied <library/copy>` when the request is
|
||||
cloned using the ``copy()`` or ``replace()`` methods, and can also be
|
||||
accessed, in your spider, from the ``response.cb_kwargs`` attribute.
|
||||
|
||||
In case of a failure to process the request, this dict can be accessed as
|
||||
``failure.request.cb_kwargs`` in the request's errback. For more information,
|
||||
see :ref:`errback-cb_kwargs`.
|
||||
.. autoattribute:: dont_filter
|
||||
|
||||
.. autoattribute:: Request.attributes
|
||||
|
||||
|
|
@ -262,7 +249,7 @@ Request objects
|
|||
|
||||
Return a Request object with the same members, except for those members
|
||||
given new values by whichever keyword arguments are specified. The
|
||||
:attr:`Request.cb_kwargs` and :attr:`Request.meta` attributes are shallow
|
||||
:attr:`~scrapy.Request.cb_kwargs` and :attr:`~scrapy.Request.meta` attributes are shallow
|
||||
copied by default (unless new values are given as arguments). See also
|
||||
:ref:`topics-request-response-ref-request-callback-arguments`.
|
||||
|
||||
|
|
@ -305,7 +292,7 @@ Example:
|
|||
In some cases you may be interested in passing arguments to those callback
|
||||
functions so you can receive the arguments later, in the second callback.
|
||||
The following example shows how to achieve this by using the
|
||||
:attr:`Request.cb_kwargs` attribute:
|
||||
:attr:`.Request.cb_kwargs` attribute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -326,10 +313,10 @@ The following example shows how to achieve this by using the
|
|||
foo=foo,
|
||||
)
|
||||
|
||||
.. caution:: :attr:`Request.cb_kwargs` was introduced in version ``1.7``.
|
||||
Prior to that, using :attr:`Request.meta` was recommended for passing
|
||||
information around callbacks. After ``1.7``, :attr:`Request.cb_kwargs`
|
||||
became the preferred way for handling user information, leaving :attr:`Request.meta`
|
||||
.. caution:: :attr:`.Request.cb_kwargs` was introduced in version ``1.7``.
|
||||
Prior to that, using :attr:`.Request.meta` was recommended for passing
|
||||
information around callbacks. After ``1.7``, :attr:`.Request.cb_kwargs`
|
||||
became the preferred way for handling user information, leaving :attr:`.Request.meta`
|
||||
for communication with components like middlewares and extensions.
|
||||
|
||||
.. _topics-request-response-ref-errbacks:
|
||||
|
|
@ -365,7 +352,7 @@ errors if needed:
|
|||
"https://example.invalid/", # DNS error expected
|
||||
]
|
||||
|
||||
def start_requests(self):
|
||||
async def start(self):
|
||||
for u in self.start_urls:
|
||||
yield scrapy.Request(
|
||||
u,
|
||||
|
|
@ -441,7 +428,7 @@ Request fingerprints
|
|||
There are some aspects of scraping, such as filtering out duplicate requests
|
||||
(see :setting:`DUPEFILTER_CLASS`) or caching responses (see
|
||||
:setting:`HTTPCACHE_POLICY`), where you need the ability to generate a short,
|
||||
unique identifier from a :class:`~scrapy.http.Request` object: a request
|
||||
unique identifier from a :class:`~scrapy.Request` object: a request
|
||||
fingerprint.
|
||||
|
||||
You often do not need to worry about request fingerprints, the default request
|
||||
|
|
@ -475,42 +462,17 @@ import path.
|
|||
Writing your own request fingerprinter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A request fingerprinter is a class that must implement the following method:
|
||||
A request fingerprinter is a :ref:`component <topics-components>` that must
|
||||
implement the following method:
|
||||
|
||||
.. currentmodule:: None
|
||||
|
||||
.. method:: fingerprint(self, request)
|
||||
.. method:: fingerprint(self, request: scrapy.Request)
|
||||
|
||||
Return a :class:`bytes` object that uniquely identifies *request*.
|
||||
|
||||
See also :ref:`request-fingerprint-restrictions`.
|
||||
|
||||
:param request: request to fingerprint
|
||||
:type request: scrapy.http.Request
|
||||
|
||||
Additionally, it may also implement the following methods:
|
||||
|
||||
.. classmethod:: from_crawler(cls, crawler)
|
||||
:noindex:
|
||||
|
||||
If present, this class method is called to create a request fingerprinter
|
||||
instance from a :class:`~scrapy.crawler.Crawler` object. It must return a
|
||||
new instance of the request fingerprinter.
|
||||
|
||||
*crawler* provides access to all Scrapy core components like settings and
|
||||
signals; it is a way for the request fingerprinter to access them and hook
|
||||
its functionality into Scrapy.
|
||||
|
||||
:param crawler: crawler that uses this request fingerprinter
|
||||
:type crawler: :class:`~scrapy.crawler.Crawler` object
|
||||
|
||||
.. classmethod:: from_settings(cls, settings)
|
||||
|
||||
If present, and ``from_crawler`` is not defined, this class method is called
|
||||
to create a request fingerprinter instance from a
|
||||
:class:`~scrapy.settings.Settings` object. It must return a new instance of
|
||||
the request fingerprinter.
|
||||
|
||||
.. currentmodule:: scrapy.http
|
||||
|
||||
The :meth:`fingerprint` method of the default request fingerprinter,
|
||||
|
|
@ -573,7 +535,7 @@ URL canonicalization or taking the request method or body into account:
|
|||
|
||||
If you need to be able to override the request fingerprinting for arbitrary
|
||||
requests from your spider callbacks, you may implement a request fingerprinter
|
||||
that reads fingerprints from :attr:`request.meta <scrapy.http.Request.meta>`
|
||||
that reads fingerprints from :attr:`request.meta <scrapy.Request.meta>`
|
||||
when available, and then falls back to
|
||||
:func:`scrapy.utils.request.fingerprint`. For example:
|
||||
|
||||
|
|
@ -588,10 +550,8 @@ when available, and then falls back to
|
|||
return request.meta["fingerprint"]
|
||||
return fingerprint(request)
|
||||
|
||||
If you need to reproduce the same fingerprinting algorithm as Scrapy 2.6
|
||||
without using the deprecated ``'2.6'`` value of the
|
||||
:setting:`REQUEST_FINGERPRINTER_IMPLEMENTATION` setting, use the following
|
||||
request fingerprinter:
|
||||
If you need to reproduce the same fingerprinting algorithm as Scrapy 2.6, use
|
||||
the following request fingerprinter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -635,7 +595,7 @@ The following built-in Scrapy components have such restrictions:
|
|||
:setting:`HTTPCACHE_DIR` also apply. Inside :setting:`HTTPCACHE_DIR`,
|
||||
the following directory structure is created:
|
||||
|
||||
- :attr:`Spider.name <scrapy.spiders.Spider.name>`
|
||||
- :attr:`.Spider.name`
|
||||
|
||||
- first byte of a request fingerprint as hexadecimal
|
||||
|
||||
|
|
@ -663,11 +623,13 @@ The following built-in Scrapy components have such restrictions:
|
|||
Request.meta special keys
|
||||
=========================
|
||||
|
||||
The :attr:`Request.meta` attribute can contain any arbitrary data, but there
|
||||
The :attr:`.Request.meta` attribute can contain any arbitrary data, but there
|
||||
are some special keys recognized by Scrapy and its built-in extensions.
|
||||
|
||||
Those are:
|
||||
|
||||
* :reqmeta:`allow_offsite`
|
||||
* :reqmeta:`autothrottle_dont_adjust_delay`
|
||||
* :reqmeta:`bindaddress`
|
||||
* :reqmeta:`cookiejar`
|
||||
* :reqmeta:`dont_cache`
|
||||
|
|
@ -684,6 +646,7 @@ Those are:
|
|||
* ``ftp_user`` (See :setting:`FTP_USER` for more info)
|
||||
* :reqmeta:`handle_httpstatus_all`
|
||||
* :reqmeta:`handle_httpstatus_list`
|
||||
* :reqmeta:`is_start_request`
|
||||
* :reqmeta:`max_retry_times`
|
||||
* :reqmeta:`proxy`
|
||||
* :reqmeta:`redirect_reasons`
|
||||
|
|
@ -786,24 +749,25 @@ call their callback instead, like in this example, pass ``fail=False`` to the
|
|||
Request subclasses
|
||||
==================
|
||||
|
||||
Here is the list of built-in :class:`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.
|
||||
|
||||
FormRequest objects
|
||||
-------------------
|
||||
|
||||
The FormRequest class extends the base :class:`Request` with functionality for
|
||||
The FormRequest class extends the base :class:`~scrapy.Request` with functionality for
|
||||
dealing with HTML forms. It uses `lxml.html forms`_ to pre-populate form
|
||||
fields with form data from :class:`Response` objects.
|
||||
|
||||
.. _lxml.html forms: https://lxml.de/lxmlhtml.html#forms
|
||||
|
||||
.. class:: scrapy.http.request.form.FormRequest
|
||||
.. class:: scrapy.http.FormRequest
|
||||
.. class:: scrapy.FormRequest(url, [formdata, ...])
|
||||
.. currentmodule:: None
|
||||
|
||||
The :class:`FormRequest` class adds a new keyword parameter to the ``__init__`` method. The
|
||||
remaining arguments are the same as for the :class:`Request` class and are
|
||||
.. class:: scrapy.FormRequest(url, [formdata, ...])
|
||||
:canonical: scrapy.http.request.form.FormRequest
|
||||
|
||||
The :class:`~scrapy.FormRequest` class adds a new keyword parameter to the ``__init__()`` method. The
|
||||
remaining arguments are the same as for the :class:`~scrapy.Request` class and are
|
||||
not documented here.
|
||||
|
||||
:param formdata: is a dictionary (or iterable of (key, value) tuples)
|
||||
|
|
@ -811,12 +775,12 @@ fields with form data from :class:`Response` objects.
|
|||
body of the request.
|
||||
:type formdata: dict or collections.abc.Iterable
|
||||
|
||||
The :class:`FormRequest` objects support the following class method in
|
||||
addition to the standard :class:`Request` methods:
|
||||
The :class:`~scrapy.FormRequest` objects support the following class method in
|
||||
addition to the standard :class:`~scrapy.Request` methods:
|
||||
|
||||
.. classmethod:: FormRequest.from_response(response, [formname=None, formid=None, formnumber=0, formdata=None, formxpath=None, formcss=None, clickdata=None, dont_click=False, ...])
|
||||
.. classmethod:: from_response(response, [formname=None, formid=None, formnumber=0, formdata=None, formxpath=None, formcss=None, clickdata=None, dont_click=False, ...])
|
||||
|
||||
Returns a new :class:`FormRequest` object with its form field values
|
||||
Returns a new :class:`~scrapy.FormRequest` object with its form field values
|
||||
pre-populated with those found in the HTML ``<form>`` element contained
|
||||
in the given response. For an example see
|
||||
:ref:`topics-request-response-ref-request-userlogin`.
|
||||
|
|
@ -838,7 +802,7 @@ fields with form data from :class:`Response` objects.
|
|||
|
||||
:param response: the response containing a HTML form which will be used
|
||||
to pre-populate the form fields
|
||||
:type response: :class:`Response` object
|
||||
:type response: :class:`~scrapy.http.Response` object
|
||||
|
||||
:param formname: if given, the form with name attribute set to this value will be used.
|
||||
:type formname: str
|
||||
|
|
@ -875,7 +839,9 @@ fields with form data from :class:`Response` objects.
|
|||
:type dont_click: bool
|
||||
|
||||
The other parameters of this class method are passed directly to the
|
||||
:class:`FormRequest` ``__init__`` method.
|
||||
:class:`~scrapy.FormRequest` ``__init__()`` method.
|
||||
|
||||
.. currentmodule:: scrapy.http
|
||||
|
||||
Request usage examples
|
||||
----------------------
|
||||
|
|
@ -884,7 +850,7 @@ Using FormRequest to send data via HTTP POST
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to simulate a HTML Form POST in your spider and send a couple of
|
||||
key-value fields, you can return a :class:`FormRequest` object (from your
|
||||
key-value fields, you can return a :class:`~scrapy.FormRequest` object (from your
|
||||
spider) like this:
|
||||
|
||||
.. skip: next
|
||||
|
|
@ -907,7 +873,7 @@ It is usual for web sites to provide pre-populated form fields through ``<input
|
|||
type="hidden">`` elements, such as session related data or authentication
|
||||
tokens (for login pages). When scraping, you'll want these fields to be
|
||||
automatically pre-populated and only override a couple of them, such as the
|
||||
user name and password. You can use the :meth:`FormRequest.from_response`
|
||||
user name and password. You can use the :meth:`.FormRequest.from_response`
|
||||
method for this job. Here's an example spider which uses it:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -942,21 +908,22 @@ method for this job. Here's an example spider which uses it:
|
|||
JsonRequest
|
||||
-----------
|
||||
|
||||
The JsonRequest class extends the base :class:`Request` class with functionality for
|
||||
The JsonRequest class extends the base :class:`~scrapy.Request` class with functionality for
|
||||
dealing with JSON requests.
|
||||
|
||||
.. class:: JsonRequest(url, [... data, dumps_kwargs])
|
||||
|
||||
The :class:`JsonRequest` class adds two new keyword parameters to the ``__init__`` method. The
|
||||
remaining arguments are the same as for the :class:`Request` class and are
|
||||
The :class:`JsonRequest` class adds two new keyword parameters to the ``__init__()`` method. The
|
||||
remaining arguments are the same as for the :class:`~scrapy.Request` class and are
|
||||
not documented here.
|
||||
|
||||
Using the :class:`JsonRequest` will set the ``Content-Type`` header to ``application/json``
|
||||
and ``Accept`` header to ``application/json, text/javascript, */*; q=0.01``
|
||||
|
||||
:param data: is any JSON serializable object that needs to be JSON encoded and assigned to body.
|
||||
if :attr:`Request.body` argument is provided this parameter will be ignored.
|
||||
if :attr:`Request.body` argument is not provided and data argument is provided :attr:`Request.method` will be
|
||||
If the :attr:`~scrapy.Request.body` argument is provided this parameter will be ignored.
|
||||
If the :attr:`~scrapy.Request.body` argument is not provided and the
|
||||
``data`` argument is provided the :attr:`~scrapy.Request.method` will be
|
||||
set to ``'POST'`` automatically.
|
||||
:type data: object
|
||||
|
||||
|
|
@ -1008,7 +975,7 @@ Response objects
|
|||
:type flags: list
|
||||
|
||||
:param request: the initial value of the :attr:`Response.request` attribute.
|
||||
This represents the :class:`Request` that generated this response.
|
||||
This represents the :class:`~scrapy.Request` that generated this response.
|
||||
:type request: scrapy.Request
|
||||
|
||||
:param certificate: an object representing the server's SSL certificate.
|
||||
|
|
@ -1044,11 +1011,12 @@ Response objects
|
|||
|
||||
.. attribute:: Response.headers
|
||||
|
||||
A dictionary-like object which contains the response headers. Values can
|
||||
be accessed using :meth:`get` to return the first header value with the
|
||||
specified name or :meth:`getlist` to return all header values with the
|
||||
specified name. For example, this call will give you all cookies in the
|
||||
headers::
|
||||
A dictionary-like (:class:`scrapy.http.headers.Headers`) object which contains
|
||||
the response headers. Values can be accessed using
|
||||
:meth:`~scrapy.http.headers.Headers.get` to return the first header value with
|
||||
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 cookies in the headers::
|
||||
|
||||
response.headers.getlist('Set-Cookie')
|
||||
|
||||
|
|
@ -1064,7 +1032,7 @@ Response objects
|
|||
|
||||
.. attribute:: Response.request
|
||||
|
||||
The :class:`Request` object that generated this response. This attribute is
|
||||
The :class:`~scrapy.Request` object that generated this response. This attribute is
|
||||
assigned in the Scrapy engine, after the response and the request have passed
|
||||
through all :ref:`Downloader Middlewares <topics-downloader-middleware>`.
|
||||
In particular, this means that:
|
||||
|
|
@ -1083,34 +1051,33 @@ Response objects
|
|||
|
||||
.. attribute:: Response.meta
|
||||
|
||||
A shortcut to the :attr:`Request.meta` attribute of the
|
||||
A shortcut to the :attr:`~scrapy.Request.meta` attribute of the
|
||||
:attr:`Response.request` object (i.e. ``self.request.meta``).
|
||||
|
||||
Unlike the :attr:`Response.request` attribute, the :attr:`Response.meta`
|
||||
attribute is propagated along redirects and retries, so you will get
|
||||
the original :attr:`Request.meta` sent from your spider.
|
||||
the original :attr:`.Request.meta` sent from your spider.
|
||||
|
||||
.. seealso:: :attr:`Request.meta` attribute
|
||||
.. seealso:: :attr:`.Request.meta` attribute
|
||||
|
||||
.. attribute:: Response.cb_kwargs
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
A shortcut to the :attr:`Request.cb_kwargs` attribute of the
|
||||
A shortcut to the :attr:`~scrapy.Request.cb_kwargs` attribute of the
|
||||
:attr:`Response.request` object (i.e. ``self.request.cb_kwargs``).
|
||||
|
||||
Unlike the :attr:`Response.request` attribute, the
|
||||
:attr:`Response.cb_kwargs` attribute is propagated along redirects and
|
||||
retries, so you will get the original :attr:`Request.cb_kwargs` sent
|
||||
from your spider.
|
||||
retries, so you will get the original :attr:`.Request.cb_kwargs` sent from your spider.
|
||||
|
||||
.. seealso:: :attr:`Request.cb_kwargs` attribute
|
||||
.. seealso:: :attr:`.Request.cb_kwargs` attribute
|
||||
|
||||
.. attribute:: Response.flags
|
||||
|
||||
A list that contains flags for this response. Flags are labels used for
|
||||
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.
|
||||
|
||||
.. attribute:: Response.certificate
|
||||
|
|
@ -1187,7 +1154,7 @@ TextResponse objects
|
|||
:class:`Response` class, which is meant to be used only for binary data,
|
||||
such as images, sounds or any media file.
|
||||
|
||||
:class:`TextResponse` objects support a new ``__init__`` method argument, in
|
||||
:class:`TextResponse` objects support a new ``__init__()`` method argument, in
|
||||
addition to the base :class:`Response` objects. The remaining functionality
|
||||
is the same as for the :class:`Response` class and is not documented here.
|
||||
|
||||
|
|
@ -1225,7 +1192,7 @@ TextResponse objects
|
|||
A string with the encoding of this response. The encoding is resolved by
|
||||
trying the following mechanisms, in order:
|
||||
|
||||
1. the encoding passed in the ``__init__`` method ``encoding`` argument
|
||||
1. the encoding passed in the ``__init__()`` method ``encoding`` argument
|
||||
|
||||
2. the encoding declared in the Content-Type HTTP header. If this
|
||||
encoding is not valid (i.e. unknown), it is ignored and the next
|
||||
|
|
@ -1279,7 +1246,7 @@ TextResponse objects
|
|||
|
||||
Constructs an absolute url by combining the Response's base url with
|
||||
a possible relative url. The base url shall be extracted from the
|
||||
``<base>`` tag, or just the Response's :attr:`url` if there is no such
|
||||
``<base>`` tag, or just :attr:`Response.url` if there is no such
|
||||
tag.
|
||||
|
||||
|
||||
|
|
@ -1311,7 +1278,7 @@ JsonResponse objects
|
|||
|
||||
.. class:: JsonResponse(url[, ...])
|
||||
|
||||
The :class:`JsonResponse` class is a subclass of :class:`TextResponse`
|
||||
that is used when the response has a `JSON MIME type
|
||||
<https://mimesniff.spec.whatwg.org/#json-mime-type>`_ in its `Content-Type`
|
||||
The :class:`JsonResponse` class is a subclass of :class:`TextResponse`
|
||||
that is used when the response has a `JSON MIME type
|
||||
<https://mimesniff.spec.whatwg.org/#json-mime-type>`_ in its `Content-Type`
|
||||
header.
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ Minimal scheduler interface
|
|||
:members:
|
||||
|
||||
|
||||
Default Scrapy scheduler
|
||||
========================
|
||||
Default scheduler
|
||||
=================
|
||||
|
||||
.. autoclass:: Scheduler
|
||||
.. autoclass:: Scheduler()
|
||||
:members:
|
||||
:special-members: __len__
|
||||
:special-members: __init__, __len__
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ Examples:
|
|||
|
||||
* ``*::text`` selects all descendant text nodes of the current selector context:
|
||||
|
||||
..skip: next
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> response.css("#images *::text").getall()
|
||||
|
|
@ -559,7 +560,7 @@ For example, suppose you want to extract all ``<p>`` elements inside ``<div>``
|
|||
elements. First, you would get all ``<div>`` elements:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
|
||||
>>> divs = response.xpath("//div")
|
||||
|
||||
At first, you may be tempted to use the following approach, which is wrong, as
|
||||
|
|
@ -591,7 +592,7 @@ Another common case would be to extract all direct ``<p>`` children:
|
|||
For more details about relative XPaths see the `Location Paths`_ section in the
|
||||
XPath specification.
|
||||
|
||||
.. _Location Paths: https://www.w3.org/TR/xpath/all/#location-paths
|
||||
.. _Location Paths: https://www.w3.org/TR/xpath-10/#location-paths
|
||||
|
||||
When querying by class, consider using CSS
|
||||
------------------------------------------
|
||||
|
|
@ -610,7 +611,7 @@ As it turns out, Scrapy selectors allow you to chain selectors, so most of the t
|
|||
you can just select by class using CSS and then switch to XPath when needed:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
|
||||
>>> from scrapy import Selector
|
||||
>>> sel = Selector(
|
||||
... text='<div class="hero shout"><time datetime="2014-07-23 19:00">Special date</time></div>'
|
||||
|
|
@ -727,7 +728,7 @@ But using the ``.`` to mean the node, works:
|
|||
>>> sel.xpath("//a[contains(., 'Next Page')]").getall()
|
||||
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
|
||||
|
||||
.. _`XPath string function`: https://www.w3.org/TR/xpath/all/#section-String-Functions
|
||||
.. _`XPath string function`: https://www.w3.org/TR/xpath-10/#section-String-Functions
|
||||
|
||||
.. _topics-selectors-xpath-variables:
|
||||
|
||||
|
|
@ -777,7 +778,7 @@ Removing namespaces
|
|||
When dealing with scraping projects, it is often quite convenient to get rid of
|
||||
namespaces altogether and just work with element names, to write more
|
||||
simple/convenient XPaths. You can use the
|
||||
:meth:`Selector.remove_namespaces` method for that.
|
||||
:meth:`.Selector.remove_namespaces` method for that.
|
||||
|
||||
Let's show an example that illustrates this with the Python Insider blog atom feed.
|
||||
|
||||
|
|
@ -801,8 +802,8 @@ This is how the file starts::
|
|||
...
|
||||
|
||||
You can see several namespace declarations including a default
|
||||
"http://www.w3.org/2005/Atom" and another one using the "gd:" prefix for
|
||||
"http://schemas.google.com/g/2005".
|
||||
``"http://www.w3.org/2005/Atom"`` and another one using the ``gd:`` prefix for
|
||||
``"http://schemas.google.com/g/2005"``.
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
|
|
@ -814,7 +815,7 @@ doesn't work (because the Atom XML namespace is obfuscating those nodes):
|
|||
>>> response.xpath("//link")
|
||||
[]
|
||||
|
||||
But once we call the :meth:`Selector.remove_namespaces` method, all
|
||||
But once we call the :meth:`.Selector.remove_namespaces` method, all
|
||||
nodes can be accessed directly by their names:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
|
@ -878,7 +879,7 @@ Example selecting links in list item with a "class" attribute ending with a digi
|
|||
>>> sel = Selector(text=doc, type="html")
|
||||
>>> sel.xpath("//li//@href").getall()
|
||||
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
|
||||
>>> sel.xpath('//li[re:test(@class, "item-\d$")]//@href').getall()
|
||||
>>> sel.xpath(r'//li[re:test(@class, "item-\d$")]//@href').getall()
|
||||
['link1.html', 'link2.html', 'link4.html', 'link5.html']
|
||||
|
||||
.. warning:: C library ``libxslt`` doesn't natively support EXSLT regular
|
||||
|
|
@ -1032,7 +1033,7 @@ whereas the CSS lookup is translated into XPath and thus runs more efficiently,
|
|||
so performance-wise its uses are limited to situations that are not easily
|
||||
described with CSS selectors.
|
||||
|
||||
Parsel also simplifies adding your own XPath extensions with
|
||||
Parsel also simplifies adding your own XPath extensions with
|
||||
:func:`~parsel.xpathfuncs.set_xpathfunc`.
|
||||
|
||||
.. _topics-selectors-ref:
|
||||
|
|
@ -1046,7 +1047,7 @@ Built-in Selectors reference
|
|||
Selector objects
|
||||
----------------
|
||||
|
||||
.. autoclass:: Selector
|
||||
.. autoclass:: scrapy.Selector
|
||||
|
||||
.. automethod:: xpath
|
||||
|
||||
|
|
@ -1126,8 +1127,8 @@ Examples
|
|||
Selector examples on HTML response
|
||||
----------------------------------
|
||||
|
||||
Here are some :class:`Selector` examples to illustrate several concepts.
|
||||
In all cases, we assume there is already a :class:`Selector` instantiated with
|
||||
Here are some :class:`~scrapy.Selector` examples to illustrate several concepts.
|
||||
In all cases, we assume there is already a :class:`~scrapy.Selector` instantiated with
|
||||
a :class:`~scrapy.http.HtmlResponse` object like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -1135,7 +1136,7 @@ a :class:`~scrapy.http.HtmlResponse` object like this:
|
|||
sel = Selector(html_response)
|
||||
|
||||
1. Select all ``<h1>`` elements from an HTML response body, returning a list of
|
||||
:class:`Selector` objects (i.e. a :class:`SelectorList` object):
|
||||
:class:`~scrapy.Selector` objects (i.e. a :class:`SelectorList` object):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -1165,7 +1166,7 @@ Selector examples on XML response
|
|||
|
||||
.. skip: start
|
||||
|
||||
Here are some examples to illustrate concepts for :class:`Selector` objects
|
||||
Here are some examples to illustrate concepts for :class:`~scrapy.Selector` objects
|
||||
instantiated with an :class:`~scrapy.http.XmlResponse` object:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -1173,7 +1174,7 @@ instantiated with an :class:`~scrapy.http.XmlResponse` object:
|
|||
sel = Selector(xml_response)
|
||||
|
||||
1. Select all ``<product>`` elements from an XML response body, returning a list
|
||||
of :class:`Selector` objects (i.e. a :class:`SelectorList` object):
|
||||
of :class:`~scrapy.Selector` objects (i.e. a :class:`SelectorList` object):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
|
|||
|
|
@ -33,42 +33,48 @@ Python :ref:`import search path <tut-searchpath>`.
|
|||
Populating the settings
|
||||
=======================
|
||||
|
||||
Settings can be populated using different mechanisms, each of which having a
|
||||
different precedence. Here is the list of them in decreasing order of
|
||||
precedence:
|
||||
Settings can be populated using different mechanisms, each of which has a
|
||||
different precedence:
|
||||
|
||||
1. Command line options (most precedence)
|
||||
2. Settings per-spider
|
||||
3. Project settings module
|
||||
4. Settings set by add-ons
|
||||
5. Default settings per-command
|
||||
6. Default global settings (less precedence)
|
||||
1. :ref:`Command-line settings <cli-settings>` (highest precedence)
|
||||
2. :ref:`Spider settings <spider-settings>`
|
||||
3. :ref:`Project settings <project-settings>`
|
||||
4. :ref:`Add-on settings <addon-settings>`
|
||||
5. :ref:`Command-specific default settings <cmd-default-settings>`
|
||||
6. :ref:`Global default settings <default-settings>` (lowest precedence)
|
||||
|
||||
The population of these settings sources is taken care of internally, but a
|
||||
manual handling is possible using API calls. See the
|
||||
:ref:`topics-api-settings` topic for reference.
|
||||
.. _cli-settings:
|
||||
|
||||
These mechanisms are described in more detail below.
|
||||
1. Command-line settings
|
||||
------------------------
|
||||
|
||||
1. Command line options
|
||||
-----------------------
|
||||
Settings set in the command line have the highest precedence, overriding any
|
||||
other settings.
|
||||
|
||||
Arguments provided by the command line are the ones that take most precedence,
|
||||
overriding any other options. You can explicitly override one (or more)
|
||||
settings using the ``-s`` (or ``--set``) command line option.
|
||||
You can explicitly override one or more settings using the ``-s`` (or
|
||||
``--set``) command-line option.
|
||||
|
||||
.. highlight:: sh
|
||||
|
||||
Example::
|
||||
|
||||
scrapy crawl myspider -s LOG_FILE=scrapy.log
|
||||
scrapy crawl myspider -s LOG_LEVEL=INFO -s LOG_FILE=scrapy.log
|
||||
|
||||
2. Settings per-spider
|
||||
----------------------
|
||||
.. _spider-settings:
|
||||
|
||||
Spiders (See the :ref:`topics-spiders` chapter for reference) can define their
|
||||
own settings that will take precedence and override the project ones. One way
|
||||
to do so is by setting their :attr:`~scrapy.Spider.custom_settings` attribute:
|
||||
2. Spider settings
|
||||
------------------
|
||||
|
||||
:ref:`Spiders <topics-spiders>` can define their own settings that will take
|
||||
precedence and override the project ones.
|
||||
|
||||
.. note:: :ref:`Pre-crawler settings <pre-crawler-settings>` cannot be defined
|
||||
per spider, and :ref:`reactor settings <reactor-settings>` should not have
|
||||
a different value per spider when :ref:`running multiple spiders in the
|
||||
same process <run-multiple-spiders>`.
|
||||
|
||||
One way to do so is by setting their :attr:`~scrapy.Spider.custom_settings`
|
||||
attribute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -83,7 +89,7 @@ to do so is by setting their :attr:`~scrapy.Spider.custom_settings` attribute:
|
|||
}
|
||||
|
||||
It's often better to implement :meth:`~scrapy.Spider.update_settings` instead,
|
||||
and settings set there should use the "spider" priority explicitly:
|
||||
and settings set there should use the ``"spider"`` priority explicitly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -121,33 +127,52 @@ arguments <spiderargs>` or other logic:
|
|||
)
|
||||
return spider
|
||||
|
||||
3. Project settings module
|
||||
--------------------------
|
||||
.. _project-settings:
|
||||
|
||||
The project settings module is the standard configuration file for your Scrapy
|
||||
project, it's where most of your custom settings will be populated. For a
|
||||
standard Scrapy project, this means you'll be adding or changing the settings
|
||||
in the ``settings.py`` file created for your project.
|
||||
3. Project settings
|
||||
-------------------
|
||||
|
||||
4. Settings set by add-ons
|
||||
--------------------------
|
||||
Scrapy projects include a settings module, usually a file called
|
||||
``settings.py``, where you should populate most settings that apply to all your
|
||||
spiders.
|
||||
|
||||
.. seealso:: :ref:`topics-settings-module-envvar`
|
||||
|
||||
.. _addon-settings:
|
||||
|
||||
4. Add-on settings
|
||||
------------------
|
||||
|
||||
:ref:`Add-ons <topics-addons>` can modify settings. They should do this with
|
||||
this priority, though this is not enforced.
|
||||
``"addon"`` priority where possible.
|
||||
|
||||
5. Default settings per-command
|
||||
-------------------------------
|
||||
.. _cmd-default-settings:
|
||||
|
||||
Each :doc:`Scrapy tool </topics/commands>` command can have its own default
|
||||
settings, which override the global default settings. Those custom command
|
||||
settings are specified in the ``default_settings`` attribute of the command
|
||||
class.
|
||||
5. Command-specific default settings
|
||||
------------------------------------
|
||||
|
||||
Each :ref:`Scrapy command <topics-commands>` can have its own default settings,
|
||||
which override the :ref:`global default settings <default-settings>`.
|
||||
|
||||
Those command-specific default settings are specified in the
|
||||
``default_settings`` attribute of each command class.
|
||||
|
||||
.. _default-settings:
|
||||
|
||||
6. Default global settings
|
||||
--------------------------
|
||||
|
||||
The global defaults are located in the ``scrapy.settings.default_settings``
|
||||
module and documented in the :ref:`topics-settings-ref` section.
|
||||
The ``scrapy.settings.default_settings`` module defines global default values
|
||||
for some :ref:`built-in settings <topics-settings-ref>`.
|
||||
|
||||
.. note:: :command:`startproject` generates a ``settings.py`` file that sets
|
||||
some settings to different values.
|
||||
|
||||
The reference documentation of settings indicates the default value if one
|
||||
exists. If :command:`startproject` sets a value, that value is documented
|
||||
as default, and the value from ``scrapy.settings.default_settings`` is
|
||||
documented as “fallback”.
|
||||
|
||||
|
||||
Compatibility with pickle
|
||||
=========================
|
||||
|
|
@ -188,7 +213,7 @@ How to access settings
|
|||
|
||||
.. highlight:: python
|
||||
|
||||
In a spider, the settings are available through ``self.settings``:
|
||||
In a spider, settings are available through ``self.settings``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -201,37 +226,137 @@ In a spider, the settings are available through ``self.settings``:
|
|||
|
||||
.. note::
|
||||
The ``settings`` attribute is set in the base Spider class after the spider
|
||||
is initialized. If you want to use the settings before the initialization
|
||||
is initialized. If you want to use settings before the initialization
|
||||
(e.g., in your spider's ``__init__()`` method), you'll need to override the
|
||||
:meth:`~scrapy.Spider.from_crawler` method.
|
||||
|
||||
Settings can be accessed through the :attr:`scrapy.crawler.Crawler.settings`
|
||||
attribute of the Crawler that is passed to ``from_crawler`` method in
|
||||
extensions, middlewares and item pipelines:
|
||||
:ref:`Components <topics-components>` can also :ref:`access settings
|
||||
<component-settings>`.
|
||||
|
||||
The ``settings`` object can be used like a :class:`dict` (e.g.
|
||||
``settings["LOG_ENABLED"]``). However, to support non-string setting values,
|
||||
which may be passed from the command line as strings, it is recommended to use
|
||||
one of the methods provided by the :class:`~scrapy.settings.Settings` API.
|
||||
|
||||
|
||||
.. _component-priority-dictionaries:
|
||||
|
||||
Component priority dictionaries
|
||||
===============================
|
||||
|
||||
A **component priority dictionary** is a :class:`dict` where keys are
|
||||
:ref:`components <topics-components>` and values are component priorities. For
|
||||
example:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"path.to.ComponentA": None,
|
||||
ComponentB: 100,
|
||||
}
|
||||
|
||||
A component can be specified either as a class object or through an import
|
||||
path.
|
||||
|
||||
.. warning:: Component priority dictionaries are regular :class:`dict` objects.
|
||||
Be careful not to define the same component more than once, e.g. with
|
||||
different import path strings or defining both an import path and a
|
||||
:class:`type` object.
|
||||
|
||||
A priority can be an :class:`int` or :data:`None`.
|
||||
|
||||
A component with priority 1 goes *before* a component with priority 2. What
|
||||
going before entails, however, depends on the corresponding setting. For
|
||||
example, in the :setting:`DOWNLOADER_MIDDLEWARES` setting, components have
|
||||
their
|
||||
:meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_request`
|
||||
method executed before that of later components, but have their
|
||||
:meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_response`
|
||||
method executed after that of later components.
|
||||
|
||||
A component with priority :data:`None` is disabled.
|
||||
|
||||
Some component priority dictionaries get merged with some built-in value. For
|
||||
example, :setting:`DOWNLOADER_MIDDLEWARES` is merged with
|
||||
:setting:`DOWNLOADER_MIDDLEWARES_BASE`. This is where :data:`None` comes in
|
||||
handy, allowing you to disable a component from the base setting in the regular
|
||||
setting:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyExtension:
|
||||
def __init__(self, log_is_enabled=False):
|
||||
if log_is_enabled:
|
||||
print("log is enabled!")
|
||||
DOWNLOADER_MIDDLEWARES = {
|
||||
"scrapy.downloadermiddlewares.offsite.OffsiteMiddleware": None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler):
|
||||
settings = crawler.settings
|
||||
return cls(settings.getbool("LOG_ENABLED"))
|
||||
|
||||
The settings object can be used like a dict (e.g.,
|
||||
``settings['LOG_ENABLED']``), but it's usually preferred to extract the setting
|
||||
in the format you need it to avoid type errors, using one of the methods
|
||||
provided by the :class:`~scrapy.settings.Settings` API.
|
||||
Special settings
|
||||
================
|
||||
|
||||
Rationale for setting names
|
||||
===========================
|
||||
The following settings work slightly differently than all other settings.
|
||||
|
||||
Setting names are usually prefixed with the component that they configure. For
|
||||
example, proper setting names for a fictional robots.txt extension would be
|
||||
``ROBOTSTXT_ENABLED``, ``ROBOTSTXT_OBEY``, ``ROBOTSTXT_CACHEDIR``, etc.
|
||||
.. _pre-crawler-settings:
|
||||
|
||||
Pre-crawler settings
|
||||
--------------------
|
||||
|
||||
**Pre-crawler settings** are settings used before the
|
||||
:class:`~scrapy.crawler.Crawler` object is created.
|
||||
|
||||
These settings cannot be :ref:`set from a spider <spider-settings>`.
|
||||
|
||||
These settings are :setting:`SPIDER_LOADER_CLASS` and settings used by the
|
||||
corresponding :ref:`component <topics-components>`, e.g.
|
||||
:setting:`SPIDER_MODULES` and :setting:`SPIDER_LOADER_WARN_ONLY` for the
|
||||
default component.
|
||||
|
||||
|
||||
.. _reactor-settings:
|
||||
|
||||
Reactor settings
|
||||
----------------
|
||||
|
||||
**Reactor settings** are settings tied to the :doc:`Twisted reactor
|
||||
<twisted:core/howto/reactor-basics>`.
|
||||
|
||||
These settings can be defined from a spider. However, because only 1 reactor
|
||||
can be used per process, these settings cannot use a different value per spider
|
||||
when :ref:`running multiple spiders in the same process
|
||||
<run-multiple-spiders>`.
|
||||
|
||||
In general, if different spiders define different values, the first defined
|
||||
value is used. However, if two spiders request a different reactor, an
|
||||
exception is raised.
|
||||
|
||||
These settings are:
|
||||
|
||||
- :setting:`ASYNCIO_EVENT_LOOP` (not possible to set per-spider when using
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess`, see below)
|
||||
|
||||
- :setting:`DNS_RESOLVER` and settings used by the corresponding
|
||||
component, e.g. :setting:`DNSCACHE_ENABLED`, :setting:`DNSCACHE_SIZE`
|
||||
and :setting:`DNS_TIMEOUT` for the default one.
|
||||
|
||||
- :setting:`REACTOR_THREADPOOL_MAXSIZE`
|
||||
|
||||
- :setting:`TWISTED_REACTOR` (ignored when using
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess`, see below)
|
||||
|
||||
:setting:`ASYNCIO_EVENT_LOOP` and :setting:`TWISTED_REACTOR` are used upon
|
||||
installing the reactor. The rest of the settings are applied when starting
|
||||
the reactor.
|
||||
|
||||
There is an additional restriction for :setting:`TWISTED_REACTOR` and
|
||||
:setting:`ASYNCIO_EVENT_LOOP` when using
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess`: when this class is instantiated,
|
||||
it installs :class:`~twisted.internet.asyncioreactor.AsyncioSelectorReactor`,
|
||||
ignoring the value of :setting:`TWISTED_REACTOR` and using the value of
|
||||
:setting:`ASYNCIO_EVENT_LOOP` that was passed to
|
||||
:meth:`AsyncCrawlerProcess.__init__()
|
||||
<scrapy.crawler.AsyncCrawlerProcess.__init__>`. If a different value for
|
||||
:setting:`TWISTED_REACTOR` or :setting:`ASYNCIO_EVENT_LOOP` is provided later,
|
||||
e.g. in :ref:`per-spider settings <spider-settings>`, an exception will be
|
||||
raised.
|
||||
|
||||
|
||||
.. _topics-settings-ref:
|
||||
|
|
@ -288,7 +413,7 @@ The AWS security token used by code that requires access to `Amazon Web services
|
|||
such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`, when using
|
||||
`temporary security credentials`_.
|
||||
|
||||
.. _temporary security credentials: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#temporary-access-keys
|
||||
.. _temporary security credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html
|
||||
|
||||
.. setting:: AWS_ENDPOINT_URL
|
||||
|
||||
|
|
@ -359,7 +484,7 @@ Note that the event loop class must inherit from :class:`asyncio.AbstractEventLo
|
|||
BOT_NAME
|
||||
--------
|
||||
|
||||
Default: ``'scrapybot'``
|
||||
Default: ``<project name>`` (:ref:`fallback <default-settings>`: ``'scrapybot'``)
|
||||
|
||||
The name of the bot implemented by this Scrapy project (also known as the
|
||||
project name). This name will be used for the logging too.
|
||||
|
|
@ -392,7 +517,7 @@ performed by the Scrapy downloader.
|
|||
CONCURRENT_REQUESTS_PER_DOMAIN
|
||||
------------------------------
|
||||
|
||||
Default: ``8``
|
||||
Default: ``1`` (:ref:`fallback <default-settings>`: ``8``)
|
||||
|
||||
The maximum number of concurrent (i.e. simultaneous) requests that will be
|
||||
performed to any single domain.
|
||||
|
|
@ -401,23 +526,38 @@ See also: :ref:`topics-autothrottle` and its
|
|||
:setting:`AUTOTHROTTLE_TARGET_CONCURRENCY` option.
|
||||
|
||||
|
||||
.. setting:: CONCURRENT_REQUESTS_PER_IP
|
||||
.. setting:: DEFAULT_DROPITEM_LOG_LEVEL
|
||||
|
||||
CONCURRENT_REQUESTS_PER_IP
|
||||
DEFAULT_DROPITEM_LOG_LEVEL
|
||||
--------------------------
|
||||
|
||||
Default: ``0``
|
||||
Default: ``"WARNING"``
|
||||
|
||||
The maximum number of concurrent (i.e. simultaneous) requests that will be
|
||||
performed to any single IP. If non-zero, the
|
||||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` setting is ignored, and this one is
|
||||
used instead. In other words, concurrency limits will be applied per IP, not
|
||||
per domain.
|
||||
Default :ref:`log level <levels>` of messages about dropped items.
|
||||
|
||||
This setting also affects :setting:`DOWNLOAD_DELAY` and
|
||||
:ref:`topics-autothrottle`: if :setting:`CONCURRENT_REQUESTS_PER_IP`
|
||||
is non-zero, download delay is enforced per IP, not per domain.
|
||||
When an item is dropped by raising :exc:`scrapy.exceptions.DropItem` from the
|
||||
:func:`process_item` method of an :ref:`item pipeline <topics-item-pipeline>`,
|
||||
a message is logged, and by default its log level is the one configured in this
|
||||
setting.
|
||||
|
||||
You may specify this log level as an integer (e.g. ``20``), as a log level
|
||||
constant (e.g. ``logging.INFO``) or as a string with the name of a log level
|
||||
constant (e.g. ``"INFO"``).
|
||||
|
||||
When writing an item pipeline, you can force a different log level by setting
|
||||
:attr:`scrapy.exceptions.DropItem.log_level` in your
|
||||
:exc:`scrapy.exceptions.DropItem` exception. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy.exceptions import DropItem
|
||||
|
||||
|
||||
class MyPipeline:
|
||||
def process_item(self, item):
|
||||
if not item.get("price"):
|
||||
raise DropItem("Missing price data", log_level="INFO")
|
||||
return item
|
||||
|
||||
.. setting:: DEFAULT_ITEM_CLASS
|
||||
|
||||
|
|
@ -567,7 +707,8 @@ connections (for ``HTTP10DownloadHandler``).
|
|||
|
||||
.. note::
|
||||
|
||||
HTTP/1.0 is rarely used nowadays so you can safely ignore this setting,
|
||||
HTTP/1.0 is rarely used nowadays and its Scrapy support is deprecated,
|
||||
so you can safely ignore this setting,
|
||||
unless you really want to use HTTP/1.0 and override
|
||||
:setting:`DOWNLOAD_HANDLERS` for ``http(s)`` scheme accordingly,
|
||||
i.e. to ``'scrapy.core.downloader.handlers.http.HTTP10DownloadHandler'``.
|
||||
|
|
@ -617,7 +758,7 @@ 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
|
||||
specific cipher that is not included in ``DEFAULT`` if a website requires it.
|
||||
|
||||
.. _OpenSSL cipher list format: https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT
|
||||
.. _OpenSSL cipher list format: https://docs.openssl.org/master/man1/openssl-ciphers/#cipher-list-format
|
||||
|
||||
.. setting:: DOWNLOADER_CLIENT_TLS_METHOD
|
||||
|
||||
|
|
@ -711,7 +852,7 @@ Whether to enable downloader stats collection.
|
|||
DOWNLOAD_DELAY
|
||||
--------------
|
||||
|
||||
Default: ``0``
|
||||
Default: ``1`` (:ref:`fallback <default-settings>`: ``0``)
|
||||
|
||||
Minimum seconds to wait between 2 consecutive requests to the same domain.
|
||||
|
||||
|
|
@ -726,9 +867,6 @@ every 10 seconds::
|
|||
This setting is also affected by the :setting:`RANDOMIZE_DOWNLOAD_DELAY`
|
||||
setting, which is enabled by default.
|
||||
|
||||
When :setting:`CONCURRENT_REQUESTS_PER_IP` is non-zero, delays are enforced
|
||||
per IP address instead of per domain.
|
||||
|
||||
Note that :setting:`DOWNLOAD_DELAY` can lower the effective per-domain
|
||||
concurrency below :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`. If the response
|
||||
time of a domain is lower than :setting:`DOWNLOAD_DELAY`, the effective
|
||||
|
|
@ -829,9 +967,9 @@ The default HTTPS handler uses HTTP/1.1. To use HTTP/2:
|
|||
- No support for the :signal:`bytes_received` and
|
||||
:signal:`headers_received` signals.
|
||||
|
||||
.. _frame size: https://tools.ietf.org/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
|
||||
.. _server pushes: https://tools.ietf.org/html/rfc7540#section-8.2
|
||||
.. _server pushes: https://datatracker.ietf.org/doc/html/rfc7540#section-8.2
|
||||
|
||||
.. setting:: DOWNLOAD_SLOTS
|
||||
|
||||
|
|
@ -845,12 +983,7 @@ Allows to define concurrency/delay parameters on per slot (domain) basis:
|
|||
.. code-block:: python
|
||||
|
||||
DOWNLOAD_SLOTS = {
|
||||
"quotes.toscrape.com": {
|
||||
"concurrency": 1,
|
||||
"delay": 2,
|
||||
"randomize_delay": False,
|
||||
"throttle": False,
|
||||
},
|
||||
"quotes.toscrape.com": {"concurrency": 1, "delay": 2, "randomize_delay": False},
|
||||
"books.toscrape.com": {"delay": 3, "randomize_delay": False},
|
||||
}
|
||||
|
||||
|
|
@ -862,9 +995,6 @@ Allows to define concurrency/delay parameters on per slot (domain) basis:
|
|||
- :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`: ``concurrency``
|
||||
- :setting:`RANDOMIZE_DOWNLOAD_DELAY`: ``randomize_delay``
|
||||
|
||||
There is no global setting for ``throttle``, whose default value is
|
||||
``None``.
|
||||
|
||||
|
||||
.. setting:: DOWNLOAD_TIMEOUT
|
||||
|
||||
|
|
@ -963,15 +1093,79 @@ Default: ``'scrapy.dupefilters.RFPDupeFilter'``
|
|||
|
||||
The class used to detect and filter duplicate requests.
|
||||
|
||||
The default (``RFPDupeFilter``) filters based on the
|
||||
The default, :class:`~scrapy.dupefilters.RFPDupeFilter`, filters based on the
|
||||
:setting:`REQUEST_FINGERPRINTER_CLASS` setting.
|
||||
|
||||
You can disable filtering of duplicate requests by setting
|
||||
:setting:`DUPEFILTER_CLASS` to ``'scrapy.dupefilters.BaseDupeFilter'``.
|
||||
Be very careful about this however, because you can get into crawling loops.
|
||||
It's usually a better idea to set the ``dont_filter`` parameter to
|
||||
``True`` on the specific :class:`~scrapy.Request` that should not be
|
||||
filtered.
|
||||
To change how duplicates are checked, you can point :setting:`DUPEFILTER_CLASS`
|
||||
to a custom subclass of :class:`~scrapy.dupefilters.RFPDupeFilter` that
|
||||
overrides its ``__init__`` method to use a :ref:`different request
|
||||
fingerprinting class <custom-request-fingerprinter>`. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy.dupefilters import RFPDupeFilter
|
||||
from scrapy.utils.request import fingerprint
|
||||
|
||||
|
||||
class CustomRequestFingerprinter:
|
||||
def fingerprint(self, request):
|
||||
return fingerprint(request, include_headers=["X-ID"])
|
||||
|
||||
|
||||
class CustomDupeFilter(RFPDupeFilter):
|
||||
|
||||
def __init__(self, path=None, debug=False, *, fingerprinter=None):
|
||||
super().__init__(
|
||||
path=path, debug=debug, fingerprinter=CustomRequestFingerprinter()
|
||||
)
|
||||
|
||||
To disable duplicate request filtering set :setting:`DUPEFILTER_CLASS` to
|
||||
``'scrapy.dupefilters.BaseDupeFilter'``. Note that not filtering out duplicate
|
||||
requests may cause crawling loops. It is usually better to set
|
||||
the ``dont_filter`` parameter to ``True`` on the ``__init__`` method of a
|
||||
specific :class:`~scrapy.Request` object that should not be filtered out.
|
||||
|
||||
A class assigned to :setting:`DUPEFILTER_CLASS` must implement the following
|
||||
interface::
|
||||
|
||||
class MyDupeFilter:
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings):
|
||||
"""Returns an instance of this duplicate request filtering class
|
||||
based on the current crawl settings."""
|
||||
return cls()
|
||||
|
||||
def request_seen(self, request):
|
||||
"""Returns ``True`` if *request* is a duplicate of another request
|
||||
seen in a previous call to :meth:`request_seen`, or ``False``
|
||||
otherwise."""
|
||||
return False
|
||||
|
||||
def open(self):
|
||||
"""Called before the spider opens. It may return a deferred."""
|
||||
pass
|
||||
|
||||
def close(self, reason):
|
||||
"""Called before the spider closes. It may return a deferred."""
|
||||
pass
|
||||
|
||||
def log(self, request, spider):
|
||||
"""Logs that a request has been filtered out.
|
||||
|
||||
It is called right after a call to :meth:`request_seen` that
|
||||
returns ``True``.
|
||||
|
||||
If :meth:`request_seen` always returns ``False``, such as in the
|
||||
case of :class:`~scrapy.dupefilters.BaseDupeFilter`, this method
|
||||
may be omitted.
|
||||
"""
|
||||
pass
|
||||
|
||||
.. autoclass:: scrapy.dupefilters.BaseDupeFilter
|
||||
|
||||
.. autoclass:: scrapy.dupefilters.RFPDupeFilter
|
||||
|
||||
|
||||
.. setting:: DUPEFILTER_DEBUG
|
||||
|
||||
|
|
@ -1001,7 +1195,8 @@ EXTENSIONS
|
|||
|
||||
Default:: ``{}``
|
||||
|
||||
A dict containing the extensions enabled in your project, and their orders.
|
||||
:ref:`Component priority dictionary <component-priority-dictionaries>` of
|
||||
enabled extensions. See :ref:`topics-extensions`.
|
||||
|
||||
.. setting:: EXTENSIONS_BASE
|
||||
|
||||
|
|
@ -1048,6 +1243,26 @@ FEED_STORAGE_GCS_ACL
|
|||
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://cloud.google.com/storage/docs/access-control/lists>`_.
|
||||
|
||||
.. setting:: FORCE_CRAWLER_PROCESS
|
||||
|
||||
FORCE_CRAWLER_PROCESS
|
||||
---------------------
|
||||
|
||||
Default: ``False``
|
||||
|
||||
If ``False``, :ref:`Scrapy commands that need a CrawlerProcess
|
||||
<topics-commands-crawlerprocess>` will decide between using
|
||||
:class:`scrapy.crawler.AsyncCrawlerProcess` and
|
||||
:class:`scrapy.crawler.CrawlerProcess` based on the value of the
|
||||
:setting:`TWISTED_REACTOR` setting, but ignoring its value in :ref:`per-spider
|
||||
settings <spider-settings>`.
|
||||
|
||||
If ``True``, these commands will always use
|
||||
:class:`~scrapy.crawler.CrawlerProcess`.
|
||||
|
||||
Set this to ``True`` if you want to set :setting:`TWISTED_REACTOR` to a
|
||||
non-default value in :ref:`per-spider settings <spider-settings>`.
|
||||
|
||||
.. setting:: FTP_PASSIVE_MODE
|
||||
|
||||
FTP_PASSIVE_MODE
|
||||
|
|
@ -1074,7 +1289,7 @@ in ``Request`` meta.
|
|||
some FTP servers explicitly ask for the user's e-mail address
|
||||
and will not allow login with the "guest" password.
|
||||
|
||||
.. _RFC 1635: https://tools.ietf.org/html/rfc1635
|
||||
.. _RFC 1635: https://datatracker.ietf.org/doc/html/rfc1635
|
||||
|
||||
.. reqmeta:: ftp_user
|
||||
.. setting:: FTP_USER
|
||||
|
|
@ -1126,6 +1341,7 @@ Default: ``{}``
|
|||
A dict containing the pipelines enabled by default in Scrapy. You should never
|
||||
modify this setting in your project, modify :setting:`ITEM_PIPELINES` instead.
|
||||
|
||||
|
||||
.. setting:: JOBDIR
|
||||
|
||||
JOBDIR
|
||||
|
|
@ -1136,6 +1352,7 @@ Default: ``None``
|
|||
A string indicating the directory for storing the state of a crawl when
|
||||
:ref:`pausing and resuming crawls <topics-jobs>`.
|
||||
|
||||
|
||||
.. setting:: LOG_ENABLED
|
||||
|
||||
LOG_ENABLED
|
||||
|
|
@ -1236,6 +1453,25 @@ Default: ``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
|
||||
|
||||
.. setting:: LOG_VERSIONS
|
||||
|
||||
LOG_VERSIONS
|
||||
------------
|
||||
|
||||
Default: ``["lxml", "libxml2", "cssselect", "parsel", "w3lib", "Twisted", "Python", "pyOpenSSL", "cryptography", "Platform"]``
|
||||
|
||||
Logs the installed versions of the specified items.
|
||||
|
||||
An item can be any installed Python package.
|
||||
|
||||
The following special items are also supported:
|
||||
|
||||
- ``libxml2``
|
||||
|
||||
- ``Platform`` (:func:`platform.platform`)
|
||||
|
||||
- ``Python``
|
||||
|
||||
.. setting:: LOGSTATS_INTERVAL
|
||||
|
||||
LOGSTATS_INTERVAL
|
||||
|
|
@ -1353,7 +1589,7 @@ email notifying about it. If zero, no warning will be produced.
|
|||
NEWSPIDER_MODULE
|
||||
----------------
|
||||
|
||||
Default: ``''``
|
||||
Default: ``"<project name>.spiders"`` (:ref:`fallback <default-settings>`: ``""``)
|
||||
|
||||
Module where to create new spiders using the :command:`genspider` command.
|
||||
|
||||
|
|
@ -1412,9 +1648,7 @@ Adjust redirect request priority relative to original request:
|
|||
ROBOTSTXT_OBEY
|
||||
--------------
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Scope: ``scrapy.downloadermiddlewares.robotstxt``
|
||||
Default: ``True`` (:ref:`fallback <default-settings>`: ``False``)
|
||||
|
||||
If enabled, Scrapy will respect robots.txt policies. For more information see
|
||||
:ref:`topics-dlmw-robots`.
|
||||
|
|
@ -1483,31 +1717,80 @@ SCHEDULER_DISK_QUEUE
|
|||
|
||||
Default: ``'scrapy.squeues.PickleLifoDiskQueue'``
|
||||
|
||||
Type of disk queue that will be used by scheduler. Other available types are
|
||||
``scrapy.squeues.PickleFifoDiskQueue``, ``scrapy.squeues.MarshalFifoDiskQueue``,
|
||||
Type of disk queue that will be used by the scheduler. Other available types
|
||||
are ``scrapy.squeues.PickleFifoDiskQueue``,
|
||||
``scrapy.squeues.MarshalFifoDiskQueue``,
|
||||
``scrapy.squeues.MarshalLifoDiskQueue``.
|
||||
|
||||
|
||||
.. setting:: SCHEDULER_MEMORY_QUEUE
|
||||
|
||||
SCHEDULER_MEMORY_QUEUE
|
||||
----------------------
|
||||
|
||||
Default: ``'scrapy.squeues.LifoMemoryQueue'``
|
||||
|
||||
Type of in-memory queue used by scheduler. Other available type is:
|
||||
Type of in-memory queue used by the scheduler. Other available type is:
|
||||
``scrapy.squeues.FifoMemoryQueue``.
|
||||
|
||||
|
||||
.. setting:: SCHEDULER_PRIORITY_QUEUE
|
||||
|
||||
SCHEDULER_PRIORITY_QUEUE
|
||||
------------------------
|
||||
|
||||
Default: ``'scrapy.pqueues.ScrapyPriorityQueue'``
|
||||
|
||||
Type of priority queue used by the scheduler. Another available type is
|
||||
``scrapy.pqueues.DownloaderAwarePriorityQueue``.
|
||||
``scrapy.pqueues.DownloaderAwarePriorityQueue`` works better than
|
||||
``scrapy.pqueues.ScrapyPriorityQueue`` when you crawl many different
|
||||
domains in parallel. But currently ``scrapy.pqueues.DownloaderAwarePriorityQueue``
|
||||
does not work together with :setting:`CONCURRENT_REQUESTS_PER_IP`.
|
||||
domains in parallel.
|
||||
|
||||
|
||||
.. setting:: SCHEDULER_START_DISK_QUEUE
|
||||
|
||||
SCHEDULER_START_DISK_QUEUE
|
||||
--------------------------
|
||||
|
||||
Default: ``'scrapy.squeues.PickleFifoDiskQueue'``
|
||||
|
||||
Type of disk queue (see :setting:`JOBDIR`) that the :ref:`scheduler
|
||||
<topics-scheduler>` uses for :ref:`start requests <start-requests>`.
|
||||
|
||||
For available choices, see :setting:`SCHEDULER_DISK_QUEUE`.
|
||||
|
||||
.. queue-common-starts
|
||||
|
||||
Use ``None`` or ``""`` to disable these separate queues entirely, and instead
|
||||
have start requests share the same queues as other requests.
|
||||
|
||||
.. note::
|
||||
|
||||
Disabling separate start request queues makes :ref:`start request order
|
||||
<start-request-order>` unintuitive: start requests will be sent in order
|
||||
only until :setting:`CONCURRENT_REQUESTS` is reached, then remaining start
|
||||
requests will be sent in reverse order.
|
||||
|
||||
.. queue-common-ends
|
||||
|
||||
|
||||
.. setting:: SCHEDULER_START_MEMORY_QUEUE
|
||||
|
||||
SCHEDULER_START_MEMORY_QUEUE
|
||||
----------------------------
|
||||
|
||||
Default: ``'scrapy.squeues.FifoMemoryQueue'``
|
||||
|
||||
Type of in-memory queue that the :ref:`scheduler <topics-scheduler>` uses for
|
||||
:ref:`start requests <start-requests>`.
|
||||
|
||||
For available choices, see :setting:`SCHEDULER_MEMORY_QUEUE`.
|
||||
|
||||
.. include:: settings.rst
|
||||
:start-after: queue-common-starts
|
||||
:end-before: queue-common-ends
|
||||
|
||||
|
||||
.. setting:: SCRAPER_SLOT_MAX_ACTIVE_SIZE
|
||||
|
||||
|
|
@ -1584,15 +1867,6 @@ 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
|
||||
warning by setting ``SPIDER_LOADER_WARN_ONLY = True``.
|
||||
|
||||
.. note::
|
||||
Some :ref:`scrapy commands <topics-commands>` run with this setting to ``True``
|
||||
already (i.e. they will only issue a warning and will not fail)
|
||||
since they do not actually need to load spider classes to work:
|
||||
:command:`scrapy runspider <runspider>`,
|
||||
:command:`scrapy settings <settings>`,
|
||||
:command:`scrapy startproject <startproject>`,
|
||||
:command:`scrapy version <version>`.
|
||||
|
||||
.. setting:: SPIDER_MIDDLEWARES
|
||||
|
||||
SPIDER_MIDDLEWARES
|
||||
|
|
@ -1628,7 +1902,7 @@ the spider. For more info see :ref:`topics-spider-middleware-setting`.
|
|||
SPIDER_MODULES
|
||||
--------------
|
||||
|
||||
Default: ``[]``
|
||||
Default: ``["<project name>.spiders"]`` (:ref:`fallback <default-settings>`: ``[]``)
|
||||
|
||||
A list of modules where Scrapy will look for spiders.
|
||||
|
||||
|
|
@ -1701,15 +1975,17 @@ TWISTED_REACTOR
|
|||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Default: ``None``
|
||||
Default: ``"twisted.internet.asyncioreactor.AsyncioSelectorReactor"``
|
||||
|
||||
Import path of a given :mod:`~twisted.internet.reactor`.
|
||||
|
||||
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
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` class or the
|
||||
:class:`~scrapy.crawler.CrawlerProcess` class.
|
||||
|
||||
If you are using the :class:`~scrapy.crawler.CrawlerRunner` class, you also
|
||||
If you are using the :class:`~scrapy.crawler.AsyncCrawlerRunner` class or the
|
||||
:class:`~scrapy.crawler.CrawlerRunner` class, you also
|
||||
need to install the correct reactor manually. You can do that using
|
||||
:func:`~scrapy.utils.reactor.install_reactor`:
|
||||
|
||||
|
|
@ -1718,15 +1994,16 @@ need to install the correct reactor manually. You can do that using
|
|||
If a reactor is already installed,
|
||||
:func:`~scrapy.utils.reactor.install_reactor` has no effect.
|
||||
|
||||
:meth:`CrawlerRunner.__init__ <scrapy.crawler.CrawlerRunner.__init__>` raises
|
||||
:exc:`Exception` if the installed reactor does not match the
|
||||
:class:`~scrapy.crawler.AsyncCrawlerRunner` and other similar classes raise an
|
||||
exception if the installed reactor does not match the
|
||||
:setting:`TWISTED_REACTOR` setting; therefore, having top-level
|
||||
:mod:`~twisted.internet.reactor` imports in project files and imported
|
||||
third-party libraries will make Scrapy raise :exc:`Exception` when
|
||||
it checks which reactor is installed.
|
||||
third-party libraries will make Scrapy raise an exception when it checks which
|
||||
reactor is installed.
|
||||
|
||||
In order to use the reactor installed by Scrapy:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
|
|
@ -1740,7 +2017,7 @@ In order to use the reactor installed by Scrapy:
|
|||
self.timeout = int(kwargs.pop("timeout", "60"))
|
||||
super(QuotesSpider, self).__init__(*args, **kwargs)
|
||||
|
||||
def start_requests(self):
|
||||
async def start(self):
|
||||
reactor.callLater(self.timeout, self.stop)
|
||||
|
||||
urls = ["https://quotes.toscrape.com/page/1"]
|
||||
|
|
@ -1755,7 +2032,7 @@ In order to use the reactor installed by Scrapy:
|
|||
self.crawler.engine.close_spider(self, "timeout")
|
||||
|
||||
|
||||
which raises :exc:`Exception`, becomes:
|
||||
which raises an exception, becomes:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -1769,7 +2046,7 @@ which raises :exc:`Exception`, becomes:
|
|||
self.timeout = int(kwargs.pop("timeout", "60"))
|
||||
super(QuotesSpider, self).__init__(*args, **kwargs)
|
||||
|
||||
def start_requests(self):
|
||||
async def start(self):
|
||||
from twisted.internet import reactor
|
||||
|
||||
reactor.callLater(self.timeout, self.stop)
|
||||
|
|
@ -1786,17 +2063,19 @@ which raises :exc:`Exception`, becomes:
|
|||
self.crawler.engine.close_spider(self, "timeout")
|
||||
|
||||
|
||||
The default value of the :setting:`TWISTED_REACTOR` setting is ``None``, which
|
||||
means that Scrapy will use the existing reactor if one is already installed, or
|
||||
install the default reactor defined by Twisted for the current platform. This
|
||||
is to maintain backward compatibility and avoid possible problems caused by
|
||||
using a non-default reactor.
|
||||
If this setting is set ``None``, Scrapy will use the existing reactor if one is
|
||||
already installed, or install the default reactor defined by Twisted for the
|
||||
current platform.
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
The :command:`startproject` command now sets this setting to
|
||||
``twisted.internet.asyncioreactor.AsyncioSelectorReactor`` in the generated
|
||||
``settings.py`` file.
|
||||
|
||||
.. versionchanged:: 2.13
|
||||
The default value was changed from ``None`` to
|
||||
``"twisted.internet.asyncioreactor.AsyncioSelectorReactor"``.
|
||||
|
||||
For additional information, see :doc:`core/howto/choosing-reactor`.
|
||||
|
||||
|
||||
|
|
@ -1835,6 +2114,21 @@ also used by :class:`~scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware
|
|||
if :setting:`ROBOTSTXT_USER_AGENT` setting is ``None`` and
|
||||
there is no overriding User-Agent header specified for the request.
|
||||
|
||||
.. setting:: WARN_ON_GENERATOR_RETURN_VALUE
|
||||
|
||||
WARN_ON_GENERATOR_RETURN_VALUE
|
||||
------------------------------
|
||||
|
||||
Default: ``True``
|
||||
|
||||
When enabled, Scrapy will warn if generator-based callback methods (like
|
||||
``parse``) contain return statements with non-``None`` values. This helps detect
|
||||
potential mistakes in spider development.
|
||||
|
||||
Disable this setting to prevent syntax errors that may occur when dynamically
|
||||
modifying generator function source code during runtime, skip AST parsing of
|
||||
callback functions, or improve performance in auto-reloading development
|
||||
environments.
|
||||
|
||||
Settings documented elsewhere:
|
||||
------------------------------
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ Those objects are:
|
|||
Example of shell session
|
||||
========================
|
||||
|
||||
.. skip: start
|
||||
|
||||
Here's an example of a typical shell session where we start by scraping the
|
||||
https://scrapy.org page, and then proceed to scrape the https://old.reddit.com/
|
||||
page. Finally, we modify the (Reddit) request method to POST and re-fetch it
|
||||
|
|
@ -232,6 +234,8 @@ After that, we can start playing with the objects:
|
|||
'X-Ua-Compatible': ['IE=edge'],
|
||||
'X-Xss-Protection': ['1; mode=block']}
|
||||
|
||||
.. skip: end
|
||||
|
||||
|
||||
.. _topics-shell-inspect-response:
|
||||
|
||||
|
|
@ -268,6 +272,8 @@ Here's an example of how you would call it from your spider:
|
|||
|
||||
# Rest of parsing code.
|
||||
|
||||
.. skip: start
|
||||
|
||||
When you run the spider, you will get something similar to this::
|
||||
|
||||
2014-01-23 17:48:31-0400 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://example.com> (referer: None)
|
||||
|
|
@ -301,6 +307,8 @@ crawling::
|
|||
2014-01-23 17:50:03-0400 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://example.net> (referer: None)
|
||||
...
|
||||
|
||||
.. skip: end
|
||||
|
||||
Note that you can't use the ``fetch`` shortcut here since the Scrapy engine is
|
||||
blocked by the shell. However, after you leave the shell, the spider will
|
||||
continue crawling where it stopped, as shown above.
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ Here is a simple example showing how you can catch signals and perform some acti
|
|||
|
||||
.. _signal-deferred:
|
||||
|
||||
Deferred signal handlers
|
||||
========================
|
||||
Asynchronous signal handlers
|
||||
============================
|
||||
|
||||
Some signals support returning :class:`~twisted.internet.defer.Deferred`
|
||||
or :term:`awaitable objects <awaitable>` from their handlers, allowing
|
||||
|
|
@ -57,9 +57,11 @@ operation to finish.
|
|||
|
||||
Let's take an example using :ref:`coroutines <topics-coroutines>`:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
import treq
|
||||
|
||||
|
||||
class SignalSpider(scrapy.Spider):
|
||||
|
|
@ -103,6 +105,7 @@ Built-in signals reference
|
|||
|
||||
Here's the list of Scrapy built-in signals and their meaning.
|
||||
|
||||
|
||||
Engine signals
|
||||
--------------
|
||||
|
||||
|
|
@ -114,7 +117,7 @@ engine_started
|
|||
|
||||
Sent when the Scrapy engine has started crawling.
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
.. note:: This signal may be fired *after* the :signal:`spider_opened` signal,
|
||||
depending on how the spider was started. So **don't** rely on this signal
|
||||
|
|
@ -129,7 +132,23 @@ engine_stopped
|
|||
Sent when the Scrapy engine is stopped (for example, when a crawling
|
||||
process has finished).
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
scheduler_empty
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. signal:: scheduler_empty
|
||||
.. function:: scheduler_empty()
|
||||
|
||||
Sent whenever the engine asks for a pending request from the
|
||||
:ref:`scheduler <topics-scheduler>` (i.e. calls its
|
||||
:meth:`~scrapy.core.scheduler.BaseScheduler.next_request` method) and the
|
||||
scheduler returns none.
|
||||
|
||||
See :ref:`start-requests-lazy` for an example.
|
||||
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
|
||||
Item signals
|
||||
------------
|
||||
|
|
@ -151,7 +170,7 @@ item_scraped
|
|||
Sent when an item has been scraped, after it has passed all the
|
||||
:ref:`topics-item-pipeline` stages (without being dropped).
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param item: the scraped item
|
||||
:type item: :ref:`item object <item-types>`
|
||||
|
|
@ -160,7 +179,7 @@ item_scraped
|
|||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
:param response: the response from where the item was scraped, or ``None``
|
||||
if it was yielded from :meth:`~scrapy.Spider.start_requests`.
|
||||
if it was yielded from :meth:`~scrapy.Spider.start`.
|
||||
:type response: :class:`~scrapy.http.Response` | ``None``
|
||||
|
||||
item_dropped
|
||||
|
|
@ -172,7 +191,7 @@ item_dropped
|
|||
Sent after an item has been dropped from the :ref:`topics-item-pipeline`
|
||||
when some stage raised a :exc:`~scrapy.exceptions.DropItem` exception.
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param item: the item dropped from the :ref:`topics-item-pipeline`
|
||||
:type item: :ref:`item object <item-types>`
|
||||
|
|
@ -181,7 +200,7 @@ item_dropped
|
|||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
:param response: the response from where the item was dropped, or ``None``
|
||||
if it was yielded from :meth:`~scrapy.Spider.start_requests`.
|
||||
if it was yielded from :meth:`~scrapy.Spider.start`.
|
||||
:type response: :class:`~scrapy.http.Response` | ``None``
|
||||
|
||||
:param exception: the exception (which must be a
|
||||
|
|
@ -198,14 +217,14 @@ item_error
|
|||
Sent when a :ref:`topics-item-pipeline` generates an error (i.e. raises
|
||||
an exception), except :exc:`~scrapy.exceptions.DropItem` exception.
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param item: the item that caused the error in the :ref:`topics-item-pipeline`
|
||||
:type item: :ref:`item object <item-types>`
|
||||
|
||||
:param response: the response being processed when the exception was
|
||||
raised, or ``None`` if it was yielded from
|
||||
:meth:`~scrapy.Spider.start_requests`.
|
||||
:meth:`~scrapy.Spider.start`.
|
||||
:type response: :class:`~scrapy.http.Response` | ``None``
|
||||
|
||||
:param spider: the spider which raised the exception
|
||||
|
|
@ -214,6 +233,7 @@ item_error
|
|||
:param failure: the exception raised
|
||||
:type failure: twisted.python.failure.Failure
|
||||
|
||||
|
||||
Spider signals
|
||||
--------------
|
||||
|
||||
|
|
@ -226,7 +246,7 @@ spider_closed
|
|||
Sent after a spider has been closed. This can be used to release per-spider
|
||||
resources reserved on :signal:`spider_opened`.
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param spider: the spider which has been closed
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
|
@ -250,7 +270,7 @@ spider_opened
|
|||
reserve per-spider resources, but can be used for any task that needs to be
|
||||
performed when a spider is opened.
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param spider: the spider which has been opened
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
|
@ -281,16 +301,16 @@ spider_idle
|
|||
accordingly (e.g. setting it to 'too_few_results' instead of
|
||||
'finished').
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param spider: the spider which has gone idle
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. note:: Scheduling some requests in your :signal:`spider_idle` handler does
|
||||
**not** guarantee that it can prevent the spider from being closed,
|
||||
although it sometimes can. That's because the spider may still remain idle
|
||||
if all the scheduled requests are rejected by the scheduler (e.g. filtered
|
||||
due to duplication).
|
||||
.. note:: Scheduling some requests in your :signal:`spider_idle` handler does
|
||||
**not** guarantee that it can prevent the spider from being closed,
|
||||
although it sometimes can. That's because the spider may still remain idle
|
||||
if all the scheduled requests are rejected by the scheduler (e.g. filtered
|
||||
due to duplication).
|
||||
|
||||
spider_error
|
||||
~~~~~~~~~~~~
|
||||
|
|
@ -300,7 +320,7 @@ spider_error
|
|||
|
||||
Sent when a spider callback generates an error (i.e. raises an exception).
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param failure: the exception raised
|
||||
:type failure: twisted.python.failure.Failure
|
||||
|
|
@ -319,12 +339,11 @@ feed_slot_closed
|
|||
|
||||
Sent when a :ref:`feed exports <topics-feed-exports>` slot is closed.
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param slot: the slot closed
|
||||
:type slot: scrapy.extensions.feedexport.FeedSlot
|
||||
|
||||
|
||||
feed_exporter_closed
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
@ -335,7 +354,7 @@ feed_exporter_closed
|
|||
during the handling of the :signal:`spider_closed` signal by the extension,
|
||||
after all feed exporting has been handled.
|
||||
|
||||
This signal supports returning deferreds from its handlers.
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
|
||||
Request signals
|
||||
|
|
@ -354,7 +373,7 @@ request_scheduled
|
|||
Raise :exc:`~scrapy.exceptions.IgnoreRequest` to drop a request before it
|
||||
reaches the scheduler.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
.. versionadded:: 2.11.2
|
||||
Allow dropping requests with :exc:`~scrapy.exceptions.IgnoreRequest`.
|
||||
|
|
@ -374,7 +393,7 @@ request_dropped
|
|||
Sent when a :class:`~scrapy.Request`, scheduled by the engine to be
|
||||
downloaded later, is rejected by the scheduler.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param request: the request that reached the scheduler
|
||||
:type request: :class:`~scrapy.Request` object
|
||||
|
|
@ -390,7 +409,7 @@ request_reached_downloader
|
|||
|
||||
Sent when a :class:`~scrapy.Request` reached downloader.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param request: the request that reached downloader
|
||||
:type request: :class:`~scrapy.Request` object
|
||||
|
|
@ -409,7 +428,7 @@ request_left_downloader
|
|||
Sent when a :class:`~scrapy.Request` leaves the downloader, even in case of
|
||||
failure.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param request: the request that reached the downloader
|
||||
:type request: :class:`~scrapy.Request` object
|
||||
|
|
@ -420,11 +439,11 @@ request_left_downloader
|
|||
bytes_received
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
.. signal:: bytes_received
|
||||
.. function:: bytes_received(data, request, spider)
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
Sent by the HTTP 1.1 and S3 download handlers when a group of bytes is
|
||||
received for a specific request. This signal might be fired multiple
|
||||
times for the same request, with partial data each time. For instance,
|
||||
|
|
@ -436,7 +455,7 @@ bytes_received
|
|||
exception. Please refer to the :ref:`topics-stop-response-download` topic
|
||||
for additional information and examples.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param data: the data received by the download handler
|
||||
:type data: :class:`bytes` object
|
||||
|
|
@ -450,11 +469,11 @@ bytes_received
|
|||
headers_received
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
.. signal:: headers_received
|
||||
.. function:: headers_received(headers, body_length, request, spider)
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
Sent by the HTTP 1.1 and S3 download handlers when the response headers are
|
||||
available for a given request, before downloading any additional content.
|
||||
|
||||
|
|
@ -463,7 +482,7 @@ headers_received
|
|||
exception. Please refer to the :ref:`topics-stop-response-download` topic
|
||||
for additional information and examples.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param headers: the headers received by the download handler
|
||||
:type headers: :class:`scrapy.http.headers.Headers` object
|
||||
|
|
@ -477,6 +496,7 @@ headers_received
|
|||
:param spider: the spider associated with the response
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
|
||||
Response signals
|
||||
----------------
|
||||
|
||||
|
|
@ -489,7 +509,7 @@ response_received
|
|||
Sent when the engine receives a new :class:`~scrapy.http.Response` from the
|
||||
downloader.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param response: the response received
|
||||
:type response: :class:`~scrapy.http.Response` object
|
||||
|
|
@ -511,9 +531,9 @@ response_downloaded
|
|||
.. signal:: response_downloaded
|
||||
.. function:: response_downloaded(response, request, spider)
|
||||
|
||||
Sent by the downloader right after a ``HTTPResponse`` is downloaded.
|
||||
Sent by the downloader right after a :class:`~scrapy.http.Response` is downloaded.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param response: the response downloaded
|
||||
:type response: :class:`~scrapy.http.Response` object
|
||||
|
|
|
|||
|
|
@ -63,18 +63,38 @@ particular setting. See each middleware documentation for more info.
|
|||
Writing your own spider middleware
|
||||
==================================
|
||||
|
||||
Each spider middleware is a Python class that defines one or more of the
|
||||
methods defined below.
|
||||
|
||||
The main entry point is the ``from_crawler`` class method, which receives a
|
||||
:class:`~scrapy.crawler.Crawler` instance. The :class:`~scrapy.crawler.Crawler`
|
||||
object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
||||
Each spider middleware is a :ref:`component <topics-components>` that defines
|
||||
one or more of these methods:
|
||||
|
||||
.. module:: scrapy.spidermiddlewares
|
||||
|
||||
.. class:: SpiderMiddleware
|
||||
|
||||
.. method:: process_spider_input(response, spider)
|
||||
.. method:: process_start(start: AsyncIterator[Any], /) -> AsyncIterator[Any]
|
||||
:async:
|
||||
|
||||
Iterate over the output of :meth:`~scrapy.Spider.start` or that
|
||||
of the :meth:`process_start` method of an earlier spider middleware,
|
||||
overriding it. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def process_start(self, start):
|
||||
async for item_or_request in start:
|
||||
yield item_or_request
|
||||
|
||||
You may yield the same type of objects as :meth:`~scrapy.Spider.start`.
|
||||
|
||||
To write spider middlewares that work on Scrapy versions lower than
|
||||
2.13, define also a synchronous ``process_start_requests()`` method
|
||||
that returns an iterable. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def process_start_requests(self, start, spider):
|
||||
yield from start
|
||||
|
||||
.. method:: process_spider_input(response)
|
||||
|
||||
This method is called for each response that goes through the spider
|
||||
middleware and into the spider, for processing.
|
||||
|
|
@ -96,11 +116,7 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
|||
:param response: the response being processed
|
||||
:type response: :class:`~scrapy.http.Response` object
|
||||
|
||||
:param spider: the spider for which this response is intended
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
|
||||
.. method:: process_spider_output(response, result, spider)
|
||||
.. method:: process_spider_output(response, result)
|
||||
|
||||
This method is called with the results returned from the Spider, after
|
||||
it has processed the response.
|
||||
|
|
@ -129,10 +145,8 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
|||
:type result: an iterable of :class:`~scrapy.Request` objects and
|
||||
:ref:`item objects <topics-items>`
|
||||
|
||||
:param spider: the spider whose result is being processed
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. method:: process_spider_output_async(response, result, spider)
|
||||
.. method:: process_spider_output_async(response, result)
|
||||
:async:
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
|
|
@ -140,7 +154,7 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
|||
which will be called instead of :meth:`process_spider_output` if
|
||||
``result`` is an :term:`asynchronous iterable`.
|
||||
|
||||
.. method:: process_spider_exception(response, exception, spider)
|
||||
.. method:: process_spider_exception(response, exception)
|
||||
|
||||
This method is called when a spider or :meth:`process_spider_output`
|
||||
method (from a previous spider middleware) raises an exception.
|
||||
|
|
@ -165,44 +179,19 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
|
|||
:param exception: the exception raised
|
||||
:type exception: :exc:`Exception` object
|
||||
|
||||
:param spider: the spider which raised the exception
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. method:: process_start_requests(start_requests, spider)
|
||||
Base class for custom spider middlewares
|
||||
----------------------------------------
|
||||
|
||||
This method is called with the start requests of the spider, and works
|
||||
similarly to the :meth:`process_spider_output` method, except that it
|
||||
doesn't have a response associated and must return only requests (not
|
||||
items).
|
||||
Scrapy provides a base class for custom spider middlewares. It's not required
|
||||
to use it but it can help with simplifying middleware implementations and
|
||||
reducing the amount of boilerplate code in :ref:`universal middlewares
|
||||
<universal-spider-middleware>`.
|
||||
|
||||
It receives an iterable (in the ``start_requests`` parameter) and must
|
||||
return another iterable of :class:`~scrapy.Request` objects and/or :ref:`item objects <topics-items>`.
|
||||
.. module:: scrapy.spidermiddlewares.base
|
||||
|
||||
.. note:: When implementing this method in your spider middleware, you
|
||||
should always return an iterable (that follows the input one) and
|
||||
not consume all ``start_requests`` iterator because it can be very
|
||||
large (or even unbounded) and cause a memory overflow. The Scrapy
|
||||
engine is designed to pull start requests while it has capacity to
|
||||
process them, so the start requests iterator can be effectively
|
||||
endless where there is some other condition for stopping the spider
|
||||
(like a time limit or item/page count).
|
||||
|
||||
:param start_requests: the start requests
|
||||
:type start_requests: an iterable of :class:`~scrapy.Request`
|
||||
|
||||
:param spider: the spider to whom the start requests belong
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
.. method:: from_crawler(cls, crawler)
|
||||
|
||||
If present, this classmethod is called to create a middleware instance
|
||||
from a :class:`~scrapy.crawler.Crawler`. It must return a new instance
|
||||
of the middleware. Crawler object provides access to all Scrapy core
|
||||
components like settings and signals; it is a way for middleware to
|
||||
access them and hook its functionality into Scrapy.
|
||||
|
||||
:param crawler: crawler that uses this middleware
|
||||
:type crawler: :class:`~scrapy.crawler.Crawler` object
|
||||
.. autoclass:: BaseSpiderMiddleware
|
||||
:members:
|
||||
|
||||
.. _topics-spider-middleware-ref:
|
||||
|
||||
|
|
@ -356,9 +345,9 @@ Default: ``'scrapy.spidermiddlewares.referer.DefaultReferrerPolicy'``
|
|||
Acceptable values for REFERRER_POLICY
|
||||
*************************************
|
||||
|
||||
- either a path to a ``scrapy.spidermiddlewares.referer.ReferrerPolicy``
|
||||
- either a path to a :class:`scrapy.spidermiddlewares.referer.ReferrerPolicy`
|
||||
subclass — a custom policy or one of the built-in ones (see classes below),
|
||||
- or one of the standard W3C-defined string values,
|
||||
- or one or more comma-separated standard W3C-defined string values,
|
||||
- or the special ``"scrapy-default"``.
|
||||
|
||||
======================================= ========================================================================
|
||||
|
|
@ -375,6 +364,8 @@ String value Class name (as a string)
|
|||
`"unsafe-url"`_ :class:`scrapy.spidermiddlewares.referer.UnsafeUrlPolicy`
|
||||
======================================= ========================================================================
|
||||
|
||||
.. autoclass:: ReferrerPolicy
|
||||
|
||||
.. autoclass:: DefaultReferrerPolicy
|
||||
.. warning::
|
||||
Scrapy's default referrer policy — just like `"no-referrer-when-downgrade"`_,
|
||||
|
|
@ -419,6 +410,14 @@ String value Class name (as a string)
|
|||
.. _"unsafe-url": https://www.w3.org/TR/referrer-policy/#referrer-policy-unsafe-url
|
||||
|
||||
|
||||
StartSpiderMiddleware
|
||||
---------------------
|
||||
|
||||
.. module:: scrapy.spidermiddlewares.start
|
||||
|
||||
.. autoclass:: StartSpiderMiddleware
|
||||
|
||||
|
||||
UrlLengthMiddleware
|
||||
-------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -12,16 +12,16 @@ parsing pages for a particular site (or, in some cases, a group of sites).
|
|||
|
||||
For spiders, the scraping cycle goes through something like this:
|
||||
|
||||
1. You start by generating the initial Requests to crawl the first URLs, and
|
||||
1. You start by generating the initial requests to crawl the first URLs, and
|
||||
specify a callback function to be called with the response downloaded from
|
||||
those requests.
|
||||
|
||||
The first requests to perform are obtained by calling the
|
||||
:meth:`~scrapy.Spider.start_requests` method which (by default)
|
||||
generates :class:`~scrapy.Request` for the URLs specified in the
|
||||
:attr:`~scrapy.Spider.start_urls` and the
|
||||
:attr:`~scrapy.Spider.parse` method as callback function for the
|
||||
Requests.
|
||||
The first requests to perform are obtained by iterating the
|
||||
:meth:`~scrapy.Spider.start` method, which by default yields a
|
||||
:class:`~scrapy.Request` object for each URL in the
|
||||
:attr:`~scrapy.Spider.start_urls` spider attribute, with the
|
||||
:attr:`~scrapy.Spider.parse` method set as :attr:`~scrapy.Request.callback`
|
||||
function to handle each :class:`~scrapy.http.Response`.
|
||||
|
||||
2. In the callback function, you parse the response (web page) and return
|
||||
:ref:`item objects <topics-items>`,
|
||||
|
|
@ -48,14 +48,7 @@ scrapy.Spider
|
|||
=============
|
||||
|
||||
.. class:: scrapy.spiders.Spider
|
||||
.. class:: scrapy.Spider()
|
||||
|
||||
This is the simplest spider, and the one from which every other spider
|
||||
must inherit (including spiders that come bundled with Scrapy, as well as spiders
|
||||
that you write yourself). It doesn't provide any special functionality. It just
|
||||
provides a default :meth:`start_requests` implementation which sends requests from
|
||||
the :attr:`start_urls` spider attribute and calls the spider's method ``parse``
|
||||
for each of the resulting responses.
|
||||
.. autoclass:: scrapy.Spider
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
|
|
@ -81,12 +74,7 @@ scrapy.Spider
|
|||
Let's say your target url is ``https://www.example.com/1.html``,
|
||||
then add ``'example.com'`` to the list.
|
||||
|
||||
.. attribute:: start_urls
|
||||
|
||||
A list of URLs where the spider will begin to crawl from, when no
|
||||
particular URLs are specified. So, the first pages downloaded will be those
|
||||
listed here. The subsequent :class:`~scrapy.Request` will be generated successively from data
|
||||
contained in the start URLs.
|
||||
.. autoattribute:: start_urls
|
||||
|
||||
.. attribute:: custom_settings
|
||||
|
||||
|
|
@ -149,7 +137,7 @@ scrapy.Spider
|
|||
|
||||
The final settings and the initialized
|
||||
:class:`~scrapy.crawler.Crawler` attributes are available in the
|
||||
:meth:`start_requests` method, handlers of the
|
||||
:meth:`start` method, handlers of the
|
||||
:signal:`engine_started` signal and later.
|
||||
|
||||
:param crawler: crawler to which the spider will be bound
|
||||
|
|
@ -201,42 +189,7 @@ scrapy.Spider
|
|||
super().update_settings(settings)
|
||||
settings.setdefault("FEEDS", {}).update(cls.custom_feed)
|
||||
|
||||
.. method:: start_requests()
|
||||
|
||||
This method must return an iterable with the first Requests to crawl and/or with :ref:`item objects
|
||||
<topics-items>` for
|
||||
this spider. It is called by Scrapy when the spider is opened for
|
||||
scraping. Scrapy calls it only once, so it is safe to implement
|
||||
:meth:`start_requests` as a generator.
|
||||
|
||||
The default implementation generates ``Request(url, dont_filter=True)``
|
||||
for each url in :attr:`start_urls`.
|
||||
|
||||
If you want to change the Requests used to start scraping a domain, this is
|
||||
the method to override. For example, if you need to start by logging in using
|
||||
a POST request, you could do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
|
||||
|
||||
class MySpider(scrapy.Spider):
|
||||
name = "myspider"
|
||||
|
||||
def start_requests(self):
|
||||
return [
|
||||
scrapy.FormRequest(
|
||||
"http://www.example.com/login",
|
||||
formdata={"user": "john", "pass": "secret"},
|
||||
callback=self.logged_in,
|
||||
)
|
||||
]
|
||||
|
||||
def logged_in(self, response):
|
||||
# here you would extract links to follow and return Requests for
|
||||
# each of them, with another callback
|
||||
pass
|
||||
.. automethod:: start
|
||||
|
||||
.. method:: parse(response)
|
||||
|
||||
|
|
@ -308,8 +261,9 @@ Return multiple Requests and items from a single callback:
|
|||
for href in response.xpath("//a/@href").getall():
|
||||
yield scrapy.Request(response.urljoin(href), self.parse)
|
||||
|
||||
Instead of :attr:`~.start_urls` you can use :meth:`~.start_requests` directly;
|
||||
to give data more structure you can use :class:`~scrapy.Item` objects:
|
||||
Instead of :attr:`~.start_urls` you can use :meth:`~scrapy.Spider.start`
|
||||
directly; to give data more structure you can use :class:`~scrapy.Item`
|
||||
objects:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
|
@ -322,7 +276,7 @@ to give data more structure you can use :class:`~scrapy.Item` objects:
|
|||
name = "example.com"
|
||||
allowed_domains = ["example.com"]
|
||||
|
||||
def start_requests(self):
|
||||
async def start(self):
|
||||
yield scrapy.Request("http://www.example.com/1.html", self.parse)
|
||||
yield scrapy.Request("http://www.example.com/2.html", self.parse)
|
||||
yield scrapy.Request("http://www.example.com/3.html", self.parse)
|
||||
|
|
@ -376,11 +330,11 @@ The above example can also be written as follows:
|
|||
class MySpider(scrapy.Spider):
|
||||
name = "myspider"
|
||||
|
||||
def start_requests(self):
|
||||
async def start(self):
|
||||
yield scrapy.Request(f"http://www.example.com/categories/{self.category}")
|
||||
|
||||
If you are :ref:`running Scrapy from a script <run-from-script>`, you can
|
||||
specify spider arguments when calling
|
||||
If you are :ref:`running Scrapy from a script <run-from-script>`, you can
|
||||
specify spider arguments when calling
|
||||
:class:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
|
||||
:class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
|
||||
|
||||
|
|
@ -410,6 +364,38 @@ used by :class:`~scrapy.downloadermiddlewares.useragent.UserAgentMiddleware`::
|
|||
Spider arguments can also be passed through the Scrapyd ``schedule.json`` API.
|
||||
See `Scrapyd documentation`_.
|
||||
|
||||
.. _start-requests:
|
||||
|
||||
Start requests
|
||||
==============
|
||||
|
||||
**Start requests** are :class:`~scrapy.Request` objects yielded from the
|
||||
:meth:`~scrapy.Spider.start` method of a spider or from the
|
||||
:meth:`~scrapy.spidermiddlewares.SpiderMiddleware.process_start` method of a
|
||||
:ref:`spider middleware <topics-spider-middleware>`.
|
||||
|
||||
.. seealso:: :ref:`start-request-order`
|
||||
|
||||
.. _start-requests-lazy:
|
||||
|
||||
Delaying start request iteration
|
||||
--------------------------------
|
||||
|
||||
You can override the :meth:`~scrapy.Spider.start` method as follows to pause
|
||||
its iteration whenever there are scheduled requests:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def start(self):
|
||||
async for item_or_request in super().start():
|
||||
if self.crawler.engine.needs_backout():
|
||||
await self.crawler.signals.wait_for(signals.scheduler_empty)
|
||||
yield item_or_request
|
||||
|
||||
This can help minimize the number of requests in the scheduler at any given
|
||||
time, to minimize resource usage (memory or disk, depending on
|
||||
:setting:`JOBDIR`).
|
||||
|
||||
.. _builtin-spiders:
|
||||
|
||||
Generic Spiders
|
||||
|
|
@ -940,10 +926,11 @@ Combine SitemapSpider with other sources of urls:
|
|||
|
||||
other_urls = ["http://www.example.com/about"]
|
||||
|
||||
def start_requests(self):
|
||||
requests = list(super(MySpider, self).start_requests())
|
||||
requests += [scrapy.Request(x, self.parse_other) for x in self.other_urls]
|
||||
return requests
|
||||
async def start(self):
|
||||
async for item_or_request in super().start():
|
||||
yield item_or_request
|
||||
for url in self.other_urls:
|
||||
yield Request(url, self.parse_other)
|
||||
|
||||
def parse_shop(self, response):
|
||||
pass # ... scrape shop here ...
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ attribute. Here is an example of an extension that access stats:
|
|||
def from_crawler(cls, crawler):
|
||||
return cls(crawler.stats)
|
||||
|
||||
.. skip: start
|
||||
|
||||
Set stat value:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -80,13 +82,15 @@ Get all stats:
|
|||
>>> stats.get_stats()
|
||||
{'custom_count': 1, 'start_time': datetime.datetime(2009, 7, 14, 21, 47, 28, 977139)}
|
||||
|
||||
.. skip: end
|
||||
|
||||
Available Stats Collectors
|
||||
==========================
|
||||
|
||||
Besides the basic :class:`StatsCollector` there are other Stats Collectors
|
||||
available in Scrapy which extend the basic Stats Collector. You can select
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ Default Username and Password can be overridden by the settings
|
|||
You need the telnet program which comes installed by default in Windows, and
|
||||
most Linux distros.
|
||||
|
||||
.. _telnet-vars:
|
||||
|
||||
Available variables in the telnet console
|
||||
=========================================
|
||||
|
||||
|
|
@ -77,8 +79,6 @@ convenience:
|
|||
+----------------+-------------------------------------------------------------------+
|
||||
| ``spider`` | the active spider |
|
||||
+----------------+-------------------------------------------------------------------+
|
||||
| ``slot`` | the engine slot |
|
||||
+----------------+-------------------------------------------------------------------+
|
||||
| ``extensions`` | the Extension Manager (Crawler.extensions attribute) |
|
||||
+----------------+-------------------------------------------------------------------+
|
||||
| ``stats`` | the Stats Collector (Crawler.stats attribute) |
|
||||
|
|
@ -97,6 +97,8 @@ convenience:
|
|||
Telnet console usage examples
|
||||
=============================
|
||||
|
||||
.. skip: start
|
||||
|
||||
Here are some example tasks you can do with the telnet console:
|
||||
|
||||
View engine status
|
||||
|
|
@ -114,10 +116,10 @@ using the telnet console::
|
|||
engine.scraper.is_idle() : False
|
||||
engine.spider.name : followall
|
||||
engine.spider_is_idle() : False
|
||||
engine.slot.closing : False
|
||||
len(engine.slot.inprogress) : 16
|
||||
len(engine.slot.scheduler.dqs or []) : 0
|
||||
len(engine.slot.scheduler.mqs) : 92
|
||||
engine._slot.closing : False
|
||||
len(engine._slot.inprogress) : 16
|
||||
len(engine._slot.scheduler.dqs or []) : 0
|
||||
len(engine._slot.scheduler.mqs) : 92
|
||||
len(engine.scraper.slot.queue) : 0
|
||||
len(engine.scraper.slot.active) : 0
|
||||
engine.scraper.slot.active_size : 0
|
||||
|
|
@ -146,6 +148,8 @@ To stop::
|
|||
>>> engine.stop()
|
||||
Connection closed by foreign host.
|
||||
|
||||
.. skip: end
|
||||
|
||||
Telnet Console signals
|
||||
======================
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Run tests, generate coverage report and open it on a browser
|
||||
#
|
||||
# Requires: coverage 3.3 or above from https://pypi.python.org/pypi/coverage
|
||||
# Requires: coverage 3.3 or above from https://pypi.org/pypi/coverage
|
||||
|
||||
coverage run --branch $(which trial) --reporter=text tests
|
||||
coverage html -i
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
from collections import deque
|
||||
from time import time
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet import reactor # noqa: TID253
|
||||
from twisted.web.resource import Resource
|
||||
from twisted.web.server import NOT_DONE_YET, Site
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ class QPSSpider(Spider):
|
|||
elif self.download_delay is not None:
|
||||
self.download_delay = float(self.download_delay)
|
||||
|
||||
async def start(self):
|
||||
for item_or_request in self.start_requests():
|
||||
yield item_or_request
|
||||
|
||||
def start_requests(self):
|
||||
url = self.benchurl
|
||||
if self.latency is not None:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ _scrapy() {
|
|||
(runspider)
|
||||
local options=(
|
||||
{'(--output)-o','(-o)--output='}'[dump scraped items into FILE (use - for stdout)]:file:_files'
|
||||
{'(--output-format)-t','(-t)--output-format='}'[format to use for dumping items with -o]:format:(FORMAT)'
|
||||
'*-a[set spider argument (may be repeated)]:value pair:(NAME=VALUE)'
|
||||
'1:spider file:_files -g \*.py'
|
||||
)
|
||||
|
|
@ -99,7 +98,6 @@ _scrapy() {
|
|||
(crawl)
|
||||
local options=(
|
||||
{'(--output)-o','(-o)--output='}'[dump scraped items into FILE (use - for stdout)]:file:_files'
|
||||
{'(--output-format)-t','(-t)--output-format='}'[format to use for dumping items with -o]:format:(FORMAT)'
|
||||
'*-a[set spider argument (may be repeated)]:value pair:(NAME=VALUE)'
|
||||
'1:spider:_scrapy_spiders'
|
||||
)
|
||||
|
|
|
|||
82
pylintrc
82
pylintrc
|
|
@ -1,82 +0,0 @@
|
|||
[MASTER]
|
||||
persistent=no
|
||||
jobs=1 # >1 hides results
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=abstract-method,
|
||||
arguments-differ,
|
||||
arguments-renamed,
|
||||
attribute-defined-outside-init,
|
||||
bad-classmethod-argument,
|
||||
bare-except,
|
||||
broad-except,
|
||||
broad-exception-raised,
|
||||
c-extension-no-member,
|
||||
consider-using-with,
|
||||
cyclic-import,
|
||||
dangerous-default-value,
|
||||
disallowed-name,
|
||||
duplicate-code, # https://github.com/PyCQA/pylint/issues/214
|
||||
eval-used,
|
||||
expression-not-assigned,
|
||||
fixme,
|
||||
function-redefined,
|
||||
global-statement,
|
||||
implicit-str-concat,
|
||||
import-error,
|
||||
import-outside-toplevel,
|
||||
inconsistent-return-statements,
|
||||
inherit-non-class,
|
||||
invalid-name,
|
||||
invalid-overridden-method,
|
||||
isinstance-second-argument-not-valid-type,
|
||||
keyword-arg-before-vararg,
|
||||
line-too-long,
|
||||
logging-format-interpolation,
|
||||
logging-fstring-interpolation,
|
||||
logging-not-lazy,
|
||||
lost-exception,
|
||||
missing-docstring,
|
||||
no-else-raise,
|
||||
no-else-return,
|
||||
no-member,
|
||||
no-method-argument,
|
||||
no-name-in-module,
|
||||
no-self-argument,
|
||||
no-value-for-parameter, # https://github.com/pylint-dev/pylint/issues/3268
|
||||
not-callable,
|
||||
pointless-exception-statement,
|
||||
pointless-statement,
|
||||
pointless-string-statement,
|
||||
protected-access,
|
||||
raise-missing-from,
|
||||
redefined-argument-from-local,
|
||||
redefined-builtin,
|
||||
redefined-outer-name,
|
||||
reimported,
|
||||
signature-differs,
|
||||
super-init-not-called,
|
||||
too-few-public-methods,
|
||||
too-many-ancestors,
|
||||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-format-args,
|
||||
too-many-function-args,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-locals,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
unbalanced-tuple-unpacking,
|
||||
unnecessary-dunder-call,
|
||||
unnecessary-pass,
|
||||
unreachable,
|
||||
unused-argument,
|
||||
unused-import,
|
||||
unused-private-member,
|
||||
unused-variable,
|
||||
unused-wildcard-import,
|
||||
used-before-assignment,
|
||||
useless-return,
|
||||
wildcard-import,
|
||||
wrong-import-position
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
[build-system]
|
||||
requires = ["hatchling>=1.27.0"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "Scrapy"
|
||||
dynamic = ["version"]
|
||||
description = "A high-level Web Crawling and Web Scraping framework"
|
||||
dependencies = [
|
||||
"Twisted>=21.7.0",
|
||||
"cryptography>=37.0.0",
|
||||
"cssselect>=0.9.1",
|
||||
"defusedxml>=0.7.1",
|
||||
"itemadapter>=0.1.0",
|
||||
"itemloaders>=1.0.1",
|
||||
"lxml>=4.6.0",
|
||||
"packaging",
|
||||
"parsel>=1.5.0",
|
||||
"protego>=0.1.15",
|
||||
"pyOpenSSL>=22.0.0",
|
||||
"queuelib>=1.4.2",
|
||||
"service_identity>=18.1.0",
|
||||
"tldextract",
|
||||
"w3lib>=1.17.0",
|
||||
"zope.interface>=5.1.0",
|
||||
# Platform-specific dependencies
|
||||
'PyDispatcher>=2.0.5; platform_python_implementation == "CPython"',
|
||||
'PyPyDispatcher>=2.1.0; platform_python_implementation == "PyPy"',
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Framework :: Scrapy",
|
||||
"Intended Audience :: Developers",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
license = "BSD-3-Clause"
|
||||
license-files = ["LICENSE", "AUTHORS"]
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.9"
|
||||
authors = [{ name = "Scrapy developers", email = "pablo@pablohoffman.com" }]
|
||||
maintainers = [{ name = "Pablo Hoffman", email = "pablo@pablohoffman.com" }]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://scrapy.org/"
|
||||
Documentation = "https://docs.scrapy.org/"
|
||||
Source = "https://github.com/scrapy/scrapy"
|
||||
Tracker = "https://github.com/scrapy/scrapy/issues"
|
||||
"Release notes" = "https://docs.scrapy.org/en/latest/news.html"
|
||||
|
||||
[project.scripts]
|
||||
scrapy = "scrapy.cmdline:execute"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"/docs",
|
||||
"/extras",
|
||||
"/scrapy",
|
||||
"/tests",
|
||||
"/tests_typing",
|
||||
"/CODE_OF_CONDUCT.md",
|
||||
"/CONTRIBUTING.md",
|
||||
"/INSTALL.md",
|
||||
"/NEWS",
|
||||
"/SECURITY.md",
|
||||
"/codecov.yml",
|
||||
"/conftest.py",
|
||||
"/tox.ini",
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "scrapy/VERSION"
|
||||
pattern = "^(?P<version>.+)$"
|
||||
|
||||
[tool.mypy]
|
||||
ignore_missing_imports = true
|
||||
implicit_reexport = false
|
||||
|
||||
# Interface classes are hard to support
|
||||
[[tool.mypy.overrides]]
|
||||
module = "twisted.internet.interfaces"
|
||||
follow_imports = "skip"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "scrapy.interfaces"
|
||||
ignore_errors = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "twisted.internet.reactor"
|
||||
follow_imports = "skip"
|
||||
|
||||
# FIXME: remove the following section once the issues are solved
|
||||
[[tool.mypy.overrides]]
|
||||
module = "scrapy.settings.default_settings"
|
||||
ignore_errors = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "twisted"
|
||||
implicit_reexport = true
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "2.13.3"
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = "{new_version}"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "docs/news.rst"
|
||||
search = "\\(unreleased\\)$"
|
||||
replace = "({now:%Y-%m-%d})"
|
||||
regex = true
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "scrapy/VERSION"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "SECURITY.md"
|
||||
parse = """(?P<major>0|[1-9]\\d*)\\.(?P<minor>0|[1-9]\\d*)"""
|
||||
serialize = ["{major}.{minor}"]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
include = ["scrapy/*"]
|
||||
omit = ["tests/*"]
|
||||
disable_warnings = ["include-ignored"]
|
||||
|
||||
[tool.coverage.paths]
|
||||
source = [
|
||||
"scrapy",
|
||||
".tox/**/site-packages/scrapy"
|
||||
]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_also = [
|
||||
"if TYPE_CHECKING:",
|
||||
"@(abc\\.)?abstractmethod",
|
||||
]
|
||||
|
||||
[tool.pylint.MASTER]
|
||||
persistent = "no"
|
||||
jobs = 1 # >1 hides results
|
||||
extension-pkg-allow-list=[
|
||||
"lxml",
|
||||
]
|
||||
|
||||
[tool.pylint."MESSAGES CONTROL"]
|
||||
enable = [
|
||||
"useless-suppression",
|
||||
]
|
||||
# Make INFO checks like useless-suppression also cause pylint to return a
|
||||
# non-zero exit code.
|
||||
fail-on = "I"
|
||||
disable = [
|
||||
# Ones we want to ignore
|
||||
"attribute-defined-outside-init",
|
||||
"broad-exception-caught",
|
||||
"consider-using-with",
|
||||
"cyclic-import",
|
||||
"disallowed-name",
|
||||
"duplicate-code", # https://github.com/pylint-dev/pylint/issues/214
|
||||
"fixme",
|
||||
"import-outside-toplevel",
|
||||
"inherit-non-class", # false positives with create_deprecated_class()
|
||||
"invalid-name",
|
||||
"invalid-overridden-method",
|
||||
"isinstance-second-argument-not-valid-type", # false positives with create_deprecated_class()
|
||||
"line-too-long",
|
||||
"logging-format-interpolation",
|
||||
"logging-fstring-interpolation",
|
||||
"logging-not-lazy",
|
||||
"missing-docstring",
|
||||
"no-member",
|
||||
"no-value-for-parameter", # https://github.com/pylint-dev/pylint/issues/3268
|
||||
"not-callable",
|
||||
"protected-access",
|
||||
"redefined-builtin",
|
||||
"redefined-outer-name",
|
||||
"too-few-public-methods",
|
||||
"too-many-ancestors",
|
||||
"too-many-arguments",
|
||||
"too-many-branches",
|
||||
"too-many-function-args",
|
||||
"too-many-instance-attributes",
|
||||
"too-many-lines",
|
||||
"too-many-locals",
|
||||
"too-many-positional-arguments",
|
||||
"too-many-public-methods",
|
||||
"too-many-return-statements",
|
||||
"unused-argument",
|
||||
"unused-import",
|
||||
"unused-variable",
|
||||
"useless-import-alias", # used as a hint to mypy
|
||||
"useless-return", # https://github.com/pylint-dev/pylint/issues/6530
|
||||
"wrong-import-position",
|
||||
|
||||
# Ones that we may want to address (fix, ignore per-line or move to "don't want to fix")
|
||||
"abstract-method",
|
||||
"arguments-differ",
|
||||
"arguments-renamed",
|
||||
"dangerous-default-value",
|
||||
"keyword-arg-before-vararg",
|
||||
"pointless-statement",
|
||||
"raise-missing-from",
|
||||
"unnecessary-dunder-call",
|
||||
"used-before-assignment",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = [
|
||||
"--reactor=asyncio",
|
||||
]
|
||||
xfail_strict = true
|
||||
python_files = ["test_*.py", "test_*/__init__.py"]
|
||||
markers = [
|
||||
"only_asyncio: marks tests as only enabled when --reactor=asyncio is passed",
|
||||
"only_not_asyncio: marks tests as only enabled when --reactor=asyncio is not passed",
|
||||
"requires_uvloop: marks tests as only enabled when uvloop is known to be working",
|
||||
"requires_botocore: marks tests that need botocore (but not boto3)",
|
||||
"requires_boto3: marks tests that need botocore and boto3",
|
||||
]
|
||||
filterwarnings = [
|
||||
"ignore::DeprecationWarning:twisted.web.static"
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = [
|
||||
# flake8-builtins
|
||||
"A",
|
||||
# flake8-async
|
||||
"ASYNC",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# flake8-comprehensions
|
||||
"C4",
|
||||
# flake8-commas
|
||||
"COM",
|
||||
# pydocstyle
|
||||
"D",
|
||||
# flake8-future-annotations
|
||||
"FA",
|
||||
# flynt
|
||||
"FLY",
|
||||
# refurb
|
||||
"FURB",
|
||||
# isort
|
||||
"I",
|
||||
# flake8-implicit-str-concat
|
||||
"ISC",
|
||||
# flake8-logging
|
||||
"LOG",
|
||||
# Perflint
|
||||
"PERF",
|
||||
# pygrep-hooks
|
||||
"PGH",
|
||||
# flake8-pie
|
||||
"PIE",
|
||||
# pylint
|
||||
"PL",
|
||||
# flake8-pytest-style
|
||||
"PT",
|
||||
# flake8-use-pathlib
|
||||
"PTH",
|
||||
# flake8-pyi
|
||||
"PYI",
|
||||
# flake8-quotes
|
||||
"Q",
|
||||
# flake8-return
|
||||
"RET",
|
||||
# flake8-raise
|
||||
"RSE",
|
||||
# Ruff-specific rules
|
||||
"RUF",
|
||||
# flake8-bandit
|
||||
"S",
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# flake8-slots
|
||||
"SLOT",
|
||||
# flake8-debugger
|
||||
"T10",
|
||||
# flake8-type-checking
|
||||
"TC",
|
||||
# flake8-tidy-imports
|
||||
"TID",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# pycodestyle warnings
|
||||
"W",
|
||||
# flake8-2020
|
||||
"YTT",
|
||||
]
|
||||
ignore = [
|
||||
# Ones we want to ignore
|
||||
|
||||
# Trailing comma missing
|
||||
"COM812",
|
||||
# Missing docstring in public module
|
||||
"D100",
|
||||
# Missing docstring in public class
|
||||
"D101",
|
||||
# Missing docstring in public method
|
||||
"D102",
|
||||
# Missing docstring in public function
|
||||
"D103",
|
||||
# Missing docstring in public package
|
||||
"D104",
|
||||
# Missing docstring in magic method
|
||||
"D105",
|
||||
# Missing docstring in public nested class
|
||||
"D106",
|
||||
# Missing docstring in __init__
|
||||
"D107",
|
||||
# One-line docstring should fit on one line with quotes
|
||||
"D200",
|
||||
# No blank lines allowed after function docstring
|
||||
"D202",
|
||||
# 1 blank line required between summary line and description
|
||||
"D205",
|
||||
# Multi-line docstring closing quotes should be on a separate line
|
||||
"D209",
|
||||
# First line should end with a period
|
||||
"D400",
|
||||
# First line should be in imperative mood; try rephrasing
|
||||
"D401",
|
||||
# First line should not be the function's "signature"
|
||||
"D402",
|
||||
# First word of the first line should be properly capitalized
|
||||
"D403",
|
||||
# `try`-`except` within a loop incurs performance overhead
|
||||
"PERF203",
|
||||
# Import alias does not rename original package
|
||||
"PLC0414",
|
||||
# Too many return statements
|
||||
"PLR0911",
|
||||
# Too many branches
|
||||
"PLR0912",
|
||||
# Too many arguments in function definition
|
||||
"PLR0913",
|
||||
# Too many statements
|
||||
"PLR0915",
|
||||
# Magic value used in comparison
|
||||
"PLR2004",
|
||||
# `for` loop variable overwritten by assignment target
|
||||
"PLW2901",
|
||||
# String contains ambiguous {}.
|
||||
"RUF001",
|
||||
# Docstring contains ambiguous {}.
|
||||
"RUF002",
|
||||
# Comment contains ambiguous {}.
|
||||
"RUF003",
|
||||
# Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
"RUF012",
|
||||
# Use of `assert` detected; needed for mypy
|
||||
"S101",
|
||||
# FTP-related functions are being called; https://github.com/scrapy/scrapy/issues/4180
|
||||
"S321",
|
||||
# Argument default set to insecure SSL protocol
|
||||
"S503",
|
||||
# Use a context manager for opening files
|
||||
"SIM115",
|
||||
# Yoda condition detected
|
||||
"SIM300",
|
||||
|
||||
# Ones that we may want to address (fix, ignore per-line or move to "don't want to fix")
|
||||
|
||||
# Assigning to `os.environ` doesn't clear the environment.
|
||||
"B003",
|
||||
# Do not use mutable data structures for argument defaults.
|
||||
"B006",
|
||||
# Loop control variable not used within the loop body.
|
||||
"B007",
|
||||
# Do not perform function calls in argument defaults.
|
||||
"B008",
|
||||
# Found useless expression.
|
||||
"B018",
|
||||
# Star-arg unpacking after a keyword argument is strongly discouraged.
|
||||
"B026",
|
||||
# No explicit stacklevel argument found.
|
||||
"B028",
|
||||
# Within an `except` clause, raise exceptions with `raise ... from`
|
||||
"B904",
|
||||
# Use capitalized environment variable
|
||||
"SIM112",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.flake8-tidy-imports]
|
||||
banned-module-level-imports = [
|
||||
"twisted.internet.reactor",
|
||||
# indirectly imports twisted.conch.insults.helper which imports twisted.internet.reactor
|
||||
"twisted.conch.manhole",
|
||||
# directly imports twisted.internet.reactor
|
||||
"twisted.protocols.ftp",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
split-on-trailing-comma = false
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
# Circular import workarounds
|
||||
"scrapy/linkextractors/__init__.py" = ["E402"]
|
||||
"scrapy/spiders/__init__.py" = ["E402"]
|
||||
|
||||
# Skip bandit in tests
|
||||
"tests/**" = ["S"]
|
||||
|
||||
# Issues pending a review:
|
||||
"docs/conf.py" = ["E402"]
|
||||
"scrapy/utils/url.py" = ["F403", "F405"]
|
||||
"tests/test_loader.py" = ["E741"]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "pep257"
|
||||
28
pytest.ini
28
pytest.ini
|
|
@ -1,28 +0,0 @@
|
|||
[pytest]
|
||||
xfail_strict = true
|
||||
usefixtures = chdir
|
||||
python_files=test_*.py __init__.py
|
||||
python_classes=
|
||||
addopts =
|
||||
--assert=plain
|
||||
--ignore=docs/_ext
|
||||
--ignore=docs/conf.py
|
||||
--ignore=docs/news.rst
|
||||
--ignore=docs/topics/dynamic-content.rst
|
||||
--ignore=docs/topics/items.rst
|
||||
--ignore=docs/topics/leaks.rst
|
||||
--ignore=docs/topics/loaders.rst
|
||||
--ignore=docs/topics/selectors.rst
|
||||
--ignore=docs/topics/shell.rst
|
||||
--ignore=docs/topics/stats.rst
|
||||
--ignore=docs/topics/telnetconsole.rst
|
||||
--ignore=docs/utils
|
||||
markers =
|
||||
only_asyncio: marks tests as only enabled when --reactor=asyncio is passed
|
||||
only_not_asyncio: marks tests as only enabled when --reactor=asyncio is not passed
|
||||
requires_uvloop: marks tests as only enabled when uvloop is known to be working
|
||||
filterwarnings =
|
||||
ignore:scrapy.downloadermiddlewares.decompression is deprecated
|
||||
ignore:Module scrapy.utils.reqser is deprecated
|
||||
ignore:typing.re is deprecated
|
||||
ignore:typing.io is deprecated
|
||||
|
|
@ -1 +1 @@
|
|||
2.11.2
|
||||
2.13.3
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import pkgutil
|
|||
import sys
|
||||
import warnings
|
||||
|
||||
from twisted import version as _txv
|
||||
|
||||
# Declare top-level shortcuts
|
||||
from scrapy.http import FormRequest, Request
|
||||
from scrapy.item import Field, Item
|
||||
|
|
@ -15,22 +13,37 @@ from scrapy.selector import Selector
|
|||
from scrapy.spiders import Spider
|
||||
|
||||
__all__ = [
|
||||
"Field",
|
||||
"FormRequest",
|
||||
"Item",
|
||||
"Request",
|
||||
"Selector",
|
||||
"Spider",
|
||||
"__version__",
|
||||
"version_info",
|
||||
"twisted_version",
|
||||
"Spider",
|
||||
"Request",
|
||||
"FormRequest",
|
||||
"Selector",
|
||||
"Item",
|
||||
"Field",
|
||||
]
|
||||
|
||||
|
||||
# Scrapy and Twisted versions
|
||||
__version__ = (pkgutil.get_data(__package__, "VERSION") or b"").decode("ascii").strip()
|
||||
version_info = tuple(int(v) if v.isdigit() else v for v in __version__.split("."))
|
||||
twisted_version = (_txv.major, _txv.minor, _txv.micro)
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if name == "twisted_version":
|
||||
import warnings # noqa: PLC0415 # pylint: disable=reimported
|
||||
|
||||
from twisted import version as _txv # noqa: PLC0415
|
||||
|
||||
from scrapy.exceptions import ScrapyDeprecationWarning # noqa: PLC0415
|
||||
|
||||
warnings.warn(
|
||||
"The scrapy.twisted_version attribute is deprecated, use twisted.version instead",
|
||||
ScrapyDeprecationWarning,
|
||||
)
|
||||
return _txv.major, _txv.minor, _txv.micro
|
||||
|
||||
raise AttributeError
|
||||
|
||||
|
||||
# Ignore noisy twisted deprecation warnings
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from scrapy.utils.misc import build_from_crawler, load_object
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from scrapy.crawler import Crawler
|
||||
from scrapy.settings import Settings
|
||||
from scrapy.settings import BaseSettings, Settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -36,7 +36,8 @@ class AddonManager:
|
|||
try:
|
||||
addoncls = load_object(clspath)
|
||||
addon = build_from_crawler(addoncls, self.crawler)
|
||||
addon.update_settings(settings)
|
||||
if hasattr(addon, "update_settings"):
|
||||
addon.update_settings(settings)
|
||||
self.addons.append(addon)
|
||||
except NotConfigured as e:
|
||||
if e.args:
|
||||
|
|
@ -52,3 +53,20 @@ class AddonManager:
|
|||
},
|
||||
extra={"crawler": self.crawler},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_pre_crawler_settings(cls, settings: BaseSettings):
|
||||
"""Update early settings that do not require a crawler instance, such as SPIDER_MODULES.
|
||||
|
||||
Similar to the load_settings method, this loads each add-on configured in the
|
||||
``ADDONS`` setting and calls their 'update_pre_crawler_settings' class method if present.
|
||||
This method doesn't have access to the crawler instance or the addons list.
|
||||
|
||||
:param settings: The :class:`~scrapy.settings.BaseSettings` object from \
|
||||
which to read the early add-on configuration
|
||||
:type settings: :class:`~scrapy.settings.Settings`
|
||||
"""
|
||||
for clspath in build_component_list(settings["ADDONS"]):
|
||||
addoncls = load_object(clspath)
|
||||
if hasattr(addoncls, "update_pre_crawler_settings"):
|
||||
addoncls.update_pre_crawler_settings(settings)
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ from typing import TYPE_CHECKING
|
|||
|
||||
import scrapy
|
||||
from scrapy.commands import BaseRunSpiderCommand, ScrapyCommand, ScrapyHelpFormatter
|
||||
from scrapy.crawler import CrawlerProcess
|
||||
from scrapy.crawler import AsyncCrawlerProcess, CrawlerProcess
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.utils.misc import walk_modules
|
||||
from scrapy.utils.project import get_project_settings, inside_project
|
||||
from scrapy.utils.python import garbage_collect
|
||||
from scrapy.utils.reactor import _asyncio_reactor_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable
|
||||
|
|
@ -31,8 +32,9 @@ class ScrapyArgumentParser(argparse.ArgumentParser):
|
|||
def _parse_optional(
|
||||
self, arg_string: str
|
||||
) -> tuple[argparse.Action | None, str, str | None] | None:
|
||||
# if starts with -: it means that is a parameter not a argument
|
||||
if arg_string[:2] == "-:":
|
||||
# Support something like ‘-o -:json’, where ‘-:json’ is a value for
|
||||
# ‘-o’, not another parameter.
|
||||
if arg_string.startswith("-:"):
|
||||
return None
|
||||
|
||||
return super()._parse_optional(arg_string)
|
||||
|
|
@ -74,7 +76,7 @@ def _get_commands_from_entry_points(
|
|||
if inspect.isclass(obj):
|
||||
cmds[entry_point.name] = obj()
|
||||
else:
|
||||
raise Exception(f"Invalid entry point {entry_point.name}")
|
||||
raise ValueError(f"Invalid entry point {entry_point.name}")
|
||||
return cmds
|
||||
|
||||
|
||||
|
|
@ -89,13 +91,16 @@ def _get_commands_dict(
|
|||
return cmds
|
||||
|
||||
|
||||
def _get_project_only_cmds(settings: BaseSettings) -> set[str]:
|
||||
return set(_get_commands_dict(settings, inproject=True)) - set(
|
||||
_get_commands_dict(settings, inproject=False)
|
||||
)
|
||||
|
||||
|
||||
def _pop_command_name(argv: list[str]) -> str | None:
|
||||
i = 0
|
||||
for arg in argv[1:]:
|
||||
if not arg.startswith("-"):
|
||||
del argv[i]
|
||||
return arg
|
||||
i += 1
|
||||
for i in range(1, len(argv)):
|
||||
if not argv[i].startswith("-"):
|
||||
return argv.pop(i)
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -123,11 +128,25 @@ def _print_commands(settings: BaseSettings, inproject: bool) -> None:
|
|||
print('Use "scrapy <command> -h" to see more info about a command')
|
||||
|
||||
|
||||
def _print_unknown_command_msg(
|
||||
settings: BaseSettings, cmdname: str, inproject: bool
|
||||
) -> None:
|
||||
proj_only_cmds = _get_project_only_cmds(settings)
|
||||
if cmdname in proj_only_cmds and not inproject:
|
||||
cmd_list = ", ".join(sorted(proj_only_cmds))
|
||||
print(
|
||||
f"The {cmdname} command is not available from this location.\n"
|
||||
f"These commands are only available from within a project: {cmd_list}.\n"
|
||||
)
|
||||
else:
|
||||
print(f"Unknown command: {cmdname}\n")
|
||||
|
||||
|
||||
def _print_unknown_command(
|
||||
settings: BaseSettings, cmdname: str, inproject: bool
|
||||
) -> None:
|
||||
_print_header(settings, inproject)
|
||||
print(f"Unknown command: {cmdname}\n")
|
||||
_print_unknown_command_msg(settings, cmdname, inproject)
|
||||
print('Use "scrapy" to see available commands')
|
||||
|
||||
|
||||
|
|
@ -184,7 +203,13 @@ def execute(argv: list[str] | None = None, settings: Settings | None = None) ->
|
|||
opts, args = parser.parse_known_args(args=argv[1:])
|
||||
_run_print_help(parser, cmd.process_options, args, opts)
|
||||
|
||||
cmd.crawler_process = CrawlerProcess(settings)
|
||||
if cmd.requires_crawler_process:
|
||||
if settings[
|
||||
"TWISTED_REACTOR"
|
||||
] == _asyncio_reactor_path and not settings.getbool("FORCE_CRAWLER_PROCESS"):
|
||||
cmd.crawler_process = AsyncCrawlerProcess(settings)
|
||||
else:
|
||||
cmd.crawler_process = CrawlerProcess(settings)
|
||||
_run_print_help(parser, _run_command, cmd, args, opts)
|
||||
sys.exit(cmd.exitcode)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import builtins
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
|
|
@ -18,12 +19,14 @@ from scrapy.utils.conf import arglist_to_dict, feed_process_params_from_cli
|
|||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
from scrapy.crawler import Crawler, CrawlerProcess
|
||||
from scrapy.crawler import Crawler, CrawlerProcessBase
|
||||
from scrapy.settings import Settings
|
||||
|
||||
|
||||
class ScrapyCommand:
|
||||
class ScrapyCommand(ABC):
|
||||
requires_project: bool = False
|
||||
crawler_process: CrawlerProcess | None = None
|
||||
requires_crawler_process: bool = True
|
||||
crawler_process: CrawlerProcessBase | None = None # set in scrapy.cmdline
|
||||
|
||||
# default settings to be used for this command instead of global defaults
|
||||
default_settings: dict[str, Any] = {}
|
||||
|
|
@ -31,7 +34,7 @@ class ScrapyCommand:
|
|||
exitcode: int = 0
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.settings: Any = None # set in scrapy.cmdline
|
||||
self.settings: Settings | None = None # set in scrapy.cmdline
|
||||
|
||||
def set_crawler(self, crawler: Crawler) -> None:
|
||||
if hasattr(self, "_crawler"):
|
||||
|
|
@ -44,6 +47,7 @@ class ScrapyCommand:
|
|||
"""
|
||||
return ""
|
||||
|
||||
@abstractmethod
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
A short description of the command
|
||||
|
|
@ -68,6 +72,7 @@ class ScrapyCommand:
|
|||
"""
|
||||
Populate option parse with options available for this command
|
||||
"""
|
||||
assert self.settings is not None
|
||||
group = parser.add_argument_group(title="Global Options")
|
||||
group.add_argument(
|
||||
"--logfile", metavar="FILE", help="log file. if omitted stderr will be used"
|
||||
|
|
@ -100,6 +105,7 @@ class ScrapyCommand:
|
|||
group.add_argument("--pdb", action="store_true", help="enable pdb on failure")
|
||||
|
||||
def process_options(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
assert self.settings is not None
|
||||
try:
|
||||
self.settings.setdict(arglist_to_dict(opts.set), priority="cmdline")
|
||||
except ValueError:
|
||||
|
|
@ -124,6 +130,7 @@ class ScrapyCommand:
|
|||
if opts.pdb:
|
||||
failure.startDebugMode()
|
||||
|
||||
@abstractmethod
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
"""
|
||||
Entry point for running commands
|
||||
|
|
@ -162,12 +169,6 @@ class BaseRunSpiderCommand(ScrapyCommand):
|
|||
help="dump scraped items into FILE, overwriting any existing file,"
|
||||
" to define format set a colon at the end of the output URI (i.e. -O FILE:FORMAT)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--output-format",
|
||||
metavar="FORMAT",
|
||||
help="format to use for dumping items",
|
||||
)
|
||||
|
||||
def process_options(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
super().process_options(args, opts)
|
||||
|
|
@ -176,11 +177,11 @@ class BaseRunSpiderCommand(ScrapyCommand):
|
|||
except ValueError:
|
||||
raise UsageError("Invalid -a value, use -a NAME=VALUE", print_help=False)
|
||||
if opts.output or opts.overwrite_output:
|
||||
assert self.settings is not None
|
||||
feeds = feed_process_params_from_cli(
|
||||
self.settings,
|
||||
opts.output,
|
||||
opts.output_format,
|
||||
opts.overwrite_output,
|
||||
overwrite_output=opts.overwrite_output,
|
||||
)
|
||||
self.settings.set("FEEDS", feeds, priority="cmdline")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess # nosec
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
|
@ -11,11 +10,11 @@ import scrapy
|
|||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.http import Response, TextResponse
|
||||
from scrapy.linkextractors import LinkExtractor
|
||||
from scrapy.utils.test import get_testenv
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
from scrapy import Request
|
||||
import argparse
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
|
|
@ -37,12 +36,10 @@ class Command(ScrapyCommand):
|
|||
|
||||
class _BenchServer:
|
||||
def __enter__(self) -> None:
|
||||
from scrapy.utils.test import get_testenv
|
||||
|
||||
pargs = [sys.executable, "-u", "-m", "scrapy.utils.benchserver"]
|
||||
self.proc = subprocess.Popen(
|
||||
self.proc = subprocess.Popen( # noqa: S603
|
||||
pargs, stdout=subprocess.PIPE, env=get_testenv()
|
||||
) # nosec
|
||||
)
|
||||
assert self.proc.stdout
|
||||
self.proc.stdout.readline()
|
||||
|
||||
|
|
@ -61,12 +58,12 @@ class _BenchSpider(scrapy.Spider):
|
|||
baseurl = "http://localhost:8998"
|
||||
link_extractor = LinkExtractor()
|
||||
|
||||
def start_requests(self) -> Iterable[Request]:
|
||||
async def start(self) -> AsyncIterator[Any]:
|
||||
qargs = {"total": self.total, "show": self.show}
|
||||
url = f"{self.baseurl}?{urlencode(qargs, doseq=True)}"
|
||||
return [scrapy.Request(url, dont_filter=True)]
|
||||
yield scrapy.Request(url, dont_filter=True)
|
||||
|
||||
def parse(self, response: Response) -> Any:
|
||||
assert isinstance(Response, TextResponse)
|
||||
assert isinstance(response, TextResponse)
|
||||
for link in self.link_extractor.extract_links(response):
|
||||
yield scrapy.Request(link.url, callback=self.parse)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ class Command(ScrapyCommand):
|
|||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
# load contracts
|
||||
assert self.settings is not None
|
||||
contracts = build_component_list(self.settings.getwithbase("SPIDER_CONTRACTS"))
|
||||
conman = ContractsManager(load_object(c) for c in contracts)
|
||||
runner = TextTestRunner(verbosity=2 if opts.verbose else 1)
|
||||
|
|
@ -80,10 +81,14 @@ class Command(ScrapyCommand):
|
|||
assert self.crawler_process
|
||||
spider_loader = self.crawler_process.spider_loader
|
||||
|
||||
async def start(self):
|
||||
for request in conman.from_spider(self, result):
|
||||
yield request
|
||||
|
||||
with set_environ(SCRAPY_CHECK="true"):
|
||||
for spidername in args or spider_loader.list():
|
||||
spidercls = spider_loader.load(spidername)
|
||||
spidercls.start_requests = lambda s: conman.from_spider(s, result) # type: ignore[assignment,method-assign,return-value]
|
||||
spidercls.start = start # type: ignore[assignment,method-assign,return-value]
|
||||
|
||||
tested_methods = conman.tested_methods_from_spidercls(spidercls)
|
||||
if opts.list:
|
||||
|
|
@ -101,10 +106,10 @@ class Command(ScrapyCommand):
|
|||
for method in sorted(methods):
|
||||
print(f" * {method}")
|
||||
else:
|
||||
start = time.time()
|
||||
start_time = time.time()
|
||||
self.crawler_process.start()
|
||||
stop = time.time()
|
||||
|
||||
result.printErrors()
|
||||
result.printSummary(start, stop)
|
||||
result.printSummary(start_time, stop)
|
||||
self.exitcode = int(not result.wasSuccessful())
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from scrapy.commands import BaseRunSpiderCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
|
|
@ -22,26 +20,15 @@ class Command(BaseRunSpiderCommand):
|
|||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
if len(args) < 1:
|
||||
raise UsageError()
|
||||
elif len(args) > 1:
|
||||
raise UsageError
|
||||
if len(args) > 1:
|
||||
raise UsageError(
|
||||
"running 'scrapy crawl' with more than one spider is not supported"
|
||||
)
|
||||
spname = args[0]
|
||||
|
||||
assert self.crawler_process
|
||||
crawl_defer = self.crawler_process.crawl(spname, **opts.spargs)
|
||||
|
||||
if getattr(crawl_defer, "result", None) is not None and issubclass(
|
||||
cast(Failure, crawl_defer.result).type, Exception
|
||||
):
|
||||
self.crawler_process.crawl(spname, **opts.spargs)
|
||||
self.crawler_process.start()
|
||||
if self.crawler_process.bootstrap_failed:
|
||||
self.exitcode = 1
|
||||
else:
|
||||
self.crawler_process.start()
|
||||
|
||||
if (
|
||||
self.crawler_process.bootstrap_failed
|
||||
or hasattr(self.crawler_process, "has_exception")
|
||||
and self.crawler_process.has_exception
|
||||
):
|
||||
self.exitcode = 1
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import sys
|
|||
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.spiderloader import get_spider_loader
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_project = True
|
||||
requires_crawler_process = False
|
||||
default_settings = {"LOG_ENABLED": False}
|
||||
|
||||
def syntax(self) -> str:
|
||||
|
|
@ -28,16 +30,18 @@ class Command(ScrapyCommand):
|
|||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
if len(args) != 1:
|
||||
raise UsageError()
|
||||
raise UsageError
|
||||
|
||||
assert self.settings is not None
|
||||
editor = self.settings["EDITOR"]
|
||||
assert self.crawler_process
|
||||
spider_loader = get_spider_loader(self.settings)
|
||||
try:
|
||||
spidercls = self.crawler_process.spider_loader.load(args[0])
|
||||
spidercls = spider_loader.load(args[0])
|
||||
except KeyError:
|
||||
return self._err(f"Spider not found: {args[0]}")
|
||||
self._err(f"Spider not found: {args[0]}")
|
||||
return
|
||||
|
||||
sfile = sys.modules[spidercls.__module__].__file__
|
||||
assert sfile
|
||||
sfile = sfile.replace(".pyc", ".py")
|
||||
self.exitcode = os.system(f'{editor} "{sfile}"') # nosec
|
||||
self.exitcode = os.system(f'{editor} "{sfile}"') # noqa: S605
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from argparse import Namespace # noqa: TC003
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from w3lib.url import is_url
|
||||
|
||||
from scrapy import Spider
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.http import Request, Response
|
||||
|
|
@ -13,12 +13,12 @@ from scrapy.utils.datatypes import SequenceExclude
|
|||
from scrapy.utils.spider import DefaultSpider, spidercls_for_request
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from scrapy import Spider
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_project = False
|
||||
|
||||
def syntax(self) -> str:
|
||||
return "[options] <url>"
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ class Command(ScrapyCommand):
|
|||
|
||||
def run(self, args: list[str], opts: Namespace) -> None:
|
||||
if len(args) != 1 or not is_url(args[0]):
|
||||
raise UsageError()
|
||||
raise UsageError
|
||||
request = Request(
|
||||
args[0],
|
||||
callback=self._print_response,
|
||||
|
|
@ -88,5 +88,11 @@ class Command(ScrapyCommand):
|
|||
spidercls = spider_loader.load(opts.spider)
|
||||
else:
|
||||
spidercls = spidercls_for_request(spider_loader, request, spidercls)
|
||||
self.crawler_process.crawl(spidercls, start_requests=lambda: [request])
|
||||
|
||||
async def start(self):
|
||||
yield request
|
||||
|
||||
spidercls.start = start # type: ignore[method-assign,attr-defined]
|
||||
|
||||
self.crawler_process.crawl(spidercls)
|
||||
self.crawler_process.start()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import string
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import scrapy
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.spiderloader import get_spider_loader
|
||||
from scrapy.utils.template import render_templatefile, string_camelcase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
|
||||
|
||||
def sanitize_module_name(module_name: str) -> str:
|
||||
"""Sanitize the given module name, by replacing dashes and points
|
||||
|
|
@ -43,7 +46,7 @@ def verify_url_scheme(url: str) -> str:
|
|||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_project = False
|
||||
requires_crawler_process = False
|
||||
default_settings = {"LOG_ENABLED": False}
|
||||
|
||||
def syntax(self) -> str:
|
||||
|
|
@ -90,6 +93,7 @@ class Command(ScrapyCommand):
|
|||
)
|
||||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
assert self.settings is not None
|
||||
if opts.list:
|
||||
self._list_templates()
|
||||
return
|
||||
|
|
@ -99,7 +103,7 @@ class Command(ScrapyCommand):
|
|||
print(template_file.read_text(encoding="utf-8"))
|
||||
return
|
||||
if len(args) != 2:
|
||||
raise UsageError()
|
||||
raise UsageError
|
||||
|
||||
name, url = args[0:2]
|
||||
url = verify_url_scheme(url)
|
||||
|
|
@ -116,7 +120,7 @@ class Command(ScrapyCommand):
|
|||
if template_file:
|
||||
self._genspider(module, name, url, opts.template, template_file)
|
||||
if opts.edit:
|
||||
self.exitcode = os.system(f'scrapy edit "{name}"') # nosec
|
||||
self.exitcode = os.system(f'scrapy edit "{name}"') # noqa: S605
|
||||
|
||||
def _generate_template_variables(
|
||||
self,
|
||||
|
|
@ -125,6 +129,7 @@ class Command(ScrapyCommand):
|
|||
url: str,
|
||||
template_name: str,
|
||||
) -> dict[str, Any]:
|
||||
assert self.settings is not None
|
||||
capitalized_module = "".join(s.capitalize() for s in module.split("_"))
|
||||
return {
|
||||
"project_name": self.settings.get("BOT_NAME"),
|
||||
|
|
@ -145,6 +150,7 @@ class Command(ScrapyCommand):
|
|||
template_file: str | os.PathLike,
|
||||
) -> None:
|
||||
"""Generate the spider module, based on the given template"""
|
||||
assert self.settings is not None
|
||||
tvars = self._generate_template_variables(module, name, url, template_name)
|
||||
if self.settings.get("NEWSPIDER_MODULE"):
|
||||
spiders_module = import_module(self.settings["NEWSPIDER_MODULE"])
|
||||
|
|
@ -152,7 +158,7 @@ class Command(ScrapyCommand):
|
|||
spiders_dir = Path(spiders_module.__file__).parent.resolve()
|
||||
else:
|
||||
spiders_module = None
|
||||
spiders_dir = Path(".")
|
||||
spiders_dir = Path()
|
||||
spider_file = f"{spiders_dir / module}.py"
|
||||
shutil.copyfile(template_file, spider_file)
|
||||
render_templatefile(spider_file, **tvars)
|
||||
|
|
@ -178,6 +184,7 @@ class Command(ScrapyCommand):
|
|||
print(f" {file.stem}")
|
||||
|
||||
def _spider_exists(self, name: str) -> bool:
|
||||
assert self.settings is not None
|
||||
if not self.settings.get("NEWSPIDER_MODULE"):
|
||||
# if run as a standalone command and file with same filename already exists
|
||||
path = Path(name + ".py")
|
||||
|
|
@ -186,12 +193,9 @@ class Command(ScrapyCommand):
|
|||
return True
|
||||
return False
|
||||
|
||||
assert (
|
||||
self.crawler_process is not None
|
||||
), "crawler_process must be set before calling run"
|
||||
|
||||
spider_loader = get_spider_loader(self.settings)
|
||||
try:
|
||||
spidercls = self.crawler_process.spider_loader.load(name)
|
||||
spidercls = spider_loader.load(name)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
|
|
@ -202,7 +206,7 @@ class Command(ScrapyCommand):
|
|||
|
||||
# a file with the same name exists in the target directory
|
||||
spiders_module = import_module(self.settings["NEWSPIDER_MODULE"])
|
||||
spiders_dir = Path(cast(str, spiders_module.__file__)).parent
|
||||
spiders_dir = Path(cast("str", spiders_module.__file__)).parent
|
||||
spiders_dir_abs = spiders_dir.resolve()
|
||||
path = spiders_dir_abs / (name + ".py")
|
||||
if path.exists():
|
||||
|
|
@ -213,6 +217,7 @@ class Command(ScrapyCommand):
|
|||
|
||||
@property
|
||||
def templates_dir(self) -> str:
|
||||
assert self.settings is not None
|
||||
return str(
|
||||
Path(
|
||||
self.settings["TEMPLATES_DIR"] or Path(scrapy.__path__[0], "templates"),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.spiderloader import get_spider_loader
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
|
|
@ -10,12 +11,14 @@ if TYPE_CHECKING:
|
|||
|
||||
class Command(ScrapyCommand):
|
||||
requires_project = True
|
||||
requires_crawler_process = False
|
||||
default_settings = {"LOG_ENABLED": False}
|
||||
|
||||
def short_desc(self) -> str:
|
||||
return "List available spiders"
|
||||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
assert self.crawler_process
|
||||
for s in sorted(self.crawler_process.spider_loader.list()):
|
||||
assert self.settings is not None
|
||||
spider_loader = get_spider_loader(self.settings)
|
||||
for s in sorted(spider_loader.list()):
|
||||
print(s)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
||||
|
||||
from itemadapter import ItemAdapter, is_item
|
||||
from itemadapter import ItemAdapter
|
||||
from twisted.internet.defer import Deferred, maybeDeferred
|
||||
from w3lib.url import is_url
|
||||
|
||||
|
|
@ -17,12 +16,14 @@ from scrapy.http import Request, Response
|
|||
from scrapy.utils import display
|
||||
from scrapy.utils.asyncgen import collect_asyncgen
|
||||
from scrapy.utils.defer import aiter_errback, deferred_from_coro
|
||||
from scrapy.utils.deprecate import argument_is_required
|
||||
from scrapy.utils.log import failure_to_exc_info
|
||||
from scrapy.utils.misc import arg_to_iter
|
||||
from scrapy.utils.spider import spidercls_for_request
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncGenerator, Coroutine, Iterable
|
||||
import argparse
|
||||
from collections.abc import AsyncGenerator, AsyncIterator, Coroutine, Iterable
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
|
|
@ -174,13 +175,12 @@ class Command(BaseRunSpiderCommand):
|
|||
display.pprint([ItemAdapter(x).asdict() for x in items], colorize=colour)
|
||||
|
||||
def print_requests(self, lvl: int | None = None, colour: bool = True) -> None:
|
||||
if lvl is None:
|
||||
if self.requests:
|
||||
requests = self.requests[max(self.requests)]
|
||||
else:
|
||||
requests = []
|
||||
else:
|
||||
if lvl is not None:
|
||||
requests = self.requests.get(lvl, [])
|
||||
elif self.requests:
|
||||
requests = self.requests[max(self.requests)]
|
||||
else:
|
||||
requests = []
|
||||
|
||||
print("# Requests ", "-" * 65)
|
||||
display.pprint(requests, colorize=colour)
|
||||
|
|
@ -212,10 +212,10 @@ class Command(BaseRunSpiderCommand):
|
|||
) -> tuple[list[Any], list[Request], argparse.Namespace, int, Spider, CallbackT]:
|
||||
items, requests = [], []
|
||||
for x in spider_output:
|
||||
if is_item(x):
|
||||
items.append(x)
|
||||
elif isinstance(x, Request):
|
||||
if isinstance(x, Request):
|
||||
requests.append(x)
|
||||
else:
|
||||
items.append(x)
|
||||
return items, requests, opts, depth, spider, callback
|
||||
|
||||
def run_callback(
|
||||
|
|
@ -225,8 +225,9 @@ class Command(BaseRunSpiderCommand):
|
|||
cb_kwargs: dict[str, Any] | None = None,
|
||||
) -> Deferred[Any]:
|
||||
cb_kwargs = cb_kwargs or {}
|
||||
d = maybeDeferred(self.iterate_spider_output, callback(response, **cb_kwargs))
|
||||
return d
|
||||
return maybeDeferred(
|
||||
self.iterate_spider_output, callback(response, **cb_kwargs)
|
||||
)
|
||||
|
||||
def get_callback_from_rules(
|
||||
self, spider: Spider, response: Response
|
||||
|
|
@ -258,17 +259,17 @@ class Command(BaseRunSpiderCommand):
|
|||
if not self.spidercls:
|
||||
logger.error("Unable to find spider for: %(url)s", {"url": url})
|
||||
|
||||
def _start_requests(spider: Spider) -> Iterable[Request]:
|
||||
async def start(spider: Spider) -> AsyncIterator[Any]:
|
||||
yield self.prepare_request(spider, Request(url), opts)
|
||||
|
||||
if self.spidercls:
|
||||
self.spidercls.start_requests = _start_requests # type: ignore[assignment,method-assign]
|
||||
self.spidercls.start = start # type: ignore[assignment,method-assign]
|
||||
|
||||
def start_parsing(self, url: str, opts: argparse.Namespace) -> None:
|
||||
assert self.crawler_process
|
||||
assert self.spidercls
|
||||
self.crawler_process.crawl(self.spidercls, **opts.spargs)
|
||||
self.pcrawler = list(self.crawler_process.crawlers)[0]
|
||||
self.pcrawler = next(iter(self.crawler_process.crawlers))
|
||||
self.crawler_process.start()
|
||||
|
||||
if not self.first_response:
|
||||
|
|
@ -282,9 +283,14 @@ class Command(BaseRunSpiderCommand):
|
|||
) -> list[Any]:
|
||||
items, requests, opts, depth, spider, callback = args
|
||||
if opts.pipelines:
|
||||
assert self.pcrawler.engine
|
||||
itemproc = self.pcrawler.engine.scraper.itemproc
|
||||
needs_spider = argument_is_required(itemproc.process_item, "spider")
|
||||
for item in items:
|
||||
itemproc.process_item(item, spider)
|
||||
if needs_spider:
|
||||
itemproc.process_item(item, spider)
|
||||
else:
|
||||
itemproc.process_item(item)
|
||||
self.add_items(depth, items)
|
||||
self.add_requests(depth, requests)
|
||||
|
||||
|
|
@ -398,9 +404,8 @@ class Command(BaseRunSpiderCommand):
|
|||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
# parse arguments
|
||||
if not len(args) == 1 or not is_url(args[0]):
|
||||
raise UsageError()
|
||||
else:
|
||||
url = args[0]
|
||||
raise UsageError
|
||||
url = args[0]
|
||||
|
||||
# prepare spidercls
|
||||
self.set_spidercls(url, opts)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
|
|
@ -8,9 +7,11 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from scrapy.commands import BaseRunSpiderCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.spiderloader import DummySpiderLoader
|
||||
from scrapy.utils.spider import iter_spider_classes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
from os import PathLike
|
||||
from types import ModuleType
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ def _import_file(filepath: str | PathLike[str]) -> ModuleType:
|
|||
if abspath.suffix not in (".py", ".pyw"):
|
||||
raise ValueError(f"Not a Python source file: {abspath}")
|
||||
dirname = str(abspath.parent)
|
||||
sys.path = [dirname] + sys.path
|
||||
sys.path = [dirname, *sys.path]
|
||||
try:
|
||||
module = import_module(abspath.stem)
|
||||
finally:
|
||||
|
|
@ -29,8 +30,7 @@ def _import_file(filepath: str | PathLike[str]) -> ModuleType:
|
|||
|
||||
|
||||
class Command(BaseRunSpiderCommand):
|
||||
requires_project = False
|
||||
default_settings = {"SPIDER_LOADER_WARN_ONLY": True}
|
||||
default_settings = {"SPIDER_LOADER_CLASS": DummySpiderLoader}
|
||||
|
||||
def syntax(self) -> str:
|
||||
return "[options] <spider_file>"
|
||||
|
|
@ -43,7 +43,7 @@ class Command(BaseRunSpiderCommand):
|
|||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
if len(args) != 1:
|
||||
raise UsageError()
|
||||
raise UsageError
|
||||
filename = Path(args[0])
|
||||
if not filename.exists():
|
||||
raise UsageError(f"File not found: {filename}\n")
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ from scrapy.settings import BaseSettings
|
|||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_project = False
|
||||
default_settings = {"LOG_ENABLED": False, "SPIDER_LOADER_WARN_ONLY": True}
|
||||
requires_crawler_process = False
|
||||
default_settings = {"LOG_ENABLED": False}
|
||||
|
||||
def syntax(self) -> str:
|
||||
return "[options]"
|
||||
|
|
@ -46,8 +46,8 @@ class Command(ScrapyCommand):
|
|||
)
|
||||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
assert self.crawler_process
|
||||
settings = self.crawler_process.settings
|
||||
assert self.settings is not None
|
||||
settings = self.settings
|
||||
if opts.get:
|
||||
s = settings.get(opts.get)
|
||||
if isinstance(s, BaseSettings):
|
||||
|
|
|
|||
|
|
@ -9,23 +9,24 @@ from __future__ import annotations
|
|||
from threading import Thread
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from scrapy import Spider
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.http import Request
|
||||
from scrapy.shell import Shell
|
||||
from scrapy.utils.defer import _schedule_coro
|
||||
from scrapy.utils.spider import DefaultSpider, spidercls_for_request
|
||||
from scrapy.utils.url import guess_scheme
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import ArgumentParser, Namespace
|
||||
|
||||
from scrapy import Spider
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_project = False
|
||||
default_settings = {
|
||||
"DUPEFILTER_CLASS": "scrapy.dupefilters.BaseDupeFilter",
|
||||
"KEEP_ALIVE": True,
|
||||
"LOGSTATS_INTERVAL": 0,
|
||||
"DUPEFILTER_CLASS": "scrapy.dupefilters.BaseDupeFilter",
|
||||
}
|
||||
|
||||
def syntax(self) -> str:
|
||||
|
|
@ -56,11 +57,10 @@ class Command(ScrapyCommand):
|
|||
help="do not handle HTTP 3xx status codes and print response as-is",
|
||||
)
|
||||
|
||||
def update_vars(self, vars: dict[str, Any]) -> None:
|
||||
def update_vars(self, vars: dict[str, Any]) -> None: # noqa: A002
|
||||
"""You can use this function to update the Scrapy objects that will be
|
||||
available in the shell
|
||||
"""
|
||||
pass
|
||||
|
||||
def run(self, args: list[str], opts: Namespace) -> None:
|
||||
url = args[0] if args else None
|
||||
|
|
@ -85,7 +85,7 @@ class Command(ScrapyCommand):
|
|||
crawler._apply_settings()
|
||||
# The Shell class needs a persistent engine in the crawler
|
||||
crawler.engine = crawler._create_engine()
|
||||
crawler.engine.start()
|
||||
_schedule_coro(crawler.engine.start_async(_start_request_processing=False))
|
||||
|
||||
self._start_crawler_thread()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import string
|
||||
from importlib.util import find_spec
|
||||
from pathlib import Path
|
||||
from shutil import copy2, copystat, ignore_patterns, move
|
||||
from stat import S_IWUSR as OWNER_WRITE_PERMISSION
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import scrapy
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.utils.template import render_templatefile, string_camelcase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
|
||||
TEMPLATES_TO_RENDER: tuple[tuple[str, ...], ...] = (
|
||||
("scrapy.cfg",),
|
||||
("${project_name}", "settings.py.tmpl"),
|
||||
|
|
@ -25,14 +27,14 @@ TEMPLATES_TO_RENDER: tuple[tuple[str, ...], ...] = (
|
|||
IGNORE = ignore_patterns("*.pyc", "__pycache__", ".svn")
|
||||
|
||||
|
||||
def _make_writable(path: str | os.PathLike) -> None:
|
||||
current_permissions = os.stat(path).st_mode
|
||||
os.chmod(path, current_permissions | OWNER_WRITE_PERMISSION)
|
||||
def _make_writable(path: Path) -> None:
|
||||
current_permissions = path.stat().st_mode
|
||||
path.chmod(current_permissions | OWNER_WRITE_PERMISSION)
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_project = False
|
||||
default_settings = {"LOG_ENABLED": False, "SPIDER_LOADER_WARN_ONLY": True}
|
||||
requires_crawler_process = False
|
||||
default_settings = {"LOG_ENABLED": False}
|
||||
|
||||
def syntax(self) -> str:
|
||||
return "<project_name> [project_dir]"
|
||||
|
|
@ -89,14 +91,11 @@ class Command(ScrapyCommand):
|
|||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
if len(args) not in (1, 2):
|
||||
raise UsageError()
|
||||
raise UsageError
|
||||
|
||||
project_name = args[0]
|
||||
|
||||
if len(args) == 2:
|
||||
project_dir = Path(args[1])
|
||||
else:
|
||||
project_dir = Path(args[0])
|
||||
project_dir = Path(args[-1])
|
||||
|
||||
if (project_dir / "scrapy.cfg").exists():
|
||||
self.exitcode = 1
|
||||
|
|
@ -133,6 +132,7 @@ class Command(ScrapyCommand):
|
|||
|
||||
@property
|
||||
def templates_dir(self) -> str:
|
||||
assert self.settings is not None
|
||||
return str(
|
||||
Path(
|
||||
self.settings["TEMPLATES_DIR"] or Path(scrapy.__path__[0], "templates"),
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ import argparse
|
|||
|
||||
import scrapy
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.utils.versions import scrapy_components_versions
|
||||
from scrapy.utils.versions import get_versions
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
default_settings = {"LOG_ENABLED": False, "SPIDER_LOADER_WARN_ONLY": True}
|
||||
requires_crawler_process = False
|
||||
default_settings = {"LOG_ENABLED": False}
|
||||
|
||||
def syntax(self) -> str:
|
||||
return "[-v]"
|
||||
|
|
@ -26,7 +27,7 @@ class Command(ScrapyCommand):
|
|||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
if opts.verbose:
|
||||
versions = scrapy_components_versions()
|
||||
versions = get_versions()
|
||||
width = max(len(n) for (n, _) in versions)
|
||||
for name, version in versions:
|
||||
print(f"{name:<{width}} : {version}")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue