mirror of https://github.com/scrapy/scrapy.git
Merge remote-tracking branch 'origin/master' into force-crawler-process-tip
This commit is contained in:
commit
6e98cdba42
|
|
@ -0,0 +1,12 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
name: Auto-close LLM PRs
|
||||
on:
|
||||
# The workflow only reads the pull request body through the API, it never
|
||||
# checks out or runs pull request code, so pull_request_target is safe here.
|
||||
on: # zizmor: ignore[dangerous-triggers]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
permissions:
|
||||
|
|
@ -11,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR body and close if LLM-written
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
name: Checks
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -13,6 +17,10 @@ concurrency:
|
|||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Make uv use the interpreter that actions/setup-python installed instead
|
||||
# of downloading one of its own.
|
||||
UV_PYTHON_PREFERENCE: only-system
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -38,21 +46,31 @@ jobs:
|
|||
TOXENV: twinecheck
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
cache-dependency-glob: |
|
||||
docs/requirements.txt
|
||||
pyproject.toml
|
||||
tox.ini
|
||||
|
||||
- name: Run check
|
||||
env: ${{ matrix.env }}
|
||||
run: |
|
||||
pip install -U tox
|
||||
tox
|
||||
run: uvx --with tox-uv tox
|
||||
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
name: codspeed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- scrapy/**
|
||||
- tests/benchmarks/**
|
||||
- .github/workflows/codspeed.yml
|
||||
- tox.ini
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Make uv use the interpreter that actions/setup-python installed
|
||||
# instead of downloading one of its own.
|
||||
UV_PYTHON_PREFERENCE: only-system
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # OIDC authentication with CodSpeed
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
tox.ini
|
||||
|
||||
- name: Install dependencies
|
||||
# tox must stay on PATH for the CodSpeed action to invoke it.
|
||||
run: |
|
||||
uv tool install --with tox-uv tox
|
||||
tox -n -e benchmark
|
||||
- name: Run benchmarks
|
||||
uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5.0.2
|
||||
with:
|
||||
mode: simulation
|
||||
run: tox -e benchmark
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
name: Publish
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
|
|
@ -9,8 +13,28 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build distribution
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
- run: |
|
||||
python -m pip install --upgrade build
|
||||
python -m build
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
|
||||
publish:
|
||||
name: Upload release to PyPI
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
|
|
@ -18,12 +42,9 @@ jobs:
|
|||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
python-version: "3.14"
|
||||
- run: |
|
||||
python -m pip install --upgrade build
|
||||
python -m build
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
name: macOS
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -12,39 +16,62 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
tests:
|
||||
name: tests (${{ matrix.python-version }}, ${{ matrix.env.TOXENV }})
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
PYTEST_ADDOPTS: -n auto
|
||||
PYTEST_ADDOPTS: ${{ matrix.coverage && '-n auto' || '-n auto --no-cov' }}
|
||||
# Make uv use the interpreter that actions/setup-python installed instead
|
||||
# of downloading one of its own.
|
||||
UV_PYTHON_PREFERENCE: only-system
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
env:
|
||||
- TOXENV: py
|
||||
include:
|
||||
- python-version: '3.14'
|
||||
env:
|
||||
TOXENV: no-reactor
|
||||
- python-version: "3.10"
|
||||
env:
|
||||
TOXENV: py
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: py
|
||||
coverage: true
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: no-reactor
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
tox.ini
|
||||
|
||||
- name: Install mitmproxy
|
||||
env:
|
||||
# mitmproxy needs a newer Python than the oldest matrix entries, so let
|
||||
# uv download one where no system interpreter is new enough.
|
||||
UV_PYTHON_PREFERENCE: system
|
||||
run: uv tool install mitmproxy
|
||||
|
||||
- name: Run tests
|
||||
env: ${{ matrix.env }}
|
||||
run: |
|
||||
pip install -U tox
|
||||
tox
|
||||
run: uvx --with tox-uv tox
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v5
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
report_type: test_results
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
name: Ubuntu
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -12,9 +16,13 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
tests:
|
||||
name: tests (${{ matrix.python-version }}, ${{ matrix.env.TOXENV }})
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PYTEST_ADDOPTS: -n auto
|
||||
PYTEST_ADDOPTS: ${{ matrix.coverage && '-n auto' || '-n auto --no-cov' }}
|
||||
# Make uv use the interpreter that actions/setup-python installed instead
|
||||
# of downloading one of its own.
|
||||
UV_PYTHON_PREFERENCE: only-system
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -34,27 +42,25 @@ jobs:
|
|||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: py
|
||||
coverage: true
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: default-reactor
|
||||
coverage: true
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: no-reactor
|
||||
# pinned due to https://github.com/pypy/pypy/issues/5388
|
||||
- python-version: pypy3.11-7.3.20
|
||||
env:
|
||||
TOXENV: pypy3
|
||||
coverage: true
|
||||
|
||||
# min deps
|
||||
- python-version: "3.10.19"
|
||||
env:
|
||||
TOXENV: min
|
||||
coverage: true
|
||||
- python-version: "3.10.19"
|
||||
env:
|
||||
TOXENV: min-default-reactor
|
||||
- python-version: "3.10.19"
|
||||
env:
|
||||
TOXENV: min-no-reactor
|
||||
coverage: true
|
||||
# pinned due to https://github.com/pypy/pypy/issues/5388
|
||||
- python-version: pypy3.11-7.3.20
|
||||
env:
|
||||
|
|
@ -62,16 +68,20 @@ jobs:
|
|||
- python-version: "3.10.19"
|
||||
env:
|
||||
TOXENV: min-extra-deps
|
||||
coverage: true
|
||||
- python-version: "3.10.19"
|
||||
env:
|
||||
TOXENV: min-botocore
|
||||
coverage: true
|
||||
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: extra-deps
|
||||
coverage: true
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: no-reactor-extra-deps
|
||||
coverage: true
|
||||
# pinned due to https://github.com/pypy/pypy/issues/5388
|
||||
- python-version: pypy3.11-7.3.20
|
||||
env:
|
||||
|
|
@ -79,15 +89,15 @@ jobs:
|
|||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: botocore
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: mitmproxy
|
||||
coverage: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
|
@ -97,17 +107,32 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install libxml2-dev libxslt-dev
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
tox.ini
|
||||
|
||||
- name: Install mitmproxy
|
||||
env:
|
||||
# mitmproxy needs a newer Python than the oldest matrix entries, so let
|
||||
# uv download one where no system interpreter is new enough.
|
||||
UV_PYTHON_PREFERENCE: system
|
||||
# mitmproxy has no PyPy wheels, so run it on CPython regardless of the
|
||||
# interpreter under test.
|
||||
run: uv tool install --python cpython mitmproxy
|
||||
|
||||
- name: Run tests
|
||||
env: ${{ matrix.env }}
|
||||
run: |
|
||||
pip install -U tox
|
||||
tox
|
||||
run: uvx --with tox-uv tox
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v5
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
report_type: test_results
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
name: Windows
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -12,9 +16,13 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
tests:
|
||||
name: tests (${{ matrix.python-version }}, ${{ matrix.env.TOXENV }})
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
PYTEST_ADDOPTS: -n auto
|
||||
PYTEST_ADDOPTS: ${{ matrix.coverage && '-n auto' || '-n auto --no-cov' }}
|
||||
# Make uv use the interpreter that actions/setup-python installed instead
|
||||
# of downloading one of its own.
|
||||
UV_PYTHON_PREFERENCE: only-system
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -22,21 +30,10 @@ jobs:
|
|||
- python-version: "3.10"
|
||||
env:
|
||||
TOXENV: py
|
||||
- python-version: "3.11"
|
||||
env:
|
||||
TOXENV: py
|
||||
- python-version: "3.12"
|
||||
env:
|
||||
TOXENV: py
|
||||
- python-version: "3.13"
|
||||
env:
|
||||
TOXENV: py
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: py
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: default-reactor
|
||||
coverage: true
|
||||
- python-version: "3.14"
|
||||
env:
|
||||
TOXENV: no-reactor
|
||||
|
|
@ -54,24 +51,39 @@ jobs:
|
|||
TOXENV: extra-deps
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
tox.ini
|
||||
|
||||
- name: Install mitmproxy
|
||||
env:
|
||||
# mitmproxy needs a newer Python than the oldest matrix entries, so let
|
||||
# uv download one where no system interpreter is new enough.
|
||||
UV_PYTHON_PREFERENCE: system
|
||||
run: uv tool install mitmproxy
|
||||
|
||||
- name: Run tests
|
||||
env: ${{ matrix.env }}
|
||||
run: |
|
||||
pip install -U tox
|
||||
tox
|
||||
run: uvx --with tox-uv tox
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v5
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
report_type: test_results
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ exclude: |
|
|||
)
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.2
|
||||
rev: v0.15.20
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [ --fix ]
|
||||
|
|
@ -16,7 +16,7 @@ repos:
|
|||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies:
|
||||
- black==25.9.0
|
||||
- black==26.5.1
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
|
|
@ -27,6 +27,11 @@ repos:
|
|||
hooks:
|
||||
- id: sphinx-lint
|
||||
- repo: https://github.com/scrapy/sphinx-scrapy
|
||||
rev: 0.8.8
|
||||
rev: 0.8.10
|
||||
hooks:
|
||||
- id: sphinx-scrapy
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.28.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
args: [--no-progress, --fix]
|
||||
|
|
|
|||
16
README.rst
16
README.rst
|
|
@ -5,7 +5,7 @@
|
|||
:alt: Scrapy
|
||||
:width: 480px
|
||||
|
||||
|version| |python_version| |ubuntu| |macos| |windows| |coverage| |conda| |deepwiki|
|
||||
|version| |python_version| |tests| |coverage| |conda| |deepwiki|
|
||||
|
||||
.. |version| image:: https://img.shields.io/pypi/v/Scrapy.svg
|
||||
:target: https://pypi.org/pypi/Scrapy
|
||||
|
|
@ -15,17 +15,9 @@
|
|||
:target: https://pypi.org/pypi/Scrapy
|
||||
:alt: Supported Python Versions
|
||||
|
||||
.. |ubuntu| image:: https://github.com/scrapy/scrapy/workflows/Ubuntu/badge.svg
|
||||
:target: https://github.com/scrapy/scrapy/actions?query=workflow%3AUbuntu
|
||||
:alt: Ubuntu
|
||||
|
||||
.. |macos| image:: https://github.com/scrapy/scrapy/workflows/macOS/badge.svg
|
||||
:target: https://github.com/scrapy/scrapy/actions?query=workflow%3AmacOS
|
||||
:alt: macOS
|
||||
|
||||
.. |windows| image:: https://github.com/scrapy/scrapy/workflows/Windows/badge.svg
|
||||
:target: https://github.com/scrapy/scrapy/actions?query=workflow%3AWindows
|
||||
:alt: Windows
|
||||
.. |tests| image:: https://img.shields.io/github/check-runs/scrapy/scrapy/master?label=tests
|
||||
:target: https://github.com/scrapy/scrapy/actions?query=branch%3Amaster
|
||||
:alt: Tests
|
||||
|
||||
.. |coverage| image:: https://img.shields.io/codecov/c/github/scrapy/scrapy/master.svg
|
||||
:target: https://codecov.io/github/scrapy/scrapy?branch=master
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.16.x | :white_check_mark: |
|
||||
| < 2.16.x | :x: |
|
||||
| 2.17.x | :white_check_mark: |
|
||||
| < 2.17.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
|
|||
28
conftest.py
28
conftest.py
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
from importlib.util import find_spec
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ from scrapy.utils.reactor import set_asyncio_event_loop_policy
|
|||
from scrapy.utils.reactorless import install_reactor_import_hook
|
||||
from tests.keys import generate_keys
|
||||
from tests.mockserver.http import MockServer
|
||||
from tests.mockserver.mitm_proxy import MitmProxy
|
||||
from tests.mockserver.mitm_proxy import MitmProxy, mitmdump_cmd
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
|
@ -50,11 +51,12 @@ if not H2_ENABLED:
|
|||
)
|
||||
)
|
||||
|
||||
try:
|
||||
import httpx # noqa: F401
|
||||
except ImportError:
|
||||
if find_spec("httpx2") is None and find_spec("httpx") is None:
|
||||
collect_ignore.append("scrapy/core/downloader/handlers/_httpx.py")
|
||||
|
||||
if find_spec("pytest_codspeed") is None:
|
||||
collect_ignore.append("tests/benchmarks")
|
||||
|
||||
|
||||
def pytest_addoption(parser, pluginmanager):
|
||||
if pluginmanager.hasplugin("twisted"):
|
||||
|
|
@ -127,16 +129,16 @@ def pytest_runtest_setup(item):
|
|||
"uvloop",
|
||||
"botocore",
|
||||
"boto3",
|
||||
"mitmproxy",
|
||||
]
|
||||
|
||||
for module in optional_deps:
|
||||
if item.get_closest_marker(f"requires_{module}"):
|
||||
try:
|
||||
importlib.import_module(module)
|
||||
except ImportError:
|
||||
pytest.skip(f"{module} is not installed")
|
||||
if item.get_closest_marker(f"requires_{module}") and find_spec(module) is None:
|
||||
pytest.skip(f"{module} is not installed")
|
||||
|
||||
if item.get_closest_marker("requires_mitmproxy") and mitmdump_cmd() is None:
|
||||
pytest.skip("mitmdump is not available")
|
||||
|
||||
|
||||
# Generate localhost certificate files, needed by some tests
|
||||
generate_keys()
|
||||
# Generate localhost certificate files, needed by some tests (but only once if xdist is used)
|
||||
if "PYTEST_XDIST_WORKER" not in os.environ:
|
||||
generate_keys()
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
:orphan:
|
||||
|
||||
======================================
|
||||
Scrapy documentation quick start guide
|
||||
======================================
|
||||
|
||||
This file provides a quick guide on how to compile the Scrapy documentation.
|
||||
|
||||
|
||||
Setup the environment
|
||||
---------------------
|
||||
|
||||
To compile the documentation you need Sphinx Python library. To install it
|
||||
and all its dependencies run the following command from this dir
|
||||
|
||||
::
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
|
||||
Compile the documentation
|
||||
-------------------------
|
||||
|
||||
To compile the documentation (to classic HTML output) run the following command
|
||||
from this dir::
|
||||
|
||||
make html
|
||||
|
||||
Documentation will be generated (in HTML format) inside the ``build/html`` dir.
|
||||
|
||||
|
||||
View the documentation
|
||||
----------------------
|
||||
|
||||
To view the documentation run the following command::
|
||||
|
||||
make htmlview
|
||||
|
||||
This command will fire up your default browser and open the main page of your
|
||||
(previously generated) HTML documentation.
|
||||
|
||||
|
||||
Start over
|
||||
----------
|
||||
|
||||
To clean up all generated documentation files and start from scratch run::
|
||||
|
||||
make clean
|
||||
|
||||
Keep in mind that this command won't touch any documentation source files.
|
||||
|
||||
|
||||
Recreating documentation on the fly
|
||||
-----------------------------------
|
||||
|
||||
There is a way to recreate the doc automatically when you make changes, you
|
||||
need to install watchdog (``pip install watchdog``) and then use::
|
||||
|
||||
make watch
|
||||
|
||||
Alternative method using tox
|
||||
----------------------------
|
||||
|
||||
To compile the documentation to HTML run the following command::
|
||||
|
||||
tox -e docs
|
||||
|
||||
Documentation will be generated inside the ``docs/_build/all`` dir.
|
||||
|
|
@ -141,6 +141,8 @@ coverage_ignore_pyobjects = [
|
|||
r"^scrapy\.linkextractors\.lxmlhtml\.LxmlParserLinkExtractor",
|
||||
]
|
||||
|
||||
# -- Options for the autodoc extension ----------------------------------------
|
||||
autodoc_member_order = "bysource"
|
||||
|
||||
# -- Options for the InterSphinx extension -----------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration
|
||||
|
|
@ -158,6 +160,7 @@ scrapy_intersphinx_enable = [
|
|||
"itemloaders",
|
||||
"parsel",
|
||||
"pytest",
|
||||
"pypug",
|
||||
"scrapy-lint",
|
||||
"sphinx",
|
||||
"tox",
|
||||
|
|
|
|||
|
|
@ -323,9 +323,10 @@ deprecation removals are documented in the :ref:`release notes <news>`.
|
|||
Tests
|
||||
=====
|
||||
|
||||
Tests are implemented using the :doc:`Twisted unit-testing framework
|
||||
<twisted:development/test-standard>`. Running tests requires
|
||||
:doc:`tox <tox:index>`.
|
||||
Tests are implemented using pytest_. Running tests requires :doc:`tox
|
||||
<tox:index>`.
|
||||
|
||||
.. _pytest: https://pytest.org
|
||||
|
||||
.. _running-tests:
|
||||
|
||||
|
|
@ -371,6 +372,21 @@ To see coverage report install :doc:`coverage <coverage:index>`
|
|||
|
||||
see output of ``coverage --help`` for more options like html or xml report.
|
||||
|
||||
Some tests need a ``mitmdump`` executable (from mitmproxy_) to test against a
|
||||
fully featured proxy server; they are skipped when one cannot be found
|
||||
(``mitmproxy`` is intentionally not a test dependency that would be installed
|
||||
into test venvs, as that sometimes leads to various dependency conflicts).
|
||||
To run these tests, make ``mitmdump`` available in one of these ways:
|
||||
|
||||
* install ``mitmproxy`` so that ``mitmdump`` is on your ``PATH``, e.g. with
|
||||
pipx_ (``pipx install mitmproxy``) or uv_ (``uv tool install mitmproxy``);
|
||||
|
||||
* have uv_ installed, in which case the tests will run
|
||||
``uvx --from mitmproxy mitmdump``;
|
||||
|
||||
* set the ``MITMDUMP`` environment variable to the path of a ``mitmdump``
|
||||
executable.
|
||||
|
||||
Writing tests
|
||||
-------------
|
||||
|
||||
|
|
@ -398,3 +414,6 @@ And their unit-tests are in::
|
|||
.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist
|
||||
.. _help wanted issues: https://github.com/scrapy/scrapy/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
|
||||
.. _test coverage: https://app.codecov.io/gh/scrapy/scrapy
|
||||
.. _mitmproxy: https://mitmproxy.org/
|
||||
.. _pipx: https://pipx.pypa.io/
|
||||
.. _uv: https://docs.astral.sh/uv/
|
||||
|
|
|
|||
38
docs/faq.rst
38
docs/faq.rst
|
|
@ -97,7 +97,7 @@ handler documentation.
|
|||
How can I scrape an item with attributes in different pages?
|
||||
------------------------------------------------------------
|
||||
|
||||
See :ref:`topics-request-response-ref-request-callback-arguments`.
|
||||
See :ref:`callback-data`.
|
||||
|
||||
How can I simulate a user login in my spider?
|
||||
---------------------------------------------
|
||||
|
|
@ -136,7 +136,7 @@ middleware with a :ref:`custom downloader middleware
|
|||
<topics-downloader-middleware-custom>` that requires less memory. For example:
|
||||
|
||||
- If your domain names are similar enough, use your own regular expression
|
||||
instead joining the strings in :attr:`~scrapy.Spider.allowed_domains` into
|
||||
instead of joining the strings in :attr:`~scrapy.Spider.allowed_domains` into
|
||||
a complex regular expression.
|
||||
|
||||
- If you can meet the installation requirements, use pyre2_ instead of
|
||||
|
|
@ -220,21 +220,15 @@ the :ref:`topics-signals-ref` to know which ones.
|
|||
What does the response status code 999 mean?
|
||||
--------------------------------------------
|
||||
|
||||
999 is a custom response status code used by Yahoo sites to throttle requests.
|
||||
999 is a custom response status code used by some sites to throttle requests.
|
||||
Try slowing down the crawling speed by using a download delay of ``2`` (or
|
||||
higher) in your spider:
|
||||
higher) for the affected domains, with the :setting:`DOWNLOAD_SLOTS` setting:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy.spiders import CrawlSpider
|
||||
|
||||
|
||||
class MySpider(CrawlSpider):
|
||||
name = "myspider"
|
||||
|
||||
download_delay = 2
|
||||
|
||||
# [ ... rest of the spider code ... ]
|
||||
DOWNLOAD_SLOTS = {
|
||||
"example.com": {"delay": 2},
|
||||
}
|
||||
|
||||
Or by setting a global download delay in your project with the
|
||||
:setting:`DOWNLOAD_DELAY` setting.
|
||||
|
|
@ -285,7 +279,8 @@ consume a lot of memory.
|
|||
In order to avoid parsing all the entire feed at once in memory, you can use
|
||||
the :func:`~scrapy.utils.iterators.xmliter_lxml` and
|
||||
:func:`~scrapy.utils.iterators.csviter` functions. In fact, this is what
|
||||
:class:`~scrapy.spiders.XMLFeedSpider` uses.
|
||||
:class:`~scrapy.spiders.XMLFeedSpider` and
|
||||
:class:`~scrapy.spiders.CSVFeedSpider` use.
|
||||
|
||||
.. autofunction:: scrapy.utils.iterators.xmliter_lxml
|
||||
|
||||
|
|
@ -331,8 +326,8 @@ section of the site (which varies each time). In that case, the credentials to
|
|||
log in would be settings, while the url of the section to scrape would be a
|
||||
spider argument.
|
||||
|
||||
I'm scraping a XML document and my XPath selector doesn't return any items
|
||||
--------------------------------------------------------------------------
|
||||
I'm scraping an XML document and my XPath selector doesn't return any items
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
You may need to remove namespaces. See :ref:`removing-namespaces`.
|
||||
|
||||
|
|
@ -360,10 +355,11 @@ method for this purpose. For example:
|
|||
def process_spider_output(self, response, result):
|
||||
for item_or_request in result:
|
||||
if isinstance(item_or_request, Request):
|
||||
yield item_or_request
|
||||
continue
|
||||
adapter = ItemAdapter(item)
|
||||
adapter = ItemAdapter(item_or_request)
|
||||
for _ in range(adapter["multiply_by"]):
|
||||
yield deepcopy(item)
|
||||
yield deepcopy(item_or_request)
|
||||
|
||||
Does Scrapy support IPv6 addresses?
|
||||
-----------------------------------
|
||||
|
|
@ -382,8 +378,9 @@ How to deal with ``<class 'ValueError'>: filedescriptor out of range in select()
|
|||
----------------------------------------------------------------------------------------------
|
||||
|
||||
This issue `has been reported`_ to appear when running broad crawls in macOS, where the default
|
||||
Twisted reactor is :class:`twisted.internet.selectreactor.SelectReactor`. Switching to a
|
||||
different reactor is possible by using the :setting:`TWISTED_REACTOR` setting.
|
||||
Twisted reactor was :class:`twisted.internet.selectreactor.SelectReactor` at that time.
|
||||
If you have switched to this reactor using the :setting:`TWISTED_REACTOR` setting you can switch
|
||||
to a different one in the same way.
|
||||
|
||||
|
||||
.. _faq-stop-response-download:
|
||||
|
|
@ -409,7 +406,6 @@ How can I make a blank request?
|
|||
|
||||
from scrapy import Request
|
||||
|
||||
|
||||
blank_request = Request("data:,")
|
||||
|
||||
In this case, the URL is set to a data URI scheme. Data URLs allow you to include data
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Having trouble? We'd like to help!
|
|||
* Ask or search questions in `StackOverflow using the scrapy tag`_.
|
||||
* Ask or search questions in the `Scrapy subreddit`_.
|
||||
* Search for questions on the archives of the `scrapy-users mailing list`_.
|
||||
* Ask a question in the `#scrapy IRC channel`_,
|
||||
* Ask a question in the `#scrapy IRC channel`_.
|
||||
* Report bugs with Scrapy in our `issue tracker`_.
|
||||
* Join the Discord community `Scrapy Discord`_.
|
||||
|
||||
|
|
@ -91,15 +91,15 @@ Basic concepts
|
|||
:doc:`topics/selectors`
|
||||
Extract the data from web pages using XPath.
|
||||
|
||||
:doc:`topics/shell`
|
||||
Test your extraction code in an interactive environment.
|
||||
|
||||
:doc:`topics/items`
|
||||
Define the data you want to scrape.
|
||||
|
||||
:doc:`topics/loaders`
|
||||
Populate your items with the extracted data.
|
||||
|
||||
:doc:`topics/shell`
|
||||
Test your extraction code in an interactive environment.
|
||||
|
||||
:doc:`topics/item-pipeline`
|
||||
Post-process and store your scraped data.
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,56 @@ just like any other Python package.
|
|||
(See :ref:`platform-specific guides <intro-install-platform-notes>`
|
||||
below for non-Python dependencies that you may need to install beforehand).
|
||||
|
||||
.. _extras:
|
||||
|
||||
Optional extras
|
||||
===============
|
||||
|
||||
Scrapy provides optional :ref:`extras <pypug:dependency-specifiers-extras>`
|
||||
that install additional dependencies to enable specific features. To install
|
||||
Scrapy with one or more extras, list them in square brackets:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip install scrapy[s3,images]
|
||||
|
||||
The following extras are available:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Extra
|
||||
- Provides
|
||||
* - ``bpython``
|
||||
- :ref:`bpython shell <shell-config>`
|
||||
* - ``brotli``
|
||||
- :ref:`Brotli response decompression <http-compression>`
|
||||
* - ``gcs``
|
||||
- :ref:`Google Cloud Storage <topics-feed-storage-gcs>` for
|
||||
:ref:`feed exports <topics-feed-exports>` and
|
||||
:ref:`media pipelines <media-pipeline-gcs>`
|
||||
* - ``httpx``
|
||||
- :ref:`httpx-handler`, including its HTTP/2 and SOCKS proxy support
|
||||
* - ``images``
|
||||
- :ref:`Images pipeline <images-pipeline>`
|
||||
* - ``ipython``
|
||||
- :ref:`IPython shell <shell-config>`
|
||||
* - ``ptpython``
|
||||
- :ref:`ptpython shell <shell-config>`
|
||||
* - ``robotparser``
|
||||
- :ref:`Robotexclusionrulesparser robots.txt parsing <rerp-parser>`
|
||||
* - ``s3``
|
||||
- :ref:`Amazon S3 <topics-feed-storage-s3>` storage for
|
||||
:ref:`feed exports <topics-feed-exports>`,
|
||||
:ref:`media pipelines <media-pipelines-s3>`, and
|
||||
:ref:`S3 downloads <s3-handler>`
|
||||
* - ``twisted-http2``
|
||||
- :ref:`twisted-http2-handler`
|
||||
* - ``uvloop``
|
||||
- `uvloop <https://github.com/MagicStack/uvloop>`_ event loop
|
||||
* - ``zstd``
|
||||
- :ref:`Zstandard response decompression <http-compression>`
|
||||
|
||||
|
||||
.. _intro-install-platform-notes:
|
||||
|
||||
|
|
@ -230,8 +280,8 @@ Installing Scrapy with PyPy on Windows is not tested.
|
|||
You can check that Scrapy is installed correctly by running ``scrapy bench``.
|
||||
If this command gives errors such as
|
||||
``TypeError: ... got 2 unexpected keyword arguments``, this means
|
||||
that setuptools was unable to pick up one PyPy-specific dependency.
|
||||
To fix this issue, run ``pip install 'PyPyDispatcher>=2.1.0'``.
|
||||
that the ``PyPyDispatcher`` dependency wasn't installed. To fix this issue, run
|
||||
``pip install 'PyPyDispatcher>=2.1.0'``.
|
||||
|
||||
|
||||
.. _intro-install-troubleshooting:
|
||||
|
|
|
|||
|
|
@ -83,16 +83,17 @@ While this enables you to do very fast crawls (sending multiple concurrent
|
|||
requests at the same time, in a fault-tolerant way) Scrapy also gives you
|
||||
control over the politeness of the crawl through :ref:`a few settings
|
||||
<topics-settings-ref>`. You can do things like setting a download delay between
|
||||
each request, limiting the amount of concurrent requests per domain or per IP, and
|
||||
each request, limiting the amount of concurrent requests per domain, and
|
||||
even :ref:`using an auto-throttling extension <topics-autothrottle>` that tries
|
||||
to figure these settings out automatically.
|
||||
|
||||
.. note::
|
||||
|
||||
This is using :ref:`feed exports <topics-feed-exports>` to generate the
|
||||
JSON file, you can easily change the export format (XML or CSV, for example) or the
|
||||
storage backend (FTP or `Amazon S3`_, for example). You can also write an
|
||||
:ref:`item pipeline <topics-item-pipeline>` to store the items in a database.
|
||||
JSON Lines file, you can easily change the export format (XML or CSV, for
|
||||
example) or the storage backend (FTP or `Amazon S3`_, for example). You can
|
||||
also write an :ref:`item pipeline <topics-item-pipeline>` to store the
|
||||
items in a database.
|
||||
|
||||
|
||||
.. _topics-whatelse:
|
||||
|
|
|
|||
|
|
@ -769,7 +769,7 @@ crawlers on top of it.
|
|||
|
||||
Also, a common pattern is to build an item with data from more than one page,
|
||||
using a :ref:`trick to pass additional data to the callbacks
|
||||
<topics-request-response-ref-request-callback-arguments>`.
|
||||
<callback-data>`.
|
||||
|
||||
|
||||
Using spider arguments
|
||||
|
|
|
|||
365
docs/news.rst
365
docs/news.rst
|
|
@ -3,6 +3,352 @@
|
|||
Release notes
|
||||
=============
|
||||
|
||||
Scrapy VERSION (unreleased)
|
||||
---------------------------
|
||||
|
||||
Backward-incompatible changes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- The following runtime usage of zope.interface_ interfaces is removed:
|
||||
|
||||
- :class:`~scrapy.spiderloader.SpiderLoader` and
|
||||
:class:`~scrapy.spiderloader.DummySpiderLoader` are no longer marked
|
||||
as implementing the ``ISpiderLoader`` interface.
|
||||
|
||||
- :func:`~scrapy.spiderloader.get_spider_loader` no longer checks that the
|
||||
configured spider loader implements the ``ISpiderLoader`` interface.
|
||||
|
||||
- :class:`~scrapy.extensions.feedexport.BlockingFeedStorage`,
|
||||
:class:`~scrapy.extensions.feedexport.FileFeedStorage` and
|
||||
:class:`~scrapy.extensions.feedexport.StdoutFeedStorage` are no longer
|
||||
marked as implementing the ``IFeedStorage`` interface.
|
||||
|
||||
(:issue:`6585`, :issue:`7731`)
|
||||
|
||||
.. _release-2.17.0:
|
||||
|
||||
Scrapy 2.17.0 (2026-07-07)
|
||||
--------------------------
|
||||
|
||||
Highlights:
|
||||
|
||||
- Security bug fixes
|
||||
|
||||
- HTTP/2 and SOCKS proxy support for ``HttpxDownloadHandler``
|
||||
|
||||
- Improved settings for changing allowed TLS versions
|
||||
|
||||
Security bug fixes
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- ``s3://`` requests now use HTTPS by default, instead of plaintext HTTP.
|
||||
|
||||
Previously, :class:`~scrapy.core.downloader.handlers.s3.S3DownloadHandler`
|
||||
sent signed S3 requests over plaintext HTTP unless
|
||||
``request.meta["is_secure"]`` was set to a true value, exposing the request
|
||||
path, the AWS ``Authorization`` header, the ``X-Amz-Security-Token`` header
|
||||
(when using temporary credentials), and the response contents to network
|
||||
attackers, who could also tamper with responses. See the `76g3-c3x4-crvx`_
|
||||
security advisory for details.
|
||||
|
||||
To restore the previous behavior for a given request, set
|
||||
``request.meta["is_secure"]`` to ``False``.
|
||||
|
||||
.. _76g3-c3x4-crvx: https://github.com/scrapy/scrapy/security/advisories/GHSA-76g3-c3x4-crvx
|
||||
|
||||
Deprecations
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- The ``DOWNLOADER_CLIENT_TLS_METHOD`` setting is deprecated. You should use
|
||||
the :setting:`DOWNLOAD_TLS_MIN_VERSION` and/or
|
||||
:setting:`DOWNLOAD_TLS_MAX_VERSION` settings instead if you want to change
|
||||
the TLS method selection.
|
||||
(:issue:`3288`, :issue:`6546`)
|
||||
|
||||
- The following spider attributes are deprecated in favor of settings:
|
||||
|
||||
- ``http_user`` (use :setting:`HTTPAUTH_USER`)
|
||||
|
||||
- ``http_pass`` (use :setting:`HTTPAUTH_PASS`)
|
||||
|
||||
- ``http_auth_domain`` (use :setting:`HTTPAUTH_DOMAIN`)
|
||||
|
||||
(:issue:`7590`)
|
||||
|
||||
- The ``scrapy.commands.ScrapyCommand.help()`` method is deprecated. It was
|
||||
never called by Scrapy.
|
||||
(:issue:`7626`, :issue:`7633`)
|
||||
|
||||
- The following TLS-related functions and constants, intended for internal
|
||||
use, are deprecated:
|
||||
|
||||
- ``scrapy.core.downloader.tls.METHOD_TLS``
|
||||
|
||||
- ``scrapy.core.downloader.tls.METHOD_TLSv10``
|
||||
|
||||
- ``scrapy.core.downloader.tls.METHOD_TLSv11``
|
||||
|
||||
- ``scrapy.core.downloader.tls.METHOD_TLSv12``
|
||||
|
||||
- ``scrapy.core.downloader.tls.openssl_methods``
|
||||
|
||||
- ``scrapy.core.downloader.tls.DEFAULT_CIPHERS``
|
||||
|
||||
- ``scrapy.utils.ssl.ffi_buf_to_string()``
|
||||
|
||||
- ``scrapy.utils.ssl.get_temp_key_info()``
|
||||
|
||||
- ``scrapy.utils.ssl.x509name_to_string()``
|
||||
|
||||
(:issue:`6546`, :issue:`7619`, :issue:`7665`)
|
||||
|
||||
- The ``CRAWLSPIDER_FOLLOW_LINKS`` setting is deprecated. You can set
|
||||
``follow=False`` in your rules to achieve the same effect.
|
||||
(:issue:`7592`)
|
||||
|
||||
- Instantiating
|
||||
:class:`~scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware`
|
||||
without a ``crawler`` argument is deprecated.
|
||||
(:issue:`7655`)
|
||||
|
||||
- Instantiating
|
||||
:class:`~scrapy.spidermiddlewares.referer.RefererMiddleware` without a
|
||||
``settings`` argument is deprecated.
|
||||
(:issue:`7664`)
|
||||
|
||||
New features
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- Added support for HTTP/2 requests to
|
||||
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`. It
|
||||
requires setting the new :setting:`HTTPX_HTTP2_ENABLED` setting to
|
||||
``True``.
|
||||
(:issue:`7575`)
|
||||
|
||||
- Added support for SOCKS proxies to
|
||||
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`.
|
||||
(:issue:`747`, :issue:`7575`)
|
||||
|
||||
- Added :setting:`DOWNLOAD_TLS_MIN_VERSION` and
|
||||
:setting:`DOWNLOAD_TLS_MAX_VERSION` settings as replacements for the
|
||||
``DOWNLOADER_CLIENT_TLS_METHOD`` setting (which is now deprecated).
|
||||
Compared to the old setting, they support specifying a range of allowed
|
||||
versions and support newer TLS versions.
|
||||
(:issue:`4821`, :issue:`6546`)
|
||||
|
||||
- Added :setting:`HTTPAUTH_USER`, :setting:`HTTPAUTH_PASS` and
|
||||
:setting:`HTTPAUTH_DOMAIN` settings and :reqmeta:`http_user`,
|
||||
:reqmeta:`http_pass` and :reqmeta:`http_auth_domain` meta keys as more
|
||||
flexible ways to set HTTP authentication data.
|
||||
(:issue:`7590`)
|
||||
|
||||
- Added a :reqmeta:`verbatim_url` meta key that can be set to ``True`` to
|
||||
skip request URL canonicalization.
|
||||
(:issue:`7473`)
|
||||
|
||||
- Added ``deny_tags`` and ``deny_attrs`` arguments to :class:`LinkExtractor
|
||||
<scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor>`.
|
||||
(:issue:`6321`, :issue:`7679`)
|
||||
|
||||
- :attr:`scrapy.Item.fields` now returns the fields in the definition order
|
||||
instead of the alphabetical one.
|
||||
(:issue:`7015`, :issue:`7694`)
|
||||
|
||||
- Added a :setting:`RETRY_GIVE_UP_LOG_LEVEL` setting, a
|
||||
:reqmeta:`give_up_log_level` meta key and a ``give_up_log_level`` argument
|
||||
of the
|
||||
:func:`~scrapy.downloadermiddlewares.retry.get_retry_request` function that
|
||||
allow changing the log level of the message logged when the retry limit has
|
||||
been reached.
|
||||
(:issue:`4622`, :issue:`5297`, :issue:`7567`)
|
||||
|
||||
- It's now possible to set :setting:`DOWNLOADER_CLIENT_TLS_CIPHERS` to
|
||||
``None`` to use the default ciphers of the underlying TLS implementation.
|
||||
(:issue:`7499`, :issue:`7665`)
|
||||
|
||||
Improvements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- :class:`~scrapy.FormRequest` is no longer deprecated, only its
|
||||
``from_response()`` method is still deprecated.
|
||||
(:issue:`7561`, :issue:`7671`)
|
||||
|
||||
- Switched the item definition in the default project template from a
|
||||
:class:`scrapy.item.Item` to a dataclass.
|
||||
(:issue:`7493`, :issue:`7513`)
|
||||
|
||||
- Fixed deprecation warnings with pyOpenSSL 26.3.0.
|
||||
(:issue:`7619`)
|
||||
|
||||
- Removed the runtime warnings for :attr:`Spider.allowed_domains
|
||||
<scrapy.Spider.allowed_domains>` containing URLs or domains with ports
|
||||
instead of just domains and for spider classes having a ``start_url``
|
||||
attribute instead of :class:`~scrapy.spiders.Spider.start_urls`. Please use
|
||||
:doc:`scrapy-lint <scrapy-lint:index>` to find mistakes in your spider code
|
||||
instead.
|
||||
(:issue:`4421`, :issue:`7627`)
|
||||
|
||||
- :func:`scrapy.utils.test.get_crawler` now disables
|
||||
:setting:`TELNETCONSOLE_ENABLED` by default.
|
||||
(:issue:`7644`)
|
||||
|
||||
- Other code refactoring and improvements.
|
||||
(:issue:`7409`, :issue:`7593`, :issue:`7594`, :issue:`7611`, :issue:`7649`)
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
- :class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler` no
|
||||
longer ignores proxy credentials for redirected or retried requests.
|
||||
(:issue:`7601`, :issue:`7630`)
|
||||
|
||||
- :class:`~scrapy.extensions.feedexport.GCSFeedStorage` now closes the
|
||||
temporary file after the upload.
|
||||
(:issue:`7546`)
|
||||
|
||||
- Fixed ``scrapy shell <URL>`` running a full spider crawl when there is a
|
||||
spider for the requested URL. This bug was introduced in Scrapy 2.13.0.
|
||||
(:issue:`7552`, :issue:`7557`)
|
||||
|
||||
- The :setting:`IMAGES_STORE_S3_ACL` and :setting:`IMAGES_STORE_GCS_ACL`
|
||||
settings are no longer ignored. This bug was introduced in Scrapy 2.12.0.
|
||||
(:issue:`7597`, :issue:`7614`)
|
||||
|
||||
- :class:`~scrapy.core.downloader.handlers.ftp.FTPDownloadHandler` now closes
|
||||
the connection after making the request.
|
||||
(:issue:`7602`, :issue:`7667`)
|
||||
|
||||
- Removed the deprecated ``spider`` argument from the pipeline defined in the
|
||||
default project template.
|
||||
(:issue:`7676`)
|
||||
|
||||
- Fixed ``scrapy genspider --edit`` not working.
|
||||
(:issue:`7260`, :issue:`7683`)
|
||||
|
||||
- When a :class:`~scrapy.crawler.Crawler` instance is passed to
|
||||
:meth:`AsyncCrawlerRunner.create_crawler()
|
||||
<scrapy.crawler.AsyncCrawlerRunner.create_crawler>` or
|
||||
:meth:`CrawlerRunner.create_crawler()
|
||||
<scrapy.crawler.CrawlerRunner.create_crawler>`, settings from both classes
|
||||
are now merged, previously only the settings from the
|
||||
:class:`~scrapy.crawler.Crawler` instance were used.
|
||||
(:issue:`1280`, :issue:`7647`)
|
||||
|
||||
- Fixed several issues with cookie handling in
|
||||
:func:`scrapy.utils.request.request_to_curl`.
|
||||
(:issue:`7603`, :issue:`7675`, :issue:`7684`)
|
||||
|
||||
- Fixed :class:`scrapy.resolver.CachingThreadedResolver` not disabling the
|
||||
cache when :setting:`DNSCACHE_ENABLED` is set to ``False``.
|
||||
(:issue:`7663`)
|
||||
|
||||
- Fixed :func:`scrapy.utils.response.open_in_browser` not removing comments
|
||||
when looking for the ``<base>`` tag.
|
||||
(:issue:`7506`)
|
||||
|
||||
- Fixed checking for deprecated methods in custom :setting:`ITEM_PROCESSOR`
|
||||
implementations.
|
||||
(:issue:`7589`)
|
||||
|
||||
- Fixed :func:`scrapy.utils.url.strip_url` corrupting some URLs with
|
||||
credentials.
|
||||
(:issue:`7604`, :issue:`7605`)
|
||||
|
||||
- :func:`scrapy.utils.misc.rel_has_nofollow` now ignores the case when
|
||||
looking for "nofollow" strings.
|
||||
(:issue:`7632`)
|
||||
|
||||
- Fixed an exception in :class:`scrapy.utils.sitemap.Sitemap` when parsing
|
||||
some malformed sitemaps.
|
||||
(:issue:`7686`, :issue:`7687`)
|
||||
|
||||
Documentation
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
- Mentioned :doc:`scrapy-lint <scrapy-lint:index>` in the docs.
|
||||
(:issue:`4421`, :issue:`7627`)
|
||||
|
||||
- Added the docs about :ref:`security considerations <security>`.
|
||||
(:issue:`7389`, :issue:`7678`)
|
||||
|
||||
- Improved the :ref:`item pipeline docs <topics-item-pipeline>`.
|
||||
(:issue:`2350`, :issue:`7676`)
|
||||
|
||||
- Documented which stats are collected by
|
||||
:class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
(:issue:`7421`)
|
||||
|
||||
- Switched documentation examples from using :class:`scrapy.item.Item` to
|
||||
using dataclasses.
|
||||
(:issue:`7493`, :issue:`7513`)
|
||||
|
||||
- Added feature comparison tables to the :ref:`download handler
|
||||
<download-handlers-ref>` docs.
|
||||
(:issue:`7575`)
|
||||
|
||||
- Improved the docs for :ref:`logging settings <logging-settings>`.
|
||||
(:issue:`6909`, :issue:`7668`)
|
||||
|
||||
- Documented a way to :ref:`improve startup time and memory usage
|
||||
<large-project-startup>` by using :setting:`SPIDER_MODULES`.
|
||||
(:issue:`7576`, :issue:`7600`)
|
||||
|
||||
- Clarified handling of the ``type`` argument of :class:`~scrapy.Selector`.
|
||||
(:issue:`7704`)
|
||||
|
||||
- Other documentation improvements and fixes.
|
||||
(:issue:`4954`,
|
||||
:issue:`6120`,
|
||||
:issue:`7286`,
|
||||
:issue:`7564`,
|
||||
:issue:`7573`,
|
||||
:issue:`7598`,
|
||||
:issue:`7599`,
|
||||
:issue:`7698`)
|
||||
|
||||
Quality assurance
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Fixed deprecation warnings with pytest 9.1.0.
|
||||
(:issue:`7621`)
|
||||
|
||||
- Type hints improvements and fixes.
|
||||
(:issue:`6958`, :issue:`7586`)
|
||||
|
||||
- CI and test improvements and fixes.
|
||||
(:issue:`5954`,
|
||||
:issue:`7002`,
|
||||
:issue:`7017`,
|
||||
:issue:`7247`,
|
||||
:issue:`7508`,
|
||||
:issue:`7545`,
|
||||
:issue:`7566`,
|
||||
:issue:`7574`,
|
||||
:issue:`7585`,
|
||||
:issue:`7595`,
|
||||
:issue:`7608`,
|
||||
:issue:`7610`,
|
||||
:issue:`7612`,
|
||||
:issue:`7616`,
|
||||
:issue:`7625`,
|
||||
:issue:`7637`,
|
||||
:issue:`7639`,
|
||||
:issue:`7640`,
|
||||
:issue:`7641`,
|
||||
:issue:`7642`,
|
||||
:issue:`7643`,
|
||||
:issue:`7644`,
|
||||
:issue:`7645`,
|
||||
:issue:`7646`,
|
||||
:issue:`7654`,
|
||||
:issue:`7655`,
|
||||
:issue:`7664`,
|
||||
:issue:`7672`,
|
||||
:issue:`7677`,
|
||||
:issue:`7680`,
|
||||
:issue:`7682`,
|
||||
:issue:`7692`)
|
||||
|
||||
.. _release-2.16.0:
|
||||
|
||||
Scrapy 2.16.0 (2026-05-19)
|
||||
|
|
@ -711,7 +1057,7 @@ Deprecations
|
|||
New features
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- Added a new setting, :setting:`REFERER_POLICIES`, to allow customizing
|
||||
- Added a new setting, :setting:`REFERRER_POLICIES`, to allow customizing
|
||||
supported referrer policies.
|
||||
|
||||
Bug fixes
|
||||
|
|
@ -1071,7 +1417,8 @@ Deprecations
|
|||
|
||||
- ``download_warnsize`` (use :setting:`DOWNLOAD_WARNSIZE`)
|
||||
|
||||
- ``max_concurrent_requests`` (use :setting:`CONCURRENT_REQUESTS`)
|
||||
- ``max_concurrent_requests`` (use
|
||||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN`)
|
||||
|
||||
- ``user_agent`` (use :setting:`USER_AGENT`)
|
||||
|
||||
|
|
@ -2319,7 +2666,7 @@ Deprecation removals
|
|||
(:issue:`6109`, :issue:`6116`)
|
||||
|
||||
- A custom class assigned to the :setting:`SPIDER_LOADER_CLASS` setting that
|
||||
does not implement the :class:`~scrapy.interfaces.ISpiderLoader` interface
|
||||
does not implement the ``ISpiderLoader`` interface
|
||||
will now raise a :exc:`zope.interface.verify.DoesNotImplement` exception at
|
||||
run time. Non-compliant classes have been triggering a deprecation warning
|
||||
since Scrapy 1.0.0.
|
||||
|
|
@ -4809,7 +5156,7 @@ Bug fixes
|
|||
* The system file mode creation mask no longer affects the permissions of
|
||||
files generated using the :command:`startproject` command (:issue:`4722`)
|
||||
|
||||
* :func:`scrapy.utils.iterators.xmliter` now supports namespaced node names
|
||||
* ``scrapy.utils.iterators.xmliter`` now supports namespaced node names
|
||||
(:issue:`861`, :issue:`4746`)
|
||||
|
||||
* :class:`~scrapy.Request` objects can now have ``about:`` URLs, which can
|
||||
|
|
@ -5722,7 +6069,7 @@ The following changes may impact custom priority queue classes:
|
|||
|
||||
* A new keyword parameter has been added: ``key``. It is a string
|
||||
that is always an empty string for memory queues and indicates the
|
||||
:setting:`JOB_DIR` value for disk queues.
|
||||
:setting:`JOBDIR` value for disk queues.
|
||||
|
||||
* The parameter for disk queues that contains data from the previous
|
||||
crawl, ``startprios`` or ``slot_startprios``, is now passed as a
|
||||
|
|
@ -6243,8 +6590,8 @@ Backward-incompatible changes
|
|||
``429``, you must override :setting:`RETRY_HTTP_CODES` accordingly.
|
||||
|
||||
* :class:`~scrapy.crawler.Crawler`,
|
||||
:class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>` and
|
||||
:class:`CrawlerRunner.create_crawler <scrapy.crawler.CrawlerRunner.create_crawler>`
|
||||
:meth:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>` and
|
||||
:meth:`CrawlerRunner.create_crawler <scrapy.crawler.CrawlerRunner.create_crawler>`
|
||||
no longer accept a :class:`~scrapy.spiders.Spider` subclass instance, they
|
||||
only accept a :class:`~scrapy.spiders.Spider` subclass now.
|
||||
|
||||
|
|
@ -6276,7 +6623,7 @@ New features
|
|||
``scrapy.pqueues.DownloaderAwarePriorityQueue``, may be
|
||||
:ref:`enabled <broad-crawls-scheduler-priority-queue>` for a significant
|
||||
scheduling improvement on crawls targeting multiple web domains, at the
|
||||
cost of no :setting:`CONCURRENT_REQUESTS_PER_IP` support (:issue:`3520`)
|
||||
cost of no ``CONCURRENT_REQUESTS_PER_IP`` support (:issue:`3520`)
|
||||
|
||||
* A new :attr:`.Request.cb_kwargs` attribute
|
||||
provides a cleaner way to pass keyword arguments to callback methods
|
||||
|
|
@ -8755,7 +9102,7 @@ New features and settings
|
|||
- In request errbacks, offending requests are now received in ``failure.request`` attribute (:rev:`2738`)
|
||||
- Big downloader refactoring to support per domain/ip concurrency limits (:rev:`2732`)
|
||||
- ``CONCURRENT_REQUESTS_PER_SPIDER`` setting has been deprecated and replaced by:
|
||||
- :setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`, :setting:`CONCURRENT_REQUESTS_PER_IP`
|
||||
- :setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`, ``CONCURRENT_REQUESTS_PER_IP``
|
||||
- check the documentation for more details
|
||||
- Added builtin caching DNS resolver (:rev:`2728`)
|
||||
- Moved Amazon AWS-related components/extensions (SQS spider queue, SimpleDB stats collector) to a separate project: [scaws](https://github.com/scrapinghub/scaws) (:rev:`2706`, :rev:`2714`)
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ sphinx
|
|||
sphinx-notfound-page
|
||||
sphinx-rtd-theme
|
||||
sphinx-rtd-dark-mode
|
||||
sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@0.8.8
|
||||
sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@0.8.10
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ sphinx-rtd-theme==3.1.0
|
|||
# via
|
||||
# -r docs/requirements.in
|
||||
# sphinx-rtd-dark-mode
|
||||
sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@c0b2ac815afc3cb8857d575cecb5d55c05e6b737
|
||||
sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@fe176adc1a8577601bc3fa39b590ebed71a7e9b8
|
||||
# via -r docs/requirements.in
|
||||
sphinx-sitemap==2.9.0
|
||||
# via sphinx-scrapy
|
||||
|
|
|
|||
|
|
@ -21,10 +21,14 @@ The ``ADDONS`` setting is a dict in which every key is an add-on class or its
|
|||
import path and the value is its priority.
|
||||
|
||||
This is an example where two add-ons are enabled in a project's
|
||||
``settings.py``::
|
||||
``settings.py``:
|
||||
|
||||
.. skip: next
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ADDONS = {
|
||||
'path.to.someaddon': 0,
|
||||
"path.to.someaddon": 0,
|
||||
SomeAddonClass: 1,
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +60,9 @@ the following methods:
|
|||
: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`)::
|
||||
:ref:`populating-settings` and :func:`scrapy.settings.BaseSettings.set`):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyAddon:
|
||||
def update_settings(self, settings):
|
||||
|
|
@ -168,7 +174,6 @@ Use a fallback component:
|
|||
|
||||
from scrapy.utils.misc import build_from_crawler, load_object
|
||||
|
||||
|
||||
FALLBACK_SETTING = "MY_FALLBACK_DOWNLOAD_HANDLER"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -172,46 +172,15 @@ SpiderLoader API
|
|||
.. module:: scrapy.spiderloader
|
||||
:synopsis: The spider loader
|
||||
|
||||
.. class:: SpiderLoader
|
||||
Custom spider loaders can be employed by specifying their path in the
|
||||
:setting:`SPIDER_LOADER_CLASS` project setting. They must implement
|
||||
:class:`SpiderLoaderProtocol`.
|
||||
|
||||
This class is in charge of retrieving and handling the spider classes
|
||||
defined across the project.
|
||||
.. autoclass:: SpiderLoaderProtocol
|
||||
:members:
|
||||
|
||||
Custom spider loaders can be employed by specifying their path in the
|
||||
:setting:`SPIDER_LOADER_CLASS` project setting. They must fully implement
|
||||
the :class:`scrapy.interfaces.ISpiderLoader` interface to guarantee an
|
||||
errorless execution.
|
||||
|
||||
.. method:: from_settings(settings)
|
||||
|
||||
This class method is used by Scrapy to create an instance of the class.
|
||||
It's called with the current project settings, and it loads the spiders
|
||||
found recursively in the modules of the :setting:`SPIDER_MODULES`
|
||||
setting.
|
||||
|
||||
:param settings: project settings
|
||||
:type settings: :class:`~scrapy.settings.Settings` instance
|
||||
|
||||
.. method:: load(spider_name)
|
||||
|
||||
Get the Spider class with the given name. It'll look into the previously
|
||||
loaded spiders for a spider class with name ``spider_name`` and will raise
|
||||
a KeyError if not found.
|
||||
|
||||
:param spider_name: spider class name
|
||||
:type spider_name: str
|
||||
|
||||
.. method:: list()
|
||||
|
||||
Get the names of the available spiders in the project.
|
||||
|
||||
.. method:: find_by_request(request)
|
||||
|
||||
List the spiders' names that can handle the given request. Will try to
|
||||
match the request's url against the domains of the spiders.
|
||||
|
||||
:param request: queried request
|
||||
:type request: :class:`~scrapy.Request` instance
|
||||
.. autoclass:: SpiderLoader
|
||||
:members:
|
||||
|
||||
.. autoclass:: DummySpiderLoader
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ asyncio
|
|||
=======
|
||||
|
||||
Scrapy supports :mod:`asyncio` natively. New projects created with
|
||||
:command:`scrapy startproject` have asyncio enabled by default, and you can use
|
||||
:command:`startproject` have asyncio enabled by default, and you can use
|
||||
:mod:`asyncio` and :mod:`asyncio`-powered libraries in any :doc:`coroutine
|
||||
<coroutines>`.
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ no additional setup is needed.
|
|||
Configuring the asyncio reactor
|
||||
===============================
|
||||
|
||||
New projects generated with :command:`scrapy startproject` have the asyncio
|
||||
New projects generated with :command:`startproject` have the asyncio
|
||||
reactor configured by default. No manual setup is needed.
|
||||
|
||||
The :setting:`TWISTED_REACTOR` setting controls which Twisted reactor Scrapy
|
||||
|
|
@ -105,6 +105,9 @@ Scrapy API requires passing a Deferred to it) using the following helpers:
|
|||
|
||||
.. autofunction:: scrapy.utils.defer.deferred_from_coro
|
||||
.. autofunction:: scrapy.utils.defer.deferred_f_from_coro_f
|
||||
|
||||
The following function helps with a reverse wrapping:
|
||||
|
||||
.. autofunction:: scrapy.utils.defer.ensure_awaitable
|
||||
|
||||
|
||||
|
|
@ -147,6 +150,12 @@ Using Scrapy without a Twisted reactor
|
|||
.. warning::
|
||||
This is currently experimental and may not be suitable for production use.
|
||||
|
||||
.. note:: As the Twisted download handlers cannot be used without a reactor,
|
||||
the default download handler in this mode is
|
||||
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler`. You
|
||||
will need to additionally install the :ref:`httpx <extras>` extra to use
|
||||
it, unless you switch to some different handler.
|
||||
|
||||
It's possible to use Scrapy without installing a Twisted reactor at all, by
|
||||
setting the :setting:`TWISTED_REACTOR_ENABLED` setting to ``False``. In this
|
||||
mode Scrapy will use the asyncio event loop directly, and most of the Scrapy
|
||||
|
|
@ -190,7 +199,7 @@ in future Scrapy versions. The following features are not available:
|
|||
:class:`~scrapy.crawler.CrawlerProcess`
|
||||
(:class:`~scrapy.crawler.AsyncCrawlerProcess` and
|
||||
:class:`~scrapy.crawler.AsyncCrawlerRunner` are available)
|
||||
* Twisted-specific DNS resolvers (the :setting:`DNS_RESOLVER` setting)
|
||||
* Twisted-specific DNS resolvers (the :setting:`TWISTED_DNS_RESOLVER` setting)
|
||||
* User and 3rd-party code that requires a reactor (see :ref:`below
|
||||
<asyncio-without-reactor-migrate>` for examples)
|
||||
|
||||
|
|
@ -218,7 +227,8 @@ for its differences and limitations compared to
|
|||
|
||||
Additionally, :class:`~scrapy.crawler.AsyncCrawlerProcess` will install a
|
||||
:term:`meta path finder` that prevents :mod:`twisted.internet.reactor` from
|
||||
being imported.
|
||||
being imported. It will be uninstalled when :meth:`AsyncCrawlerProcess.start()
|
||||
<scrapy.crawler.AsyncCrawlerProcess.start>` exits.
|
||||
|
||||
.. _asyncio-without-reactor-migrate:
|
||||
|
||||
|
|
@ -257,6 +267,7 @@ Here are some examples of APIs and patterns that need a replacement:
|
|||
|
||||
Scrapy provides unified helpers for some of these examples:
|
||||
|
||||
.. autofunction:: scrapy.utils.asyncio.sleep
|
||||
.. autofunction:: scrapy.utils.asyncio.call_later
|
||||
.. autofunction:: scrapy.utils.asyncio.create_looping_call
|
||||
.. autoclass:: scrapy.utils.asyncio.AsyncioLoopingCall
|
||||
|
|
@ -315,8 +326,7 @@ implementations, :class:`~asyncio.ProactorEventLoop` (default) and
|
|||
:class:`~asyncio.SelectorEventLoop` works with Twisted.
|
||||
|
||||
Scrapy changes the event loop class to :class:`~asyncio.SelectorEventLoop`
|
||||
automatically when you change the :setting:`TWISTED_REACTOR` setting or call
|
||||
:func:`~scrapy.utils.reactor.install_reactor`.
|
||||
automatically when installing the asyncio reactor.
|
||||
|
||||
.. note:: Other libraries you use may require
|
||||
:class:`~asyncio.ProactorEventLoop`, e.g. because it supports
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ AutoThrottle algorithm adjusts download delays based on the following rules:
|
|||
.. _download-latency:
|
||||
|
||||
In Scrapy, the download latency is measured as the time elapsed between
|
||||
establishing the TCP connection and receiving the HTTP headers.
|
||||
sending the request and receiving the HTTP headers.
|
||||
|
||||
Note that these latencies are very hard to measure accurately in a cooperative
|
||||
multitasking environment because Scrapy may be busy processing a spider
|
||||
|
|
@ -88,6 +88,8 @@ server) is, and this extension builds on that premise.
|
|||
Prevent specific requests from triggering slot delay adjustments
|
||||
================================================================
|
||||
|
||||
.. versionadded:: 2.12.0
|
||||
|
||||
AutoThrottle adjusts the delay of download slots based on the latencies of
|
||||
responses that belong to that download slot. The only exceptions are non-200
|
||||
responses, which are only taken into account to increase that delay, but
|
||||
|
|
@ -104,10 +106,9 @@ delay of its download slot:
|
|||
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. 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
|
||||
|
|
|
|||
|
|
@ -114,8 +114,8 @@ some usage help and the available commands::
|
|||
scrapy <command> [options] [args]
|
||||
|
||||
Available commands:
|
||||
crawl Run a spider
|
||||
fetch Fetch a URL using the Scrapy downloader
|
||||
runspider Run a spider from a Python file, no project required
|
||||
[...]
|
||||
|
||||
The first line will print the currently active project if you're inside a
|
||||
|
|
@ -199,6 +199,7 @@ Global commands:
|
|||
* :command:`fetch`
|
||||
* :command:`view`
|
||||
* :command:`version`
|
||||
* :command:`bench`
|
||||
|
||||
Project-only commands:
|
||||
|
||||
|
|
@ -207,7 +208,6 @@ Project-only commands:
|
|||
* :command:`list`
|
||||
* :command:`edit`
|
||||
* :command:`parse`
|
||||
* :command:`bench`
|
||||
|
||||
.. command:: startproject
|
||||
|
||||
|
|
@ -263,7 +263,9 @@ crawl
|
|||
* Syntax: ``scrapy crawl <spider>``
|
||||
* Requires project: *yes*
|
||||
|
||||
Start crawling using a spider.
|
||||
Start crawling using the spider with the given :attr:`~scrapy.Spider.name`,
|
||||
which must be one of those that :command:`list` reports. To run a spider from a
|
||||
file instead, use :command:`runspider`.
|
||||
|
||||
Supported options:
|
||||
|
||||
|
|
@ -309,11 +311,25 @@ Usage examples::
|
|||
* parse_item
|
||||
|
||||
$ scrapy check
|
||||
[FAILED] first_spider:parse_item
|
||||
>>> 'RetailPricex' field is missing
|
||||
F.F.
|
||||
======================================================================
|
||||
FAIL: [first_spider] parse (@returns post-hook)
|
||||
----------------------------------------------------------------------
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
scrapy.exceptions.ContractFail: Returned 92 requests, expected 0..4
|
||||
|
||||
[FAILED] first_spider:parse
|
||||
>>> Returned 92 requests, expected 0..4
|
||||
======================================================================
|
||||
FAIL: [first_spider] parse_item (@scrapes post-hook)
|
||||
----------------------------------------------------------------------
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
scrapy.exceptions.ContractFail: Missing fields: RetailPricex
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 4 contracts in 0.174s
|
||||
|
||||
FAILED (failures=2)
|
||||
|
||||
.. skip: end
|
||||
|
||||
|
|
@ -377,7 +393,7 @@ Supported options:
|
|||
|
||||
* ``--spider=SPIDER``: bypass spider autodetection and force use of specific spider
|
||||
|
||||
* ``--headers``: print the response's HTTP headers instead of the response's body
|
||||
* ``--headers``: print the request's and response's HTTP headers instead of the response's body
|
||||
|
||||
* ``--no-redirect``: do not follow HTTP 3xx redirects (default is to follow them)
|
||||
|
||||
|
|
@ -387,15 +403,19 @@ Usage examples::
|
|||
[ ... html content here ... ]
|
||||
|
||||
$ scrapy fetch --nolog --headers http://www.example.com/
|
||||
{'Accept-Ranges': ['bytes'],
|
||||
'Age': ['1263 '],
|
||||
'Connection': ['close '],
|
||||
'Content-Length': ['596'],
|
||||
'Content-Type': ['text/html; charset=UTF-8'],
|
||||
'Date': ['Wed, 18 Aug 2010 23:59:46 GMT'],
|
||||
'Etag': ['"573c1-254-48c9c87349680"'],
|
||||
'Last-Modified': ['Fri, 30 Jul 2010 15:30:18 GMT'],
|
||||
'Server': ['Apache/2.2.3 (CentOS)']}
|
||||
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
||||
> Accept-Language: en
|
||||
> User-Agent: Scrapy/2.16.0 (+https://scrapy.org)
|
||||
> Accept-Encoding: gzip, deflate, br
|
||||
>
|
||||
< Date: Wed, 08 Jul 2026 06:15:01 GMT
|
||||
< Content-Type: text/html
|
||||
< Server: cloudflare
|
||||
< Last-Modified: Wed, 01 Jul 2026 17:50:18 GMT
|
||||
< Allow: GET, HEAD
|
||||
< Cf-Cache-Status: HIT
|
||||
< Age: 8184
|
||||
< Cf-Ray: a17cf3b80eddf141-DME
|
||||
|
||||
.. command:: view
|
||||
|
||||
|
|
@ -476,7 +496,7 @@ Supported options:
|
|||
|
||||
* ``--spider=SPIDER``: bypass spider autodetection and force use of specific spider
|
||||
|
||||
* ``--a NAME=VALUE``: set spider argument (may be repeated)
|
||||
* ``-a NAME=VALUE``: set spider argument (may be repeated)
|
||||
|
||||
* ``--callback`` or ``-c``: spider method to use as callback for parsing the
|
||||
response
|
||||
|
|
@ -553,8 +573,9 @@ runspider
|
|||
* Syntax: ``scrapy runspider <spider_file.py>``
|
||||
* Requires project: *no*
|
||||
|
||||
Run a spider self-contained in a Python file, without having to create a
|
||||
project.
|
||||
Run the spider defined in the given Python file, without requiring a project.
|
||||
|
||||
Supported options: the same as :command:`crawl`.
|
||||
|
||||
Example usage::
|
||||
|
||||
|
|
@ -605,7 +626,10 @@ shouldn't matter to the user running the command, but when the user :ref:`needs
|
|||
a non-default Twisted reactor <disable-asyncio>`, it may be important.
|
||||
|
||||
Scrapy decides which of these two classes to use based on the value of the
|
||||
:setting:`TWISTED_REACTOR` setting. If the setting value is the default one
|
||||
:setting:`TWISTED_REACTOR` and :setting:`TWISTED_REACTOR_ENABLED` settings.
|
||||
With :setting:`TWISTED_REACTOR_ENABLED` set to ``False`` it will use
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess`. Otherwise, if the
|
||||
:setting:`TWISTED_REACTOR` value is the default one
|
||||
(``'twisted.internet.asyncioreactor.AsyncioSelectorReactor'``),
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` will be used, otherwise
|
||||
:class:`~scrapy.crawler.CrawlerProcess` will be used. The :ref:`spider settings
|
||||
|
|
@ -644,6 +668,8 @@ Example:
|
|||
|
||||
COMMANDS_MODULE = "mybot.commands"
|
||||
|
||||
.. note:: This is a :ref:`pre-crawler setting <pre-crawler-settings>`.
|
||||
|
||||
.. _Deploying your project: https://scrapyd.readthedocs.io/en/latest/deploy.html
|
||||
|
||||
Register commands via setup.py entry points
|
||||
|
|
|
|||
|
|
@ -9,37 +9,22 @@ A Scrapy component is any class whose objects are built using
|
|||
|
||||
That includes the classes that you may assign to the following settings:
|
||||
|
||||
- :setting:`ADDONS`
|
||||
|
||||
- :setting:`TWISTED_DNS_RESOLVER`
|
||||
|
||||
- :setting:`DOWNLOAD_HANDLERS`
|
||||
|
||||
- :setting:`DOWNLOADER_MIDDLEWARES`
|
||||
|
||||
- :setting:`DUPEFILTER_CLASS`
|
||||
|
||||
- :setting:`EXTENSIONS`
|
||||
|
||||
- :setting:`FEED_EXPORTERS`
|
||||
|
||||
- :setting:`FEED_STORAGES`
|
||||
|
||||
- :setting:`ITEM_PIPELINES`
|
||||
|
||||
- :setting:`SCHEDULER`
|
||||
|
||||
- :setting:`SCHEDULER_DISK_QUEUE`
|
||||
|
||||
- :setting:`SCHEDULER_MEMORY_QUEUE`
|
||||
|
||||
- :setting:`SCHEDULER_PRIORITY_QUEUE`
|
||||
|
||||
- :setting:`SCHEDULER_START_DISK_QUEUE`
|
||||
|
||||
- :setting:`SCHEDULER_START_MEMORY_QUEUE`
|
||||
|
||||
- :setting:`SPIDER_MIDDLEWARES`
|
||||
- :setting:`ADDONS`
|
||||
- :setting:`DOWNLOAD_HANDLERS`
|
||||
- :setting:`DOWNLOADER_MIDDLEWARES`
|
||||
- :setting:`DUPEFILTER_CLASS`
|
||||
- :setting:`EXTENSIONS`
|
||||
- :setting:`FEED_EXPORTERS`
|
||||
- :setting:`FEED_STORAGES`
|
||||
- :setting:`ITEM_PIPELINES`
|
||||
- :setting:`SCHEDULER`
|
||||
- :setting:`SCHEDULER_DISK_QUEUE`
|
||||
- :setting:`SCHEDULER_MEMORY_QUEUE`
|
||||
- :setting:`SCHEDULER_PRIORITY_QUEUE`
|
||||
- :setting:`SCHEDULER_START_DISK_QUEUE`
|
||||
- :setting:`SCHEDULER_START_MEMORY_QUEUE`
|
||||
- :setting:`SPIDER_MIDDLEWARES`
|
||||
- :setting:`TWISTED_DNS_RESOLVER`
|
||||
|
||||
Third-party Scrapy components may also let you define additional Scrapy
|
||||
components, usually configurable through :ref:`settings <topics-settings>`, to
|
||||
|
|
|
|||
|
|
@ -30,43 +30,15 @@ You can use the following contracts:
|
|||
|
||||
.. module:: scrapy.contracts.default
|
||||
|
||||
.. class:: UrlContract
|
||||
.. autoclass:: UrlContract
|
||||
|
||||
This contract (``@url``) sets the sample URL used when checking other
|
||||
contract conditions for this spider. This contract is mandatory. All
|
||||
callbacks lacking this contract are ignored when running the checks::
|
||||
.. autoclass:: CallbackKeywordArgumentsContract
|
||||
|
||||
@url url
|
||||
.. autoclass:: MetadataContract
|
||||
|
||||
.. class:: CallbackKeywordArgumentsContract
|
||||
.. autoclass:: ReturnsContract
|
||||
|
||||
This contract (``@cb_kwargs``) sets the :attr:`cb_kwargs <scrapy.Request.cb_kwargs>`
|
||||
attribute for the sample request. It must be a valid JSON dictionary.
|
||||
::
|
||||
|
||||
@cb_kwargs {"arg1": "value1", "arg2": "value2", ...}
|
||||
|
||||
.. class:: MetadataContract
|
||||
|
||||
This contract (``@meta``) sets the :attr:`meta <scrapy.Request.meta>`
|
||||
attribute for the sample request. It must be a valid JSON dictionary.
|
||||
::
|
||||
|
||||
@meta {"arg1": "value1", "arg2": "value2", ...}
|
||||
|
||||
.. class:: ReturnsContract
|
||||
|
||||
This contract (``@returns``) sets lower and upper bounds for the items and
|
||||
requests returned by the spider. The upper bound is optional::
|
||||
|
||||
@returns item(s)|request(s) [min [max]]
|
||||
|
||||
.. class:: ScrapesContract
|
||||
|
||||
This contract (``@scrapes``) checks that all the items returned by the
|
||||
callback have the specified fields::
|
||||
|
||||
@scrapes field_1 field_2 ...
|
||||
.. autoclass:: ScrapesContract
|
||||
|
||||
Use the :command:`check` command to run the contract checks.
|
||||
|
||||
|
|
@ -89,30 +61,16 @@ override three methods:
|
|||
|
||||
.. module:: scrapy.contracts
|
||||
|
||||
.. class:: Contract(method, *args)
|
||||
.. autoclass:: Contract
|
||||
|
||||
:param method: callback function to which the contract is associated
|
||||
:type method: collections.abc.Callable
|
||||
.. automethod:: adjust_request_args
|
||||
|
||||
:param args: list of arguments passed into the docstring (whitespace
|
||||
separated)
|
||||
:type args: list
|
||||
|
||||
.. method:: Contract.adjust_request_args(args)
|
||||
|
||||
This receives a ``dict`` as an argument containing default arguments
|
||||
for request object. :class:`~scrapy.Request` is used by default,
|
||||
but this can be changed with the ``request_cls`` attribute.
|
||||
If multiple contracts in chain have this attribute defined, the last one is used.
|
||||
|
||||
Must return the same or a modified version of it.
|
||||
|
||||
.. method:: Contract.pre_process(response)
|
||||
.. method:: pre_process(response)
|
||||
|
||||
This allows hooking in various checks on the response received from the
|
||||
sample request, before it's being passed to the callback.
|
||||
|
||||
.. method:: Contract.post_process(output)
|
||||
.. method:: post_process(output)
|
||||
|
||||
This allows processing the output of the callback. Iterators are
|
||||
converted to lists before being passed to this hook.
|
||||
|
|
|
|||
|
|
@ -16,12 +16,14 @@ 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
|
||||
- The :meth:`~scrapy.Spider.start` spider method, which *must* be
|
||||
defined as an :term:`asynchronous generator`.
|
||||
|
||||
.. versionadded:: 2.13
|
||||
|
||||
- :class:`~scrapy.Request` callbacks.
|
||||
- :class:`~scrapy.Request` :ref:`callbacks <callbacks>`, which may
|
||||
also be defined as :term:`asynchronous generators <asynchronous
|
||||
generator>`.
|
||||
|
||||
- The :meth:`process_item` method of
|
||||
:ref:`item pipelines <topics-item-pipeline>`.
|
||||
|
|
@ -204,13 +206,15 @@ 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
|
||||
:meth:`~scrapy.spiders.Spider.start`, callbacks, pipelines and
|
||||
:meth:`~scrapy.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);
|
||||
* calling asynchronous Scrapy methods like :meth:`ExecutionEngine.download`
|
||||
(see :ref:`the screenshot pipeline example<ScreenshotPipeline>`).
|
||||
* calling asynchronous Scrapy methods like
|
||||
:meth:`ExecutionEngine.download_async()
|
||||
<scrapy.core.engine.ExecutionEngine.download_async>` (see :ref:`the
|
||||
screenshot pipeline example <ScreenshotPipeline>`).
|
||||
|
||||
.. _aio-libs: https://github.com/aio-libs
|
||||
|
||||
|
|
@ -268,5 +272,5 @@ You can also send multiple requests in parallel:
|
|||
yield {
|
||||
"h1": response.css("h1::text").get(),
|
||||
"price": responses[0].css(".price::text").get(),
|
||||
"price2": responses[1].css(".color::text").get(),
|
||||
"color": responses[1].css(".color::text").get(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,6 @@ also request each page to get every quote on the site:
|
|||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
import json
|
||||
|
||||
|
||||
class QuoteSpider(scrapy.Spider):
|
||||
|
|
@ -256,7 +255,7 @@ also request each page to get every quote on the site:
|
|||
start_urls = ["https://quotes.toscrape.com/api/quotes?page=1"]
|
||||
|
||||
def parse(self, response):
|
||||
data = json.loads(response.text)
|
||||
data = response.json()
|
||||
for quote in data["quotes"]:
|
||||
yield {"quote": quote["text"]}
|
||||
if data["has_next"]:
|
||||
|
|
|
|||
|
|
@ -78,33 +78,15 @@ Writing your own download handler
|
|||
A download handler is a :ref:`component <topics-components>` that defines
|
||||
the following API:
|
||||
|
||||
.. class:: SampleDownloadHandler
|
||||
|
||||
.. attribute:: lazy
|
||||
:type: bool
|
||||
|
||||
If ``False``, the handler will be instantiated when Scrapy is
|
||||
initialized.
|
||||
|
||||
If ``True``, the handler will only be instantiated when the first
|
||||
request handled by it needs to be downloaded.
|
||||
|
||||
.. method:: download_request(request: Request) -> Response:
|
||||
:async:
|
||||
|
||||
Download the given request and return a response.
|
||||
|
||||
.. method:: close() -> None
|
||||
:async:
|
||||
|
||||
Clean up any resources used by the handler.
|
||||
.. autoclass:: scrapy.core.downloader.handlers.DownloadHandlerProtocol
|
||||
:members:
|
||||
|
||||
An optional base class for custom handlers is provided:
|
||||
|
||||
.. autoclass:: scrapy.core.downloader.handlers.base.BaseDownloadHandler
|
||||
:members:
|
||||
:undoc-members:
|
||||
:member-order: bysource
|
||||
:exclude-members: close, download_request, lazy
|
||||
|
||||
.. _download-handlers-exceptions:
|
||||
|
||||
|
|
@ -173,6 +155,8 @@ of this package for more information.
|
|||
H2DownloadHandler
|
||||
-----------------
|
||||
|
||||
.. note:: Requires the :ref:`twisted-http2 <extras>` extra.
|
||||
|
||||
.. autoclass:: scrapy.core.downloader.handlers.http2.H2DownloadHandler
|
||||
|
||||
| Supported scheme: ``https``.
|
||||
|
|
@ -185,9 +169,6 @@ for them.
|
|||
|
||||
It's implemented using :mod:`twisted.web.client` and the ``h2`` library.
|
||||
|
||||
For this handler to work you need to install the ``Twisted[http2]`` extra
|
||||
dependency.
|
||||
|
||||
If you want to use this handler you need to replace the default one for the
|
||||
``https`` scheme:
|
||||
|
||||
|
|
@ -273,9 +254,13 @@ Other limitations:
|
|||
|
||||
- HTTPS proxies to HTTPS destinations are not supported.
|
||||
|
||||
.. _httpx-handler:
|
||||
|
||||
HttpxDownloadHandler
|
||||
--------------------
|
||||
|
||||
.. note:: Requires the :ref:`httpx <extras>` extra.
|
||||
|
||||
.. versionadded:: 2.15.0
|
||||
|
||||
.. autoclass:: scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler
|
||||
|
|
@ -288,7 +273,9 @@ HttpxDownloadHandler
|
|||
This handler supports ``http://host/path`` and ``https://host/path`` URLs and
|
||||
uses the HTTP/1.1 or HTTP/2 protocol for them.
|
||||
|
||||
It's implemented using the ``httpx`` library and needs it to be installed.
|
||||
It's implemented using the httpx2_ library.
|
||||
|
||||
.. _httpx2: https://httpx2.pydantic.dev/
|
||||
|
||||
If you want to use this handler you need to replace the default ones for the
|
||||
``http`` and ``https`` schemes:
|
||||
|
|
@ -311,8 +298,8 @@ Features and limitations
|
|||
|
||||
=========================== =======================================
|
||||
HTTP proxies Yes
|
||||
SOCKS proxies Yes (SOCKS5; requires ``httpx[socks]``)
|
||||
HTTP/2 Yes (requires ``httpx[http2]``)
|
||||
SOCKS proxies Yes (SOCKS5)
|
||||
HTTP/2 Yes
|
||||
``response.certificate`` DER bytes
|
||||
Per-request ``bindaddress`` No (not supported by the library)
|
||||
TLS implementation Standard library ``ssl``
|
||||
|
|
@ -329,12 +316,11 @@ Other limitations:
|
|||
HTTPX_HTTP2_ENABLED
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Whether to enable HTTP/2 support in this handler. The ``httpx[http2]`` extra
|
||||
needs to be installed if you want to enable this setting.
|
||||
|
||||
.. versionadded:: VERSION
|
||||
Whether to enable HTTP/2 support in this handler.
|
||||
|
||||
Built-in non-HTTP download handlers reference
|
||||
=============================================
|
||||
|
|
@ -378,9 +364,13 @@ This handler supports ``ftp://host/path`` FTP URIs.
|
|||
|
||||
It's implemented using :mod:`twisted.protocols.ftp`.
|
||||
|
||||
.. _s3-handler:
|
||||
|
||||
S3DownloadHandler
|
||||
-----------------
|
||||
|
||||
.. note:: Requires the :ref:`s3 <extras>` extra.
|
||||
|
||||
.. autoclass:: scrapy.core.downloader.handlers.s3.S3DownloadHandler
|
||||
|
||||
| Supported scheme: ``s3``.
|
||||
|
|
@ -390,4 +380,6 @@ S3DownloadHandler
|
|||
|
||||
This handler supports ``s3://bucket/path`` S3 URIs.
|
||||
|
||||
It's implemented using the ``botocore`` library and needs it to be installed.
|
||||
It's implemented using the botocore_ library.
|
||||
|
||||
.. _botocore: https://github.com/boto/botocore
|
||||
|
|
|
|||
|
|
@ -351,6 +351,8 @@ HttpAuthMiddleware
|
|||
HTTPAUTH_USER
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Default: ``""``
|
||||
|
||||
The username to use for HTTP basic authentication, applied to all requests
|
||||
|
|
@ -361,6 +363,8 @@ whose URL matches :setting:`HTTPAUTH_DOMAIN`.
|
|||
HTTPAUTH_PASS
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Default: ``""``
|
||||
|
||||
The password to use for HTTP basic authentication.
|
||||
|
|
@ -370,6 +374,8 @@ The password to use for HTTP basic authentication.
|
|||
HTTPAUTH_DOMAIN
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Default: ``None``
|
||||
|
||||
The domain (and its subdomains) to which HTTP basic authentication credentials
|
||||
|
|
@ -499,7 +505,7 @@ Filesystem storage backend (default)
|
|||
|
||||
* ``response_body`` - the plain response body
|
||||
|
||||
* ``response_headers`` - the request headers (in raw HTTP format)
|
||||
* ``response_headers`` - the response headers (in raw HTTP format)
|
||||
|
||||
* ``meta`` - some metadata of this cache resource in Python ``repr()``
|
||||
format (grep-friendly format)
|
||||
|
|
@ -541,7 +547,7 @@ defines the methods described below.
|
|||
.. method:: open_spider(spider)
|
||||
|
||||
This method gets called after a spider has been opened for crawling. It handles
|
||||
the :signal:`open_spider <spider_opened>` signal.
|
||||
the :signal:`spider_opened` signal.
|
||||
|
||||
:param spider: the spider which has been opened
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
|
@ -549,7 +555,7 @@ defines the methods described below.
|
|||
.. method:: close_spider(spider)
|
||||
|
||||
This method gets called after a spider has been closed. It handles
|
||||
the :signal:`close_spider <spider_closed>` signal.
|
||||
the :signal:`spider_closed` signal.
|
||||
|
||||
:param spider: the spider which has been closed
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
|
@ -558,6 +564,10 @@ defines the methods described below.
|
|||
|
||||
Return response if present in cache, or ``None`` otherwise.
|
||||
|
||||
If this method raises an exception, e.g. because the cache entry is
|
||||
corrupted, the middleware logs a warning and handles the request as a
|
||||
cache miss.
|
||||
|
||||
:param spider: the spider which generated the request
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
|
|
@ -585,8 +595,8 @@ In order to use your storage backend, set:
|
|||
HTTPCache middleware settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :class:`HttpCacheMiddleware` can be configured through the following
|
||||
settings:
|
||||
:class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware` can be
|
||||
configured through the following settings:
|
||||
|
||||
.. setting:: HTTPCACHE_ENABLED
|
||||
|
||||
|
|
@ -721,6 +731,8 @@ We assume that the spider will not issue Cache-Control directives
|
|||
in requests unless it actually needs them, so directives in requests are
|
||||
not filtered.
|
||||
|
||||
.. _http-compression:
|
||||
|
||||
HttpCompressionMiddleware
|
||||
-------------------------
|
||||
|
||||
|
|
@ -732,14 +744,12 @@ HttpCompressionMiddleware
|
|||
This middleware allows compressed (gzip, deflate) traffic to be
|
||||
sent/received from web sites.
|
||||
|
||||
This middleware also supports decoding `brotli-compressed`_ as well as
|
||||
`zstd-compressed`_ responses, provided that `brotli`_ or `zstandard`_ is
|
||||
installed, respectively.
|
||||
This middleware also supports decoding `brotli-compressed`_ responses with
|
||||
the :ref:`brotli <extras>` extra, and `zstd-compressed`_
|
||||
responses with the :ref:`zstd <extras>` extra.
|
||||
|
||||
.. _brotli-compressed: https://www.ietf.org/rfc/rfc7932.txt
|
||||
.. _brotli: https://pypi.org/project/Brotli/
|
||||
.. _zstd-compressed: https://www.ietf.org/rfc/rfc8478.txt
|
||||
.. _zstandard: https://pypi.org/project/zstandard/
|
||||
|
||||
|
||||
HttpCompressionMiddleware Settings
|
||||
|
|
@ -808,7 +818,6 @@ HttpProxyMiddleware settings
|
|||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. setting:: HTTPPROXY_ENABLED
|
||||
.. setting:: HTTPPROXY_AUTH_ENCODING
|
||||
|
||||
HTTPPROXY_ENABLED
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
|
@ -817,6 +826,8 @@ Default: ``True``
|
|||
|
||||
Whether or not to enable the :class:`HttpProxyMiddleware`.
|
||||
|
||||
.. setting:: HTTPPROXY_AUTH_ENCODING
|
||||
|
||||
HTTPPROXY_AUTH_ENCODING
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
@ -861,9 +872,9 @@ OffsiteMiddleware
|
|||
.. 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.
|
||||
``True`` or :attr:`Request.meta <scrapy.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
|
||||
------------------
|
||||
|
|
@ -978,7 +989,7 @@ Whether the Meta Refresh middleware will be enabled.
|
|||
METAREFRESH_IGNORE_TAGS
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Default: ``[]``
|
||||
Default: ``["noscript"]``
|
||||
|
||||
Meta tags within these tags are ignored.
|
||||
|
||||
|
|
@ -1008,17 +1019,6 @@ RetryMiddleware
|
|||
A middleware to retry failed requests that are potentially caused by
|
||||
temporary problems such as a connection timeout or HTTP 500 error.
|
||||
|
||||
Failed pages are collected on the scraping process and rescheduled at the
|
||||
end, once the spider has finished crawling all regular (non failed) pages.
|
||||
|
||||
The :class:`RetryMiddleware` can be configured through the following
|
||||
settings (see the settings documentation for more info):
|
||||
|
||||
* :setting:`RETRY_ENABLED`
|
||||
* :setting:`RETRY_TIMES`
|
||||
* :setting:`RETRY_HTTP_CODES`
|
||||
* :setting:`RETRY_EXCEPTIONS`
|
||||
|
||||
.. reqmeta:: dont_retry
|
||||
|
||||
If :attr:`Request.meta <scrapy.Request.meta>` has ``dont_retry`` key
|
||||
|
|
@ -1085,7 +1085,7 @@ Default::
|
|||
'twisted.internet.error.ConnectionDone',
|
||||
'twisted.internet.error.ConnectError',
|
||||
'twisted.internet.error.ConnectionLost',
|
||||
IOError,
|
||||
OSError,
|
||||
'scrapy.core.downloader.handlers.http11.TunnelError',
|
||||
]
|
||||
|
||||
|
|
@ -1104,6 +1104,8 @@ exception propagation, see
|
|||
RETRY_GIVE_UP_LOG_LEVEL
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Default: ``"ERROR"``
|
||||
|
||||
:ref:`Logging level <levels>` used for the message logged when a request
|
||||
|
|
@ -1241,8 +1243,7 @@ Based on `Robotexclusionrulesparser <https://pypi.org/project/robotexclusionrule
|
|||
|
||||
In order to use this parser:
|
||||
|
||||
* Install ``Robotexclusionrulesparser`` by running
|
||||
``pip install robotexclusionrulesparser``
|
||||
* Install the :ref:`robotparser <extras>` extra.
|
||||
|
||||
* Set :setting:`ROBOTSTXT_PARSER` setting to
|
||||
``scrapy.robotstxt.RerpRobotParser``
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ data from it depends on the type of response:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
selector = Selector(data["html"])
|
||||
selector = Selector(text=data["html"])
|
||||
|
||||
- If the response is JavaScript, or HTML with a ``<script/>`` element
|
||||
containing the desired data, see :ref:`topics-parsing-javascript`.
|
||||
|
|
|
|||
|
|
@ -1,115 +1,25 @@
|
|||
.. _topics-exceptions:
|
||||
.. _topics-exceptions-ref:
|
||||
|
||||
==========
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
Here's a list of all exceptions included in Scrapy and their usage, except for
|
||||
the :ref:`download handler exceptions <download-handlers-exceptions>`.
|
||||
|
||||
.. module:: scrapy.exceptions
|
||||
:synopsis: Scrapy exceptions
|
||||
|
||||
.. _topics-exceptions-ref:
|
||||
.. autoexception:: CloseSpider
|
||||
|
||||
Built-in Exceptions reference
|
||||
=============================
|
||||
.. autoexception:: DontCloseSpider
|
||||
|
||||
Here's a list of all exceptions included in Scrapy and their usage.
|
||||
.. autoexception:: DropItem
|
||||
|
||||
.. autoexception:: IgnoreRequest
|
||||
|
||||
CloseSpider
|
||||
-----------
|
||||
.. autoexception:: NotConfigured
|
||||
|
||||
.. exception:: CloseSpider(reason='cancelled')
|
||||
.. autoexception:: NotSupported
|
||||
|
||||
This exception can be raised from a spider callback to request the spider to be
|
||||
closed/stopped. Supported arguments:
|
||||
|
||||
:param reason: the reason for closing
|
||||
:type reason: str
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def parse_page(self, response):
|
||||
if "Bandwidth exceeded" in response.body:
|
||||
raise CloseSpider("bandwidth_exceeded")
|
||||
|
||||
DontCloseSpider
|
||||
---------------
|
||||
|
||||
.. exception:: DontCloseSpider
|
||||
|
||||
This exception can be raised in a :signal:`spider_idle` signal handler to
|
||||
prevent the spider from being closed.
|
||||
|
||||
DropItem
|
||||
--------
|
||||
|
||||
.. exception:: DropItem
|
||||
|
||||
The exception that must be raised by item pipeline stages to stop processing an
|
||||
Item. For more information see :ref:`topics-item-pipeline`.
|
||||
|
||||
IgnoreRequest
|
||||
-------------
|
||||
|
||||
.. exception:: IgnoreRequest
|
||||
|
||||
This exception can be raised by the Scheduler or any downloader middleware to
|
||||
indicate that the request should be ignored.
|
||||
|
||||
NotConfigured
|
||||
-------------
|
||||
|
||||
.. exception:: NotConfigured
|
||||
|
||||
This exception can be raised by some components to indicate that they will
|
||||
remain disabled. Those components include:
|
||||
|
||||
- Extensions
|
||||
- Item pipelines
|
||||
- Downloader middlewares
|
||||
- Spider middlewares
|
||||
|
||||
The exception must be raised in the component's ``__init__`` method.
|
||||
|
||||
NotSupported
|
||||
------------
|
||||
|
||||
.. exception:: NotSupported
|
||||
|
||||
This exception is raised to indicate an unsupported feature.
|
||||
|
||||
StopDownload
|
||||
-------------
|
||||
|
||||
.. exception:: StopDownload(fail=True)
|
||||
|
||||
Raised from a :class:`~scrapy.signals.bytes_received` or :class:`~scrapy.signals.headers_received`
|
||||
signal handler to indicate that no further bytes should be downloaded for a response.
|
||||
|
||||
The ``fail`` boolean parameter controls which method will handle the resulting
|
||||
response:
|
||||
|
||||
* If ``fail=True`` (default), the request errback is called. The response object is
|
||||
available as the ``response`` attribute of the ``StopDownload`` exception,
|
||||
which is in turn stored as the ``value`` attribute of the received
|
||||
:class:`~twisted.python.failure.Failure` object. This means that in an errback
|
||||
defined as ``def errback(self, failure)``, the response can be accessed though
|
||||
``failure.value.response``.
|
||||
|
||||
* If ``fail=False``, the request callback is called instead.
|
||||
|
||||
In both cases, the response could have its body truncated: the body contains
|
||||
all bytes received up until the exception is raised, including the bytes
|
||||
received in the signal handler that raises the exception. Also, the response
|
||||
object is marked with ``"download_stopped"`` in its :attr:`~scrapy.http.Response.flags`
|
||||
attribute.
|
||||
|
||||
.. note:: ``fail`` is a keyword-only parameter, i.e. raising
|
||||
``StopDownload(False)`` or ``StopDownload(True)`` will raise
|
||||
a :class:`TypeError`.
|
||||
|
||||
See the documentation for the :class:`~scrapy.signals.bytes_received` and
|
||||
:class:`~scrapy.signals.headers_received` signals
|
||||
and the :ref:`topics-stop-response-download` topic for additional information and examples.
|
||||
.. autoexception:: StopDownload
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ output examples, which assume you're exporting these two items:
|
|||
BaseItemExporter
|
||||
----------------
|
||||
|
||||
.. class:: BaseItemExporter(fields_to_export=None, export_empty_fields=False, encoding='utf-8', indent=0, dont_fail=False)
|
||||
.. class:: BaseItemExporter(fields_to_export=None, export_empty_fields=False, encoding=None, indent=None, dont_fail=False)
|
||||
|
||||
This is the (abstract) base class for all Item Exporters. It provides
|
||||
support for common features used by all (concrete) Item Exporters, such as
|
||||
|
|
@ -211,13 +211,27 @@ BaseItemExporter
|
|||
|
||||
- ``None`` (all fields [2]_, default)
|
||||
|
||||
- A list of fields::
|
||||
Fields are exported in declaration order, i.e. the order in which
|
||||
they are defined in the :ref:`item class <item-types>`. For
|
||||
:class:`dict` items, which have no declared fields, the key order of
|
||||
each item is used instead.
|
||||
|
||||
['field1', 'field2']
|
||||
.. versionchanged:: VERSION
|
||||
Fields of non-\ :class:`dict` items used to be exported in the
|
||||
order in which they had been populated, except in
|
||||
:class:`CsvItemExporter`, which has always used declaration order.
|
||||
|
||||
- A dict where keys are fields and values are output names::
|
||||
- A list of fields:
|
||||
|
||||
{'field1': 'Field 1', 'field2': 'Field 2'}
|
||||
.. code-block:: python
|
||||
|
||||
["field1", "field2"]
|
||||
|
||||
- A dict where keys are fields and values are output names:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{"field1": "Field 1", "field2": "Field 2"}
|
||||
|
||||
.. [1] Not all exporters respect the specified field order.
|
||||
.. [2] When using :ref:`item objects <item-types>` that do not expose
|
||||
|
|
@ -239,7 +253,7 @@ BaseItemExporter
|
|||
|
||||
.. attribute:: indent
|
||||
|
||||
Amount of spaces used to indent the output on each level. Defaults to ``0``.
|
||||
Amount of spaces used to indent the output on each level. Defaults to ``None``.
|
||||
|
||||
* ``indent=None`` selects the most compact representation,
|
||||
all items in the same line with no indentation
|
||||
|
|
@ -273,7 +287,9 @@ XmlItemExporter
|
|||
The additional keyword arguments of this ``__init__`` method are passed to the
|
||||
:class:`BaseItemExporter` ``__init__`` method.
|
||||
|
||||
A typical output of this exporter would be::
|
||||
A typical output of this exporter would be:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<items>
|
||||
|
|
@ -291,11 +307,17 @@ XmlItemExporter
|
|||
exported by serializing each value inside a ``<value>`` element. This is for
|
||||
convenience, as multi-valued fields are very common.
|
||||
|
||||
For example, the item::
|
||||
For example, the item:
|
||||
|
||||
Item(name=['John', 'Doe'], age='23')
|
||||
.. skip: next
|
||||
|
||||
Would be serialized as::
|
||||
.. code-block:: python
|
||||
|
||||
Item(name=["John", "Doe"], age="23")
|
||||
|
||||
Would be serialized as:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<items>
|
||||
|
|
@ -328,7 +350,7 @@ CsvItemExporter
|
|||
|
||||
:param join_multivalued: The char (or chars) that will be used for joining
|
||||
multi-valued fields, if found.
|
||||
:type include_headers_line: str
|
||||
:type join_multivalued: str
|
||||
|
||||
:param errors: The optional string that specifies how encoding and decoding
|
||||
errors are to be handled. For more information see
|
||||
|
|
@ -342,14 +364,14 @@ CsvItemExporter
|
|||
|
||||
A typical output of this exporter would be::
|
||||
|
||||
product,price
|
||||
name,price
|
||||
Color TV,1200
|
||||
DVD player,200
|
||||
|
||||
PickleItemExporter
|
||||
------------------
|
||||
|
||||
.. class:: PickleItemExporter(file, protocol=0, **kwargs)
|
||||
.. class:: PickleItemExporter(file, protocol=4, **kwargs)
|
||||
|
||||
Exports items in pickle format to the given file-like object.
|
||||
|
||||
|
|
@ -379,10 +401,12 @@ PprintItemExporter
|
|||
The additional keyword arguments of this ``__init__`` method are passed to the
|
||||
:class:`BaseItemExporter` ``__init__`` method.
|
||||
|
||||
A typical output of this exporter would be::
|
||||
A typical output of this exporter would be:
|
||||
|
||||
{'name': 'Color TV', 'price': '1200'}
|
||||
{'name': 'DVD player', 'price': '200'}
|
||||
.. code-block:: python
|
||||
|
||||
{"name": "Color TV", "price": "1200"}
|
||||
{"name": "DVD player", "price": "200"}
|
||||
|
||||
Longer lines (when present) are pretty-formatted.
|
||||
|
||||
|
|
@ -400,7 +424,9 @@ JsonItemExporter
|
|||
:param file: the file-like object to use for exporting the data. Its ``write`` method should
|
||||
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
|
||||
|
||||
A typical output of this exporter would be::
|
||||
A typical output of this exporter would be:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[{"name": "Color TV", "price": "1200"},
|
||||
{"name": "DVD player", "price": "200"}]
|
||||
|
|
@ -429,7 +455,9 @@ JsonLinesItemExporter
|
|||
:param file: the file-like object to use for exporting the data. Its ``write`` method should
|
||||
accept ``bytes`` (a disk file opened in binary mode, a ``io.BytesIO`` object, etc)
|
||||
|
||||
A typical output of this exporter would be::
|
||||
A typical output of this exporter would be:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{"name": "Color TV", "price": "1200"}
|
||||
{"name": "DVD player", "price": "200"}
|
||||
|
|
|
|||
|
|
@ -136,18 +136,10 @@ Core Stats extension
|
|||
Enable the collection of core statistics, provided the stats collection is
|
||||
enabled (see :ref:`topics-stats`).
|
||||
|
||||
The following stats are collected:
|
||||
|
||||
* ``start_time``: start date/time of the crawl (:class:`~datetime.datetime`).
|
||||
* ``finish_time``: end date/time of the crawl (:class:`~datetime.datetime`).
|
||||
* ``elapsed_time_seconds``: total crawl duration in seconds (:class:`float`).
|
||||
* ``finish_reason``: the closing reason string (e.g. ``"finished"``,
|
||||
``"closespider_timeout"``).
|
||||
* ``item_scraped_count``: total number of items that passed all pipelines.
|
||||
* ``item_dropped_count``: total number of items dropped by a pipeline.
|
||||
* ``item_dropped_reasons_count/<ExceptionName>``: per-exception drop count
|
||||
(e.g. ``item_dropped_reasons_count/DropItem``).
|
||||
* ``response_received_count``: total number of HTTP responses received.
|
||||
The following stats are collected: :stat:`elapsed_time_seconds`,
|
||||
:stat:`finish_reason`, :stat:`finish_time`, :stat:`item_dropped_count`,
|
||||
:stat:`item_dropped_reasons_count/{exception}`, :stat:`item_scraped_count`,
|
||||
:stat:`response_received_count`, :stat:`start_time`.
|
||||
|
||||
Log Count extension
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -190,7 +182,7 @@ Monitors the memory used by the Scrapy process that runs the spider and:
|
|||
|
||||
1. sends a :signal:`memusage_warning_reached` signal when it exceeds
|
||||
:setting:`MEMUSAGE_WARNING_MB`
|
||||
2. closes the spider with the `"memusage_exceeded"` reason when it exceeds
|
||||
2. closes the spider with the ``"memusage_exceeded"`` reason when it exceeds
|
||||
:setting:`MEMUSAGE_LIMIT_MB`
|
||||
|
||||
This extension is enabled by the :setting:`MEMUSAGE_ENABLED` setting and
|
||||
|
|
@ -214,7 +206,8 @@ An extension for debugging memory usage. It collects information about:
|
|||
* objects left alive that shouldn't. For more info, see :ref:`topics-leaks-trackrefs`
|
||||
|
||||
To enable this extension, turn on the :setting:`MEMDEBUG_ENABLED` setting. The
|
||||
info will be stored in the stats.
|
||||
info will be stored in the :stat:`memdebug/gc_garbage_count` and
|
||||
:stat:`memdebug/live_refs/{cls}` stats.
|
||||
|
||||
.. _topics-extensions-ref-spiderstate:
|
||||
|
||||
|
|
@ -341,9 +334,6 @@ closing the spider. If the spider generates more than that number of errors,
|
|||
it will be closed with the reason ``closespider_errorcount``. If zero (or non
|
||||
set), spiders won't be closed by number of errors.
|
||||
|
||||
.. module:: scrapy.extensions.debug
|
||||
:synopsis: Extensions for debugging Scrapy
|
||||
|
||||
.. module:: scrapy.extensions.periodic_log
|
||||
:synopsis: Periodic stats logging
|
||||
|
||||
|
|
@ -418,7 +408,7 @@ Example extension configuration:
|
|||
custom_settings = {
|
||||
"LOG_LEVEL": "INFO",
|
||||
"PERIODIC_LOG_STATS": {
|
||||
"include": ["downloader/", "scheduler/", "log_count/", "item_scraped_count/"],
|
||||
"include": ["downloader/", "scheduler/", "log_count/", "item_scraped_count"],
|
||||
},
|
||||
"PERIODIC_LOG_DELTA": {"include": ["downloader/"]},
|
||||
"PERIODIC_LOG_TIMING_ENABLED": True,
|
||||
|
|
@ -463,6 +453,9 @@ Default: ``False``
|
|||
Debugging extensions
|
||||
--------------------
|
||||
|
||||
.. module:: scrapy.extensions.debug
|
||||
:synopsis: Extensions for debugging Scrapy
|
||||
|
||||
Stack trace dump extension
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ Marshal
|
|||
- Value for the ``format`` key in the :setting:`FEEDS` setting: ``marshal``
|
||||
- Exporter used: :class:`~scrapy.exporters.MarshalItemExporter`
|
||||
|
||||
|
||||
.. _topics-feed-storage:
|
||||
|
||||
Storages
|
||||
|
|
@ -106,14 +105,13 @@ The storages backends supported out of the box are:
|
|||
|
||||
- :ref:`topics-feed-storage-fs`
|
||||
- :ref:`topics-feed-storage-ftp`
|
||||
- :ref:`topics-feed-storage-s3` (requires boto3_)
|
||||
- :ref:`topics-feed-storage-gcs` (requires `google-cloud-storage`_)
|
||||
- :ref:`topics-feed-storage-s3` (requires the :ref:`s3 <extras>` extra)
|
||||
- :ref:`topics-feed-storage-gcs` (requires the :ref:`gcs <extras>` extra)
|
||||
- :ref:`topics-feed-storage-stdout`
|
||||
|
||||
Some storage backends may be unavailable if the required external libraries are
|
||||
not available. For example, the S3 backend is only available if the boto3_
|
||||
library is installed.
|
||||
|
||||
Some storage backends may be unavailable if the required :ref:`extras <extras>`
|
||||
are not installed. For example, the S3 backend requires the :ref:`s3 <extras>`
|
||||
extra.
|
||||
|
||||
.. _topics-feed-uri-params:
|
||||
|
||||
|
|
@ -143,6 +141,11 @@ Here are some examples to illustrate:
|
|||
.. note:: :ref:`Spider arguments <spiderargs>` become spider attributes, hence
|
||||
they can also be used as storage URI parameters.
|
||||
|
||||
.. note:: Only ``%(...)s`` parameters are replaced. Any other percent
|
||||
character is kept as-is, so percent-encoded URIs (e.g. ``%20`` for a
|
||||
space or percent-encoded FTP credentials) and :class:`pathlib.Path`
|
||||
keys containing ``%(...)s`` parameters both work as expected.
|
||||
|
||||
|
||||
.. _topics-feed-storage-backends:
|
||||
|
||||
|
|
@ -161,7 +164,7 @@ The feeds are stored in the local filesystem.
|
|||
- Required external libraries: none
|
||||
|
||||
Note that for the local filesystem storage (only) you can omit the scheme if
|
||||
you specify an absolute path like ``/tmp/export.csv`` (Unix systems only).
|
||||
you specify a path (e.g. ``/tmp/export.csv``).
|
||||
Alternatively you can also use a :class:`pathlib.Path` object.
|
||||
|
||||
.. _topics-feed-storage-ftp:
|
||||
|
|
@ -204,7 +207,7 @@ The feeds are stored on `Amazon S3`_.
|
|||
|
||||
- ``s3://aws_key:aws_secret@mybucket/path/to/export.csv``
|
||||
|
||||
- Required external libraries: `boto3`_ >= 1.20.0
|
||||
- Required extras: :ref:`s3 <extras>`
|
||||
|
||||
The AWS credentials can be passed as user/password in the URI, or they can be
|
||||
passed through the following settings:
|
||||
|
|
@ -215,12 +218,13 @@ passed through the following settings:
|
|||
|
||||
.. _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:
|
||||
You can also define a custom ACL, custom endpoint, region name and connection
|
||||
pool size for exported feeds using these settings:
|
||||
|
||||
- :setting:`FEED_STORAGE_S3_ACL`
|
||||
- :setting:`AWS_ENDPOINT_URL`
|
||||
- :setting:`AWS_REGION_NAME`
|
||||
- :setting:`AWS_MAX_POOL_CONNECTIONS`
|
||||
|
||||
The default value for the ``overwrite`` key in the :setting:`FEEDS` for this
|
||||
storage backend is: ``True``.
|
||||
|
|
@ -244,7 +248,7 @@ The feeds are stored on `Google Cloud Storage`_.
|
|||
|
||||
- ``gs://mybucket/path/to/export.csv``
|
||||
|
||||
- Required external libraries: `google-cloud-storage`_.
|
||||
- Required extras: :ref:`gcs <extras>`
|
||||
|
||||
For more information about authentication, please refer to `Google Cloud documentation <https://docs.cloud.google.com/docs/authentication>`_.
|
||||
|
||||
|
|
@ -261,7 +265,6 @@ storage backend is: ``True``.
|
|||
|
||||
This storage backend uses :ref:`delayed file delivery <delayed-file-delivery>`.
|
||||
|
||||
.. _google-cloud-storage: https://docs.cloud.google.com/storage/docs/reference/libraries#client-libraries-install-python
|
||||
|
||||
|
||||
.. _topics-feed-storage-stdout:
|
||||
|
|
@ -429,33 +432,37 @@ This setting is required for enabling the feed export feature.
|
|||
|
||||
See :ref:`topics-feed-storage-backends` for supported URI schemes.
|
||||
|
||||
For instance::
|
||||
For instance:
|
||||
|
||||
.. skip: next
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
'items.json': {
|
||||
'format': 'json',
|
||||
'encoding': 'utf8',
|
||||
'store_empty': False,
|
||||
'item_classes': [MyItemClass1, 'myproject.items.MyItemClass2'],
|
||||
'fields': None,
|
||||
'indent': 4,
|
||||
'item_export_kwargs': {
|
||||
'export_empty_fields': True,
|
||||
"items.json": {
|
||||
"format": "json",
|
||||
"encoding": "utf8",
|
||||
"store_empty": False,
|
||||
"item_classes": [MyItemClass1, "myproject.items.MyItemClass2"],
|
||||
"fields": None,
|
||||
"indent": 4,
|
||||
"item_export_kwargs": {
|
||||
"export_empty_fields": True,
|
||||
},
|
||||
},
|
||||
'/home/user/documents/items.xml': {
|
||||
'format': 'xml',
|
||||
'fields': ['name', 'price'],
|
||||
'item_filter': MyCustomFilter1,
|
||||
'encoding': 'latin1',
|
||||
'indent': 8,
|
||||
"/home/user/documents/items.xml": {
|
||||
"format": "xml",
|
||||
"fields": ["name", "price"],
|
||||
"item_filter": MyCustomFilter1,
|
||||
"encoding": "latin1",
|
||||
"indent": 8,
|
||||
},
|
||||
pathlib.Path('items.csv.gz'): {
|
||||
'format': 'csv',
|
||||
'fields': ['price', 'name'],
|
||||
'item_filter': 'myproject.filters.MyCustomFilter2',
|
||||
'postprocessing': [MyPlugin1, 'scrapy.extensions.postprocessing.GzipPlugin'],
|
||||
'gzip_compresslevel': 5,
|
||||
pathlib.Path("items.csv.gz"): {
|
||||
"format": "csv",
|
||||
"fields": ["price", "name"],
|
||||
"item_filter": "myproject.filters.MyCustomFilter2",
|
||||
"postprocessing": [MyPlugin1, "scrapy.extensions.postprocessing.GzipPlugin"],
|
||||
"gzip_compresslevel": 5,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -528,10 +535,6 @@ safe numeric encoding (``\uXXXX`` sequences) for historic reasons.
|
|||
|
||||
Use ``"utf-8"`` if you want UTF-8 for JSON too.
|
||||
|
||||
.. versionchanged:: 2.8
|
||||
The :command:`startproject` command now sets this setting to
|
||||
``"utf-8"`` in the generated ``settings.py`` file.
|
||||
|
||||
.. setting:: FEED_EXPORT_FIELDS
|
||||
|
||||
FEED_EXPORT_FIELDS
|
||||
|
|
@ -619,6 +622,7 @@ Default:
|
|||
"file": "scrapy.extensions.feedexport.FileFeedStorage",
|
||||
"stdout": "scrapy.extensions.feedexport.StdoutFeedStorage",
|
||||
"s3": "scrapy.extensions.feedexport.S3FeedStorage",
|
||||
"gs": "scrapy.extensions.feedexport.GCSFeedStorage",
|
||||
"ftp": "scrapy.extensions.feedexport.FTPFeedStorage",
|
||||
}
|
||||
|
||||
|
|
@ -760,8 +764,8 @@ The function signature should be as follows:
|
|||
:param spider: source spider of the feed items
|
||||
:type spider: scrapy.Spider
|
||||
|
||||
.. caution:: The function should return a new dictionary, modifying
|
||||
the received ``params`` in-place is deprecated.
|
||||
.. caution:: The function must return a new dictionary instead of modifying
|
||||
the received ``params`` in-place.
|
||||
|
||||
For example, to include the :attr:`name <scrapy.Spider.name>` of the
|
||||
source spider in the feed URI:
|
||||
|
|
@ -788,6 +792,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/userguide/acl-overview.html#canned-acl
|
||||
.. _Google Cloud Storage: https://cloud.google.com/storage/
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ Write items to MongoDB
|
|||
|
||||
In this example we'll write items to MongoDB_ using pymongo_.
|
||||
MongoDB address and database name are specified in Scrapy settings;
|
||||
MongoDB collection is named after item class.
|
||||
MongoDB collection is specified in a class attribute.
|
||||
|
||||
The main point of this example is to show how to :ref:`get the crawler
|
||||
<from-crawler>` and how to clean up the resources properly.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ Item Types
|
|||
|
||||
Scrapy supports the following types of items, via the `itemadapter`_ library:
|
||||
:ref:`dictionaries <dict-items>`, :ref:`Item objects <item-objects>`,
|
||||
:ref:`dataclass objects <dataclass-items>`, and :ref:`attrs objects <attrs-items>`.
|
||||
:ref:`dataclass objects <dataclass-items>`, :ref:`attrs objects <attrs-items>`
|
||||
and :ref:`Pydantic models <pydantic-items>`.
|
||||
|
||||
.. _itemadapter: https://github.com/scrapy/itemadapter
|
||||
|
||||
|
|
@ -61,8 +62,8 @@ its ``__init__`` method.
|
|||
: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`).
|
||||
:mod:`scrapy.utils.trackref` tracks :class:`Item` objects to help find memory
|
||||
leaks (see :ref:`topics-leaks-trackrefs`).
|
||||
|
||||
Example:
|
||||
|
||||
|
|
@ -262,7 +263,7 @@ Creating items
|
|||
|
||||
>>> product = Product(name="Desktop PC", price=1000)
|
||||
>>> print(product)
|
||||
Product(name='Desktop PC', price=1000)
|
||||
{'name': 'Desktop PC', 'price': 1000}
|
||||
|
||||
|
||||
Getting field values
|
||||
|
|
@ -376,10 +377,12 @@ Creating dicts from items:
|
|||
>>> dict(product) # create a dict from all populated values
|
||||
{'price': 1000, 'name': 'Desktop PC'}
|
||||
|
||||
Creating items from dicts:
|
||||
Creating items from dicts:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Product({"name": "Laptop PC", "price": 1500})
|
||||
Product(price=1500, name='Laptop PC')
|
||||
{'name': 'Laptop PC', 'price': 1500}
|
||||
|
||||
>>> Product({"name": "Laptop PC", "lala": 1500}) # warning: unknown field in dict
|
||||
Traceback (most recent call last):
|
||||
|
|
|
|||
|
|
@ -83,6 +83,14 @@ stopping it cleanly. Forced, sudden or otherwise unclean shutdown can lead to
|
|||
data corruption in the job directory, which may prevent the spider from
|
||||
resuming correctly.
|
||||
|
||||
Scrapy version changes
|
||||
----------------------
|
||||
|
||||
The contents of a job directory are an implementation detail of the Scrapy
|
||||
version that wrote them. A job must be resumed with the same Scrapy version
|
||||
that paused it; after upgrading or downgrading Scrapy, start a new job with a
|
||||
new job directory.
|
||||
|
||||
Cookies expiration
|
||||
------------------
|
||||
|
||||
|
|
@ -96,9 +104,13 @@ Request serialization
|
|||
---------------------
|
||||
|
||||
For persistence to work, :class:`~scrapy.Request` objects must be
|
||||
serializable with :mod:`pickle`, except for the ``callback`` and ``errback``
|
||||
values passed to their ``__init__`` method, which must be methods of the
|
||||
running :class:`~scrapy.Spider` class.
|
||||
serializable with :mod:`pickle`, except for the :ref:`callback
|
||||
<callbacks>` and :ref:`errback
|
||||
<errbacks>` values passed to their ``__init__``
|
||||
method, which must be methods of the running :class:`~scrapy.Spider` class.
|
||||
|
||||
Requests that cannot be serialized are kept in memory only: they are still
|
||||
sent, but they are lost when the crawl is paused.
|
||||
|
||||
If you wish to log the requests that couldn't be serialized, you can set the
|
||||
:setting:`SCHEDULER_DEBUG` setting to ``True`` in the project's settings page.
|
||||
|
|
@ -151,8 +163,8 @@ Where:
|
|||
- :class:`~scrapy.pqueues.ScrapyPriorityQueue` creates the ``{priority}{s?}``
|
||||
directories.
|
||||
|
||||
- :class:`scrapy.squeues.PickleLifoDiskQueue`, a subclass of
|
||||
:class:`queuelib.LifoDiskQueue` that uses :mod:`pickle` to serialize
|
||||
- :class:`scrapy.squeues.PickleFifoDiskQueue`, a subclass of
|
||||
:class:`queuelib.FifoDiskQueue` that uses :mod:`pickle` to serialize
|
||||
:class:`dict` representations of :class:`scrapy.Request` objects, creates
|
||||
the ``info.json`` and ``q{00000}`` files.
|
||||
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ 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.
|
||||
:mod:`scrapy.utils.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.
|
||||
|
||||
You can enter the telnet console and inspect how many objects (of the classes
|
||||
mentioned above) are currently alive using the ``prefs()`` function which is an
|
||||
|
|
@ -93,7 +93,7 @@ You can get the oldest object of each class using the
|
|||
Which objects are tracked?
|
||||
--------------------------
|
||||
|
||||
The objects tracked by ``trackrefs`` are all from these classes (and all its
|
||||
The objects tracked by ``trackref`` are all from these classes (and all its
|
||||
subclasses):
|
||||
|
||||
* :class:`scrapy.Request`
|
||||
|
|
@ -106,10 +106,15 @@ A real example
|
|||
--------------
|
||||
|
||||
Let's see a concrete example of a hypothetical case of memory leaks.
|
||||
Suppose we have some spider with a line similar to this one::
|
||||
Suppose we have some spider with a line similar to this one:
|
||||
|
||||
return Request(f"http://www.somenastyspider.com/product.php?pid={product_id}",
|
||||
callback=self.parse, cb_kwargs={'referer': response})
|
||||
.. code-block:: python
|
||||
|
||||
return Request(
|
||||
f"http://www.somenastyspider.com/product.php?pid={product_id}",
|
||||
callback=self.parse,
|
||||
cb_kwargs={"referer": response},
|
||||
)
|
||||
|
||||
That line is passing a response reference inside a request which effectively
|
||||
ties the response lifetime to the requests' one, and that would definitely
|
||||
|
|
@ -164,7 +169,7 @@ Too many spiders?
|
|||
-----------------
|
||||
|
||||
If your project has too many spiders executed in parallel,
|
||||
the output of :func:`prefs` can be difficult to read.
|
||||
the output of ``prefs()`` can be difficult to read.
|
||||
For this reason, that function has a ``ignore`` argument which can be used to
|
||||
ignore a particular class (and all its subclasses). For
|
||||
example, this won't show any live references to spiders:
|
||||
|
|
@ -182,30 +187,13 @@ scrapy.utils.trackref module
|
|||
|
||||
Here are the functions available in the :mod:`~scrapy.utils.trackref` module.
|
||||
|
||||
.. class:: object_ref
|
||||
.. autoclass:: object_ref
|
||||
|
||||
Inherit from this class if you want to track live
|
||||
instances with the ``trackref`` module.
|
||||
.. autofunction:: print_live_refs(ignore=NoneType)
|
||||
|
||||
.. function:: print_live_refs(class_name, ignore=NoneType)
|
||||
.. autofunction:: get_oldest
|
||||
|
||||
Print a report of live references, grouped by class name.
|
||||
|
||||
:param ignore: if given, all objects from the specified class (or tuple of
|
||||
classes) will be ignored.
|
||||
:type ignore: type or tuple
|
||||
|
||||
.. function:: get_oldest(class_name)
|
||||
|
||||
Return the oldest object alive with the given class name, or ``None`` if
|
||||
none is found. Use :func:`print_live_refs` first to get a list of all
|
||||
tracked live objects per class name.
|
||||
|
||||
.. function:: iter_all(class_name)
|
||||
|
||||
Return an iterator over all objects alive with the given class name, or
|
||||
``None`` if none is found. Use :func:`print_live_refs` first to get a list
|
||||
of all tracked live objects per class name.
|
||||
.. autofunction:: iter_all
|
||||
|
||||
.. skip: end
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ Link extractor reference
|
|||
|
||||
The link extractor class is
|
||||
:class:`scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor`. For convenience it
|
||||
can also be imported as ``scrapy.linkextractors.LinkExtractor``::
|
||||
can also be imported as ``scrapy.linkextractors.LinkExtractor``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy.linkextractors import LinkExtractor
|
||||
|
||||
|
|
@ -47,108 +49,7 @@ LxmlLinkExtractor
|
|||
:synopsis: lxml's HTMLParser-based link extractors
|
||||
|
||||
|
||||
.. class:: LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), restrict_css=(), tags=('a', 'area'), attrs=('href',), canonicalize=False, unique=True, process_value=None, strip=True)
|
||||
|
||||
LxmlLinkExtractor is the recommended link extractor with handy filtering
|
||||
options. It is implemented using lxml's robust HTMLParser.
|
||||
|
||||
:param allow: a single regular expression (or list of regular expressions)
|
||||
that the (absolute) urls must match in order to be extracted. If not
|
||||
given (or empty), it will match all links.
|
||||
:type allow: str or list
|
||||
|
||||
:param deny: a single regular expression (or list of regular expressions)
|
||||
that the (absolute) urls must match in order to be excluded (i.e. not
|
||||
extracted). It has precedence over the ``allow`` parameter. If not
|
||||
given (or empty) it won't exclude any links.
|
||||
:type deny: str or list
|
||||
|
||||
:param allow_domains: a single value or a list of string containing
|
||||
domains which will be considered for extracting the links
|
||||
:type allow_domains: str or list
|
||||
|
||||
:param deny_domains: a single value or a list of strings containing
|
||||
domains which won't be considered for extracting the links
|
||||
:type deny_domains: str or list
|
||||
|
||||
:param deny_extensions: a single value or list of strings containing
|
||||
extensions that should be ignored when extracting links.
|
||||
If not given, it will default to
|
||||
:data:`scrapy.linkextractors.IGNORED_EXTENSIONS`.
|
||||
|
||||
:type deny_extensions: list
|
||||
|
||||
:param restrict_xpaths: is an XPath (or list of XPath's) which defines
|
||||
regions inside the response where links should be extracted from.
|
||||
If given, only the text selected by those XPath will be scanned for
|
||||
links.
|
||||
:type restrict_xpaths: str or list
|
||||
|
||||
:param restrict_css: a CSS selector (or list of selectors) which defines
|
||||
regions inside the response where links should be extracted from.
|
||||
Has the same behaviour as ``restrict_xpaths``.
|
||||
:type restrict_css: str or list
|
||||
|
||||
:param restrict_text: a single regular expression (or list of regular expressions)
|
||||
that the link's text must match in order to be extracted. If not
|
||||
given (or empty), it will match all links. If a list of regular expressions is
|
||||
given, the link will be extracted if it matches at least one.
|
||||
:type restrict_text: str or list
|
||||
|
||||
:param tags: a tag or a list of tags to consider when extracting links.
|
||||
Defaults to ``('a', 'area')``.
|
||||
:type tags: str or list
|
||||
|
||||
:param attrs: an attribute or list of attributes which should be considered when looking
|
||||
for links to extract (only for those tags specified in the ``tags``
|
||||
parameter). Defaults to ``('href',)``
|
||||
:type attrs: list
|
||||
|
||||
:param canonicalize: canonicalize each extracted url (using
|
||||
w3lib.url.canonicalize_url). Defaults to ``False``.
|
||||
Note that canonicalize_url is meant for duplicate checking;
|
||||
it can change the URL visible at server side, so the response can be
|
||||
different for requests with canonicalized and raw URLs. If you're
|
||||
using LinkExtractor to follow links it is more robust to
|
||||
keep the default ``canonicalize=False``.
|
||||
:type canonicalize: bool
|
||||
|
||||
:param unique: whether duplicate filtering should be applied to extracted
|
||||
links.
|
||||
:type unique: bool
|
||||
|
||||
:param process_value: a function which receives each value extracted from
|
||||
the tag and attributes scanned and can modify the value and return a
|
||||
new one, or return ``None`` to ignore the link altogether. If not
|
||||
given, ``process_value`` defaults to ``lambda x: x``.
|
||||
|
||||
.. highlight:: html
|
||||
|
||||
For example, to extract links from this code::
|
||||
|
||||
<a href="javascript:goToPage('../other/page.html'); return false">Link text</a>
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
You can use the following function in ``process_value``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def process_value(value):
|
||||
m = re.search(r"javascript:goToPage\('(.*?)'", value)
|
||||
if m:
|
||||
return m.group(1)
|
||||
|
||||
:type process_value: collections.abc.Callable
|
||||
|
||||
:param strip: whether to strip whitespaces from extracted attributes.
|
||||
According to HTML5 standard, leading and trailing whitespaces
|
||||
must be stripped from ``href`` attributes of ``<a>``, ``<area>``
|
||||
and many other elements, ``src`` attribute of ``<img>``, ``<iframe>``
|
||||
elements, etc., so LinkExtractor strips space chars by default.
|
||||
Set ``strip=False`` to turn it off (e.g. if you're extracting urls
|
||||
from elements or attributes which allow leading/trailing whitespaces).
|
||||
:type strip: bool
|
||||
.. autoclass:: LxmlLinkExtractor
|
||||
|
||||
.. automethod:: extract_links
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ data that will be assigned to the ``name`` field later.
|
|||
|
||||
Afterwards, similar calls are used for ``price`` and ``stock`` fields
|
||||
(the latter using a CSS selector with the :meth:`~ItemLoader.add_css` method),
|
||||
and finally the ``last_update`` field is populated directly with a literal value
|
||||
and finally the ``last_updated`` field is populated directly with a literal value
|
||||
(``today``) using a different method: :meth:`~ItemLoader.add_value`.
|
||||
|
||||
Finally, when all data is collected, the :meth:`ItemLoader.load_item` method is
|
||||
|
|
@ -264,7 +264,7 @@ metadata. Here is an example:
|
|||
>>> il.add_value("name", ["Welcome to my", "<strong>website</strong>"])
|
||||
>>> il.add_value("price", ["€", "<span>1000</span>"])
|
||||
>>> il.load_item()
|
||||
{'name': 'Welcome to my website', 'price': '1000'}
|
||||
Product(name='Welcome to my website', price='1000')
|
||||
|
||||
.. skip: end
|
||||
|
||||
|
|
@ -273,8 +273,8 @@ The precedence order, for both input and output processors, is as follows:
|
|||
1. Item Loader field-specific attributes: ``field_in`` and ``field_out`` (most
|
||||
precedence)
|
||||
2. Field metadata (``input_processor`` and ``output_processor`` key)
|
||||
3. Item Loader defaults: :meth:`ItemLoader.default_input_processor` and
|
||||
:meth:`ItemLoader.default_output_processor` (least precedence)
|
||||
3. Item Loader defaults: :attr:`ItemLoader.default_input_processor` and
|
||||
:attr:`ItemLoader.default_output_processor` (least precedence)
|
||||
|
||||
See also: :ref:`topics-loaders-extending`.
|
||||
|
||||
|
|
@ -323,8 +323,8 @@ There are several ways to modify Item Loader context values:
|
|||
loader = ItemLoader(product, unit="cm")
|
||||
|
||||
3. On Item Loader declaration, for those input/output processors that support
|
||||
instantiating them with an Item Loader context. :class:`~processor.MapCompose` is one of
|
||||
them:
|
||||
instantiating them with an Item Loader context.
|
||||
:class:`~itemloaders.processors.MapCompose` is one of them:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -350,7 +350,9 @@ When parsing related values from a subsection of a document, it can be
|
|||
useful to create nested loaders. Imagine you're extracting details from
|
||||
a footer of a page that looks something like:
|
||||
|
||||
Example::
|
||||
Example:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<footer>
|
||||
<a class="social" href="https://facebook.com/whatever">Like Us</a>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@
|
|||
Logging
|
||||
=======
|
||||
|
||||
.. note::
|
||||
:mod:`scrapy.log` has been deprecated alongside its functions in favor of
|
||||
explicit calls to the Python standard logging. Keep reading to learn more
|
||||
about the new logging system.
|
||||
|
||||
Scrapy uses :mod:`logging` for event logging. We'll
|
||||
provide some simple examples to get you started, but for more advanced
|
||||
use-cases it's strongly suggested to read thoroughly its documentation.
|
||||
|
|
|
|||
|
|
@ -41,11 +41,10 @@ this:
|
|||
2. The item is returned from the spider and goes to the item pipeline.
|
||||
|
||||
3. When the item reaches the :class:`FilesPipeline`, the URLs in the
|
||||
``file_urls`` field are scheduled for download using the standard
|
||||
Scrapy scheduler and downloader (which means the scheduler and downloader
|
||||
middlewares are reused), but with a higher priority, processing them before other
|
||||
pages are scraped. The item remains "locked" at that particular pipeline stage
|
||||
until the files have finish downloading (or fail for some reason).
|
||||
``file_urls`` field are downloaded using the standard Scrapy downloader
|
||||
(which means the downloader middlewares are used, but the spider middlewares
|
||||
aren't). The item remains "locked" at that particular pipeline stage until
|
||||
the files have finished downloading (or failed for some reason).
|
||||
|
||||
4. When the files are downloaded, another field (``files``) will be populated
|
||||
with the results. This field will contain a list of dicts with information
|
||||
|
|
@ -61,6 +60,8 @@ this:
|
|||
Using the Images Pipeline
|
||||
=========================
|
||||
|
||||
.. note:: Requires the :ref:`images <extras>` extra.
|
||||
|
||||
Using the :class:`ImagesPipeline` is a lot like using the :class:`FilesPipeline`,
|
||||
except the default field names used are different: you use ``image_urls`` for
|
||||
the image URLs of an item and it will populate an ``images`` field for the information
|
||||
|
|
@ -70,12 +71,6 @@ The advantage of using the :class:`ImagesPipeline` for image files is that you
|
|||
can configure some extra functions like generating thumbnails and filtering
|
||||
the images based on their size.
|
||||
|
||||
The Images Pipeline requires Pillow_ 8.3.2 or greater. It is used for
|
||||
thumbnailing and normalizing images to JPEG/RGB format.
|
||||
|
||||
.. _Pillow: https://github.com/python-pillow/Pillow
|
||||
|
||||
|
||||
.. _topics-media-pipeline-enabling:
|
||||
|
||||
Enabling your Media Pipeline
|
||||
|
|
@ -232,12 +227,13 @@ set the :setting:`FEED_STORAGE_FTP_ACTIVE` setting to ``True``.
|
|||
Amazon S3 storage
|
||||
-----------------
|
||||
|
||||
.. note:: Requires the :ref:`s3 <extras>` extra.
|
||||
|
||||
.. setting:: FILES_STORE_S3_ACL
|
||||
.. setting:: IMAGES_STORE_S3_ACL
|
||||
|
||||
If botocore_ >= 1.13.45 is installed, :setting:`FILES_STORE` and
|
||||
:setting:`IMAGES_STORE` can represent an Amazon S3 bucket. Scrapy will
|
||||
automatically upload the files to the bucket.
|
||||
:setting:`FILES_STORE` and :setting:`IMAGES_STORE` can represent an Amazon S3
|
||||
bucket. Scrapy will automatically upload the files to the bucket.
|
||||
|
||||
For example, this is a valid :setting:`IMAGES_STORE` value:
|
||||
|
||||
|
|
@ -272,7 +268,9 @@ For self-hosting you also might feel the need not to use SSL and not to verify S
|
|||
AWS_USE_SSL = False # or True (None by default)
|
||||
AWS_VERIFY = False # or True (None by default)
|
||||
|
||||
.. _botocore: https://github.com/boto/botocore
|
||||
To reuse connections for as many files as you check or upload in parallel, set
|
||||
:setting:`AWS_MAX_POOL_CONNECTIONS` accordingly.
|
||||
|
||||
.. _canned ACLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
|
||||
.. _Minio: https://github.com/minio/minio
|
||||
.. _Zenko CloudServer: https://www.zenko.io/cloudserver/
|
||||
|
|
@ -283,13 +281,13 @@ For self-hosting you also might feel the need not to use SSL and not to verify S
|
|||
Google Cloud Storage
|
||||
---------------------
|
||||
|
||||
.. note:: Requires the :ref:`gcs <extras>` extra.
|
||||
|
||||
.. setting:: FILES_STORE_GCS_ACL
|
||||
.. setting:: IMAGES_STORE_GCS_ACL
|
||||
|
||||
:setting:`FILES_STORE` and :setting:`IMAGES_STORE` can represent a Google Cloud Storage
|
||||
bucket. Scrapy will automatically upload the files to the bucket. (requires `google-cloud-storage`_ )
|
||||
|
||||
.. _google-cloud-storage: https://docs.cloud.google.com/storage/docs/reference/libraries#client-libraries-install-python
|
||||
:setting:`FILES_STORE` and :setting:`IMAGES_STORE` can represent a Google Cloud
|
||||
Storage bucket. Scrapy will automatically upload the files to the bucket.
|
||||
|
||||
For example, these are valid :setting:`IMAGES_STORE` and :setting:`GCS_PROJECT_ID` settings:
|
||||
|
||||
|
|
@ -371,11 +369,12 @@ For the Images Pipeline, set :setting:`IMAGES_URLS_FIELD` and/or
|
|||
If you need something more complex and want to override the custom pipeline
|
||||
behaviour, see :ref:`topics-media-pipeline-override`.
|
||||
|
||||
If you have multiple image pipelines inheriting from ImagePipeline and you want
|
||||
to have different settings in different pipelines you can set setting keys
|
||||
preceded with uppercase name of your pipeline class. E.g. if your pipeline is
|
||||
called MyPipeline and you want to have custom IMAGES_URLS_FIELD you define
|
||||
setting MYPIPELINE_IMAGES_URLS_FIELD and your custom settings will be used.
|
||||
If you have multiple image pipelines inheriting from :class:`ImagesPipeline`
|
||||
and you want to have different settings in different pipelines you can set
|
||||
setting keys preceded with uppercase name of your pipeline class. E.g. if your
|
||||
pipeline is called ``MyPipeline`` and you want to have custom
|
||||
:setting:`IMAGES_URLS_FIELD` you define setting
|
||||
``MYPIPELINE_IMAGES_URLS_FIELD`` and your custom settings will be used.
|
||||
|
||||
|
||||
Additional features
|
||||
|
|
@ -470,7 +469,9 @@ When using the Images Pipeline, you can drop images which are too small, by
|
|||
specifying the minimum allowed size in the :setting:`IMAGES_MIN_HEIGHT` and
|
||||
:setting:`IMAGES_MIN_WIDTH` settings.
|
||||
|
||||
For example::
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
IMAGES_MIN_HEIGHT = 110
|
||||
IMAGES_MIN_WIDTH = 110
|
||||
|
|
@ -493,7 +494,9 @@ Allowing redirections
|
|||
By default media pipelines ignore redirects, i.e. an HTTP redirection
|
||||
to a media file URL request will mean the media download is considered failed.
|
||||
|
||||
To handle media redirections, set this setting to ``True``::
|
||||
To handle media redirections, set this setting to ``True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
MEDIA_ALLOW_REDIRECTS = True
|
||||
|
||||
|
|
@ -547,10 +550,9 @@ See here the methods that you can override in your custom Files Pipeline:
|
|||
|
||||
.. method:: FilesPipeline.get_media_requests(item, info)
|
||||
|
||||
As seen on the workflow, the pipeline will get the URLs of the images to
|
||||
download from the item. In order to do this, you can override the
|
||||
:meth:`~get_media_requests` method and return a Request for each
|
||||
file URL:
|
||||
As seen on the workflow, the pipeline will get the requests for the files
|
||||
to download from the item by calling this method. You can override it to
|
||||
change what requests are returned:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -590,8 +592,9 @@ See here the methods that you can override in your custom Files Pipeline:
|
|||
* ``downloaded`` - file was downloaded.
|
||||
* ``uptodate`` - file was not downloaded, as it was downloaded recently,
|
||||
according to the file expiration policy.
|
||||
* ``cached`` - file was already scheduled for download, by another item
|
||||
sharing the same file.
|
||||
* ``cached`` - file was taken from a cache (the response has a
|
||||
``"cached"`` flag, e.g. from
|
||||
:class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`).
|
||||
|
||||
The list of tuples received by :meth:`~item_completed` is
|
||||
guaranteed to retain the same order of the requests returned from the
|
||||
|
|
@ -618,9 +621,6 @@ See here the methods that you can override in your custom Files Pipeline:
|
|||
(False, Failure(...)),
|
||||
]
|
||||
|
||||
By default the :meth:`get_media_requests` method returns ``None`` which
|
||||
means there are no files to download for the item.
|
||||
|
||||
.. method:: FilesPipeline.item_completed(results, item, info)
|
||||
|
||||
The :meth:`FilesPipeline.item_completed` method called when all file
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ Run Scrapy from a script
|
|||
You can use the :ref:`API <topics-api>` to run Scrapy from a script, instead of
|
||||
the typical way of running Scrapy via ``scrapy crawl``.
|
||||
|
||||
Remember that Scrapy is built on top of the Twisted
|
||||
asynchronous networking library, so you need to run it inside the Twisted reactor.
|
||||
Remember that Scrapy requires a Twisted reactor or (with
|
||||
:setting:`TWISTED_REACTOR_ENABLED` set to ``False``) an asyncio event loop, so
|
||||
you need to run one of those in your script for it to work (helpers described
|
||||
below can do it for you).
|
||||
|
||||
The first utility you can use to run your spiders is
|
||||
:class:`scrapy.crawler.AsyncCrawlerProcess` or
|
||||
|
|
@ -245,6 +247,110 @@ Using :func:`asyncio.run` with :class:`~scrapy.crawler.AsyncCrawlerRunner`:
|
|||
|
||||
asyncio.run(main())
|
||||
|
||||
.. _run-spiders-in-apps:
|
||||
|
||||
Running spiders inside existing applications
|
||||
============================================
|
||||
|
||||
You may want to run Scrapy spiders inside an existing application. In simple
|
||||
cases (e.g. task queues that spawn a process for every task, or applications
|
||||
that can execute tasks synchronously in the same process) you can use the same
|
||||
approach as for standalone scripts (see :ref:`run-from-script`). More complex
|
||||
cases, e.g. asynchronous web applications, have additional caveats and
|
||||
limitations.
|
||||
|
||||
If the application runs its own Twisted reactor, you can use
|
||||
:class:`~scrapy.crawler.AsyncCrawlerRunner` or
|
||||
:class:`~scrapy.crawler.CrawlerRunner` to run spiders using this reactor, see
|
||||
:ref:`run-from-script` for examples.
|
||||
|
||||
If the application doesn't run a Twisted reactor or an asyncio event loop (for
|
||||
example, a Django web app deployed with a WSGI server such as uWSGI), you can
|
||||
use :class:`~scrapy.crawler.AsyncCrawlerProcess` with
|
||||
:setting:`TWISTED_REACTOR_ENABLED` set to ``False``, so that Scrapy starts and
|
||||
stops an asyncio event loop for every spider run:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
from django.http import HttpResponse
|
||||
from scrapy.crawler import AsyncCrawlerProcess
|
||||
|
||||
|
||||
class MySpider(scrapy.Spider):
|
||||
# Your spider definition
|
||||
...
|
||||
|
||||
|
||||
def crawl_view(request):
|
||||
process = AsyncCrawlerProcess(settings={"TWISTED_REACTOR_ENABLED": False})
|
||||
process.crawl(MySpider)
|
||||
process.start() # returns when the spider finishes
|
||||
return HttpResponse("Crawling finished")
|
||||
|
||||
If the application runs its own asyncio event loop (for example, a Django web
|
||||
app deployed with an ASGI server such as uvicorn), you can use
|
||||
:class:`~scrapy.crawler.AsyncCrawlerRunner` with
|
||||
:setting:`TWISTED_REACTOR_ENABLED` set to ``False``, so that Scrapy uses the
|
||||
existing event loop:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
from django.http import HttpResponse
|
||||
from scrapy.crawler import AsyncCrawlerRunner
|
||||
|
||||
|
||||
class MySpider(scrapy.Spider):
|
||||
# Your spider definition
|
||||
...
|
||||
|
||||
|
||||
async def crawl_view(request):
|
||||
runner = AsyncCrawlerRunner(settings={"TWISTED_REACTOR_ENABLED": False})
|
||||
await runner.crawl(MySpider) # completes when the spider finishes
|
||||
return HttpResponse("Crawling finished")
|
||||
|
||||
.. note:: Running Scrapy without a Twisted reactor is experimental and has
|
||||
some limitations, described in :ref:`asyncio-without-reactor`.
|
||||
|
||||
.. _run-in-notebook:
|
||||
|
||||
Running spiders in Jupyter notebooks
|
||||
====================================
|
||||
|
||||
You can run Scrapy spiders in Jupyter notebooks. You need to use
|
||||
:class:`~scrapy.crawler.AsyncCrawlerRunner` with
|
||||
:setting:`TWISTED_REACTOR_ENABLED` set to ``False`` for this, so that Scrapy
|
||||
uses the event loop provided by the notebook kernel. As
|
||||
:class:`~scrapy.crawler.AsyncCrawlerRunner` doesn't configure logging, and you
|
||||
most likely want to see the spider log in the notebook, you should call
|
||||
:func:`scrapy.utils.log.configure_logging`. Here is a full example, which
|
||||
supports rerunning both as a single cell and as separate cells:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy import Spider
|
||||
from scrapy.crawler import AsyncCrawlerRunner
|
||||
from scrapy.utils.log import configure_logging
|
||||
|
||||
configure_logging()
|
||||
|
||||
|
||||
class BooksSpider(Spider):
|
||||
name = "books"
|
||||
start_urls = ["https://books.toscrape.com"]
|
||||
|
||||
def parse(self, response):
|
||||
for book in response.css("h3"):
|
||||
yield {"title": book.css("a::attr(title)").get()}
|
||||
|
||||
|
||||
runner = AsyncCrawlerRunner({"TWISTED_REACTOR_ENABLED": False})
|
||||
await runner.crawl(BooksSpider)
|
||||
|
||||
.. note:: Running Scrapy without a Twisted reactor is experimental and has
|
||||
some limitations, described in :ref:`asyncio-without-reactor`.
|
||||
|
||||
.. _run-multiple-spiders:
|
||||
|
||||
|
|
@ -427,8 +533,7 @@ Here are some tips to keep in mind when dealing with these kinds of sites:
|
|||
* if possible, use `Common Crawl`_ to fetch pages, instead of hitting the sites
|
||||
directly
|
||||
* use a pool of rotating IPs. For example, the free `Tor project`_ or paid
|
||||
services like `ProxyMesh`_. An open source alternative is `scrapoxy`_, a
|
||||
super proxy that you can attach your own proxies to.
|
||||
services like `ProxyMesh`_.
|
||||
* for HTTPS websites, if blocking appears related to TLS behavior, consider
|
||||
adjusting the :setting:`DOWNLOAD_TLS_MIN_VERSION` and
|
||||
:setting:`DOWNLOAD_TLS_MAX_VERSION` settings, since some websites may respond
|
||||
|
|
@ -453,5 +558,4 @@ projects that detects common mistakes and anti-patterns.
|
|||
.. _ProxyMesh: https://proxymesh.com/
|
||||
.. _Common Crawl: https://commoncrawl.org/
|
||||
.. _testspiders: https://github.com/scrapinghub/testspiders
|
||||
.. _scrapoxy: https://scrapoxy.io/
|
||||
.. _Zyte API: https://docs.zyte.com/zyte-api/get-started.html
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ Request objects
|
|||
|
||||
.. invisible-code-block: python
|
||||
|
||||
from scrapy.http import Request
|
||||
from scrapy import Request
|
||||
|
||||
1. Using a dict:
|
||||
|
||||
|
|
@ -205,10 +205,11 @@ Request objects
|
|||
Request metadata can also be accessed through the
|
||||
:attr:`~scrapy.http.Response.meta` attribute of a response.
|
||||
|
||||
To pass data from one spider callback to another, consider using
|
||||
:attr:`cb_kwargs` instead. However, request metadata may be the right
|
||||
choice in certain scenarios, such as to maintain some debugging data
|
||||
across all follow-up requests (e.g. the source URL).
|
||||
To pass your own data from one spider callback to another, use
|
||||
:attr:`cb_kwargs` instead, see :ref:`callback-data`. However, request
|
||||
metadata may be the right choice in certain scenarios, such as to
|
||||
maintain some debugging data across all follow-up requests (e.g. the
|
||||
source URL).
|
||||
|
||||
A common use of request metadata is to define request-specific
|
||||
parameters for Scrapy components (extensions, middlewares, etc.). For
|
||||
|
|
@ -238,6 +239,9 @@ Request objects
|
|||
Also mind that the :meth:`copy` and :meth:`replace` request methods
|
||||
:doc:`shallow-copy <library/copy>` request metadata.
|
||||
|
||||
.. seealso:: :class:`~scrapy.spidermiddlewares.metacopy.MetaCopyDetectionMiddleware`
|
||||
for a built-in middleware that warns about this issue at run time.
|
||||
|
||||
.. autoattribute:: dont_filter
|
||||
|
||||
.. autoattribute:: Request.attributes
|
||||
|
|
@ -245,18 +249,20 @@ Request objects
|
|||
.. method:: Request.copy()
|
||||
|
||||
Return a new Request which is a copy of this Request. See also:
|
||||
:ref:`topics-request-response-ref-request-callback-arguments`.
|
||||
:ref:`callback-data`.
|
||||
|
||||
.. method:: Request.replace([url, method, headers, body, cookies, meta, flags, encoding, priority, dont_filter, callback, errback, cb_kwargs])
|
||||
.. method:: Request.replace([url, method, headers, body, cookies, meta, flags, encoding, priority, dont_filter, callback, errback, cb_kwargs, cls])
|
||||
|
||||
Return a Request object with the same members, except for those members
|
||||
given new values by whichever keyword arguments are specified. The
|
||||
: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`.
|
||||
:ref:`callback-data`.
|
||||
|
||||
.. automethod:: from_curl
|
||||
|
||||
.. automethod:: to_curl
|
||||
|
||||
.. automethod:: to_dict
|
||||
|
||||
|
||||
|
|
@ -339,159 +345,7 @@ Other functions related to requests
|
|||
|
||||
.. autofunction:: scrapy.utils.request.request_from_dict
|
||||
|
||||
|
||||
.. _topics-request-response-ref-request-callback-arguments:
|
||||
|
||||
Passing additional data to callback functions
|
||||
---------------------------------------------
|
||||
|
||||
The callback of a request is a function that will be called when the response
|
||||
of that request is downloaded. The callback function will be called with the
|
||||
downloaded :class:`Response` object as its first argument.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def parse_page1(self, response):
|
||||
return scrapy.Request(
|
||||
"http://www.example.com/some_page.html", callback=self.parse_page2
|
||||
)
|
||||
|
||||
|
||||
def parse_page2(self, response):
|
||||
# this would log http://www.example.com/some_page.html
|
||||
self.logger.info("Visited %s", response.url)
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def parse(self, response):
|
||||
request = scrapy.Request(
|
||||
"http://www.example.com/index.html",
|
||||
callback=self.parse_page2,
|
||||
cb_kwargs=dict(main_url=response.url),
|
||||
)
|
||||
request.cb_kwargs["foo"] = "bar" # add more arguments for the callback
|
||||
yield request
|
||||
|
||||
|
||||
def parse_page2(self, response, main_url, foo):
|
||||
yield dict(
|
||||
main_url=main_url,
|
||||
other_url=response.url,
|
||||
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`
|
||||
for communication with components like middlewares and extensions.
|
||||
|
||||
.. _topics-request-response-ref-errbacks:
|
||||
|
||||
Using errbacks to catch exceptions in request processing
|
||||
--------------------------------------------------------
|
||||
|
||||
The errback of a request is a function that will be called when an exception
|
||||
is raise while processing it.
|
||||
|
||||
It receives a :exc:`~twisted.python.failure.Failure` as first parameter and can
|
||||
be used to track connection establishment timeouts, DNS errors etc.
|
||||
|
||||
Here's an example spider logging all errors and catching some specific
|
||||
errors if needed:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import scrapy
|
||||
|
||||
from scrapy.spidermiddlewares.httperror import HttpError
|
||||
from twisted.internet.error import DNSLookupError
|
||||
from twisted.internet.error import TimeoutError, TCPTimedOutError
|
||||
|
||||
|
||||
class ErrbackSpider(scrapy.Spider):
|
||||
name = "errback_example"
|
||||
start_urls = [
|
||||
"http://www.httpbin.org/", # HTTP 200 expected
|
||||
"http://www.httpbin.org/status/404", # Not found error
|
||||
"http://www.httpbin.org/status/500", # server issue
|
||||
"http://www.httpbin.org:12345/", # non-responding host, timeout expected
|
||||
"https://example.invalid/", # DNS error expected
|
||||
]
|
||||
|
||||
async def start(self):
|
||||
for u in self.start_urls:
|
||||
yield scrapy.Request(
|
||||
u,
|
||||
callback=self.parse_httpbin,
|
||||
errback=self.errback_httpbin,
|
||||
dont_filter=True,
|
||||
)
|
||||
|
||||
def parse_httpbin(self, response):
|
||||
self.logger.info("Got successful response from {}".format(response.url))
|
||||
# do something useful here...
|
||||
|
||||
def errback_httpbin(self, failure):
|
||||
# log all failures
|
||||
self.logger.error(repr(failure))
|
||||
|
||||
# in case you want to do something special for some errors,
|
||||
# you may need the failure's type:
|
||||
|
||||
if failure.check(HttpError):
|
||||
# these exceptions come from HttpError spider middleware
|
||||
# you can get the non-200 response
|
||||
response = failure.value.response
|
||||
self.logger.error("HttpError on %s", response.url)
|
||||
|
||||
elif failure.check(DNSLookupError):
|
||||
# this is the original request
|
||||
request = failure.request
|
||||
self.logger.error("DNSLookupError on %s", request.url)
|
||||
|
||||
elif failure.check(TimeoutError, TCPTimedOutError):
|
||||
request = failure.request
|
||||
self.logger.error("TimeoutError on %s", request.url)
|
||||
|
||||
|
||||
.. _errback-cb_kwargs:
|
||||
|
||||
Accessing additional data in errback functions
|
||||
----------------------------------------------
|
||||
|
||||
In case of a failure to process the request, you may be interested in
|
||||
accessing arguments to the callback functions so you can process further
|
||||
based on the arguments in the errback. The following example shows how to
|
||||
achieve this by using ``Failure.request.cb_kwargs``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def parse(self, response):
|
||||
request = scrapy.Request(
|
||||
"http://www.example.com/index.html",
|
||||
callback=self.parse_page2,
|
||||
errback=self.errback_page2,
|
||||
cb_kwargs=dict(main_url=response.url),
|
||||
)
|
||||
yield request
|
||||
|
||||
|
||||
def parse_page2(self, response, main_url):
|
||||
pass
|
||||
|
||||
|
||||
def errback_page2(self, failure):
|
||||
yield dict(
|
||||
main_url=failure.request.cb_kwargs["main_url"],
|
||||
)
|
||||
.. autofunction:: scrapy.utils.httpobj.urlparse_cached
|
||||
|
||||
|
||||
.. _request-fingerprints:
|
||||
|
|
@ -695,6 +549,319 @@ The following built-in Scrapy components have such restrictions:
|
|||
45-character-long keys must be supported.
|
||||
|
||||
|
||||
.. _callbacks:
|
||||
|
||||
Callbacks
|
||||
=========
|
||||
|
||||
A callback is a function that Scrapy calls with the :class:`Response` of a
|
||||
:class:`~scrapy.Request` once that request has been downloaded, so that you can
|
||||
extract data from that response and generate additional requests to continue
|
||||
the crawl:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy import Request, Spider
|
||||
|
||||
|
||||
class BookSpider(Spider):
|
||||
name = "books"
|
||||
|
||||
async def start(self):
|
||||
yield Request("https://books.toscrape.com/", callback=self.parse_home)
|
||||
|
||||
def parse_home(self, response):
|
||||
for url in response.css("h3 a::attr(href)").getall():
|
||||
yield Request(response.urljoin(url), callback=self.parse_book)
|
||||
|
||||
def parse_book(self, response):
|
||||
yield {"title": response.css("h1::text").get()}
|
||||
|
||||
Requests may also define an :ref:`errback <errbacks>`, which Scrapy calls
|
||||
instead of the callback when an exception is raised while processing the
|
||||
request or its response, e.g. a connection error or, by default, a non-2xx
|
||||
response.
|
||||
|
||||
|
||||
.. _callback-assignment:
|
||||
|
||||
Assigning a callback to a request
|
||||
---------------------------------
|
||||
|
||||
To assign a callback to a request, use the ``callback`` parameter of
|
||||
:class:`~scrapy.Request`, which sets the :attr:`.Request.callback` attribute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy import Request
|
||||
|
||||
|
||||
def parse_home(response): ...
|
||||
|
||||
|
||||
request = Request("https://books.toscrape.com/", callback=parse_home)
|
||||
|
||||
Requests with no callback, i.e. with :attr:`~scrapy.Request.callback` set to
|
||||
``None``, are handled by the :meth:`~scrapy.Spider.parse` method of the spider:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
request = Request("https://books.toscrape.com/") # Handled by parse()
|
||||
|
||||
If a request is never meant to reach a spider callback, e.g. because a
|
||||
:ref:`component <topics-components>` sends it and handles its response itself,
|
||||
assign the special :func:`~scrapy.http.request.NO_CALLBACK` value to it
|
||||
instead, so that :ref:`downloader middlewares <topics-downloader-middleware>`
|
||||
can tell such requests apart.
|
||||
|
||||
While :attr:`~scrapy.Request.callback` only accepts callables, some spider
|
||||
classes let you also define a callback by name: both :attr:`CrawlSpider.rules
|
||||
<scrapy.spiders.CrawlSpider.rules>` and :attr:`SitemapSpider.sitemap_rules
|
||||
<scrapy.spiders.SitemapSpider.sitemap_rules>` accept the name of a spider
|
||||
method as a string.
|
||||
|
||||
|
||||
.. _writing-callbacks:
|
||||
|
||||
Writing a callback
|
||||
------------------
|
||||
|
||||
Any callable can be a callback, as long as it takes the response as its first
|
||||
positional parameter, and any :ref:`additional callback data <callback-data>`
|
||||
as keyword parameters. Spider methods are the most common choice, but plain
|
||||
functions, lambda expressions and other callable objects work as well.
|
||||
|
||||
.. note:: If you enable :ref:`job persistence <topics-jobs>` through the
|
||||
:setting:`JOBDIR` setting, callbacks must be methods of the running spider.
|
||||
Requests with any other callback cannot be serialized, so they are kept in
|
||||
memory only and lost when you pause the crawl. See
|
||||
:ref:`request-serialization`.
|
||||
|
||||
A callback can be:
|
||||
|
||||
- A regular function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def parse(self, response):
|
||||
return {"url": response.url}
|
||||
|
||||
- A generator function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def parse(self, response):
|
||||
yield {"url": response.url}
|
||||
|
||||
- A coroutine function, i.e. defined with ``async def``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def parse(self, response):
|
||||
return {"url": response.url}
|
||||
|
||||
- An asynchronous generator function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def parse(self, response):
|
||||
yield {"url": response.url}
|
||||
|
||||
The last two allow using ``await``, ``async for`` and ``async with`` in your
|
||||
callback. See :ref:`topics-coroutines`.
|
||||
|
||||
|
||||
.. _callback-output:
|
||||
|
||||
Callback output
|
||||
---------------
|
||||
|
||||
A callback may return or yield any of the following:
|
||||
|
||||
- ``None``, which does nothing.
|
||||
|
||||
Callbacks that produce no output at all, e.g. callbacks that only log
|
||||
information about the response, are perfectly valid. ``None`` values within
|
||||
an iterable of callback output are ignored as well.
|
||||
|
||||
- A :class:`~scrapy.Request` object, which Scrapy schedules, downloads and
|
||||
eventually sends to its own callback.
|
||||
|
||||
- An :ref:`item object <topics-items>`, which Scrapy sends to the
|
||||
:ref:`item pipelines <topics-item-pipeline>`.
|
||||
|
||||
Any object that is neither ``None`` nor a :class:`~scrapy.Request` object
|
||||
is treated as an item.
|
||||
|
||||
- An iterable of any of the values above, e.g. a list or, more commonly, a
|
||||
generator.
|
||||
|
||||
:term:`Asynchronous iterables <asynchronous iterable>`, e.g. an
|
||||
:term:`asynchronous generator`, are also supported.
|
||||
|
||||
.. note:: When a callback *returns* an object, Scrapy iterates that object if
|
||||
it supports iteration, except for :class:`dict`, :class:`~scrapy.Item`,
|
||||
:class:`str` and :class:`bytes` objects, which are always handled as single
|
||||
items.
|
||||
|
||||
.. note:: In a generator callback, a ``return`` statement with a value does not
|
||||
produce any output, since such a value is not part of what the generator
|
||||
yields. Scrapy logs a warning when it detects such a callback, see
|
||||
:setting:`WARN_ON_GENERATOR_RETURN_VALUE`.
|
||||
|
||||
Before Scrapy acts on the output of a callback, that output goes through the
|
||||
:meth:`~scrapy.spidermiddlewares.SpiderMiddleware.process_spider_output` method
|
||||
of your :ref:`spider middlewares <topics-spider-middleware>`, which may modify
|
||||
it or drop part of it.
|
||||
|
||||
If a callback raises an exception, the :attr:`~scrapy.Request.errback` of the
|
||||
request is *not* called. The exception goes through the
|
||||
:meth:`~scrapy.spidermiddlewares.SpiderMiddleware.process_spider_exception`
|
||||
method of your spider middlewares instead and, unless one of them handles it,
|
||||
Scrapy logs it and sends the :signal:`spider_error` signal.
|
||||
|
||||
|
||||
.. _callback-data:
|
||||
.. _topics-request-response-ref-request-callback-arguments:
|
||||
|
||||
Passing additional data to callback functions
|
||||
---------------------------------------------
|
||||
|
||||
In some cases you may be interested in passing data to a callback in addition
|
||||
to the response, e.g. data extracted from the response that triggered the
|
||||
request. The following example shows how to achieve this by using the
|
||||
:attr:`.Request.cb_kwargs` attribute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy import Request
|
||||
|
||||
|
||||
def parse(self, response):
|
||||
request = Request(
|
||||
"http://www.example.com/index.html",
|
||||
callback=self.parse_page2,
|
||||
cb_kwargs=dict(main_url=response.url),
|
||||
)
|
||||
request.cb_kwargs["foo"] = "bar" # add more arguments for the callback
|
||||
yield request
|
||||
|
||||
|
||||
def parse_page2(self, response, main_url, foo):
|
||||
yield dict(
|
||||
main_url=main_url,
|
||||
other_url=response.url,
|
||||
foo=foo,
|
||||
)
|
||||
|
||||
:attr:`.Request.cb_kwargs` is the recommended way to pass your own data to a
|
||||
callback. Use :attr:`.Request.meta` only for data aimed at :ref:`components
|
||||
<topics-components>`, such as middlewares and extensions.
|
||||
|
||||
.. _errbacks:
|
||||
.. _topics-request-response-ref-errbacks:
|
||||
|
||||
Errbacks
|
||||
========
|
||||
|
||||
The errback of a request is a function that will be called when an exception
|
||||
is raise while processing it.
|
||||
|
||||
It receives a :exc:`~twisted.python.failure.Failure` as first parameter and can
|
||||
be used to track connection establishment timeouts, DNS errors etc.
|
||||
|
||||
Here's an example spider logging all errors and catching some specific
|
||||
errors if needed:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy import Request, Spider
|
||||
from scrapy.spidermiddlewares.httperror import HttpError
|
||||
from twisted.internet.error import DNSLookupError
|
||||
from twisted.internet.error import TimeoutError, TCPTimedOutError
|
||||
|
||||
|
||||
class ErrbackSpider(Spider):
|
||||
name = "errback_example"
|
||||
start_urls = [
|
||||
"http://www.httpbin.org/", # HTTP 200 expected
|
||||
"http://www.httpbin.org/status/404", # Not found error
|
||||
"http://www.httpbin.org/status/500", # server issue
|
||||
"http://www.httpbin.org:12345/", # non-responding host, timeout expected
|
||||
"https://example.invalid/", # DNS error expected
|
||||
]
|
||||
|
||||
async def start(self):
|
||||
for u in self.start_urls:
|
||||
yield Request(
|
||||
u,
|
||||
callback=self.parse_httpbin,
|
||||
errback=self.errback_httpbin,
|
||||
dont_filter=True,
|
||||
)
|
||||
|
||||
def parse_httpbin(self, response):
|
||||
self.logger.info(f"Got successful response from {response.url}")
|
||||
# do something useful here...
|
||||
|
||||
def errback_httpbin(self, failure):
|
||||
# log all failures
|
||||
self.logger.error(repr(failure))
|
||||
|
||||
# in case you want to do something special for some errors,
|
||||
# you may need the failure's type:
|
||||
|
||||
if failure.check(HttpError):
|
||||
# these exceptions come from HttpError spider middleware
|
||||
# you can get the non-200 response
|
||||
response = failure.value.response
|
||||
self.logger.error("HttpError on %s", response.url)
|
||||
|
||||
elif failure.check(DNSLookupError):
|
||||
# this is the original request
|
||||
request = failure.request
|
||||
self.logger.error("DNSLookupError on %s", request.url)
|
||||
|
||||
elif failure.check(TimeoutError, TCPTimedOutError):
|
||||
request = failure.request
|
||||
self.logger.error("TimeoutError on %s", request.url)
|
||||
|
||||
|
||||
.. _errback-cb_kwargs:
|
||||
|
||||
Accessing additional data in errback functions
|
||||
----------------------------------------------
|
||||
|
||||
In case of a failure to process the request, you may be interested in
|
||||
accessing arguments to the callback functions so you can process further
|
||||
based on the arguments in the errback. The following example shows how to
|
||||
achieve this by using ``Failure.request.cb_kwargs``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy import Request
|
||||
|
||||
|
||||
def parse(self, response):
|
||||
request = Request(
|
||||
"http://www.example.com/index.html",
|
||||
callback=self.parse_page2,
|
||||
errback=self.errback_page2,
|
||||
cb_kwargs=dict(main_url=response.url),
|
||||
)
|
||||
yield request
|
||||
|
||||
|
||||
def parse_page2(self, response, main_url):
|
||||
pass
|
||||
|
||||
|
||||
def errback_page2(self, failure):
|
||||
yield dict(
|
||||
main_url=failure.request.cb_kwargs["main_url"],
|
||||
)
|
||||
|
||||
|
||||
.. _topics-request-meta:
|
||||
|
||||
Request.meta special keys
|
||||
|
|
@ -717,6 +884,7 @@ Those are:
|
|||
* :reqmeta:`download_fail_on_dataloss`
|
||||
* :reqmeta:`download_latency`
|
||||
* :reqmeta:`download_maxsize`
|
||||
* :reqmeta:`download_slot`
|
||||
* :reqmeta:`download_warnsize`
|
||||
* :reqmeta:`download_timeout`
|
||||
* ``ftp_password`` (See :setting:`FTP_PASSWORD` for more info)
|
||||
|
|
@ -806,6 +974,8 @@ Whether or not to fail on broken responses. See:
|
|||
give_up_log_level
|
||||
-----------------
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
:ref:`Logging level <levels>` used for the message logged when a request
|
||||
exceeds its retries. See :setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
|
||||
|
||||
|
|
@ -814,6 +984,8 @@ exceeds its retries. See :setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
|
|||
http_auth_domain
|
||||
----------------
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Overrides :setting:`HTTPAUTH_DOMAIN` for this request.
|
||||
|
||||
.. reqmeta:: http_pass
|
||||
|
|
@ -821,6 +993,8 @@ Overrides :setting:`HTTPAUTH_DOMAIN` for this request.
|
|||
http_pass
|
||||
---------
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Overrides :setting:`HTTPAUTH_PASS` for this request.
|
||||
|
||||
.. reqmeta:: http_user
|
||||
|
|
@ -828,6 +1002,8 @@ Overrides :setting:`HTTPAUTH_PASS` for this request.
|
|||
http_user
|
||||
---------
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Overrides :setting:`HTTPAUTH_USER` for this request.
|
||||
|
||||
.. reqmeta:: max_retry_times
|
||||
|
|
@ -844,6 +1020,8 @@ The meta key is used set retry times per request. When set, the
|
|||
verbatim_url
|
||||
------------
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Set this key to ``True`` to keep the request URL as passed to
|
||||
:class:`~scrapy.Request`, without URL percent-encoding.
|
||||
|
||||
|
|
@ -854,7 +1032,6 @@ characters that would otherwise be canonicalized get different fingerprints.
|
|||
In this mode, the ``keep_fragments`` parameter is ignored, and it is
|
||||
effectively true.
|
||||
|
||||
|
||||
.. _topics-stop-response-download:
|
||||
|
||||
Stopping the download of a Response
|
||||
|
|
@ -1018,9 +1195,13 @@ Response objects
|
|||
:meth:`~scrapy.http.headers.Headers.get` to return the last header value with
|
||||
the specified name or :meth:`~scrapy.http.headers.Headers.getlist` to return
|
||||
all header values with the specified name. For example, this call will give you
|
||||
all cookies in the headers::
|
||||
all cookies in the headers:
|
||||
|
||||
response.headers.getlist('Set-Cookie')
|
||||
.. skip: next
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response.headers.getlist("Set-Cookie")
|
||||
|
||||
.. attribute:: Response.body
|
||||
|
||||
|
|
@ -1076,7 +1257,7 @@ Response objects
|
|||
.. attribute:: Response.flags
|
||||
|
||||
A list that contains flags for this response. Flags are labels used for
|
||||
tagging Responses. For example: ``'cached'``, ``'redirected``', etc. And
|
||||
tagging Responses. For example: ``'cached'``, ``'redirected'``', etc. And
|
||||
they're shown on the string representation of the Response (``__str__()``
|
||||
method) which is used by the engine for logging.
|
||||
|
||||
|
|
@ -1091,8 +1272,8 @@ Response objects
|
|||
|
||||
The IP address of the server from which the Response originated.
|
||||
|
||||
This attribute is currently only populated by the HTTP 1.1 download
|
||||
handler, i.e. for ``http(s)`` responses. For other handlers,
|
||||
This attribute is currently only populated by the HTTP download
|
||||
handlers, i.e. for ``http(s)`` responses. For other handlers,
|
||||
:attr:`ip_address` is always ``None``.
|
||||
|
||||
.. attribute:: Response.protocol
|
||||
|
|
@ -1110,7 +1291,7 @@ Response objects
|
|||
|
||||
Returns a new Response which is a copy of this Response.
|
||||
|
||||
.. method:: Response.replace([url, status, headers, body, request, flags, cls])
|
||||
.. method:: Response.replace([url, status, headers, body, request, flags, certificate, ip_address, protocol, cls])
|
||||
|
||||
Returns a Response object with the same members, except for those members
|
||||
given new values by whichever keyword arguments are specified. The
|
||||
|
|
@ -1122,7 +1303,11 @@ Response objects
|
|||
a possible relative url.
|
||||
|
||||
This is a wrapper over :func:`~urllib.parse.urljoin`, it's merely an alias for
|
||||
making this call::
|
||||
making this call:
|
||||
|
||||
.. skip: next
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
urllib.parse.urljoin(response.url, url)
|
||||
|
||||
|
|
@ -1211,21 +1396,31 @@ TextResponse objects
|
|||
|
||||
.. method:: TextResponse.jmespath(query)
|
||||
|
||||
A shortcut to ``TextResponse.selector.jmespath(query)``::
|
||||
.. skip: start
|
||||
|
||||
response.jmespath('object.[*]')
|
||||
A shortcut to ``TextResponse.selector.jmespath(query)``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response.jmespath("object.[*]")
|
||||
|
||||
.. method:: TextResponse.xpath(query)
|
||||
|
||||
A shortcut to ``TextResponse.selector.xpath(query)``::
|
||||
A shortcut to ``TextResponse.selector.xpath(query)``:
|
||||
|
||||
response.xpath('//p')
|
||||
.. code-block:: python
|
||||
|
||||
response.xpath("//p")
|
||||
|
||||
.. method:: TextResponse.css(query)
|
||||
|
||||
A shortcut to ``TextResponse.selector.css(query)``::
|
||||
A shortcut to ``TextResponse.selector.css(query)``:
|
||||
|
||||
response.css('p')
|
||||
.. code-block:: python
|
||||
|
||||
response.css("p")
|
||||
|
||||
.. skip: end
|
||||
|
||||
.. automethod:: TextResponse.follow
|
||||
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ Examples:
|
|||
|
||||
* ``*::text`` selects all descendant text nodes of the current selector context:
|
||||
|
||||
..skip: next
|
||||
.. skip: next
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> response.css("#images *::text").getall()
|
||||
|
|
@ -634,8 +634,7 @@ Example:
|
|||
.. code-block:: pycon
|
||||
|
||||
>>> from scrapy import Selector
|
||||
>>> sel = Selector(
|
||||
... text="""
|
||||
>>> sel = Selector(text="""
|
||||
... <ul class="list">
|
||||
... <li>1</li>
|
||||
... <li>2</li>
|
||||
|
|
@ -645,8 +644,8 @@ Example:
|
|||
... <li>4</li>
|
||||
... <li>5</li>
|
||||
... <li>6</li>
|
||||
... </ul>"""
|
||||
... )
|
||||
... </ul>""")
|
||||
...
|
||||
>>> xp = lambda x: sel.xpath(x).getall()
|
||||
|
||||
This gets all first ``<li>`` elements under whatever it is its parent:
|
||||
|
|
@ -948,11 +947,9 @@ with groups of itemscopes and corresponding itemprops:
|
|||
>>> sel = Selector(text=doc, type="html")
|
||||
>>> for scope in sel.xpath("//div[@itemscope]"):
|
||||
... print("current scope:", scope.xpath("@itemtype").getall())
|
||||
... props = scope.xpath(
|
||||
... """
|
||||
... props = scope.xpath("""
|
||||
... set:difference(./descendant::*/@itemprop,
|
||||
... .//*[@itemscope]/*/@itemprop)"""
|
||||
... )
|
||||
... .//*[@itemscope]/*/@itemprop)""")
|
||||
... print(f" properties: {props.getall()}")
|
||||
... print("")
|
||||
...
|
||||
|
|
|
|||
|
|
@ -305,10 +305,21 @@ These settings cannot be :ref:`set from a spider <spider-settings>`.
|
|||
|
||||
These settings are:
|
||||
|
||||
- :setting:`TWISTED_REACTOR_ENABLED`
|
||||
- :setting:`ADDONS`
|
||||
- :setting:`COMMANDS_MODULE`
|
||||
- :setting:`FORCE_CRAWLER_PROCESS`
|
||||
- :setting:`SPIDER_LOADER_CLASS` and settings used by the corresponding
|
||||
spider loader class, e.g. :setting:`SPIDER_MODULES` and
|
||||
:setting:`SPIDER_LOADER_WARN_ONLY` for the default spider loader class.
|
||||
- :setting:`TWISTED_REACTOR_ENABLED`
|
||||
|
||||
:setting:`ADDONS` is a special case: it can be set from a spider, but the
|
||||
``update_pre_crawler_settings()`` method of :ref:`add-ons <topics-addons>`
|
||||
enabled that way is not called.
|
||||
|
||||
:setting:`TWISTED_REACTOR` also acts as a pre-crawler setting when running a
|
||||
:ref:`command that needs a CrawlerProcess <topics-commands-crawlerprocess>`,
|
||||
since its project-level value determines the crawler process class.
|
||||
|
||||
.. _reactor-settings:
|
||||
|
||||
|
|
@ -409,6 +420,39 @@ Default: ``{}``
|
|||
A dict containing paths to the add-ons enabled in your project and their
|
||||
priorities. For more information, see :ref:`topics-addons`.
|
||||
|
||||
.. note:: This is a :ref:`pre-crawler setting <pre-crawler-settings>`, with a
|
||||
caveat described in that section.
|
||||
|
||||
.. setting:: ASYNCIO_EVENT_LOOP
|
||||
|
||||
ASYNCIO_EVENT_LOOP
|
||||
------------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
Import path of a given ``asyncio`` event loop class.
|
||||
|
||||
If the asyncio reactor is enabled (see :setting:`TWISTED_REACTOR`) or when
|
||||
:ref:`running Scrapy without a reactor <asyncio-without-reactor>` this setting
|
||||
can be used to specify the
|
||||
asyncio event loop to be used with it. Set the setting to the import path of the
|
||||
desired asyncio event loop class. If the setting is set to ``None`` the default asyncio
|
||||
event loop will be used.
|
||||
|
||||
If you are installing the asyncio reactor manually using the :func:`~scrapy.utils.reactor.install_reactor`
|
||||
function, you can use the ``event_loop_path`` parameter to indicate the import path of the event loop
|
||||
class to be used.
|
||||
|
||||
Note that the event loop class must inherit from :class:`asyncio.AbstractEventLoop`.
|
||||
|
||||
.. caution:: Please be aware that, when using a non-default event loop
|
||||
(either defined via :setting:`ASYNCIO_EVENT_LOOP` or installed with
|
||||
:func:`~scrapy.utils.reactor.install_reactor`), Scrapy will call
|
||||
:func:`asyncio.set_event_loop`, which will set the specified event loop
|
||||
as the current loop for the current OS thread.
|
||||
|
||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||
|
||||
.. setting:: AWS_ACCESS_KEY_ID
|
||||
|
||||
AWS_ACCESS_KEY_ID
|
||||
|
|
@ -419,6 +463,44 @@ Default: ``None``
|
|||
The AWS access key used by code that requires access to `Amazon Web services`_,
|
||||
such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`.
|
||||
|
||||
.. setting:: AWS_ENDPOINT_URL
|
||||
|
||||
AWS_ENDPOINT_URL
|
||||
----------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
Endpoint URL used for S3-like storage, for example Minio or s3.scality.
|
||||
|
||||
.. setting:: AWS_MAX_POOL_CONNECTIONS
|
||||
|
||||
AWS_MAX_POOL_CONNECTIONS
|
||||
------------------------
|
||||
|
||||
.. versionadded:: VERSION
|
||||
|
||||
Default: ``None``
|
||||
|
||||
Maximum number of connections that AWS clients, such as those of the
|
||||
:ref:`S3 feed storage backend <topics-feed-storage-s3>` and of the
|
||||
:ref:`S3 media pipeline storage backend <media-pipelines-s3>`, keep in their
|
||||
connection pool.
|
||||
|
||||
If ``None``, the value of :setting:`REACTOR_THREADPOOL_MAXSIZE` is used.
|
||||
|
||||
Values lower than the number of parallel AWS calls do not limit those calls, but
|
||||
their connections are closed instead of reused, which hurts performance, and
|
||||
``Connection pool is full, discarding connection`` warnings are logged.
|
||||
|
||||
.. setting:: AWS_REGION_NAME
|
||||
|
||||
AWS_REGION_NAME
|
||||
---------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
The name of the region associated with the AWS client.
|
||||
|
||||
.. setting:: AWS_SECRET_ACCESS_KEY
|
||||
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
|
|
@ -442,15 +524,6 @@ such as the :ref:`S3 feed storage backend <topics-feed-storage-s3>`, when using
|
|||
|
||||
.. _temporary security credentials: https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html
|
||||
|
||||
.. setting:: AWS_ENDPOINT_URL
|
||||
|
||||
AWS_ENDPOINT_URL
|
||||
----------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
Endpoint URL used for S3-like storage, for example Minio or s3.scality.
|
||||
|
||||
.. setting:: AWS_USE_SSL
|
||||
|
||||
AWS_USE_SSL
|
||||
|
|
@ -471,43 +544,6 @@ Default: ``None``
|
|||
Verify SSL connection between Scrapy and S3 or S3-like storage. By default
|
||||
SSL verification will occur.
|
||||
|
||||
.. setting:: AWS_REGION_NAME
|
||||
|
||||
AWS_REGION_NAME
|
||||
---------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
The name of the region associated with the AWS client.
|
||||
|
||||
.. setting:: ASYNCIO_EVENT_LOOP
|
||||
|
||||
ASYNCIO_EVENT_LOOP
|
||||
------------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
Import path of a given ``asyncio`` event loop class.
|
||||
|
||||
If the asyncio reactor is enabled (see :setting:`TWISTED_REACTOR`) this setting can be used to specify the
|
||||
asyncio event loop to be used with it. Set the setting to the import path of the
|
||||
desired asyncio event loop class. If the setting is set to ``None`` the default asyncio
|
||||
event loop will be used.
|
||||
|
||||
If you are installing the asyncio reactor manually using the :func:`~scrapy.utils.reactor.install_reactor`
|
||||
function, you can use the ``event_loop_path`` parameter to indicate the import path of the event loop
|
||||
class to be used.
|
||||
|
||||
Note that the event loop class must inherit from :class:`asyncio.AbstractEventLoop`.
|
||||
|
||||
.. caution:: Please be aware that, when using a non-default event loop
|
||||
(either defined via :setting:`ASYNCIO_EVENT_LOOP` or installed with
|
||||
:func:`~scrapy.utils.reactor.install_reactor`), Scrapy will call
|
||||
:func:`asyncio.set_event_loop`, which will set the specified event loop
|
||||
as the current loop for the current OS thread.
|
||||
|
||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||
|
||||
.. setting:: BOT_NAME
|
||||
|
||||
BOT_NAME
|
||||
|
|
@ -539,7 +575,7 @@ CONCURRENT_REQUESTS
|
|||
Default: ``16``
|
||||
|
||||
The maximum number of concurrent (i.e. simultaneous) requests that will be
|
||||
performed by the Scrapy downloader.
|
||||
performed by the Scrapy downloader. Use ``0`` for no limit.
|
||||
|
||||
.. setting:: CONCURRENT_REQUESTS_PER_DOMAIN
|
||||
|
||||
|
|
@ -554,6 +590,8 @@ performed to any single domain.
|
|||
See also: :ref:`topics-autothrottle` and its
|
||||
:setting:`AUTOTHROTTLE_TARGET_CONCURRENCY` option.
|
||||
|
||||
It is possible to change this setting per domain by using
|
||||
:setting:`DOWNLOAD_SLOTS`.
|
||||
|
||||
.. setting:: DEFAULT_DROPITEM_LOG_LEVEL
|
||||
|
||||
|
|
@ -593,7 +631,7 @@ When writing an item pipeline, you can force a different log level by setting
|
|||
DEFAULT_ITEM_CLASS
|
||||
------------------
|
||||
|
||||
Default: ``'scrapy.Item'``
|
||||
Default: ``'scrapy.item.Item'``
|
||||
|
||||
The default class that will be used for instantiating items in the :ref:`the
|
||||
Scrapy shell <topics-shell>`.
|
||||
|
|
@ -687,7 +725,7 @@ Whether to enable DNS in-memory cache.
|
|||
:class:`~scrapy.resolver.CachingThreadedResolver` and
|
||||
:class:`~scrapy.resolver.CachingHostnameResolver`. It has no effect when
|
||||
:setting:`TWISTED_REACTOR_ENABLED` is ``False``, and may have no effect
|
||||
either when :setting:`DNS_RESOLVER` is set to a different resolver.
|
||||
either when :setting:`TWISTED_DNS_RESOLVER` is set to a different resolver.
|
||||
|
||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||
|
||||
|
|
@ -702,25 +740,6 @@ DNS in-memory cache size, see :setting:`DNSCACHE_ENABLED`.
|
|||
|
||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||
|
||||
.. setting:: TWISTED_DNS_RESOLVER
|
||||
|
||||
TWISTED_DNS_RESOLVER
|
||||
--------------------
|
||||
|
||||
Default: ``'scrapy.resolver.CachingThreadedResolver'``
|
||||
|
||||
The class to be used by Twisted to resolve DNS names. The default
|
||||
``scrapy.resolver.CachingThreadedResolver`` supports specifying a timeout for
|
||||
DNS requests via the :setting:`DNS_TIMEOUT` setting, but works only with IPv4
|
||||
addresses. Scrapy provides an alternative resolver,
|
||||
``scrapy.resolver.CachingHostnameResolver``, which supports IPv4/IPv6 addresses but does not
|
||||
take the :setting:`DNS_TIMEOUT` setting into account.
|
||||
|
||||
.. note::
|
||||
This setting has no effect when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
|
||||
|
||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||
|
||||
.. setting:: DNS_TIMEOUT
|
||||
|
||||
DNS_TIMEOUT
|
||||
|
|
@ -734,7 +753,7 @@ Timeout for processing of DNS queries in seconds. Float is supported.
|
|||
This setting is only used by
|
||||
:class:`~scrapy.resolver.CachingThreadedResolver`. It has no effect when
|
||||
:setting:`TWISTED_REACTOR_ENABLED` is ``False``, and may have no effect
|
||||
either when :setting:`DNS_RESOLVER` is set to a different resolver.
|
||||
either when :setting:`TWISTED_DNS_RESOLVER` is set to a different resolver.
|
||||
|
||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||
|
||||
|
|
@ -766,6 +785,9 @@ specific cipher that is not included in ``DEFAULT`` if a website requires it.
|
|||
Set this setting to ``None`` to use the default ciphers of the underlying TLS
|
||||
implementation.
|
||||
|
||||
.. versionchanged:: 2.17.0
|
||||
Added support for setting this to ``None``.
|
||||
|
||||
.. _OpenSSL cipher list format: https://docs.openssl.org/master/man1/openssl-ciphers/#cipher-list-format
|
||||
|
||||
.. note::
|
||||
|
|
@ -781,6 +803,8 @@ implementation.
|
|||
DOWNLOAD_TLS_MAX_VERSION
|
||||
------------------------
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Default: ``None``
|
||||
|
||||
Use this setting to change the maximum version of the TLS protocol allowed to
|
||||
|
|
@ -816,6 +840,8 @@ modern environments.
|
|||
DOWNLOAD_TLS_MIN_VERSION
|
||||
------------------------
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
|
||||
Default: ``None``
|
||||
|
||||
Use this setting to change the minimum version of the TLS protocol allowed to
|
||||
|
|
@ -907,7 +933,9 @@ Use :setting:`DOWNLOAD_DELAY` to throttle your crawling speed, to avoid hitting
|
|||
servers too hard.
|
||||
|
||||
Decimal numbers are supported. For example, to send a maximum of 4 requests
|
||||
every 10 seconds::
|
||||
every 10 seconds:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
DOWNLOAD_DELAY = 2.5
|
||||
|
||||
|
|
@ -925,13 +953,8 @@ desired.
|
|||
|
||||
.. _spider-download_delay-attribute:
|
||||
|
||||
.. note::
|
||||
|
||||
This delay can be set per spider using :attr:`download_delay` spider attribute.
|
||||
|
||||
It is also possible to change this setting per domain, although it requires
|
||||
non-trivial code. See the implementation of the :ref:`AutoThrottle
|
||||
<topics-autothrottle>` extension for an example.
|
||||
It is possible to change this setting per domain by using
|
||||
:setting:`DOWNLOAD_SLOTS`.
|
||||
|
||||
.. setting:: DOWNLOAD_BIND_ADDRESS
|
||||
|
||||
|
|
@ -1240,7 +1263,9 @@ 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::
|
||||
interface:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyDupeFilter:
|
||||
|
||||
|
|
@ -1323,6 +1348,7 @@ Default:
|
|||
|
||||
{
|
||||
"scrapy.extensions.corestats.CoreStats": 0,
|
||||
"scrapy.extensions.logcount.LogCount": 0,
|
||||
"scrapy.extensions.telnet.TelnetConsole": 0,
|
||||
"scrapy.extensions.memusage.MemoryUsage": 0,
|
||||
"scrapy.extensions.memdebug.MemoryDebugger": 0,
|
||||
|
|
@ -1345,6 +1371,8 @@ and the :ref:`list of available extensions <topics-extensions-ref>`.
|
|||
FEED_TEMPDIR
|
||||
------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
The Feed Temp dir allows you to set a custom folder to save crawler
|
||||
temporary files before uploading with :ref:`FTP feed storage <topics-feed-storage-ftp>` and
|
||||
:ref:`Amazon S3 <topics-feed-storage-s3>`.
|
||||
|
|
@ -1354,6 +1382,8 @@ temporary files before uploading with :ref:`FTP feed storage <topics-feed-storag
|
|||
FEED_STORAGE_GCS_ACL
|
||||
--------------------
|
||||
|
||||
Default: ``""``
|
||||
|
||||
The Access Control List (ACL) used when storing items to :ref:`Google Cloud Storage <topics-feed-storage-gcs>`.
|
||||
For more information on how to set this value, please refer to the column *JSON API* in `Google Cloud documentation <https://docs.cloud.google.com/storage/docs/access-control/lists>`_.
|
||||
|
||||
|
|
@ -1365,18 +1395,25 @@ FORCE_CRAWLER_PROCESS
|
|||
Default: ``False``
|
||||
|
||||
If ``False``, :ref:`Scrapy commands that need a CrawlerProcess
|
||||
<topics-commands-crawlerprocess>` will decide between using
|
||||
<topics-commands-crawlerprocess>`, when :setting:`TWISTED_REACTOR_ENABLED`
|
||||
is set to ``True``, will decide between using
|
||||
:class:`scrapy.crawler.AsyncCrawlerProcess` and
|
||||
:class:`scrapy.crawler.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`.
|
||||
:class:`~scrapy.crawler.CrawlerProcess` when :setting:`TWISTED_REACTOR_ENABLED`
|
||||
is set to ``True``.
|
||||
|
||||
When :setting:`TWISTED_REACTOR_ENABLED` is set to ``False``,
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` will be used in all cases.
|
||||
|
||||
Set this to ``True`` if you want to set :setting:`TWISTED_REACTOR` to a
|
||||
non-default value in :ref:`per-spider settings <spider-settings>`.
|
||||
|
||||
.. note:: This is a :ref:`pre-crawler setting <pre-crawler-settings>`.
|
||||
|
||||
.. setting:: FTP_PASSIVE_MODE
|
||||
|
||||
FTP_PASSIVE_MODE
|
||||
|
|
@ -1622,6 +1659,8 @@ The following special items are also supported:
|
|||
|
||||
- ``Python``
|
||||
|
||||
- ``pyOpenSSL``
|
||||
|
||||
.. setting:: LOGSTATS_INTERVAL
|
||||
|
||||
LOGSTATS_INTERVAL
|
||||
|
|
@ -1641,21 +1680,6 @@ Default: ``False``
|
|||
|
||||
Whether to enable memory debugging.
|
||||
|
||||
.. setting:: MEMDEBUG_NOTIFY
|
||||
|
||||
MEMDEBUG_NOTIFY
|
||||
---------------
|
||||
|
||||
Default: ``[]``
|
||||
|
||||
When memory debugging is enabled a memory report will be sent to the specified
|
||||
addresses if this setting is not empty, otherwise the report will be written to
|
||||
the log.
|
||||
|
||||
Example::
|
||||
|
||||
MEMDEBUG_NOTIFY = ['user@example.com']
|
||||
|
||||
.. setting:: MEMUSAGE_ENABLED
|
||||
|
||||
MEMUSAGE_ENABLED
|
||||
|
|
@ -1729,9 +1753,11 @@ Default: ``"<project name>.spiders"`` (:ref:`fallback <default-settings>`: ``""`
|
|||
|
||||
Module where to create new spiders using the :command:`genspider` command.
|
||||
|
||||
Example::
|
||||
Example:
|
||||
|
||||
NEWSPIDER_MODULE = 'mybot.spiders_dev'
|
||||
.. code-block:: python
|
||||
|
||||
NEWSPIDER_MODULE = "mybot.spiders_dev"
|
||||
|
||||
.. setting:: RANDOMIZE_DOWNLOAD_DELAY
|
||||
|
||||
|
|
@ -1749,7 +1775,10 @@ significant similarities in the time between their requests.
|
|||
|
||||
The randomization policy is the same used by `wget`_ ``--random-wait`` option.
|
||||
|
||||
If :setting:`DOWNLOAD_DELAY` is zero (default) this option has no effect.
|
||||
If :setting:`DOWNLOAD_DELAY` is zero this option has no effect.
|
||||
|
||||
It is possible to change this setting per domain by using
|
||||
:setting:`DOWNLOAD_SLOTS`.
|
||||
|
||||
.. _wget: https://www.gnu.org/software/wget/manual/wget.html
|
||||
|
||||
|
|
@ -1810,7 +1839,7 @@ The parser backend to use for parsing ``robots.txt`` files. For more information
|
|||
.. setting:: ROBOTSTXT_USER_AGENT
|
||||
|
||||
ROBOTSTXT_USER_AGENT
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
--------------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
|
|
@ -1838,7 +1867,8 @@ Default: ``False``
|
|||
|
||||
Setting to ``True`` will log debug information about the requests scheduler.
|
||||
This currently logs (only once) if the requests cannot be serialized to disk.
|
||||
Stats counter (``scheduler/unserializable``) tracks the number of times this happens.
|
||||
The :stat:`scheduler/unserializable` stat tracks the number of times this
|
||||
happens.
|
||||
|
||||
Example entry in logs::
|
||||
|
||||
|
|
@ -1965,6 +1995,8 @@ Default:
|
|||
|
||||
{
|
||||
"scrapy.contracts.default.UrlContract": 1,
|
||||
"scrapy.contracts.default.CallbackKeywordArgumentsContract": 1,
|
||||
"scrapy.contracts.default.MetadataContract": 1,
|
||||
"scrapy.contracts.default.ReturnsContract": 2,
|
||||
"scrapy.contracts.default.ScrapesContract": 3,
|
||||
}
|
||||
|
|
@ -2029,6 +2061,7 @@ Default:
|
|||
.. code-block:: python
|
||||
|
||||
{
|
||||
"scrapy.spidermiddlewares.start.StartSpiderMiddleware": 25,
|
||||
"scrapy.spidermiddlewares.httperror.HttpErrorMiddleware": 50,
|
||||
"scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
|
||||
"scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
|
||||
|
|
@ -2104,6 +2137,25 @@ command.
|
|||
The project name must not conflict with the name of custom files or directories
|
||||
in the ``project`` subdirectory.
|
||||
|
||||
.. setting:: TWISTED_DNS_RESOLVER
|
||||
|
||||
TWISTED_DNS_RESOLVER
|
||||
--------------------
|
||||
|
||||
Default: ``'scrapy.resolver.CachingThreadedResolver'``
|
||||
|
||||
The class to be used by Twisted to resolve DNS names. The default
|
||||
``scrapy.resolver.CachingThreadedResolver`` supports specifying a timeout for
|
||||
DNS requests via the :setting:`DNS_TIMEOUT` setting, but works only with IPv4
|
||||
addresses. Scrapy provides an alternative resolver,
|
||||
``scrapy.resolver.CachingHostnameResolver``, which supports IPv4/IPv6 addresses but does not
|
||||
take the :setting:`DNS_TIMEOUT` setting into account.
|
||||
|
||||
.. note::
|
||||
This setting has no effect when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
|
||||
|
||||
.. note:: This is a :ref:`reactor setting <reactor-settings>`.
|
||||
|
||||
.. setting:: TWISTED_REACTOR_ENABLED
|
||||
|
||||
TWISTED_REACTOR_ENABLED
|
||||
|
|
@ -2142,6 +2194,9 @@ Default: ``"twisted.internet.asyncioreactor.AsyncioSelectorReactor"``
|
|||
|
||||
Import path of a given :mod:`~twisted.internet.reactor`.
|
||||
|
||||
.. note::
|
||||
This setting has no effect when :setting:`TWISTED_REACTOR_ENABLED` is ``False``.
|
||||
|
||||
Scrapy will install this reactor if no other reactor is installed yet, such as
|
||||
when the ``scrapy`` CLI program is invoked or when using the
|
||||
:class:`~scrapy.crawler.AsyncCrawlerProcess` class or the
|
||||
|
|
@ -2178,7 +2233,7 @@ In order to use the reactor installed by Scrapy:
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.timeout = int(kwargs.pop("timeout", "60"))
|
||||
super(QuotesSpider, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def start(self):
|
||||
reactor.callLater(self.timeout, self.stop)
|
||||
|
|
@ -2207,7 +2262,7 @@ which raises an exception, becomes:
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.timeout = int(kwargs.pop("timeout", "60"))
|
||||
super(QuotesSpider, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def start(self):
|
||||
from twisted.internet import reactor
|
||||
|
|
@ -2245,7 +2300,7 @@ URLLENGTH_LIMIT
|
|||
|
||||
Default: ``2083``
|
||||
|
||||
Scope: ``spidermiddlewares.urllength``
|
||||
Scope: ``scrapy.spidermiddlewares.urllength``
|
||||
|
||||
The maximum URL length to allow for crawled URLs.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,30 +17,35 @@ spider, without having to run the spider to test every change.
|
|||
Once you get familiarized with the Scrapy shell, you'll see that it's an
|
||||
invaluable tool for developing and debugging your spiders.
|
||||
|
||||
.. _shell-config:
|
||||
|
||||
Configuring the shell
|
||||
=====================
|
||||
|
||||
If you have `IPython`_ installed, the Scrapy shell will use it (instead of the
|
||||
standard Python console). The `IPython`_ console is much more powerful and
|
||||
provides smart auto-completion and colorized output, among other things.
|
||||
With the :ref:`ptpython <extras>` extra, the Scrapy shell will use ptpython_
|
||||
instead of the :term:`REPL`. ptpython provides syntax highlighting, smart
|
||||
auto-completion, and more.
|
||||
|
||||
We highly recommend you install `IPython`_, especially if you're working on
|
||||
Unix systems (where `IPython`_ excels). See the `IPython installation guide`_
|
||||
for more info.
|
||||
Failing that, with the :ref:`ipython <extras>` extra, the Scrapy shell will
|
||||
use IPython_ instead. IPython provides smart auto-completion, colorized
|
||||
output, and more.
|
||||
|
||||
Scrapy also has support for `bpython`_, and will try to use it where `IPython`_
|
||||
is unavailable.
|
||||
Scrapy also has support for `bpython`_ via the :ref:`bpython <extras>` extra,
|
||||
and will try to use it where neither ptpython nor IPython is available.
|
||||
|
||||
Through Scrapy's settings you can configure it to use any one of
|
||||
``ipython``, ``bpython`` or the standard ``python`` shell, regardless of which
|
||||
are installed. This is done by setting the ``SCRAPY_PYTHON_SHELL`` environment
|
||||
variable; or by defining it in your :ref:`scrapy.cfg <topics-config-settings>`::
|
||||
``ptpython``, ``ipython``, ``bpython`` or the standard ``python`` shell,
|
||||
regardless of which are installed. This is done by setting the
|
||||
``SCRAPY_PYTHON_SHELL`` environment variable; or by defining it in your
|
||||
:ref:`scrapy.cfg <topics-config-settings>`:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[settings]
|
||||
shell = bpython
|
||||
|
||||
.. _ptpython: https://github.com/prompt-toolkit/ptpython
|
||||
.. _IPython: https://ipython.org/
|
||||
.. _IPython installation guide: https://ipython.org/install/
|
||||
.. _bpython: https://bpython-interpreter.org/
|
||||
|
||||
Launch the shell
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ Here is a simple example showing how you can catch signals and perform some acti
|
|||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, *args, **kwargs):
|
||||
spider = super(DmozSpider, cls).from_crawler(crawler, *args, **kwargs)
|
||||
spider = super().from_crawler(crawler, *args, **kwargs)
|
||||
crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
|
||||
return spider
|
||||
|
||||
|
|
@ -60,6 +60,8 @@ Let's take an example using :ref:`coroutines <topics-coroutines>`:
|
|||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
||||
import json
|
||||
|
||||
import scrapy
|
||||
import treq
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ Let's take an example using :ref:`coroutines <topics-coroutines>`:
|
|||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, *args, **kwargs):
|
||||
spider = super(SignalSpider, cls).from_crawler(crawler, *args, **kwargs)
|
||||
spider = super().from_crawler(crawler, *args, **kwargs)
|
||||
crawler.signals.connect(spider.item_scraped, signal=signals.item_scraped)
|
||||
return spider
|
||||
|
||||
|
|
@ -452,7 +454,7 @@ bytes_received
|
|||
.. signal:: bytes_received
|
||||
.. function:: bytes_received(data, request, spider)
|
||||
|
||||
Sent by the HTTP 1.1 and S3 download handlers when a group of bytes is
|
||||
Sent by some download handlers when a group of bytes is
|
||||
received for a specific request. This signal might be fired multiple
|
||||
times for the same request, with partial data each time. For instance,
|
||||
a possible scenario for a 25 kb response would be two signals fired
|
||||
|
|
@ -480,7 +482,7 @@ headers_received
|
|||
.. signal:: headers_received
|
||||
.. function:: headers_received(headers, body_length, request, spider)
|
||||
|
||||
Sent by the HTTP 1.1 and S3 download handlers when the response headers are
|
||||
Sent by some download handlers when the response headers are
|
||||
available for a given request, before downloading any additional content.
|
||||
|
||||
Handlers for this signal can stop the download of a response while it
|
||||
|
|
@ -502,6 +504,27 @@ headers_received
|
|||
:param spider: the spider associated with the response
|
||||
:type spider: :class:`~scrapy.Spider` object
|
||||
|
||||
robots_parsed
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. signal:: robots_parsed
|
||||
.. function:: robots_parsed(robotparser, request)
|
||||
|
||||
.. versionadded:: VERSION
|
||||
|
||||
Sent by
|
||||
:class:`~scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware` after it
|
||||
downloads and parses a :file:`robots.txt` file, for the host that *request*
|
||||
targets.
|
||||
|
||||
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
|
||||
|
||||
:param robotparser: the parser holding the parsed :file:`robots.txt` contents
|
||||
:type robotparser: :class:`~scrapy.robotstxt.RobotParser` object
|
||||
|
||||
:param request: the request that triggered the :file:`robots.txt` download
|
||||
:type request: :class:`~scrapy.Request` object
|
||||
|
||||
|
||||
Response signals
|
||||
----------------
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ previous (or subsequent) middleware being applied.
|
|||
If you want to disable a builtin middleware (the ones defined in
|
||||
:setting:`SPIDER_MIDDLEWARES_BASE`, and enabled by default) you must define it
|
||||
in your project :setting:`SPIDER_MIDDLEWARES` setting and assign ``None`` as its
|
||||
value. For example, if you want to disable the off-site middleware:
|
||||
value. For example, if you want to disable the referer middleware:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -316,6 +316,35 @@ Default: ``False``
|
|||
Pass all responses, regardless of its status code.
|
||||
|
||||
|
||||
MetaCopyDetectionMiddleware
|
||||
---------------------------
|
||||
|
||||
.. module:: scrapy.spidermiddlewares.metacopy
|
||||
:synopsis: Meta Copy Detection Spider Middleware
|
||||
|
||||
.. class:: MetaCopyDetectionMiddleware
|
||||
|
||||
Warns when a spider yields a request that contains internal meta keys which
|
||||
should not be copied from :attr:`response.meta <scrapy.http.Response.meta>`
|
||||
into new requests. See :attr:`~scrapy.http.Request.meta` to learn why.
|
||||
|
||||
Only 1 warning is emitted per crawl.
|
||||
|
||||
MetaCopyDetectionMiddleware settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. setting:: META_COPY_WARN_SKIP_KEYS
|
||||
|
||||
META_COPY_WARN_SKIP_KEYS
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Default: ``[]``
|
||||
|
||||
A list of internal meta key names to exclude from the internal-keys check.
|
||||
Use this when you intentionally copy one of the monitored keys and want to
|
||||
suppress the resulting warning without disabling the middleware entirely.
|
||||
|
||||
|
||||
RefererMiddleware
|
||||
-----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -4,43 +4,31 @@
|
|||
Spiders
|
||||
=======
|
||||
|
||||
Spiders are classes which define how a certain site (or a group of sites) will be
|
||||
scraped, including how to perform the crawl (i.e. follow links) and how to
|
||||
extract structured data from their pages (i.e. scraping items). In other words,
|
||||
Spiders are the place where you define the custom behaviour for crawling and
|
||||
parsing pages for a particular site (or, in some cases, a group of sites).
|
||||
Spiders are classes that define how a site, or a group of sites, is scraped:
|
||||
which requests to send, and how to parse their responses to extract data and to
|
||||
send additional requests.
|
||||
|
||||
For spiders, the scraping cycle goes through something like this:
|
||||
A crawl goes as follows:
|
||||
|
||||
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.
|
||||
1. Scrapy iterates the :meth:`~scrapy.Spider.start` method of the spider to
|
||||
get the initial requests. By default, that method yields a
|
||||
:class:`~scrapy.Request` object for each URL in
|
||||
:attr:`~scrapy.Spider.start_urls`, with :meth:`~scrapy.Spider.parse` as
|
||||
:ref:`callback <callbacks>`.
|
||||
|
||||
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. Scrapy downloads each request and calls its callback with the resulting
|
||||
:class:`~scrapy.http.Response`.
|
||||
|
||||
2. In the callback function, you parse the response (web page) and return
|
||||
:ref:`item objects <topics-items>`,
|
||||
:class:`~scrapy.Request` objects, or an iterable of these objects.
|
||||
Those Requests will also contain a callback (maybe
|
||||
the same) and will then be downloaded by Scrapy and then their
|
||||
response handled by the specified callback.
|
||||
3. Callbacks parse the response, typically using :ref:`topics-selectors`, and
|
||||
return or yield :ref:`item objects <topics-items>` with the extracted data
|
||||
and :class:`~scrapy.Request` objects to continue the crawl, which go back
|
||||
to step 2. See :ref:`callback-output`.
|
||||
|
||||
3. In callback functions, you parse the page contents, typically using
|
||||
:ref:`topics-selectors` (but you can also use BeautifulSoup, lxml or whatever
|
||||
mechanism you prefer) and generate items with the parsed data.
|
||||
4. Items go through :ref:`item pipelines <topics-item-pipeline>`, and are
|
||||
usually stored through :ref:`topics-feed-exports`.
|
||||
|
||||
4. Finally, the items returned from the spider will be typically persisted to a
|
||||
database (in some :ref:`Item Pipeline <topics-item-pipeline>`) or written to
|
||||
a file using :ref:`topics-feed-exports`.
|
||||
|
||||
Even though this cycle applies (more or less) to any kind of spider, there are
|
||||
different kinds of default spiders bundled into Scrapy for different purposes.
|
||||
We will talk about those types here.
|
||||
Scrapy includes different spider classes for different purposes, described
|
||||
below.
|
||||
|
||||
.. _topics-spiders-ref:
|
||||
|
||||
|
|
@ -191,28 +179,7 @@ scrapy.Spider
|
|||
|
||||
.. automethod:: start
|
||||
|
||||
.. method:: parse(response)
|
||||
|
||||
This is the default callback used by Scrapy to process downloaded
|
||||
responses, when their requests don't specify a callback.
|
||||
|
||||
The ``parse`` method is in charge of processing the response and returning
|
||||
scraped data and/or more URLs to follow. Other Requests callbacks have
|
||||
the same requirements as the :class:`~scrapy.Spider` class.
|
||||
|
||||
This method, as well as any other Request callback, must return a
|
||||
:class:`~scrapy.Request` object, an :ref:`item object <topics-items>`, an
|
||||
iterable of :class:`~scrapy.Request` objects and/or :ref:`item objects
|
||||
<topics-items>`, or ``None``.
|
||||
|
||||
:param response: the response to parse
|
||||
:type response: :class:`~scrapy.http.Response`
|
||||
|
||||
.. method:: log(message, [level, component])
|
||||
|
||||
Wrapper that sends a log message through the Spider's :attr:`logger`,
|
||||
kept for backward compatibility. For more information see
|
||||
:ref:`topics-logging-from-spiders`.
|
||||
.. automethod:: parse
|
||||
|
||||
.. method:: closed(reason)
|
||||
|
||||
|
|
@ -314,7 +281,7 @@ Spiders can access arguments in their `__init__` methods:
|
|||
name = "myspider"
|
||||
|
||||
def __init__(self, category=None, *args, **kwargs):
|
||||
super(MySpider, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.start_urls = [f"http://www.example.com/categories/{category}"]
|
||||
# ...
|
||||
|
||||
|
|
@ -335,8 +302,8 @@ The above example can also be written as follows:
|
|||
|
||||
If you are :ref:`running Scrapy from a script <run-from-script>`, you can
|
||||
specify spider arguments when calling
|
||||
:class:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
|
||||
:class:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
|
||||
:meth:`CrawlerProcess.crawl <scrapy.crawler.CrawlerProcess.crawl>` or
|
||||
:meth:`CrawlerRunner.crawl <scrapy.crawler.CrawlerRunner.crawl>`:
|
||||
|
||||
.. skip: next
|
||||
.. code-block:: python
|
||||
|
|
@ -593,7 +560,7 @@ Let's now take a look at an example CrawlSpider with rules:
|
|||
This spider would start crawling example.com's home page, collecting category
|
||||
links, and item links, parsing the latter with the ``parse_item`` method. For
|
||||
each item response, some data will be extracted from the HTML using XPath, and
|
||||
an :class:`~scrapy.Item` will be filled with it.
|
||||
a dictionary will be filled with it.
|
||||
|
||||
XMLFeedSpider
|
||||
-------------
|
||||
|
|
@ -614,7 +581,7 @@ XMLFeedSpider
|
|||
|
||||
A string which defines the iterator to use. It can be either:
|
||||
|
||||
- ``'iternodes'`` - a fast iterator based on regular expressions
|
||||
- ``'iternodes'`` - a fast iterator based on ``lxml``
|
||||
|
||||
- ``'html'`` - an iterator which uses :class:`~scrapy.Selector`.
|
||||
Keep in mind this uses DOM parsing and must load all DOM in memory
|
||||
|
|
@ -628,9 +595,11 @@ XMLFeedSpider
|
|||
|
||||
.. attribute:: itertag
|
||||
|
||||
A string with the name of the node (or element) to iterate in. Example::
|
||||
A string with the name of the node (or element) to iterate in. Example:
|
||||
|
||||
itertag = 'product'
|
||||
.. code-block:: python
|
||||
|
||||
itertag = "product"
|
||||
|
||||
.. attribute:: namespaces
|
||||
|
||||
|
|
@ -643,12 +612,17 @@ XMLFeedSpider
|
|||
You can then specify nodes with namespaces in the :attr:`itertag`
|
||||
attribute.
|
||||
|
||||
Example::
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy.spiders import XMLFeedSpider
|
||||
|
||||
|
||||
class YourSpider(XMLFeedSpider):
|
||||
|
||||
namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]
|
||||
itertag = 'n:url'
|
||||
namespaces = [("n", "http://www.sitemaps.org/schemas/sitemap/0.9")]
|
||||
itertag = "n:url"
|
||||
# ...
|
||||
|
||||
Apart from these new attributes, this spider has the following overridable
|
||||
|
|
@ -808,9 +782,11 @@ SitemapSpider
|
|||
the regular expression. ``callback`` can be a string (indicating the
|
||||
name of a spider method) or a callable.
|
||||
|
||||
For example::
|
||||
For example:
|
||||
|
||||
sitemap_rules = [('/product/', 'parse_product')]
|
||||
.. code-block:: python
|
||||
|
||||
sitemap_rules = [("/product/", "parse_product")]
|
||||
|
||||
Rules are applied in order, and only the first one that matches will be
|
||||
used.
|
||||
|
|
@ -832,7 +808,9 @@ SitemapSpider
|
|||
are links for the same website in another language passed within
|
||||
the same ``url`` block.
|
||||
|
||||
For example::
|
||||
For example:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<url>
|
||||
<loc>http://example.com/</loc>
|
||||
|
|
@ -850,7 +828,9 @@ SitemapSpider
|
|||
This is a filter function that could be overridden to select sitemap entries
|
||||
based on their attributes.
|
||||
|
||||
For example::
|
||||
For example:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<url>
|
||||
<loc>http://example.com/</loc>
|
||||
|
|
@ -953,6 +933,7 @@ Combine SitemapSpider with other sources of urls:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from scrapy import Request
|
||||
from scrapy.spiders import SitemapSpider
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ Collector, and can be accessed through the :attr:`~scrapy.crawler.Crawler.stats`
|
|||
attribute of the :ref:`topics-api-crawler`, as illustrated by the examples in
|
||||
the :ref:`topics-stats-usecases` section below.
|
||||
|
||||
However, the Stats Collector is always available, so you can always import it
|
||||
in your module and use its API (to increment or set new stat keys), regardless
|
||||
The Stats Collector API is always available, so you can always use it (to
|
||||
increment or set new stat keys), regardless
|
||||
of whether the stats collection is enabled or not. If it's disabled, the API
|
||||
will still work but it won't collect anything. This is aimed at simplifying the
|
||||
stats collector usage: you should spend no more than one line of code for
|
||||
|
|
@ -21,8 +21,7 @@ using the Stats Collector from.
|
|||
Another feature of the Stats Collector is that it's very efficient (when
|
||||
enabled) and extremely efficient (almost unnoticeable) when disabled.
|
||||
|
||||
The Stats Collector keeps a stats table per open spider which is automatically
|
||||
opened when the spider is opened, and closed when the spider is closed.
|
||||
See :ref:`topics-stats-reference` below for the stats that Scrapy sets.
|
||||
|
||||
.. _topics-stats-usecases:
|
||||
|
||||
|
|
@ -87,37 +86,659 @@ Get all stats:
|
|||
Available Stats Collectors
|
||||
==========================
|
||||
|
||||
.. currentmodule:: scrapy.statscollectors
|
||||
|
||||
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`.
|
||||
|
||||
.. currentmodule:: scrapy.statscollectors
|
||||
|
||||
MemoryStatsCollector
|
||||
--------------------
|
||||
|
||||
.. class:: MemoryStatsCollector
|
||||
|
||||
A simple stats collector that keeps the stats of the last scraping run (for
|
||||
each spider) in memory, after they're closed. The stats can be accessed
|
||||
through the :attr:`spider_stats` attribute, which is a dict keyed by spider
|
||||
domain name.
|
||||
|
||||
This is the default Stats Collector used in Scrapy.
|
||||
|
||||
.. attribute:: spider_stats
|
||||
|
||||
A dict of dicts (keyed by spider name) containing the stats of the last
|
||||
scraping run for each spider.
|
||||
.. autoclass:: MemoryStatsCollector
|
||||
:members:
|
||||
|
||||
DummyStatsCollector
|
||||
-------------------
|
||||
|
||||
.. class:: DummyStatsCollector
|
||||
.. autoclass:: DummyStatsCollector
|
||||
|
||||
A Stats collector which does nothing but is very efficient (because it does
|
||||
nothing). This stats collector can be set via the :setting:`STATS_CLASS`
|
||||
setting, to disable stats collect in order to improve performance. However,
|
||||
the performance penalty of stats collection is usually marginal compared to
|
||||
other Scrapy workload like parsing pages.
|
||||
.. _topics-stats-reference:
|
||||
|
||||
Built-in stats reference
|
||||
========================
|
||||
|
||||
Scrapy sets the following :ref:`stats <topics-stats>`. Components other than
|
||||
those built into Scrapy may set additional stats; see their documentation.
|
||||
|
||||
Stat keys that contain a ``{placeholder}`` below stand for a family of stats,
|
||||
one per actual value of the placeholder.
|
||||
|
||||
.. note:: Most stats are set by a specific :ref:`component
|
||||
<topics-components>`, and are only present if that component is enabled and
|
||||
its code path is reached. A stat that is missing from
|
||||
:meth:`~scrapy.statscollectors.StatsCollector.get_stats` output is
|
||||
equivalent to a counter of 0.
|
||||
|
||||
.. stat:: downloader/exception_count
|
||||
|
||||
``downloader/exception_count``
|
||||
Number of exceptions raised while downloading requests.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
.. stat:: downloader/exception_type_count/{exception_type}
|
||||
|
||||
``downloader/exception_type_count/{exception_type}``
|
||||
Number of exceptions raised while downloading requests, per exception type,
|
||||
where ``{exception_type}`` is the import path of the exception class, e.g.
|
||||
``twisted.internet.error.DNSLookupError``.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
.. stat:: downloader/request_bytes
|
||||
|
||||
``downloader/request_bytes``
|
||||
Total size, in bytes, of the requests sent, counting the request line, the
|
||||
headers and the body. As with :stat:`downloader/request_count`, requests
|
||||
served from the cache are also counted.
|
||||
|
||||
It is an approximation, reconstructed from each :class:`~scrapy.Request`
|
||||
object instead of measured on the wire, so it does not account for the
|
||||
actual bytes that the :ref:`download handler
|
||||
<topics-download-handlers>` sends, e.g. transport-level overhead.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
.. stat:: downloader/request_count
|
||||
|
||||
``downloader/request_count``
|
||||
Number of requests sent.
|
||||
|
||||
Requests that :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`
|
||||
serves from the cache are also counted, even though they are never sent,
|
||||
because it handles requests after
|
||||
:class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
.. stat:: downloader/request_method_count/{method}
|
||||
|
||||
``downloader/request_method_count/{method}``
|
||||
Number of requests sent, per HTTP method, e.g. ``GET`` or ``POST``. As with
|
||||
:stat:`downloader/request_count`, requests served from the cache are also
|
||||
counted.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
.. stat:: downloader/response_bytes
|
||||
|
||||
``downloader/response_bytes``
|
||||
Total size, in bytes, of the responses received, counting the status line,
|
||||
the headers and the body. It covers the same responses as
|
||||
:stat:`downloader/response_count`.
|
||||
|
||||
The body is counted as received, i.e. still compressed for responses that
|
||||
used ``Content-Encoding``, because
|
||||
:class:`~scrapy.downloadermiddlewares.stats.DownloaderStats` handles
|
||||
responses before
|
||||
:class:`~scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware`
|
||||
decompresses them. See :stat:`httpcompression/response_bytes` for
|
||||
decompressed sizes.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
.. stat:: downloader/response_count
|
||||
|
||||
``downloader/response_count``
|
||||
Number of responses received.
|
||||
|
||||
It counts responses that :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`
|
||||
serves from the cache, even though they do not come from the network, and
|
||||
responses that a downloader middleware consumes before they reach your
|
||||
spider, e.g. redirect responses that :class:`~scrapy.downloadermiddlewares.redirect.RedirectMiddleware`
|
||||
turns into new requests. Compare with :stat:`response_received_count`.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
.. stat:: downloader/response_status_count/{status_code}
|
||||
|
||||
``downloader/response_status_count/{status_code}``
|
||||
Number of responses received, per HTTP status code, e.g. ``200`` or
|
||||
``404``. It covers the same responses as :stat:`downloader/response_count`.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.stats.DownloaderStats`.
|
||||
|
||||
.. stat:: dupefilter/filtered
|
||||
|
||||
``dupefilter/filtered``
|
||||
Number of requests dropped as duplicates.
|
||||
|
||||
Set by :class:`~scrapy.dupefilters.RFPDupeFilter`.
|
||||
|
||||
.. stat:: elapsed_time_seconds
|
||||
|
||||
``elapsed_time_seconds``
|
||||
Time, as a :class:`float`, in seconds, between the :signal:`spider_opened`
|
||||
and the :signal:`spider_closed` signals.
|
||||
|
||||
Set by :class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
|
||||
.. stat:: feedexport/failed_count/{storage}
|
||||
|
||||
``feedexport/failed_count/{storage}``
|
||||
Number of :ref:`feeds <topics-feed-exports>` that could not be stored, per
|
||||
:ref:`storage backend <topics-feed-storage-backends>`, where ``{storage}``
|
||||
is the class name of the storage backend, e.g. ``FileFeedStorage``.
|
||||
|
||||
.. stat:: feedexport/success_count/{storage}
|
||||
|
||||
``feedexport/success_count/{storage}``
|
||||
Number of :ref:`feeds <topics-feed-exports>` stored successfully, per
|
||||
:ref:`storage backend <topics-feed-storage-backends>`, where ``{storage}``
|
||||
is the class name of the storage backend, e.g. ``FileFeedStorage``.
|
||||
|
||||
.. stat:: file_count
|
||||
|
||||
``file_count``
|
||||
Number of files handled by the :ref:`media pipelines
|
||||
<topics-media-pipeline>`.
|
||||
|
||||
.. stat:: file_status_count/{status}
|
||||
|
||||
``file_status_count/{status}``
|
||||
Number of files handled by the :ref:`media pipelines
|
||||
<topics-media-pipeline>`, per status, where ``{status}`` is one of:
|
||||
|
||||
- ``downloaded``: the file was downloaded.
|
||||
|
||||
- ``cached``: the file came from the
|
||||
:class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`
|
||||
cache.
|
||||
|
||||
- ``uptodate``: the file was already in the storage backend and had not
|
||||
:ref:`expired <file-expiration>`, so it was not downloaded again.
|
||||
|
||||
.. stat:: finish_reason
|
||||
|
||||
``finish_reason``
|
||||
String indicating why the crawl finished. It matches the *reason* argument
|
||||
of the :signal:`spider_closed` signal.
|
||||
|
||||
Scrapy uses the following reasons:
|
||||
|
||||
- ``cancelled``: the spider was closed without a more specific reason,
|
||||
e.g. because :exc:`~scrapy.exceptions.CloseSpider` was raised without
|
||||
one.
|
||||
|
||||
- ``closespider_errorcount``: see :setting:`CLOSESPIDER_ERRORCOUNT`.
|
||||
|
||||
- ``closespider_itemcount``: see :setting:`CLOSESPIDER_ITEMCOUNT`.
|
||||
|
||||
- ``closespider_pagecount``: see :setting:`CLOSESPIDER_PAGECOUNT`.
|
||||
|
||||
- ``closespider_pagecount_no_item``: see
|
||||
:setting:`CLOSESPIDER_PAGECOUNT_NO_ITEM`.
|
||||
|
||||
- ``closespider_timeout``: see :setting:`CLOSESPIDER_TIMEOUT`.
|
||||
|
||||
- ``closespider_timeout_no_item``: see
|
||||
:setting:`CLOSESPIDER_TIMEOUT_NO_ITEM`.
|
||||
|
||||
- ``finished``: the spider became idle with no pending requests, i.e. it
|
||||
finished normally.
|
||||
|
||||
- ``memusage_exceeded``: see :setting:`MEMUSAGE_LIMIT_MB`.
|
||||
|
||||
- ``shutdown``: the crawl was interrupted, e.g. by a system signal such
|
||||
as ``SIGINT`` (:kbd:`Ctrl-C`).
|
||||
|
||||
Third-party components and your own code may use any other reason, e.g. by
|
||||
raising :exc:`~scrapy.exceptions.CloseSpider` with it.
|
||||
|
||||
Set by :class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
|
||||
.. stat:: finish_time
|
||||
|
||||
``finish_time``
|
||||
Timezone-aware :class:`~datetime.datetime` object, in UTC, indicating when
|
||||
the :signal:`spider_closed` signal was sent.
|
||||
|
||||
Set by :class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
|
||||
.. stat:: httpcache/errorrecovery
|
||||
|
||||
``httpcache/errorrecovery``
|
||||
Number of times that a stale cached response was used because downloading a
|
||||
fresh response raised an exception.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/firsthand
|
||||
|
||||
``httpcache/firsthand``
|
||||
Number of responses that were downloaded without a matching cache entry to
|
||||
validate against, i.e. responses for requests counted in
|
||||
:stat:`httpcache/miss`.
|
||||
|
||||
It is lower than :stat:`httpcache/miss` when some of those requests yield
|
||||
no response, either because they are dropped (see
|
||||
:stat:`httpcache/ignore`) or because their download fails.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/hit
|
||||
|
||||
``httpcache/hit``
|
||||
Number of requests served from the cache.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/ignore
|
||||
|
||||
``httpcache/ignore``
|
||||
Number of requests dropped because they were not in the cache and
|
||||
:setting:`HTTPCACHE_IGNORE_MISSING` is ``True``.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/invalidate
|
||||
|
||||
``httpcache/invalidate``
|
||||
Number of times that a cached response failed validation and was replaced
|
||||
with a freshly downloaded response.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/miss
|
||||
|
||||
``httpcache/miss``
|
||||
Number of requests for which no cache entry could be read, either because
|
||||
there was none or because reading it failed, in which case the request is
|
||||
also counted in :stat:`httpcache/retrieve_error`. Those requests are
|
||||
downloaded (see :stat:`httpcache/firsthand`), or dropped if
|
||||
:setting:`HTTPCACHE_IGNORE_MISSING` is ``True`` (see
|
||||
:stat:`httpcache/ignore`).
|
||||
|
||||
Requests with a stale cache entry are not counted here; see
|
||||
:stat:`httpcache/revalidate` and :stat:`httpcache/invalidate`.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/retrieve_error
|
||||
|
||||
``httpcache/retrieve_error``
|
||||
Number of cache entries that could not be read, and hence were treated as
|
||||
cache misses. Those requests are also counted in :stat:`httpcache/miss`.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/revalidate
|
||||
|
||||
``httpcache/revalidate``
|
||||
Number of times that a cached response was successfully validated against
|
||||
the target server, and hence used instead of the fresh response.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/store
|
||||
|
||||
``httpcache/store``
|
||||
Number of responses stored in the cache.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcache/uncacheable
|
||||
|
||||
``httpcache/uncacheable``
|
||||
Number of responses not stored in the cache because the
|
||||
:setting:`HTTPCACHE_POLICY` did not allow it.
|
||||
|
||||
Every response considered for caching is counted either here or in
|
||||
:stat:`httpcache/store`, so ``httpcache/store + httpcache/uncacheable``
|
||||
equals ``httpcache/firsthand + httpcache/invalidate``.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`.
|
||||
|
||||
.. stat:: httpcompression/response_bytes
|
||||
|
||||
``httpcompression/response_bytes``
|
||||
Total size, in bytes, of decompressed response bodies, counting only the
|
||||
body and only responses that were actually decompressed. Compare with
|
||||
:stat:`downloader/response_bytes`.
|
||||
|
||||
Set by
|
||||
:class:`~scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware`.
|
||||
|
||||
.. stat:: httpcompression/response_count
|
||||
|
||||
``httpcompression/response_count``
|
||||
Number of decompressed responses.
|
||||
|
||||
Set by
|
||||
:class:`~scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware`.
|
||||
|
||||
.. stat:: httperror/response_ignored_count
|
||||
|
||||
``httperror/response_ignored_count``
|
||||
Number of responses dropped because of their HTTP status code.
|
||||
|
||||
Set by :class:`~scrapy.spidermiddlewares.httperror.HttpErrorMiddleware`.
|
||||
|
||||
.. stat:: httperror/response_ignored_status_count/{status_code}
|
||||
|
||||
``httperror/response_ignored_status_count/{status_code}``
|
||||
Number of responses dropped because of their HTTP status code, per HTTP
|
||||
status code, e.g. ``404``.
|
||||
|
||||
Set by :class:`~scrapy.spidermiddlewares.httperror.HttpErrorMiddleware`.
|
||||
|
||||
.. stat:: item_dropped_count
|
||||
|
||||
``item_dropped_count``
|
||||
Number of items dropped by an :ref:`item pipeline
|
||||
<topics-item-pipeline>`, i.e. number of times that the
|
||||
:signal:`item_dropped` signal was sent.
|
||||
|
||||
Set by :class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
|
||||
.. stat:: item_dropped_reasons_count/{exception}
|
||||
|
||||
``item_dropped_reasons_count/{exception}``
|
||||
Number of items dropped, per exception, where ``{exception}`` is the class
|
||||
name of the exception that caused the item to be dropped.
|
||||
|
||||
Only :exc:`~scrapy.exceptions.DropItem` and its subclasses drop items, and
|
||||
each one is counted under its own class name, e.g.
|
||||
``item_dropped_reasons_count/DropItem`` for
|
||||
:exc:`~scrapy.exceptions.DropItem` itself and
|
||||
``item_dropped_reasons_count/MyDropItem`` for a ``MyDropItem`` subclass of
|
||||
it. Any other exception raised by an :ref:`item pipeline
|
||||
<topics-item-pipeline>` triggers the :signal:`item_error` signal instead of
|
||||
:signal:`item_dropped`, and is not counted here or in
|
||||
:stat:`item_dropped_count`.
|
||||
|
||||
Set by :class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
|
||||
.. stat:: item_scraped_count
|
||||
|
||||
``item_scraped_count``
|
||||
Number of items that passed all :ref:`item pipelines
|
||||
<topics-item-pipeline>`, i.e. number of times that the
|
||||
:signal:`item_scraped` signal was sent.
|
||||
|
||||
Set by :class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
|
||||
.. stat:: items_per_minute
|
||||
|
||||
``items_per_minute``
|
||||
Average number of items scraped per minute during the crawl.
|
||||
|
||||
It is ``None`` if the crawl took less than a minute.
|
||||
|
||||
Set by :class:`~scrapy.extensions.logstats.LogStats`.
|
||||
|
||||
.. stat:: log_count/{level}
|
||||
|
||||
``log_count/{level}``
|
||||
Number of log messages, per logging level name, e.g. ``INFO`` or
|
||||
``WARNING``.
|
||||
|
||||
Only messages that the :setting:`LOG_LEVEL` setting allows are counted.
|
||||
|
||||
Set by :class:`~scrapy.extensions.logcount.LogCount`.
|
||||
|
||||
.. stat:: memdebug/gc_garbage_count
|
||||
|
||||
``memdebug/gc_garbage_count``
|
||||
Number of objects in :data:`gc.garbage` when the spider is closed.
|
||||
|
||||
Set by :class:`~scrapy.extensions.memdebug.MemoryDebugger`, which requires
|
||||
:setting:`MEMDEBUG_ENABLED` to be ``True``.
|
||||
|
||||
.. stat:: memdebug/live_refs/{cls}
|
||||
|
||||
``memdebug/live_refs/{cls}``
|
||||
Number of live objects of class ``{cls}`` when the spider is closed, as
|
||||
reported by :ref:`trackref <topics-leaks-trackrefs>`, e.g.
|
||||
``memdebug/live_refs/HtmlResponse``.
|
||||
|
||||
Only set for classes with at least 1 live object.
|
||||
|
||||
Set by :class:`~scrapy.extensions.memdebug.MemoryDebugger`, which requires
|
||||
:setting:`MEMDEBUG_ENABLED` to be ``True``.
|
||||
|
||||
.. stat:: memusage/limit_reached
|
||||
|
||||
``memusage/limit_reached``
|
||||
``1`` if memory usage exceeded :setting:`MEMUSAGE_LIMIT_MB`, which also
|
||||
stops the crawl.
|
||||
|
||||
Set by :class:`~scrapy.extensions.memusage.MemoryUsage`.
|
||||
|
||||
.. stat:: memusage/max
|
||||
|
||||
``memusage/max``
|
||||
Maximum peak memory usage, in bytes, observed during the crawl.
|
||||
|
||||
Set by :class:`~scrapy.extensions.memusage.MemoryUsage`.
|
||||
|
||||
.. stat:: memusage/startup
|
||||
|
||||
``memusage/startup``
|
||||
Peak memory usage, in bytes, when the engine started.
|
||||
|
||||
Set by :class:`~scrapy.extensions.memusage.MemoryUsage`.
|
||||
|
||||
.. stat:: memusage/warning_reached
|
||||
|
||||
``memusage/warning_reached``
|
||||
``1`` if memory usage exceeded :setting:`MEMUSAGE_WARNING_MB`.
|
||||
|
||||
Set by :class:`~scrapy.extensions.memusage.MemoryUsage`.
|
||||
|
||||
.. stat:: offsite/domains
|
||||
|
||||
``offsite/domains``
|
||||
Number of distinct domains for which at least 1 request was dropped for
|
||||
being offsite.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.offsite.OffsiteMiddleware`.
|
||||
|
||||
.. stat:: offsite/filtered
|
||||
|
||||
``offsite/filtered``
|
||||
Number of requests dropped for being offsite.
|
||||
|
||||
Set by :class:`~scrapy.downloadermiddlewares.offsite.OffsiteMiddleware`.
|
||||
|
||||
.. stat:: request_depth_count/{depth}
|
||||
|
||||
``request_depth_count/{depth}``
|
||||
Number of requests scheduled at depth ``{depth}``, e.g.
|
||||
``request_depth_count/2``.
|
||||
|
||||
Set by :class:`~scrapy.spidermiddlewares.depth.DepthMiddleware`, which
|
||||
requires :setting:`DEPTH_STATS_VERBOSE` to be ``True`` for this stat.
|
||||
|
||||
.. stat:: request_depth_max
|
||||
|
||||
``request_depth_max``
|
||||
Maximum depth reached.
|
||||
|
||||
Set by :class:`~scrapy.spidermiddlewares.depth.DepthMiddleware`.
|
||||
|
||||
.. stat:: response_received_count
|
||||
|
||||
``response_received_count``
|
||||
Number of responses received, i.e. number of times that the
|
||||
:signal:`response_received` signal was sent.
|
||||
|
||||
Unlike :stat:`downloader/response_count`, it does not count responses that
|
||||
a downloader middleware consumes before they reach the engine, e.g.
|
||||
redirect responses that :class:`~scrapy.downloadermiddlewares.redirect.RedirectMiddleware`
|
||||
turns into new requests. Both count responses that :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`
|
||||
serves from the cache.
|
||||
|
||||
Set by :class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
|
||||
.. stat:: responses_per_minute
|
||||
|
||||
``responses_per_minute``
|
||||
Average number of responses received per minute during the crawl.
|
||||
|
||||
It is ``None`` if the crawl took less than a minute.
|
||||
|
||||
Set by :class:`~scrapy.extensions.logstats.LogStats`.
|
||||
|
||||
.. stat:: retry/count
|
||||
|
||||
``retry/count``
|
||||
Number of requests retried.
|
||||
|
||||
Set by :func:`~scrapy.downloadermiddlewares.retry.get_retry_request`, which
|
||||
:class:`~scrapy.downloadermiddlewares.retry.RetryMiddleware` uses.
|
||||
|
||||
.. stat:: retry/max_reached
|
||||
|
||||
``retry/max_reached``
|
||||
Number of requests that were not retried because they had already been
|
||||
retried :setting:`RETRY_TIMES` times.
|
||||
|
||||
Set by :func:`~scrapy.downloadermiddlewares.retry.get_retry_request`, which
|
||||
:class:`~scrapy.downloadermiddlewares.retry.RetryMiddleware` uses.
|
||||
|
||||
.. stat:: retry/reason_count/{reason}
|
||||
|
||||
``retry/reason_count/{reason}``
|
||||
Number of requests retried, per reason, e.g.
|
||||
``retry/reason_count/twisted.internet.error.TimeoutError`` or
|
||||
``retry/reason_count/504 Gateway Time-out``.
|
||||
|
||||
Set by :func:`~scrapy.downloadermiddlewares.retry.get_retry_request`, which
|
||||
:class:`~scrapy.downloadermiddlewares.retry.RetryMiddleware` uses.
|
||||
|
||||
.. note:: Code calling
|
||||
:func:`~scrapy.downloadermiddlewares.retry.get_retry_request` may pass a
|
||||
custom *stats_base_key*, in which case ``retry`` is replaced with that key
|
||||
in the 3 stats above.
|
||||
|
||||
.. stat:: robotstxt/exception_count/{exception_type}
|
||||
|
||||
``robotstxt/exception_count/{exception_type}``
|
||||
Number of exceptions raised while downloading ``robots.txt`` files, per
|
||||
exception type, where ``{exception_type}`` is the string representation of
|
||||
the exception class, e.g. ``<class
|
||||
'twisted.internet.error.DNSLookupError'>``.
|
||||
|
||||
Set by
|
||||
:class:`~scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware`.
|
||||
|
||||
.. stat:: robotstxt/forbidden
|
||||
|
||||
``robotstxt/forbidden``
|
||||
Number of requests dropped for being disallowed by ``robots.txt``.
|
||||
|
||||
Set by
|
||||
:class:`~scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware`.
|
||||
|
||||
.. stat:: robotstxt/request_count
|
||||
|
||||
``robotstxt/request_count``
|
||||
Number of ``robots.txt`` files requested, i.e. 1 per network location for
|
||||
which at least 1 request was sent.
|
||||
|
||||
Set by
|
||||
:class:`~scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware`.
|
||||
|
||||
.. stat:: robotstxt/response_count
|
||||
|
||||
``robotstxt/response_count``
|
||||
Number of ``robots.txt`` responses received.
|
||||
|
||||
Set by
|
||||
:class:`~scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware`.
|
||||
|
||||
.. stat:: robotstxt/response_status_count/{status_code}
|
||||
|
||||
``robotstxt/response_status_count/{status_code}``
|
||||
Number of ``robots.txt`` responses received, per HTTP status code, e.g.
|
||||
``404``.
|
||||
|
||||
Set by
|
||||
:class:`~scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware`.
|
||||
|
||||
.. stat:: scheduler/dequeued
|
||||
|
||||
``scheduler/dequeued``
|
||||
Number of requests read from the :ref:`scheduler <topics-scheduler>`.
|
||||
|
||||
.. stat:: scheduler/dequeued/disk
|
||||
|
||||
``scheduler/dequeued/disk``
|
||||
Number of requests read from the disk queue of the :ref:`scheduler
|
||||
<topics-scheduler>`.
|
||||
|
||||
.. stat:: scheduler/dequeued/memory
|
||||
|
||||
``scheduler/dequeued/memory``
|
||||
Number of requests read from the memory queue of the :ref:`scheduler
|
||||
<topics-scheduler>`.
|
||||
|
||||
.. stat:: scheduler/enqueued
|
||||
|
||||
``scheduler/enqueued``
|
||||
Number of requests stored into the :ref:`scheduler <topics-scheduler>`.
|
||||
|
||||
.. stat:: scheduler/enqueued/disk
|
||||
|
||||
``scheduler/enqueued/disk``
|
||||
Number of requests stored into the disk queue of the :ref:`scheduler
|
||||
<topics-scheduler>`.
|
||||
|
||||
.. stat:: scheduler/enqueued/memory
|
||||
|
||||
``scheduler/enqueued/memory``
|
||||
Number of requests stored into the memory queue of the :ref:`scheduler
|
||||
<topics-scheduler>`.
|
||||
|
||||
.. stat:: scheduler/unserializable
|
||||
|
||||
``scheduler/unserializable``
|
||||
Number of requests that could not be stored into the disk queue of the
|
||||
:ref:`scheduler <topics-scheduler>` because they could not be
|
||||
:ref:`serialized <request-serialization>`, and hence were stored into the
|
||||
memory queue instead.
|
||||
|
||||
.. stat:: spider_exceptions/count
|
||||
|
||||
``spider_exceptions/count``
|
||||
Number of unhandled exceptions raised by spider callbacks.
|
||||
|
||||
Set by the :ref:`scraper <topics-architecture>`.
|
||||
|
||||
.. stat:: spider_exceptions/{exception}
|
||||
|
||||
``spider_exceptions/{exception}``
|
||||
Number of unhandled exceptions raised by spider callbacks, per exception,
|
||||
where ``{exception}`` is the class name of the exception, e.g.
|
||||
``spider_exceptions/ValueError``.
|
||||
|
||||
Set by the :ref:`scraper <topics-architecture>`.
|
||||
|
||||
.. stat:: start_time
|
||||
|
||||
``start_time``
|
||||
Timezone-aware :class:`~datetime.datetime` object, in UTC, indicating when
|
||||
the :signal:`spider_opened` signal was sent.
|
||||
|
||||
Set by :class:`~scrapy.extensions.corestats.CoreStats`.
|
||||
|
||||
.. stat:: urllength/request_ignored_count
|
||||
|
||||
``urllength/request_ignored_count``
|
||||
Number of requests dropped for having a URL longer than
|
||||
:setting:`URLLENGTH_LIMIT`.
|
||||
|
||||
Set by :class:`~scrapy.spidermiddlewares.urllength.UrlLengthMiddleware`.
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ disable it if you want. For more information about the extension itself see
|
|||
How to access the telnet console
|
||||
================================
|
||||
|
||||
The telnet console listens in the TCP port defined in the
|
||||
:setting:`TELNETCONSOLE_PORT` setting, which defaults to ``6023``. To access
|
||||
the console you need to type::
|
||||
The telnet console listens on the first available TCP port from the range
|
||||
defined in the :setting:`TELNETCONSOLE_PORT` setting, which defaults to
|
||||
``[6023, 6073]``. To access the console you need to type::
|
||||
|
||||
telnet localhost 6023
|
||||
Trying localhost...
|
||||
|
|
@ -107,8 +107,8 @@ Here are some example tasks you can do with the telnet console:
|
|||
View engine status
|
||||
------------------
|
||||
|
||||
You can use the ``est()`` method of the Scrapy engine to quickly show its state
|
||||
using the telnet console::
|
||||
You can use the ``est()`` method provided by the console to quickly show the
|
||||
engine status::
|
||||
|
||||
telnet localhost 6023
|
||||
>>> est()
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ API stability
|
|||
|
||||
API stability was one of the major goals for the *1.0* release.
|
||||
|
||||
Methods or functions that start with a single dash (``_``) are private and
|
||||
should never be relied as stable.
|
||||
Methods or functions that start with a single underscore (``_``) are private
|
||||
and should never be relied upon as stable.
|
||||
|
||||
Also, keep in mind that stable doesn't mean complete: stable APIs could grow
|
||||
new methods or functionality but the existing methods should keep working the
|
||||
|
|
|
|||
|
|
@ -16,23 +16,20 @@ class QPSSpider(Spider):
|
|||
name = "qps"
|
||||
benchurl = "http://localhost:8880/"
|
||||
|
||||
# Max concurrency is limited by global CONCURRENT_REQUESTS setting
|
||||
max_concurrent_requests = 8
|
||||
# Requests per second goal
|
||||
qps = None # same as: 1 / download_delay
|
||||
download_delay = None
|
||||
qps = None # same as: 1 / DOWNLOAD_DELAY
|
||||
# time in seconds to delay server responses
|
||||
latency = None
|
||||
# number of slots to create
|
||||
slots = 1
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, **kw)
|
||||
if self.qps is not None:
|
||||
self.qps = float(self.qps)
|
||||
self.download_delay = 1 / self.qps
|
||||
elif self.download_delay is not None:
|
||||
self.download_delay = float(self.download_delay)
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, *args, **kwargs):
|
||||
spider = super().from_crawler(crawler, *args, **kwargs)
|
||||
if spider.qps is not None:
|
||||
spider.qps = float(spider.qps)
|
||||
crawler.settings.set("DOWNLOAD_DELAY", 1 / spider.qps, priority="spider")
|
||||
return spider
|
||||
|
||||
async def start(self):
|
||||
url = self.benchurl
|
||||
|
|
|
|||
|
|
@ -60,6 +60,25 @@ Source = "https://github.com/scrapy/scrapy"
|
|||
Tracker = "https://github.com/scrapy/scrapy/issues"
|
||||
"Release notes" = "https://docs.scrapy.org/en/latest/news.html"
|
||||
|
||||
[project.optional-dependencies]
|
||||
bpython = ["bpython>=0.7.1"]
|
||||
brotli = [
|
||||
"brotli>=1.2.0; implementation_name != 'pypy'",
|
||||
"brotlicffi>=1.2.0.0; implementation_name == 'pypy'",
|
||||
]
|
||||
gcs = ["google-cloud-storage>=1.29.0"]
|
||||
httpx = ["httpx2[http2,socks]>=2.0.0"]
|
||||
images = ["Pillow>=8.3.2"]
|
||||
ipython = ["ipython>=8.15.0"]
|
||||
ptpython = ["ptpython>=3.0.23"]
|
||||
robotparser = ["robotexclusionrulesparser>=1.6.2"]
|
||||
s3 = ["boto3>=1.20.0"]
|
||||
twisted-http2 = ["Twisted[http2]>=21.7.0"]
|
||||
uvloop = [
|
||||
"uvloop>=0.16.0; platform_system != 'Windows' and implementation_name != 'pypy'",
|
||||
]
|
||||
zstd = ["zstandard>=0.16.0; implementation_name != 'pypy'"]
|
||||
|
||||
[project.scripts]
|
||||
scrapy = "scrapy.cmdline:execute"
|
||||
|
||||
|
|
@ -94,7 +113,48 @@ untyped_calls_exclude = [
|
|||
[[tool.mypy.overrides]]
|
||||
module = "tests.*"
|
||||
allow_untyped_defs = true
|
||||
allow_incomplete_defs = true # 48 errors
|
||||
allow_incomplete_defs = true # 59 errors
|
||||
|
||||
# TODO
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"tests.spiders",
|
||||
"tests.test_closespider",
|
||||
"tests.test_cmdline",
|
||||
"tests.test_contracts",
|
||||
"tests.test_downloaderslotssettings",
|
||||
"tests.test_dupefilters",
|
||||
"tests.test_engine_loop",
|
||||
"tests.test_exporters",
|
||||
"tests.test_extension_statsmailer",
|
||||
"tests.test_extension_throttle",
|
||||
"tests.test_feedexport",
|
||||
"tests.test_feedexport_postprocess",
|
||||
"tests.test_feedexport_storages",
|
||||
"tests.test_feedexport_uri_params",
|
||||
"tests.test_item",
|
||||
"tests.test_linkextractors",
|
||||
"tests.test_loader",
|
||||
"tests.test_logformatter",
|
||||
"tests.test_mail",
|
||||
"tests.test_pipeline_crawl",
|
||||
"tests.test_pipeline_files",
|
||||
"tests.test_pipeline_images",
|
||||
"tests.test_pipeline_media",
|
||||
"tests.test_pipelines",
|
||||
"tests.test_pqueues",
|
||||
"tests.test_scheduler_base",
|
||||
"tests.test_settings",
|
||||
"tests.test_spider",
|
||||
"tests.test_spider_crawl",
|
||||
"tests.test_spidermiddleware_output_chain",
|
||||
"tests.test_spidermiddleware_process_start",
|
||||
"tests.test_spider_sitemap",
|
||||
"tests.test_squeues",
|
||||
"tests.test_squeues_request",
|
||||
"tests.test_stats",
|
||||
"tests.utils.bases.spider",
|
||||
]
|
||||
check_untyped_defs = false
|
||||
|
||||
# Interface classes are hard to support
|
||||
|
|
@ -131,13 +191,12 @@ module = [
|
|||
"pyftpdlib.*",
|
||||
"pytest_twisted",
|
||||
"robotexclusionrulesparser",
|
||||
"testfixtures",
|
||||
"zope.interface.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "2.16.0"
|
||||
current_version = "2.17.0"
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = "{new_version}"
|
||||
|
|
@ -269,7 +328,7 @@ markers = [
|
|||
"requires_uvloop: marks tests as only enabled when uvloop is known to be working",
|
||||
"requires_botocore: marks tests that need botocore (but not boto3)",
|
||||
"requires_boto3: marks tests that need botocore and boto3",
|
||||
"requires_mitmproxy: marks tests that need mitmproxy",
|
||||
"requires_mitmproxy: marks tests that need a mitmdump executable",
|
||||
"requires_internet: marks tests that need real Internet access",
|
||||
]
|
||||
filterwarnings = [
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.16.0
|
||||
2.17.0
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class AddonManager:
|
|||
|
||||
:param settings: The :class:`~scrapy.settings.BaseSettings` object from \
|
||||
which to read the early add-on configuration
|
||||
:type settings: :class:`~scrapy.settings.Settings`
|
||||
:type settings: :class:`~scrapy.settings.BaseSettings`
|
||||
"""
|
||||
for clspath in build_component_list(settings["ADDONS"]):
|
||||
addoncls = load_object(clspath)
|
||||
|
|
|
|||
|
|
@ -225,13 +225,11 @@ def _run_command(cmd: ScrapyCommand, args: list[str], opts: argparse.Namespace)
|
|||
def _run_command_profiled(
|
||||
cmd: ScrapyCommand, args: list[str], opts: argparse.Namespace
|
||||
) -> None:
|
||||
if opts.profile:
|
||||
sys.stderr.write(f"scrapy: writing cProfile stats to {opts.profile!r}\n")
|
||||
sys.stderr.write(f"scrapy: writing cProfile stats to {opts.profile!r}\n")
|
||||
loc = locals()
|
||||
p = cProfile.Profile()
|
||||
p.runctx("cmd.run(args, opts)", globals(), loc)
|
||||
if opts.profile:
|
||||
p.dump_stats(opts.profile)
|
||||
p.dump_stats(opts.profile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class ScrapyCommand(ABC):
|
|||
def long_desc(self) -> str:
|
||||
"""A long description of the command. Return short description when not
|
||||
available. It cannot contain newlines since contents will be formatted
|
||||
by optparser which removes newlines and wraps text.
|
||||
by argparse which removes newlines and wraps text.
|
||||
"""
|
||||
return self.short_desc()
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class Command(BaseRunSpiderCommand):
|
|||
return "[options] <spider>"
|
||||
|
||||
def short_desc(self) -> str:
|
||||
return "Run a spider"
|
||||
return "Run a spider of the current project, by name"
|
||||
|
||||
def run(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||
if len(args) < 1:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,29 @@
|
|||
import argparse
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, ClassVar
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.spiderloader import get_spider_loader
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
|
||||
|
||||
def _edit_file(editor: str, file_path: str | os.PathLike[str]) -> int:
|
||||
"""Open ``file_path`` with ``editor`` and return the editor exit code.
|
||||
|
||||
``editor`` may include arguments (e.g. ``"code -w"``); it is split with
|
||||
:func:`shlex.split` and the file is passed as a separate argument, so no
|
||||
shell is involved.
|
||||
"""
|
||||
return subprocess.call([*shlex.split(editor), os.fspath(file_path)]) # noqa: S603
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_project = True
|
||||
|
|
@ -45,4 +62,4 @@ class Command(ScrapyCommand):
|
|||
sfile = sys.modules[spidercls.__module__].__file__
|
||||
assert sfile
|
||||
sfile = sfile.replace(".pyc", ".py")
|
||||
self.exitcode = os.system(f'{editor} "{sfile}"') # noqa: S605
|
||||
self.exitcode = _edit_file(editor, Path(sfile))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import string
|
||||
from importlib import import_module
|
||||
|
|
@ -10,12 +9,14 @@ from urllib.parse import urlparse
|
|||
|
||||
import scrapy
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.commands.edit import _edit_file
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.spiderloader import get_spider_loader
|
||||
from scrapy.utils.template import render_templatefile, string_camelcase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
def sanitize_module_name(module_name: str) -> str:
|
||||
|
|
@ -31,10 +32,7 @@ def sanitize_module_name(module_name: str) -> str:
|
|||
|
||||
def extract_domain(url: str) -> str:
|
||||
"""Extract domain name from URL string"""
|
||||
o = urlparse(url)
|
||||
if o.scheme == "" and o.netloc == "":
|
||||
o = urlparse("//" + url.lstrip("/"))
|
||||
return o.netloc
|
||||
return urlparse(url).netloc
|
||||
|
||||
|
||||
def verify_url_scheme(url: str) -> str:
|
||||
|
|
@ -118,9 +116,11 @@ class Command(ScrapyCommand):
|
|||
|
||||
template_file = self._find_template(opts.template)
|
||||
if template_file:
|
||||
self._genspider(module, name, url, opts.template, template_file)
|
||||
spider_file = self._genspider(
|
||||
module, name, url, opts.template, template_file
|
||||
)
|
||||
if opts.edit:
|
||||
self.exitcode = os.system(f'scrapy edit "{name}"') # noqa: S605
|
||||
self.exitcode = _edit_file(self.settings["EDITOR"], spider_file)
|
||||
|
||||
def _generate_template_variables(
|
||||
self,
|
||||
|
|
@ -148,7 +148,7 @@ class Command(ScrapyCommand):
|
|||
url: str,
|
||||
template_name: str,
|
||||
template_file: str | os.PathLike[str],
|
||||
) -> None:
|
||||
) -> Path:
|
||||
"""Generate the spider module, based on the given template"""
|
||||
assert self.settings is not None
|
||||
tvars = self._generate_template_variables(module, name, url, template_name)
|
||||
|
|
@ -168,6 +168,7 @@ class Command(ScrapyCommand):
|
|||
)
|
||||
if spiders_module:
|
||||
print(f"in module:\n {spiders_module.__name__}.{module}")
|
||||
return Path(spider_file)
|
||||
|
||||
def _find_template(self, template: str) -> Path | None:
|
||||
template_file = Path(self.templates_dir, f"{template}.tmpl")
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class Command(BaseRunSpiderCommand):
|
|||
spider: Spider | None = None
|
||||
items: ClassVar[dict[int, list[Any]]] = {}
|
||||
requests: ClassVar[dict[int, list[Request]]] = {}
|
||||
spidercls: type[Spider] | None
|
||||
spidercls: type[Spider] | None = None
|
||||
|
||||
first_response = None
|
||||
|
||||
|
|
@ -346,6 +346,8 @@ class Command(BaseRunSpiderCommand):
|
|||
self.first_response = response
|
||||
|
||||
cb = self._get_callback(spider=spider, opts=opts, response=response)
|
||||
assert response.request
|
||||
response.request.callback = cb
|
||||
|
||||
# parse items and requests
|
||||
depth: int = response.meta["_depth"]
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class Command(BaseRunSpiderCommand):
|
|||
return "[options] <spider_file>"
|
||||
|
||||
def short_desc(self) -> str:
|
||||
return "Run a self-contained spider (without creating a project)"
|
||||
return "Run a spider from a Python file, no project required"
|
||||
|
||||
def long_desc(self) -> str:
|
||||
return "Run the spider defined in the given file"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,16 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class Contract:
|
||||
"""Abstract class for contracts"""
|
||||
"""Base class for :ref:`custom contracts <topics-contracts>`.
|
||||
|
||||
*method* is the callback function to which the contract is associated.
|
||||
|
||||
*args* is the list of arguments passed into the docstring, separated by
|
||||
whitespace.
|
||||
|
||||
Subclasses may override :meth:`adjust_request_args`, and define a
|
||||
``pre_process`` method or a ``post_process`` method, or both.
|
||||
"""
|
||||
|
||||
request_cls: type[Request] | None = None
|
||||
name: str
|
||||
|
|
@ -90,6 +99,13 @@ class Contract:
|
|||
return request
|
||||
|
||||
def adjust_request_args(self, args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Receive a ``dict`` with the default arguments for the sample request
|
||||
and return it, either unmodified or with changes.
|
||||
|
||||
:class:`~scrapy.Request` is used by default, but this can be changed
|
||||
with the ``request_cls`` attribute. If multiple contracts in the chain
|
||||
define this attribute, the last one is used.
|
||||
"""
|
||||
return args
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,15 @@ if TYPE_CHECKING:
|
|||
|
||||
# contracts
|
||||
class UrlContract(Contract):
|
||||
"""Contract to set the url of the request (mandatory)
|
||||
@url http://scrapy.org
|
||||
"""Sets (``@url``) the sample URL used when checking the other contract
|
||||
conditions of a callback.
|
||||
|
||||
This contract is mandatory: callbacks lacking it are ignored when running
|
||||
the checks.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
@url url
|
||||
"""
|
||||
|
||||
name = "url"
|
||||
|
|
@ -27,10 +34,14 @@ class UrlContract(Contract):
|
|||
|
||||
|
||||
class CallbackKeywordArgumentsContract(Contract):
|
||||
"""Contract to set the keyword arguments for the request.
|
||||
The value should be a JSON-encoded dictionary, e.g.:
|
||||
"""Sets (``@cb_kwargs``) the :attr:`cb_kwargs <scrapy.Request.cb_kwargs>`
|
||||
attribute of the sample request.
|
||||
|
||||
@cb_kwargs {"arg1": "some value"}
|
||||
Its value must be a valid JSON dictionary.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
@cb_kwargs {"arg1": "value1", "arg2": "value2", ...}
|
||||
"""
|
||||
|
||||
name = "cb_kwargs"
|
||||
|
|
@ -41,10 +52,14 @@ class CallbackKeywordArgumentsContract(Contract):
|
|||
|
||||
|
||||
class MetadataContract(Contract):
|
||||
"""Contract to set metadata arguments for the request.
|
||||
The value should be JSON-encoded dictionary, e.g.:
|
||||
"""Sets (``@meta``) the :attr:`meta <scrapy.Request.meta>` attribute of the
|
||||
sample request.
|
||||
|
||||
@meta {"arg1": "some value"}
|
||||
Its value must be a valid JSON dictionary.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
@meta {"arg1": "value1", "arg2": "value2", ...}
|
||||
"""
|
||||
|
||||
name = "meta"
|
||||
|
|
@ -55,16 +70,29 @@ class MetadataContract(Contract):
|
|||
|
||||
|
||||
class ReturnsContract(Contract):
|
||||
"""Contract to check the output of a callback
|
||||
"""Sets (``@returns``) lower and upper bounds for the items and requests
|
||||
returned by a callback.
|
||||
|
||||
general form:
|
||||
@returns request(s)/item(s) [min=1 [max]]
|
||||
The upper bound is optional:
|
||||
|
||||
e.g.:
|
||||
@returns request
|
||||
@returns request 2
|
||||
@returns request 2 10
|
||||
@returns request 0 10
|
||||
.. code-block:: none
|
||||
|
||||
@returns item(s)|request(s) [min [max]]
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
@returns request
|
||||
@returns request 2
|
||||
@returns request 2 10
|
||||
@returns request 0 10
|
||||
|
||||
Set both bounds to the same value to require an exact number:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
@returns request 2 2
|
||||
"""
|
||||
|
||||
name = "returns"
|
||||
|
|
@ -115,8 +143,12 @@ class ReturnsContract(Contract):
|
|||
|
||||
|
||||
class ScrapesContract(Contract):
|
||||
"""Contract to check presence of fields in scraped items
|
||||
@scrapes page_name page_body
|
||||
"""Checks (``@scrapes``) that all items returned by a callback have the
|
||||
specified fields.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
@scrapes field_1 field_2 ...
|
||||
"""
|
||||
|
||||
name = "scrapes"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ from scrapy.utils.defer import (
|
|||
deferred_from_coro,
|
||||
maybe_deferred_to_future,
|
||||
)
|
||||
from scrapy.utils.deprecate import warn_on_deprecated_spider_attribute
|
||||
from scrapy.utils.httpobj import urlparse_cached
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -80,22 +79,6 @@ class Slot:
|
|||
)
|
||||
|
||||
|
||||
def _get_concurrency_delay(
|
||||
concurrency: int, spider: Spider, settings: BaseSettings
|
||||
) -> tuple[int, float]:
|
||||
delay: float = settings.getfloat("DOWNLOAD_DELAY")
|
||||
if hasattr(spider, "download_delay"):
|
||||
delay = spider.download_delay
|
||||
|
||||
if hasattr(spider, "max_concurrent_requests"): # pragma: no cover
|
||||
warn_on_deprecated_spider_attribute(
|
||||
"max_concurrent_requests", "CONCURRENT_REQUESTS"
|
||||
)
|
||||
concurrency = spider.max_concurrent_requests
|
||||
|
||||
return concurrency, delay
|
||||
|
||||
|
||||
class Downloader:
|
||||
DOWNLOAD_SLOT = "download_slot"
|
||||
_SLOT_GC_INTERVAL: float = 60.0 # seconds
|
||||
|
|
@ -112,6 +95,9 @@ class Downloader:
|
|||
"CONCURRENT_REQUESTS_PER_DOMAIN"
|
||||
)
|
||||
self.ip_concurrency: int = self.settings.getint("CONCURRENT_REQUESTS_PER_IP")
|
||||
# Default delay of new slots. AutoThrottle overrides it to apply
|
||||
# AUTOTHROTTLE_START_DELAY.
|
||||
self._delay: float = self.settings.getfloat("DOWNLOAD_DELAY")
|
||||
self.randomize_delay: bool = self.settings.getbool("RANDOMIZE_DOWNLOAD_DELAY")
|
||||
self.middleware: DownloaderMiddlewareManager = (
|
||||
DownloaderMiddlewareManager.from_crawler(crawler)
|
||||
|
|
@ -138,7 +124,8 @@ class Downloader:
|
|||
self.active.remove(request)
|
||||
|
||||
def needs_backout(self) -> bool:
|
||||
return len(self.active) >= self.total_concurrency
|
||||
# A total concurrency of 0 means no limit.
|
||||
return 0 < self.total_concurrency <= len(self.active)
|
||||
|
||||
@_warn_spider_arg
|
||||
def _get_slot(
|
||||
|
|
@ -146,16 +133,11 @@ class Downloader:
|
|||
) -> tuple[str, Slot]:
|
||||
key = self.get_slot_key(request)
|
||||
if key not in self.slots:
|
||||
assert self.crawler.spider
|
||||
slot_settings = self.per_slot_settings.get(key, {})
|
||||
conc = self.ip_concurrency or self.domain_concurrency
|
||||
conc, delay = _get_concurrency_delay(
|
||||
conc, self.crawler.spider, self.settings
|
||||
)
|
||||
conc, delay = (
|
||||
slot_settings.get("concurrency", conc),
|
||||
slot_settings.get("delay", delay),
|
||||
conc = slot_settings.get(
|
||||
"concurrency", self.ip_concurrency or self.domain_concurrency
|
||||
)
|
||||
delay = slot_settings.get("delay", self._delay)
|
||||
randomize_delay = slot_settings.get("randomize_delay", self.randomize_delay)
|
||||
new_slot = Slot(conc, delay, randomize_delay)
|
||||
self.slots[key] = new_slot
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from twisted.internet.ssl import (
|
|||
from twisted.web.client import BrowserLikePolicyForHTTPS
|
||||
from twisted.web.iweb import IPolicyForHTTPS
|
||||
from zope.interface.declarations import implementer
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from scrapy.core.downloader.tls import (
|
||||
_TWISTED_VERSION_MAP,
|
||||
|
|
@ -47,8 +46,13 @@ class _ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
|
|||
instance.
|
||||
|
||||
The purpose of this custom class is to provide a ``creatorForNetloc()``
|
||||
method that returns a ``_ScrapyClientTLSOptions`` instance configured based
|
||||
on TLS settings provided to the factory.
|
||||
method that returns:
|
||||
|
||||
- a ``_ScrapyClientTLSOptions26`` or ``_ScrapyClientTLSOptions`` instance
|
||||
configured based on TLS settings provided to the factory (when the
|
||||
certificate verification is disabled);
|
||||
- a result of ``optionsForClientTLS()`` called with those TLS settings
|
||||
(when the certificate verification is enabled).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -227,7 +231,6 @@ class _AcceptableProtocolsContextFactory:
|
|||
# all of this with _ScrapyClientContextFactory.acceptableProtocols.
|
||||
|
||||
def __init__(self, context_factory: Any, acceptable_protocols: list[bytes]):
|
||||
verifyObject(IPolicyForHTTPS, context_factory)
|
||||
self._wrapped_context_factory: Any = context_factory
|
||||
self._acceptable_protocols: list[bytes] = acceptable_protocols
|
||||
|
||||
|
|
|
|||
|
|
@ -39,11 +39,23 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class DownloadHandlerProtocol(Protocol):
|
||||
"""Interface that :ref:`download handlers <topics-download-handlers>` must
|
||||
implement.
|
||||
|
||||
Besides implementing this protocol, the contract of a download handler
|
||||
includes **never** calling :meth:`crawler.engine.download_async()
|
||||
<scrapy.core.engine.ExecutionEngine.download_async>`.
|
||||
"""
|
||||
|
||||
lazy: bool
|
||||
"""Whether to delay instantiation of the handler; see :ref:`lazy
|
||||
<lazy-download-handlers>`."""
|
||||
|
||||
async def download_request(self, request: Request) -> Response: ...
|
||||
async def download_request(self, request: Request) -> Response:
|
||||
"""Download *request* and return a response."""
|
||||
|
||||
async def close(self) -> None: ...
|
||||
async def close(self) -> None:
|
||||
"""Clean up any resources used by the handler."""
|
||||
|
||||
|
||||
class DownloadHandlers:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ from ._base_streaming import BaseStreamingDownloadHandler, _BaseResponseArgs
|
|||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from httpcore import AsyncNetworkStream
|
||||
from httpcore2 import AsyncNetworkStream
|
||||
|
||||
from scrapy import Request
|
||||
from scrapy.crawler import Crawler
|
||||
|
|
@ -39,8 +39,11 @@ if TYPE_CHECKING:
|
|||
HAS_SOCKS = HAS_HTTP2 = False
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
try:
|
||||
import httpx2 as httpx
|
||||
except ImportError: # pragma: no cover
|
||||
import httpx # type: ignore[import-not-found,no-redef]
|
||||
except ImportError: # pragma: no cover
|
||||
httpx = None # type: ignore[assignment]
|
||||
else:
|
||||
# a small hack to avoid importing these optional extras unconditionally
|
||||
|
|
@ -84,19 +87,20 @@ class HttpxDownloadHandler(_Base):
|
|||
self._enable_h2: bool = crawler.settings.getbool("HTTPX_HTTP2_ENABLED")
|
||||
if self._enable_h2 and not HAS_HTTP2: # pragma: no cover
|
||||
raise NotConfigured(
|
||||
f"HTTP/2 support in {type(self).__name__} requires the 'httpx[http2]' extra to be installed."
|
||||
f"HTTP/2 support in {type(self).__name__} requires the 'httpx2[http2]' extra to be installed."
|
||||
)
|
||||
self._ssl_context: ssl.SSLContext = _make_ssl_context(crawler.settings)
|
||||
self._bind_host: str | None = self._get_bind_address_host()
|
||||
self._limits: httpx.Limits = httpx.Limits(
|
||||
# hard limit on simultaneous connections
|
||||
max_connections=self._pool_size_total,
|
||||
# hard limit on simultaneous connections (None for no limit, which
|
||||
# is what a CONCURRENT_REQUESTS of 0 means)
|
||||
max_connections=self._pool_size_total or None,
|
||||
# total number of idle connections in the pool (extra ones are closed)
|
||||
max_keepalive_connections=self._pool_size_total,
|
||||
max_keepalive_connections=self._pool_size_total or None,
|
||||
)
|
||||
|
||||
self._default_client: httpx.AsyncClient = self._make_client()
|
||||
# httpx doesn't support per-request proxies: https://github.com/encode/httpx/discussions/3183,
|
||||
# httpx2 doesn't support per-request proxies: https://github.com/pydantic/httpx2/issues/818,
|
||||
# so we keep a pool of clients per proxy URL. LRU eviction can be added here if needed.
|
||||
self._proxy_clients: dict[str, httpx.AsyncClient] = {}
|
||||
|
||||
|
|
@ -104,7 +108,7 @@ class HttpxDownloadHandler(_Base):
|
|||
def _check_deps_installed() -> None:
|
||||
if httpx is None: # pragma: no cover
|
||||
raise NotConfigured(
|
||||
"HttpxDownloadHandler requires the httpx library to be installed."
|
||||
"HttpxDownloadHandler requires the httpx2 library to be installed."
|
||||
)
|
||||
|
||||
def _make_client(self, proxy_url: str | None = None) -> httpx.AsyncClient:
|
||||
|
|
@ -128,7 +132,7 @@ class HttpxDownloadHandler(_Base):
|
|||
proxy=proxy,
|
||||
),
|
||||
)
|
||||
# https://github.com/encode/httpx/discussions/1566
|
||||
# https://github.com/pydantic/httpx2/issues/368
|
||||
for header_name in ("accept", "accept-encoding", "user-agent"):
|
||||
client.headers.pop(header_name, None)
|
||||
return client
|
||||
|
|
@ -149,7 +153,7 @@ class HttpxDownloadHandler(_Base):
|
|||
proxy = self._extract_proxy_url_with_creds(request)
|
||||
if proxy and proxy.startswith("socks") and not HAS_SOCKS: # pragma: no cover
|
||||
raise ValueError(
|
||||
f"SOCKS proxy support in {type(self).__name__} requires the 'httpx[socks]' extra to be installed."
|
||||
f"SOCKS proxy support in {type(self).__name__} requires the 'httpx2[socks]' extra to be installed."
|
||||
)
|
||||
client = self._get_client(proxy)
|
||||
headers = self._request_headers(request).to_tuple_list()
|
||||
|
|
@ -175,7 +179,7 @@ class HttpxDownloadHandler(_Base):
|
|||
raise DownloadConnectionRefusedError(str(e)) from e
|
||||
except httpx.ProxyError as e:
|
||||
raise DownloadConnectionRefusedError(str(e)) from e
|
||||
except DOWNLOAD_FAILED_EXCEPTIONS as e:
|
||||
except DOWNLOAD_FAILED_EXCEPTIONS as e: # pylint: disable=catching-non-exception
|
||||
raise DownloadFailedError(str(e)) from e
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -218,7 +222,7 @@ class HttpxDownloadHandler(_Base):
|
|||
def _log_tls_info(self, response: httpx.Response, request: Request) -> None:
|
||||
network_stream: AsyncNetworkStream = response.extensions["network_stream"]
|
||||
extra_ssl_object = network_stream.get_extra_info("ssl_object")
|
||||
if isinstance(extra_ssl_object, ssl.SSLObject):
|
||||
if isinstance(extra_ssl_object, ssl.SSLObject): # pragma: no branch
|
||||
_log_sslobj_debug_info(extra_ssl_object)
|
||||
|
||||
async def close(self) -> None:
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
An asynchronous FTP file download handler for scrapy which somehow emulates an http response.
|
||||
|
||||
FTP connection parameters are passed using the request meta field:
|
||||
- ftp_user (required)
|
||||
- ftp_password (required)
|
||||
- ftp_passive (by default, enabled) sets FTP connection passive mode
|
||||
- ftp_user (optional, falls back to FTP_USER)
|
||||
- ftp_password (optional, falls back to FTP_PASSWORD)
|
||||
- ftp_passive (optional, falls back to FTP_PASSIVE_MODE) sets FTP connection passive mode
|
||||
- ftp_local_filename
|
||||
- If not given, file data will come in the response.body, as a normal scrapy Response,
|
||||
which will imply that the entire file will be on memory.
|
||||
|
|
@ -126,5 +126,4 @@ class FTPDownloadHandler(BaseDownloadHandler):
|
|||
headers = {"local filename": protocol.filename or b"", "size": protocol.size}
|
||||
body = protocol.filename or protocol.body.read()
|
||||
respcls = responsetypes.from_args(url=request.url, body=body)
|
||||
# hints for Headers-related types may need to be fixed to not use AnyStr
|
||||
return respcls(url=request.url, status=200, body=body, headers=headers) # type: ignore[arg-type]
|
||||
return respcls(url=request.url, status=200, body=body, headers=headers)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,6 @@ class HTTP11DownloadHandler(BaseHttpDownloadHandler):
|
|||
self._disconnect_timeout: int = 1
|
||||
|
||||
async def download_request(self, request: Request) -> Response:
|
||||
"""Return a deferred for the HTTP download"""
|
||||
if hasattr(self._crawler.spider, "download_maxsize"): # pragma: no cover
|
||||
warn_on_deprecated_spider_attribute("download_maxsize", "DOWNLOAD_MAXSIZE")
|
||||
if hasattr(self._crawler.spider, "download_warnsize"): # pragma: no cover
|
||||
|
|
@ -283,7 +282,7 @@ def _tunnel_request_data(
|
|||
|
||||
|
||||
class _TunnelingAgent(Agent):
|
||||
"""An agent that uses a L{TunnelingTCP4ClientEndpoint} to make HTTPS
|
||||
"""An agent that uses a ``_TunnelingTCP4ClientEndpoint`` to make HTTPS
|
||||
downloads. It may look strange that we have chosen to subclass Agent and not
|
||||
ProxyAgent but consider that after the tunnel is opened the proxy is
|
||||
transparent to the client; thus the agent should behave like there is no
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from scrapy.core.downloader.handlers.base import BaseDownloadHandler
|
||||
from scrapy.exceptions import NotConfigured
|
||||
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
|
||||
from scrapy.utils.boto import is_botocore_available
|
||||
from scrapy.utils.httpobj import urlparse_cached
|
||||
from scrapy.utils.misc import build_from_crawler, load_object
|
||||
|
|
@ -49,7 +50,16 @@ class S3DownloadHandler(BaseDownloadHandler):
|
|||
|
||||
async def download_request(self, request: Request) -> Response:
|
||||
p = urlparse_cached(request)
|
||||
scheme = "https" if request.meta.get("is_secure") else "http"
|
||||
if request.meta.get("is_secure") is False:
|
||||
warnings.warn(
|
||||
"Passing is_secure=False for s3:// requests is deprecated."
|
||||
" In future Scrapy releases this flag will be ignored.",
|
||||
ScrapyDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
scheme = "http"
|
||||
else:
|
||||
scheme = "https"
|
||||
bucket = p.hostname
|
||||
path = p.path + "?" + p.query if p.query else p.path
|
||||
url = f"{scheme}://{bucket}.s3.amazonaws.com{path}"
|
||||
|
|
|
|||
|
|
@ -470,16 +470,17 @@ class ExecutionEngine:
|
|||
"""
|
||||
if self.spider is None:
|
||||
raise RuntimeError(f"No open spider to crawl: {request}")
|
||||
try:
|
||||
response_or_request = await maybe_deferred_to_future(
|
||||
self._download(request)
|
||||
)
|
||||
finally:
|
||||
assert self._slot is not None
|
||||
self._slot.remove_request(request)
|
||||
if isinstance(response_or_request, Request):
|
||||
return await self.download_async(response_or_request)
|
||||
return response_or_request
|
||||
while True:
|
||||
try:
|
||||
response_or_request = await maybe_deferred_to_future(
|
||||
self._download(request)
|
||||
)
|
||||
finally:
|
||||
assert self._slot is not None
|
||||
self._slot.remove_request(request)
|
||||
if not isinstance(response_or_request, Request):
|
||||
return response_or_request
|
||||
request = response_or_request
|
||||
|
||||
@inlineCallbacks
|
||||
def _download(
|
||||
|
|
|
|||
|
|
@ -114,11 +114,7 @@ class H2ConnectionPool:
|
|||
d.errback(ResponseFailed(errors))
|
||||
|
||||
def close_connections(self) -> None:
|
||||
"""Close all the HTTP/2 connections and remove them from pool
|
||||
|
||||
Returns:
|
||||
Deferred that fires when all connections have been closed
|
||||
"""
|
||||
"""Close all the HTTP/2 connections and remove them from pool."""
|
||||
for conn in self._connections.values():
|
||||
assert conn.transport is not None # typing
|
||||
conn.transport.abortConnection()
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class H2ClientProtocol(Protocol, TimeoutMixin):
|
|||
uri is used to verify that incoming client requests have correct
|
||||
base URL.
|
||||
settings -- Scrapy project settings
|
||||
conn_lost_deferred -- Deferred fires with the reason: Failure to notify
|
||||
conn_lost_deferred -- Deferred that fires with the list of underlying exceptions to notify
|
||||
that connection was lost
|
||||
tls_verbose_logging -- Whether to log TLS details
|
||||
"""
|
||||
|
|
@ -375,7 +375,7 @@ class H2ClientProtocol(Protocol, TimeoutMixin):
|
|||
|
||||
def _handle_events(self, events: list[Event]) -> None:
|
||||
"""Private method which acts as a bridge between the events
|
||||
received from the HTTP/2 data and IH2EventsHandler
|
||||
received from the HTTP/2 data and the handlers in this class.
|
||||
|
||||
Arguments:
|
||||
events -- A list of events that the remote peer triggered by sending data
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ class Stream:
|
|||
0, self.metadata["remaining_content_length"]
|
||||
)
|
||||
|
||||
# End the stream if no more data needs to be send
|
||||
# End the stream if no more data needs to be sent
|
||||
if self.metadata["remaining_content_length"] == 0:
|
||||
self._protocol.conn.end_stream(self.stream_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -131,10 +131,10 @@ class Scheduler(BaseScheduler):
|
|||
(:setting:`SCHEDULER_PRIORITY_QUEUE`) that sort requests by
|
||||
:attr:`~scrapy.http.Request.priority`.
|
||||
|
||||
By default, a single, memory-based priority queue is used for all requests.
|
||||
When using :setting:`JOBDIR`, a disk-based priority queue is also created,
|
||||
By default, memory-based priority queues are used for all requests.
|
||||
When using :setting:`JOBDIR`, disk-based priority queues are also created,
|
||||
and only unserializable requests are stored in the memory-based priority
|
||||
queue. For a given priority value, requests in memory take precedence over
|
||||
queues. For a given priority value, requests in memory take precedence over
|
||||
requests in disk.
|
||||
|
||||
Each priority queue stores requests in separate internal queues, one per
|
||||
|
|
@ -209,8 +209,8 @@ class Scheduler(BaseScheduler):
|
|||
-------------------------
|
||||
|
||||
While pending requests are below the configured values of
|
||||
:setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`
|
||||
or :setting:`CONCURRENT_REQUESTS_PER_IP`, those requests are sent
|
||||
:setting:`CONCURRENT_REQUESTS` or
|
||||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN`, those requests are sent
|
||||
concurrently.
|
||||
|
||||
As a result, the first few requests of a crawl may not follow the desired
|
||||
|
|
@ -289,11 +289,11 @@ class Scheduler(BaseScheduler):
|
|||
|
||||
:param dqclass: A class to be used as persistent request queue.
|
||||
The value for the :setting:`SCHEDULER_DISK_QUEUE` setting is used by default.
|
||||
:type dqclass: class
|
||||
:type dqclass: type
|
||||
|
||||
:param mqclass: A class to be used as non-persistent request queue.
|
||||
The value for the :setting:`SCHEDULER_MEMORY_QUEUE` setting is used by default.
|
||||
:type mqclass: class
|
||||
:type mqclass: type
|
||||
|
||||
:param logunser: A boolean that indicates whether or not unserializable requests should be logged.
|
||||
The value for the :setting:`SCHEDULER_DEBUG` setting is used by default.
|
||||
|
|
@ -306,7 +306,7 @@ class Scheduler(BaseScheduler):
|
|||
|
||||
:param pqclass: A class to be used as priority queue for requests.
|
||||
The value for the :setting:`SCHEDULER_PRIORITY_QUEUE` setting is used by default.
|
||||
:type pqclass: class
|
||||
:type pqclass: type
|
||||
|
||||
:param crawler: The crawler object corresponding to the current crawl.
|
||||
:type crawler: :class:`scrapy.crawler.Crawler`
|
||||
|
|
@ -342,7 +342,7 @@ class Scheduler(BaseScheduler):
|
|||
def open(self, spider: Spider) -> Deferred[None] | None:
|
||||
"""
|
||||
(1) initialize the memory queue
|
||||
(2) initialize the disk queue if the ``jobdir`` attribute is a valid directory
|
||||
(2) initialize the disk queue if the ``jobdir`` argument wasn't empty
|
||||
(3) return the result of the dupefilter's ``open`` method
|
||||
"""
|
||||
self.spider: Spider = spider
|
||||
|
|
@ -366,8 +366,8 @@ class Scheduler(BaseScheduler):
|
|||
Unless the received request is filtered out by the Dupefilter, attempt to push
|
||||
it into the disk queue, falling back to pushing it into the memory queue.
|
||||
|
||||
Increment the appropriate stats, such as: ``scheduler/enqueued``,
|
||||
``scheduler/enqueued/disk``, ``scheduler/enqueued/memory``.
|
||||
Increment the appropriate stats, such as: :stat:`scheduler/enqueued`,
|
||||
:stat:`scheduler/enqueued/disk`, :stat:`scheduler/enqueued/memory`.
|
||||
|
||||
Return ``True`` if the request was stored successfully, ``False`` otherwise.
|
||||
"""
|
||||
|
|
@ -390,8 +390,8 @@ class Scheduler(BaseScheduler):
|
|||
falling back to the disk queue if the memory queue is empty.
|
||||
Return ``None`` if there are no more enqueued requests.
|
||||
|
||||
Increment the appropriate stats, such as: ``scheduler/dequeued``,
|
||||
``scheduler/dequeued/disk``, ``scheduler/dequeued/memory``.
|
||||
Increment the appropriate stats, such as: :stat:`scheduler/dequeued`,
|
||||
:stat:`scheduler/dequeued/disk`, :stat:`scheduler/dequeued/memory`.
|
||||
"""
|
||||
request: Request | None = self.mqs.pop()
|
||||
assert self.stats is not None
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ class Scraper:
|
|||
self, output: Any, response: Response | Failure
|
||||
) -> Deferred[None]:
|
||||
"""Process each Request/Item (given in the output parameter) returned
|
||||
from the given spider.
|
||||
from the spider.
|
||||
|
||||
Items are sent to the item pipelines, requests are scheduled.
|
||||
"""
|
||||
|
|
@ -451,7 +451,7 @@ class Scraper:
|
|||
self, output: Any, response: Response | Failure
|
||||
) -> None:
|
||||
"""Process each Request/Item (given in the output parameter) returned
|
||||
from the given spider.
|
||||
from the spider.
|
||||
|
||||
Items are sent to the item pipelines, requests are scheduled.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -40,7 +40,11 @@ from scrapy.utils.reactor import (
|
|||
verify_installed_asyncio_event_loop,
|
||||
verify_installed_reactor,
|
||||
)
|
||||
from scrapy.utils.reactorless import install_reactor_import_hook
|
||||
from scrapy.utils.reactorless import (
|
||||
ReactorImportHook,
|
||||
install_reactor_import_hook,
|
||||
uninstall_reactor_import_hook,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Awaitable, Generator, Iterable
|
||||
|
|
@ -97,6 +101,10 @@ class Crawler:
|
|||
return
|
||||
|
||||
self.addons.load_settings(self.settings)
|
||||
self._apply_deprecated_spider_attr("download_delay", "DOWNLOAD_DELAY")
|
||||
self._apply_deprecated_spider_attr(
|
||||
"max_concurrent_requests", "CONCURRENT_REQUESTS_PER_DOMAIN"
|
||||
)
|
||||
self.stats = load_object(self.settings["STATS_CLASS"])(self)
|
||||
|
||||
lf_cls: type[LogFormatter] = load_object(self.settings["LOG_FORMATTER"])
|
||||
|
|
@ -175,6 +183,30 @@ class Crawler:
|
|||
f"on the command line) to use the spider's reactor."
|
||||
) from None
|
||||
|
||||
def _apply_deprecated_spider_attr(self, attr: str, setting: str) -> None:
|
||||
"""Bridge a deprecated spider attribute onto *setting*, warning about
|
||||
the deprecation (and about being ignored when *setting* is already set
|
||||
at spider or higher priority)."""
|
||||
spider = self.spider if self.spider is not None else self.spidercls
|
||||
if not hasattr(spider, attr):
|
||||
return
|
||||
if (self.settings.getpriority(setting) or 0) >= SETTINGS_PRIORITIES["spider"]:
|
||||
warnings.warn(
|
||||
f"The {attr!r} spider attribute is deprecated. It is also being "
|
||||
f"ignored because {setting} is already set at spider or higher "
|
||||
f"priority. Remove the {attr!r} attribute from your spider.",
|
||||
category=ScrapyDeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return
|
||||
warnings.warn(
|
||||
f"The {attr!r} spider attribute is deprecated. Use the {setting} "
|
||||
f"setting instead.",
|
||||
category=ScrapyDeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
self.settings.set(setting, getattr(spider, attr), priority="spider")
|
||||
|
||||
def _apply_reactorless_default_settings(self) -> None:
|
||||
"""Change some setting defaults when not using a Twisted reactor.
|
||||
|
||||
|
|
@ -555,8 +587,8 @@ class AsyncCrawlerRunner(CrawlerRunnerBase):
|
|||
"""
|
||||
Run a crawler with the provided arguments.
|
||||
|
||||
It will call the given Crawler's :meth:`~Crawler.crawl` method, while
|
||||
keeping track of it so it can be stopped later.
|
||||
It will call the given Crawler's :meth:`~Crawler.crawl_async` method,
|
||||
while keeping track of it so it can be stopped later.
|
||||
|
||||
If ``crawler_or_spidercls`` isn't a :class:`~scrapy.crawler.Crawler`
|
||||
instance, this method will try to create one using this parameter as
|
||||
|
|
@ -797,7 +829,7 @@ class CrawlerProcess(CrawlerProcessBase, CrawlerRunner):
|
|||
"""
|
||||
This method starts a :mod:`~twisted.internet.reactor`, adjusts its pool
|
||||
size to :setting:`REACTOR_THREADPOOL_MAXSIZE`, and installs a DNS
|
||||
resolver based on :setting:`DNSCACHE_ENABLED`.
|
||||
resolver based on :setting:`TWISTED_DNS_RESOLVER`.
|
||||
|
||||
If ``stop_after_crawl`` is True, the reactor will be stopped after all
|
||||
crawlers have finished, using :meth:`join`.
|
||||
|
|
@ -861,6 +893,7 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
|
|||
super().__init__(settings, install_root_handler)
|
||||
logger.debug("Using AsyncCrawlerProcess")
|
||||
self._reactorless_loop: asyncio.AbstractEventLoop | None = None
|
||||
self._reactor_import_hook: ReactorImportHook | None = None
|
||||
# We want the asyncio event loop to be installed early, so that it's
|
||||
# always the correct one. And as we do that, we can also install the
|
||||
# reactor here.
|
||||
|
|
@ -873,7 +906,7 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
|
|||
"TWISTED_REACTOR_ENABLED is False but a Twisted reactor is installed."
|
||||
)
|
||||
self._reactorless_loop = set_asyncio_event_loop(loop_path)
|
||||
install_reactor_import_hook()
|
||||
self._reactor_import_hook = install_reactor_import_hook()
|
||||
elif is_reactor_installed():
|
||||
# The user could install a reactor before this class is instantiated.
|
||||
# We need to make sure the reactor is the correct one and the loop
|
||||
|
|
@ -899,10 +932,10 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
|
|||
|
||||
When using a reactor it adjusts its pool size to
|
||||
:setting:`REACTOR_THREADPOOL_MAXSIZE` and installs a DNS resolver based
|
||||
on :setting:`DNSCACHE_ENABLED`.
|
||||
on :setting:`TWISTED_DNS_RESOLVER`.
|
||||
|
||||
If ``stop_after_crawl`` is True, the reactor will be stopped after all
|
||||
crawlers have finished, using :meth:`join`.
|
||||
If ``stop_after_crawl`` is True, the reactor/event loop will be stopped
|
||||
after all crawlers have finished, using :meth:`join`.
|
||||
|
||||
:param bool stop_after_crawl: stop or not the reactor when all
|
||||
crawlers have finished
|
||||
|
|
@ -1003,6 +1036,10 @@ class AsyncCrawlerProcess(CrawlerProcessBase, AsyncCrawlerRunner):
|
|||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
loop.run_until_complete(loop.shutdown_default_executor())
|
||||
finally:
|
||||
# loop.close() can raise, so we uninstall the hook first
|
||||
if self._reactor_import_hook: # pragma: no branch
|
||||
uninstall_reactor_import_hook(self._reactor_import_hook)
|
||||
self._reactor_import_hook = None
|
||||
self._reactorless_main_task = None
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from scrapy.http.cookies import CookieJar
|
|||
from scrapy.utils.decorators import _warn_spider_arg
|
||||
from scrapy.utils.httpobj import urlparse_cached
|
||||
from scrapy.utils.python import to_unicode
|
||||
from scrapy.utils.request import _decode_cookie, _to_verbose_cookies
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
|
@ -134,29 +135,10 @@ class CookiesMiddleware:
|
|||
Given a dict consisting of cookie components, return its string representation.
|
||||
Decode from bytes if necessary.
|
||||
"""
|
||||
decoded = {}
|
||||
decoded = _decode_cookie(cookie, request)
|
||||
if decoded is None:
|
||||
return None
|
||||
flags = set()
|
||||
for key in ("name", "value", "path", "domain"):
|
||||
value = cookie.get(key)
|
||||
if value is None:
|
||||
if key in {"name", "value"}:
|
||||
msg = f"Invalid cookie found in request {request}: {cookie} ('{key}' is missing)"
|
||||
logger.warning(msg)
|
||||
return None
|
||||
continue
|
||||
if isinstance(value, (bool, float, int, str)):
|
||||
decoded[key] = str(value)
|
||||
else:
|
||||
assert isinstance(value, bytes)
|
||||
try:
|
||||
decoded[key] = value.decode("utf8")
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(
|
||||
"Non UTF-8 encoded cookie found in request %s: %s",
|
||||
request,
|
||||
cookie,
|
||||
)
|
||||
decoded[key] = value.decode("latin1", errors="replace")
|
||||
for flag in ("secure",):
|
||||
value = cookie.get(flag, _UNSET)
|
||||
if value is _UNSET or not value:
|
||||
|
|
@ -177,11 +159,7 @@ class CookiesMiddleware:
|
|||
"""
|
||||
if not request.cookies:
|
||||
return ()
|
||||
cookies: Iterable[VerboseCookie]
|
||||
if isinstance(request.cookies, dict):
|
||||
cookies = tuple({"name": k, "value": v} for k, v in request.cookies.items())
|
||||
else:
|
||||
cookies = request.cookies
|
||||
cookies: Iterable[VerboseCookie] = _to_verbose_cookies(request.cookies)
|
||||
for cookie in cookies:
|
||||
cookie.setdefault("secure", urlparse_cached(request).scheme == "https")
|
||||
formatted = filter(None, (self._format_cookie(c, request) for c in cookies))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from email.utils import formatdate
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
|
@ -28,6 +29,9 @@ if TYPE_CHECKING:
|
|||
from scrapy.statscollectors import StatsCollector
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpCacheMiddleware:
|
||||
DOWNLOAD_EXCEPTIONS = (
|
||||
ConnectionDone,
|
||||
|
|
@ -77,9 +81,20 @@ class HttpCacheMiddleware:
|
|||
return None
|
||||
|
||||
# Look for cached response and check if expired
|
||||
cachedresponse: Response | None = self.storage.retrieve_response(
|
||||
self.crawler.spider, request
|
||||
)
|
||||
cachedresponse: Response | None
|
||||
try:
|
||||
cachedresponse = self.storage.retrieve_response(
|
||||
self.crawler.spider, request
|
||||
)
|
||||
except Exception:
|
||||
self.stats.inc_value("httpcache/retrieve_error")
|
||||
logger.warning(
|
||||
f"Could not read the cache entry for {request}, treating it as a "
|
||||
f"cache miss.",
|
||||
exc_info=True,
|
||||
extra={"spider": self.crawler.spider},
|
||||
)
|
||||
cachedresponse = None
|
||||
if cachedresponse is None:
|
||||
self.stats.inc_value("httpcache/miss")
|
||||
if self.ignore_missing:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from importlib.util import find_spec
|
||||
from itertools import chain
|
||||
from logging import getLogger
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
|
@ -51,16 +52,12 @@ else:
|
|||
else:
|
||||
ACCEPTED_ENCODINGS.append(b"br")
|
||||
|
||||
try:
|
||||
import zstandard # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
if find_spec("zstandard") is not None:
|
||||
ACCEPTED_ENCODINGS.append(b"zstd")
|
||||
|
||||
|
||||
class HttpCompressionMiddleware:
|
||||
"""This middleware allows compressed (gzip, deflate) traffic to be
|
||||
"""This middleware allows compressed (gzip, deflate etc.) traffic to be
|
||||
sent/received from websites"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -123,12 +120,14 @@ class HttpCompressionMiddleware:
|
|||
response.body, content_encoding, max_size
|
||||
)
|
||||
except _DecompressionMaxSizeExceeded as e:
|
||||
raise IgnoreRequest(
|
||||
msg = (
|
||||
f"Ignored response {response} because its body "
|
||||
f"({len(response.body)} B compressed, "
|
||||
f"{e.decompressed_size} B decompressed so far) exceeded "
|
||||
f"DOWNLOAD_MAXSIZE ({max_size} B) during decompression."
|
||||
) from e
|
||||
)
|
||||
logger.warning(msg)
|
||||
raise IgnoreRequest(msg) from e
|
||||
if len(response.body) < warn_size <= len(decoded_body):
|
||||
logger.warning(
|
||||
f"{response} body size after decompression "
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class OffsiteMiddleware:
|
|||
)
|
||||
self.stats.inc_value("offsite/domains")
|
||||
self.stats.inc_value("offsite/filtered")
|
||||
raise IgnoreRequest
|
||||
raise IgnoreRequest(f"Filtered offsite request to {domain!r}")
|
||||
|
||||
def should_follow(self, request: Request, spider: Spider) -> bool:
|
||||
regex = self.host_regex
|
||||
|
|
|
|||
|
|
@ -196,10 +196,7 @@ class BaseRedirectMiddleware:
|
|||
|
||||
|
||||
class RedirectMiddleware(BaseRedirectMiddleware):
|
||||
"""
|
||||
Handle redirection of requests based on response status
|
||||
and meta-refresh html tag.
|
||||
"""
|
||||
"""Handle redirection of requests based on response status."""
|
||||
|
||||
@_warn_spider_arg
|
||||
def process_response(
|
||||
|
|
@ -251,6 +248,8 @@ class RedirectMiddleware(BaseRedirectMiddleware):
|
|||
|
||||
|
||||
class MetaRefreshMiddleware(BaseRedirectMiddleware):
|
||||
"""Handle redirection of requests based on meta-refresh html tag."""
|
||||
|
||||
enabled_setting = "METAREFRESH_ENABLED"
|
||||
|
||||
def __init__(self, settings: BaseSettings):
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ problems such as a connection timeout or HTTP 500 error.
|
|||
You can change the behaviour of this middleware by modifying the scraping settings:
|
||||
RETRY_TIMES - how many times to retry a failed page
|
||||
RETRY_HTTP_CODES - which HTTP response codes to retry
|
||||
|
||||
Failed pages are collected on the scraping process and rescheduled at the end,
|
||||
once the spider has finished crawling all regular (non-failed) pages.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -17,7 +14,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from scrapy.exceptions import NotConfigured
|
||||
from scrapy.utils.decorators import _warn_spider_arg
|
||||
from scrapy.utils.misc import load_object
|
||||
from scrapy.utils.misc import _load_objects
|
||||
from scrapy.utils.python import global_object_name
|
||||
from scrapy.utils.response import response_status_message
|
||||
|
||||
|
|
@ -70,8 +67,9 @@ def get_retry_request(
|
|||
and :ref:`stats <topics-stats>`, and to provide extra logging context (see
|
||||
:func:`logging.debug`).
|
||||
|
||||
*reason* is a string or an :class:`Exception` object that indicates the
|
||||
reason why the request needs to be retried. It is used to name retry stats.
|
||||
*reason* is a string, an :class:`Exception` subclass or an
|
||||
:class:`Exception` object that indicates the reason why the request needs
|
||||
to be retried. It is used to name retry stats.
|
||||
|
||||
*max_retry_times* is a number that determines the maximum number of times
|
||||
that *request* can be retried. If not specified or ``None``, the number is
|
||||
|
|
@ -89,6 +87,9 @@ def get_retry_request(
|
|||
message logged when a request exceeds its retries. See
|
||||
:setting:`RETRY_GIVE_UP_LOG_LEVEL` for details.
|
||||
|
||||
.. versionadded:: 2.17.0
|
||||
The *give_up_log_level* parameter.
|
||||
|
||||
*stats_base_key* is a string to be used as the base key for the
|
||||
retry-related job stats
|
||||
"""
|
||||
|
|
@ -148,10 +149,7 @@ class RetryMiddleware:
|
|||
self.retry_http_codes = {int(x) for x in settings.getlist("RETRY_HTTP_CODES")}
|
||||
self.priority_adjust = settings.getint("RETRY_PRIORITY_ADJUST")
|
||||
self.give_up_log_level = settings["RETRY_GIVE_UP_LOG_LEVEL"]
|
||||
self.exceptions_to_retry = tuple(
|
||||
load_object(x) if isinstance(x, str) else x
|
||||
for x in settings.getlist("RETRY_EXCEPTIONS")
|
||||
)
|
||||
self.exceptions_to_retry = _load_objects(settings.getlist("RETRY_EXCEPTIONS"))
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from twisted.internet.defer import Deferred
|
||||
|
||||
from scrapy import signals
|
||||
from scrapy.exceptions import IgnoreRequest, NotConfigured
|
||||
from scrapy.http import Request, Response
|
||||
from scrapy.http.request import NO_CALLBACK
|
||||
|
|
@ -98,7 +99,7 @@ class RobotsTxtMiddleware:
|
|||
assert self.crawler.stats
|
||||
try:
|
||||
resp = await self.crawler.engine.download_async(robotsreq)
|
||||
self._parse_robots(resp, netloc)
|
||||
await self._parse_robots(resp, netloc, request)
|
||||
except Exception as e:
|
||||
if not isinstance(e, IgnoreRequest):
|
||||
logger.error(
|
||||
|
|
@ -115,13 +116,20 @@ class RobotsTxtMiddleware:
|
|||
return await maybe_deferred_to_future(parser)
|
||||
return parser
|
||||
|
||||
def _parse_robots(self, response: Response, netloc: str) -> None:
|
||||
async def _parse_robots(
|
||||
self, response: Response, netloc: str, request: Request
|
||||
) -> None:
|
||||
assert self.crawler.stats
|
||||
self.crawler.stats.inc_value("robotstxt/response_count")
|
||||
self.crawler.stats.inc_value(
|
||||
f"robotstxt/response_status_count/{response.status}"
|
||||
)
|
||||
rp = self._parserimpl.from_crawler(self.crawler, response.body)
|
||||
await self.crawler.signals.send_catch_log_async(
|
||||
signal=signals.robots_parsed,
|
||||
robotparser=rp,
|
||||
request=request,
|
||||
)
|
||||
rp_dfd = self._parsers[netloc]
|
||||
assert isinstance(rp_dfd, Deferred)
|
||||
self._parsers[netloc] = rp
|
||||
|
|
|
|||
|
|
@ -16,7 +16,15 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class NotConfigured(Exception):
|
||||
"""Indicates a missing configuration situation"""
|
||||
"""Raised by a :ref:`component <topics-components>` from its ``__init__()``
|
||||
or :meth:`from_crawler` method to indicate that it will remain disabled.
|
||||
|
||||
Only the following components can be disabled this way:
|
||||
|
||||
- :ref:`Downloader middlewares <topics-downloader-middleware>`
|
||||
- :ref:`Extensions <topics-extensions>`
|
||||
- :ref:`Item pipelines <topics-item-pipeline>`
|
||||
- :ref:`Spider middlewares <topics-spider-middleware>`"""
|
||||
|
||||
|
||||
class _InvalidOutput(TypeError):
|
||||
|
|
@ -30,15 +38,37 @@ class _InvalidOutput(TypeError):
|
|||
|
||||
|
||||
class IgnoreRequest(Exception):
|
||||
"""Indicates a decision was made not to process a request"""
|
||||
"""Raised to indicate that a request should be ignored.
|
||||
|
||||
A :ref:`downloader middleware <topics-downloader-middleware>` can raise it
|
||||
from its
|
||||
:meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_request`
|
||||
or
|
||||
:meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_response`
|
||||
method to drop a request, and a :signal:`request_scheduled` signal handler
|
||||
can raise it to drop a request before it reaches the
|
||||
:ref:`scheduler <topics-scheduler>`."""
|
||||
|
||||
|
||||
class DontCloseSpider(Exception):
|
||||
"""Request the spider not to be closed yet"""
|
||||
"""Raised in a :signal:`spider_idle` signal handler to prevent the spider
|
||||
from being closed."""
|
||||
|
||||
|
||||
class CloseSpider(Exception):
|
||||
"""Raise this from callbacks to request the spider to be closed"""
|
||||
"""Raised from a :ref:`spider callback <topics-spiders>` to request the
|
||||
spider to be closed/stopped.
|
||||
|
||||
*reason* is a string with the reason for closing.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def parse_page(self, response):
|
||||
if "Bandwidth exceeded" in response.text:
|
||||
raise CloseSpider("bandwidth_exceeded")
|
||||
"""
|
||||
|
||||
def __init__(self, reason: str = "cancelled"):
|
||||
super().__init__()
|
||||
|
|
@ -46,10 +76,27 @@ class CloseSpider(Exception):
|
|||
|
||||
|
||||
class StopDownload(Exception):
|
||||
"""
|
||||
Stop the download of the body for a given response.
|
||||
The 'fail' boolean parameter indicates whether or not the resulting partial response
|
||||
should be handled by the request errback. Note that 'fail' is a keyword-only argument.
|
||||
"""Raised from a :class:`~scrapy.signals.bytes_received` or
|
||||
:class:`~scrapy.signals.headers_received` signal handler to :ref:`stop the
|
||||
download <topics-stop-response-download>` of the response body.
|
||||
|
||||
The ``fail`` boolean parameter controls which method will handle the
|
||||
resulting response:
|
||||
|
||||
* If ``fail=True`` (default), the request errback is called. The response
|
||||
object is available as the ``response`` attribute of the ``StopDownload``
|
||||
exception, which is in turn stored as the ``value`` attribute of the
|
||||
received :class:`~twisted.python.failure.Failure` object. This means that
|
||||
in an errback defined as ``def errback(self, failure)``, the response can
|
||||
be accessed though ``failure.value.response``.
|
||||
|
||||
* If ``fail=False``, the request callback is called instead.
|
||||
|
||||
In both cases, the response could have its body truncated: the body contains
|
||||
all bytes received up until the exception is raised, including the bytes
|
||||
received in the signal handler that raises the exception. Also, the response
|
||||
object is marked with ``"download_stopped"`` in its
|
||||
:attr:`~scrapy.http.Response.flags` attribute.
|
||||
"""
|
||||
|
||||
response: Response | None
|
||||
|
|
@ -91,7 +138,8 @@ class UnsupportedURLSchemeError(Exception):
|
|||
|
||||
|
||||
class DropItem(Exception):
|
||||
"""Drop item from the item pipeline"""
|
||||
"""Raised from the :meth:`process_item` method of an :ref:`item pipeline
|
||||
<topics-item-pipeline>` to stop the processing of an item."""
|
||||
|
||||
def __init__(self, message: str, log_level: str | None = None):
|
||||
super().__init__(message)
|
||||
|
|
@ -99,7 +147,14 @@ class DropItem(Exception):
|
|||
|
||||
|
||||
class NotSupported(Exception):
|
||||
"""Indicates a feature or method is not supported"""
|
||||
"""Raised to indicate that a requested feature is not supported.
|
||||
|
||||
For example, Scrapy raises it when text-parsing shortcuts such as
|
||||
:meth:`response.css() <scrapy.http.TextResponse.css>` or
|
||||
:meth:`response.xpath() <scrapy.http.TextResponse.xpath>` are used on a
|
||||
:class:`~scrapy.http.Response` whose content is not text, or when sending a
|
||||
request whose URL scheme has no matching :ref:`download handler
|
||||
<topics-download-handlers>`."""
|
||||
|
||||
|
||||
# Commands
|
||||
|
|
@ -115,7 +170,7 @@ class UsageError(Exception):
|
|||
|
||||
class ScrapyDeprecationWarning(Warning):
|
||||
"""Warning category for deprecated features, since the default
|
||||
DeprecationWarning is silenced on Python 2.7+
|
||||
:exc:`DeprecationWarning` is silenced.
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Item Exporters are used to export/serialize items into different formats.
|
|||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import logging
|
||||
import marshal
|
||||
import pickle
|
||||
import pprint
|
||||
|
|
@ -24,6 +25,8 @@ from scrapy.utils.serialize import ScrapyJSONEncoder
|
|||
if TYPE_CHECKING:
|
||||
from json import JSONEncoder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"BaseItemExporter",
|
||||
"CsvItemExporter",
|
||||
|
|
@ -71,6 +74,17 @@ class BaseItemExporter(ABC):
|
|||
def finish_exporting(self) -> None: # noqa: B027
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _get_populated_field_names(adapter: ItemAdapter) -> Iterable[str]:
|
||||
"""Return the populated field names of *adapter*, in declaration order.
|
||||
|
||||
Populated fields that are not declared, which some item types allow,
|
||||
come last, in item order.
|
||||
"""
|
||||
populated = set(adapter.keys())
|
||||
declared = (name for name in adapter.field_names() if name in populated)
|
||||
return dict.fromkeys([*declared, *adapter.keys()])
|
||||
|
||||
def _get_serialized_fields(
|
||||
self, item: Any, default_value: Any = None, include_empty: bool | None = None
|
||||
) -> Iterable[tuple[str, Any]]:
|
||||
|
|
@ -83,7 +97,11 @@ class BaseItemExporter(ABC):
|
|||
include_empty = self.export_empty_fields
|
||||
|
||||
if self.fields_to_export is None:
|
||||
field_iter = item.field_names() if include_empty else item.keys()
|
||||
field_iter = (
|
||||
item.field_names()
|
||||
if include_empty
|
||||
else self._get_populated_field_names(item)
|
||||
)
|
||||
elif isinstance(self.fields_to_export, Mapping):
|
||||
if include_empty:
|
||||
field_iter = self.fields_to_export.items()
|
||||
|
|
@ -254,6 +272,8 @@ class CsvItemExporter(BaseItemExporter):
|
|||
self.csv_writer = csv.writer(self.stream, **self._kwargs)
|
||||
self._headers_not_written = True
|
||||
self._join_multivalued = join_multivalued
|
||||
self._autodetected_fields = False
|
||||
self._data_loss_warned = False
|
||||
|
||||
def serialize_field(
|
||||
self, field: Mapping[str, Any] | Field, name: str, value: Any
|
||||
|
|
@ -274,6 +294,22 @@ class CsvItemExporter(BaseItemExporter):
|
|||
self._headers_not_written = False
|
||||
self._write_headers_and_set_fields_to_export(item)
|
||||
|
||||
if (
|
||||
self._autodetected_fields
|
||||
and self.fields_to_export is not None
|
||||
and not self._data_loss_warned
|
||||
):
|
||||
item_fields = ItemAdapter(item).field_names()
|
||||
dropped_fields = set(item_fields) - set(self.fields_to_export)
|
||||
|
||||
if dropped_fields:
|
||||
dropped_fields_display = sorted(dropped_fields)
|
||||
logger.warning(
|
||||
f"CSVExporter dropped fields {dropped_fields_display}. "
|
||||
f"To avoid this, fully configure your FEED_EXPORT_FIELDS setting. "
|
||||
f"See: https://docs.scrapy.org/en/latest/topics/feed-exports.html#feed-export-fields",
|
||||
)
|
||||
self._data_loss_warned = True
|
||||
fields = self._get_serialized_fields(item, default_value="", include_empty=True)
|
||||
values = list(self._build_row(x for _, x in fields))
|
||||
self.csv_writer.writerow(values)
|
||||
|
|
@ -293,6 +329,7 @@ class CsvItemExporter(BaseItemExporter):
|
|||
if not self.fields_to_export:
|
||||
# use declared field names, or keys if the item is a dict
|
||||
self.fields_to_export = ItemAdapter(item).field_names()
|
||||
self._autodetected_fields = True
|
||||
fields: Iterable[str]
|
||||
if isinstance(self.fields_to_export, Mapping):
|
||||
fields = self.fields_to_export.values()
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class CloseSpider:
|
|||
self.task = None
|
||||
|
||||
if self.task_no_item:
|
||||
if self.task_no_item.running:
|
||||
if self.task_no_item.running: # pragma: no branch
|
||||
self.task_no_item.stop()
|
||||
self.task_no_item = None
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import re
|
|||
import sys
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Coroutine
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
|
@ -22,12 +22,13 @@ from urllib.parse import unquote, urlparse
|
|||
|
||||
from twisted.internet.defer import Deferred, DeferredList
|
||||
from w3lib.url import file_uri_to_path
|
||||
from zope.interface import Interface, implementer
|
||||
from zope.interface import Interface
|
||||
|
||||
from scrapy import Spider, signals
|
||||
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
|
||||
from scrapy.extensions.postprocessing import PostProcessingManager
|
||||
from scrapy.utils.asyncio import is_asyncio_available, run_in_thread
|
||||
from scrapy.utils.boto import _get_max_pool_connections
|
||||
from scrapy.utils.conf import feed_complete_default_values_from_settings
|
||||
from scrapy.utils.defer import deferred_from_coro, ensure_awaitable
|
||||
from scrapy.utils.ftp import ftp_store_file
|
||||
|
|
@ -47,6 +48,33 @@ if TYPE_CHECKING:
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Printf-style placeholders (e.g. %(time)s) used to build feed URIs. Any other
|
||||
# percent character in a URI (e.g. percent-encoding such as %20 or %23) must be
|
||||
# treated as a literal rather than as the start of a placeholder.
|
||||
_FEED_URI_PLACEHOLDER_RE = re.compile(
|
||||
r"%\([^)]+\)[-+ #0]*(?:\d+|\*)?(?:\.(?:\d+|\*))?[diouxXeEfFgGcrsa]"
|
||||
)
|
||||
|
||||
|
||||
def apply_uri_params(uri_template: str, uri_params: dict[str, Any]) -> str:
|
||||
"""Return *uri_template* with its ``%(...)s`` placeholders replaced using
|
||||
*uri_params*, leaving any other percent character untouched.
|
||||
|
||||
This allows feed URIs to contain percent-encoded characters (e.g. ``%20``
|
||||
in a path with spaces or ``%23`` in FTP credentials) without them being
|
||||
misinterpreted as printf-style formatting directives.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
last = 0
|
||||
for match in _FEED_URI_PLACEHOLDER_RE.finditer(uri_template):
|
||||
parts.append(uri_template[last : match.start()].replace("%", "%%"))
|
||||
parts.append(match.group(0))
|
||||
last = match.end()
|
||||
parts.append(uri_template[last:].replace("%", "%%"))
|
||||
return "".join(parts) % uri_params
|
||||
|
||||
|
||||
UriParamsCallableT: TypeAlias = Callable[
|
||||
[dict[str, Any], Spider], dict[str, Any] | None
|
||||
]
|
||||
|
|
@ -88,25 +116,18 @@ class ItemFilter:
|
|||
return True # accept all items by default
|
||||
|
||||
|
||||
class IFeedStorage(Interface): # type: ignore[misc]
|
||||
"""Interface that all Feed Storages must implement"""
|
||||
|
||||
class _IFeedStorage(Interface): # type: ignore[misc] # pragma: no cover
|
||||
# pylint: disable=no-self-argument
|
||||
|
||||
def __init__(uri, *, feed_options=None): # type: ignore[no-untyped-def] # pylint: disable=super-init-not-called
|
||||
"""Initialize the storage with the parameters given in the URI and the
|
||||
feed-specific options (see :setting:`FEEDS`)"""
|
||||
def __init__(uri, *, feed_options=None): ... # type: ignore[no-untyped-def] # pylint: disable=super-init-not-called
|
||||
|
||||
def open(spider): # type: ignore[no-untyped-def]
|
||||
"""Open the storage for the given spider. It must return a file-like
|
||||
object that will be used for the exporters"""
|
||||
def open(spider): ... # type: ignore[no-untyped-def]
|
||||
|
||||
def store(file): # type: ignore[no-untyped-def]
|
||||
"""Store the given file stream"""
|
||||
def store(file): ... # type: ignore[no-untyped-def]
|
||||
|
||||
|
||||
class FeedStorageProtocol(Protocol):
|
||||
"""Reimplementation of ``IFeedStorage`` that can be used in type hints."""
|
||||
"""Protocol that all Feed Storages must follow."""
|
||||
|
||||
def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None):
|
||||
"""Initialize the storage with the parameters given in the URI and the
|
||||
|
|
@ -120,7 +141,6 @@ class FeedStorageProtocol(Protocol):
|
|||
"""Store the given file stream"""
|
||||
|
||||
|
||||
@implementer(IFeedStorage)
|
||||
class BlockingFeedStorage(ABC):
|
||||
def open(self, spider: Spider) -> IO[bytes]:
|
||||
path = spider.crawler.settings["FEED_TEMPDIR"]
|
||||
|
|
@ -137,7 +157,6 @@ class BlockingFeedStorage(ABC):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
@implementer(IFeedStorage)
|
||||
class StdoutFeedStorage:
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -164,7 +183,6 @@ class StdoutFeedStorage:
|
|||
pass
|
||||
|
||||
|
||||
@implementer(IFeedStorage)
|
||||
class FileFeedStorage:
|
||||
def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None):
|
||||
self.path: str = file_uri_to_path(uri) if uri.startswith("file:") else uri
|
||||
|
|
@ -196,11 +214,14 @@ class S3FeedStorage(BlockingFeedStorage):
|
|||
feed_options: dict[str, Any] | None = None,
|
||||
session_token: str | None = None,
|
||||
region_name: str | None = None,
|
||||
max_pool_connections: int | None = None,
|
||||
):
|
||||
try:
|
||||
import boto3.session # noqa: PLC0415
|
||||
except ImportError:
|
||||
raise NotConfigured("missing boto3 library") from None
|
||||
from botocore.config import Config # noqa: PLC0415
|
||||
|
||||
u = urlparse(uri)
|
||||
assert u.hostname
|
||||
self.bucketname: str = u.hostname
|
||||
|
|
@ -211,6 +232,7 @@ class S3FeedStorage(BlockingFeedStorage):
|
|||
self.acl: str | None = acl
|
||||
self.endpoint_url: str | None = endpoint_url
|
||||
self.region_name: str | None = region_name
|
||||
self.max_pool_connections: int | None = max_pool_connections
|
||||
|
||||
boto3_session = boto3.session.Session()
|
||||
self.s3_client = boto3_session.client(
|
||||
|
|
@ -220,6 +242,11 @@ class S3FeedStorage(BlockingFeedStorage):
|
|||
aws_session_token=self.session_token,
|
||||
endpoint_url=self.endpoint_url,
|
||||
region_name=self.region_name,
|
||||
config=(
|
||||
Config(max_pool_connections=self.max_pool_connections)
|
||||
if self.max_pool_connections is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
if feed_options and feed_options.get("overwrite", True) is False:
|
||||
|
|
@ -245,6 +272,7 @@ class S3FeedStorage(BlockingFeedStorage):
|
|||
acl=crawler.settings["FEED_STORAGE_S3_ACL"] or None,
|
||||
endpoint_url=crawler.settings["AWS_ENDPOINT_URL"] or None,
|
||||
region_name=crawler.settings["AWS_REGION_NAME"] or None,
|
||||
max_pool_connections=_get_max_pool_connections(crawler.settings),
|
||||
feed_options=feed_options,
|
||||
)
|
||||
|
||||
|
|
@ -437,7 +465,7 @@ class FeedSlot:
|
|||
)
|
||||
|
||||
def finish_exporting(self) -> None:
|
||||
if self._exporting:
|
||||
if self._exporting: # pragma: no branch
|
||||
assert self.exporter
|
||||
self.exporter.finish_exporting()
|
||||
self._exporting = False
|
||||
|
|
@ -458,7 +486,7 @@ class FeedExporter:
|
|||
self.feeds = {}
|
||||
self.slots: list[FeedSlot] = []
|
||||
self.filters: dict[str, ItemFilter] = {}
|
||||
self._pending_close_coros: list[Coroutine[Any, Any, None]] = []
|
||||
self._pending_close_tasks: list[asyncio.Task[None] | Deferred[None]] = []
|
||||
|
||||
if not self.settings["FEEDS"] and not self.settings["FEED_URI"]:
|
||||
raise NotConfigured
|
||||
|
|
@ -473,7 +501,7 @@ class FeedExporter:
|
|||
)
|
||||
uri = self.settings["FEED_URI"]
|
||||
# handle pathlib.Path objects
|
||||
uri = str(uri) if not isinstance(uri, Path) else uri.absolute().as_uri()
|
||||
uri = str(uri.absolute()) if isinstance(uri, Path) else str(uri)
|
||||
feed_options = {"format": self.settings["FEED_FORMAT"]}
|
||||
self.feeds[uri] = feed_complete_default_values_from_settings(
|
||||
feed_options, self.settings
|
||||
|
|
@ -485,9 +513,9 @@ class FeedExporter:
|
|||
for settings_uri, feed_options in self.settings.getdict("FEEDS").items():
|
||||
# handle pathlib.Path objects
|
||||
uri = (
|
||||
str(settings_uri)
|
||||
if not isinstance(settings_uri, Path)
|
||||
else settings_uri.absolute().as_uri()
|
||||
str(settings_uri.absolute())
|
||||
if isinstance(settings_uri, Path)
|
||||
else str(settings_uri)
|
||||
)
|
||||
self.feeds[uri] = feed_complete_default_values_from_settings(
|
||||
feed_options, self.settings
|
||||
|
|
@ -514,7 +542,7 @@ class FeedExporter:
|
|||
self.slots.append(
|
||||
self._start_new_batch(
|
||||
batch_id=1,
|
||||
uri=uri % uri_params,
|
||||
uri=apply_uri_params(uri, uri_params),
|
||||
feed_options=feed_options,
|
||||
spider=spider,
|
||||
uri_template=uri,
|
||||
|
|
@ -522,23 +550,44 @@ class FeedExporter:
|
|||
)
|
||||
|
||||
async def close_spider(self, spider: Spider) -> None:
|
||||
self._pending_close_coros.extend(
|
||||
self._close_slot(slot, spider) for slot in self.slots
|
||||
)
|
||||
for slot in self.slots:
|
||||
self._schedule_slot_close(slot, spider)
|
||||
|
||||
if self._pending_close_coros:
|
||||
if self._pending_close_tasks: # pragma: no branch
|
||||
if is_asyncio_available():
|
||||
await asyncio.wait(
|
||||
[asyncio.create_task(coro) for coro in self._pending_close_coros]
|
||||
cast("list[asyncio.Task[None]]", list(self._pending_close_tasks))
|
||||
)
|
||||
else:
|
||||
await DeferredList(
|
||||
deferred_from_coro(coro) for coro in self._pending_close_coros
|
||||
cast("list[Deferred[None]]", list(self._pending_close_tasks))
|
||||
)
|
||||
|
||||
# Send FEED_EXPORTER_CLOSED signal
|
||||
await self.crawler.signals.send_catch_log_async(signals.feed_exporter_closed)
|
||||
|
||||
def _schedule_slot_close(
|
||||
self, slot: FeedSlot, spider: Spider
|
||||
) -> asyncio.Task[None] | Deferred[None]:
|
||||
"""Start closing the slot without waiting for it to finish, keeping
|
||||
track of the pending work so that it can be awaited in
|
||||
:meth:`close_spider` if it hasn't finished by then."""
|
||||
aw: asyncio.Task[None] | Deferred[None]
|
||||
coro = self._close_slot(slot, spider)
|
||||
if is_asyncio_available():
|
||||
aw = asyncio.create_task(coro)
|
||||
self._pending_close_tasks.append(aw)
|
||||
aw.add_done_callback(self._pending_close_tasks.remove)
|
||||
else:
|
||||
aw = deferred_from_coro(coro)
|
||||
self._pending_close_tasks.append(aw)
|
||||
aw.addBoth(self._untrack_pending_close_task, aw)
|
||||
return aw
|
||||
|
||||
def _untrack_pending_close_task(self, result: Any, aw: Deferred[None]) -> Any:
|
||||
self._pending_close_tasks.remove(aw)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_file(slot_: FeedSlot) -> IO[bytes]:
|
||||
assert slot_.file
|
||||
|
|
@ -635,11 +684,11 @@ class FeedExporter:
|
|||
uri_params = self._get_uri_params(
|
||||
spider, self.feeds[slot.uri_template]["uri_params"], slot
|
||||
)
|
||||
self._pending_close_coros.append(self._close_slot(slot, spider))
|
||||
self._schedule_slot_close(slot, spider)
|
||||
slots.append(
|
||||
self._start_new_batch(
|
||||
batch_id=slot.batch_id + 1,
|
||||
uri=slot.uri_template % uri_params,
|
||||
uri=apply_uri_params(slot.uri_template, uri_params),
|
||||
feed_options=self.feeds[slot.uri_template],
|
||||
spider=spider,
|
||||
uri_template=slot.uri_template,
|
||||
|
|
@ -731,3 +780,14 @@ class FeedExporter:
|
|||
feed_options.get("item_filter", ItemFilter)
|
||||
)
|
||||
return item_filter_class(feed_options)
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any: # pragma: no cover
|
||||
if name == "IFeedStorage":
|
||||
warnings.warn(
|
||||
"scrapy.extensions.feedexport.IFeedStorage is deprecated.",
|
||||
ScrapyDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _IFeedStorage
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class LogCount:
|
|||
"""Install a log handler that counts log messages by level.
|
||||
|
||||
The handler installed is :class:`scrapy.utils.log.LogCounterHandler`.
|
||||
The counts are stored in stats as ``log_count/<level>``.
|
||||
The counts are stored in the :stat:`log_count/{level}` stat.
|
||||
|
||||
.. versionadded:: 2.14
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class LogStats:
|
||||
"""Log basic scraping stats periodically like:
|
||||
* RPM - Requests per Minute
|
||||
* RPM - Responses per Minute
|
||||
* IPM - Items per Minute
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class MemoryUsage:
|
|||
|
||||
def engine_stopped(self) -> None:
|
||||
for tsk in self.tasks:
|
||||
if tsk.running:
|
||||
if tsk.running: # pragma: no branch
|
||||
tsk.stop()
|
||||
|
||||
def update(self) -> None:
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ class PeriodicLog:
|
|||
):
|
||||
self.stats: StatsCollector = stats
|
||||
self.interval: float = interval
|
||||
self.multiplier: float = 60.0 / self.interval
|
||||
self.task: AsyncioLoopingCall | LoopingCall | None = None
|
||||
self.encoder: JSONEncoder = ScrapyJSONEncoder(sort_keys=True, indent=4)
|
||||
self.ext_stats_enabled: bool = bool(ext_stats)
|
||||
|
|
@ -165,5 +164,5 @@ class PeriodicLog:
|
|||
|
||||
def spider_closed(self, spider: Spider, reason: str) -> None:
|
||||
self.log()
|
||||
if self.task and self.task.running:
|
||||
if self.task and self.task.running: # pragma: no branch
|
||||
self.task.stop()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue