diff --git a/.bandit.yml b/.bandit.yml index 41f1bb597..2aae8a0aa 100644 --- a/.bandit.yml +++ b/.bandit.yml @@ -1,5 +1,6 @@ skips: - B101 +- B113 # https://github.com/PyCQA/bandit/issues/1010 - B105 - B301 - B303 @@ -17,3 +18,4 @@ skips: - B503 - B603 - B605 +exclude_dirs: ['tests'] diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b949d81c4..f76bf783d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.7.1 +current_version = 2.11.0 commit = True tag = True tag_name = {new_version} diff --git a/.flake8 b/.flake8 index 0c64d009e..544d72956 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] max-line-length = 119 -ignore = W503 +ignore = W503, E203 exclude = docs/conf.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..dbcebfa0a --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,7 @@ +# .git-blame-ignore-revs +# adding black formatter to all the code +e211ec0aa26ecae0da8ae55d064ea60e1efe4d0d +# re applying black to the code with default line length +303f0a70fcf8067adf0a909c2096a5009162383a +# reaplying black again and removing line length on pre-commit black config +c5cdd0d30ceb68ccba04af0e71d1b8e6678e2962 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..63cae77e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,13 @@ +--- +name: Question / Help +about: Ask a question about Scrapy or ask for help with your Scrapy code. +--- + +Thanks for taking an interest in Scrapy! + +The Scrapy GitHub issue tracker is not meant for questions or help. Please ask +for help in the [Scrapy community resources](https://scrapy.org/community/) +instead. + +The GitHub issue tracker's purpose is to deal with bug reports and feature +requests for the project itself. diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e9f9a6aea..d6fc0f6c5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,6 +1,10 @@ name: Checks on: [push, pull_request] +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + jobs: checks: runs-on: ubuntu-latest @@ -8,27 +12,21 @@ jobs: fail-fast: false matrix: include: - - python-version: "3.11" - env: - TOXENV: security - - python-version: "3.11" - env: - TOXENV: flake8 - - python-version: "3.11" + - python-version: "3.12" env: TOXENV: pylint - - python-version: 3.7 + - python-version: 3.8 env: TOXENV: typing - python-version: "3.11" # Keep in sync with .readthedocs.yml env: TOXENV: docs - - python-version: "3.11" + - python-version: "3.12" env: TOXENV: twinecheck steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -40,3 +38,9 @@ jobs: run: | pip install -U tox tox + + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eee9a4f02..affaa32a5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,31 +1,25 @@ name: Publish -on: [push] +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true jobs: publish: runs-on: ubuntu-latest - if: startsWith(github.event.ref, 'refs/tags/') - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - - name: Check Tag - id: check-release-tag - run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/[0-9]+[.][0-9]+[.][0-9]+(rc[0-9]+|[.]dev[0-9]+)?$ ]]; then - echo ::set-output name=release_tag::true - fi - - - name: Publish to PyPI - if: steps.check-release-tag.outputs.release_tag == 'true' - run: | - pip install --upgrade build twine - python -m build - export TWINE_USERNAME=__token__ - export TWINE_PASSWORD=${{ secrets.PYPI_TOKEN }} - twine upload dist/* + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.12 + - run: | + pip install --upgrade build twine + python -m build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.6.4 + with: + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/tests-macos.yml b/.github/workflows/tests-macos.yml index 174d245ca..252176464 100644 --- a/.github/workflows/tests-macos.yml +++ b/.github/workflows/tests-macos.yml @@ -1,16 +1,20 @@ name: macOS on: [push, pull_request] +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: runs-on: macos-11 strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 8fcf90a18..f50a4d104 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -1,6 +1,10 @@ name: Ubuntu on: [push, pull_request] +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: runs-on: ubuntu-latest @@ -8,9 +12,6 @@ jobs: fail-fast: false matrix: include: - - python-version: 3.8 - env: - TOXENV: py - python-version: 3.9 env: TOXENV: py @@ -20,30 +21,45 @@ jobs: - python-version: "3.11" env: TOXENV: py - - python-version: "3.11" + - python-version: "3.12" + env: + TOXENV: py + - python-version: "3.12" env: TOXENV: asyncio - python-version: pypy3.9 env: TOXENV: pypy3 + - python-version: pypy3.10 + env: + TOXENV: pypy3 # pinned deps - - python-version: 3.7.13 + - python-version: 3.8.17 env: TOXENV: pinned - - python-version: 3.7.13 + - python-version: 3.8.17 env: TOXENV: asyncio-pinned - - python-version: pypy3.7 + - python-version: pypy3.8 env: TOXENV: pypy3-pinned + - python-version: 3.8.17 + env: + TOXENV: extra-deps-pinned + - python-version: 3.8.17 + env: + TOXENV: botocore-pinned - - python-version: "3.11" + - python-version: "3.12" env: TOXENV: extra-deps + - python-version: "3.12" + env: + TOXENV: botocore steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -51,7 +67,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install system libraries - if: matrix.python-version == 'pypy3.9' || contains(matrix.env.TOXENV, 'pinned') + if: contains(matrix.python-version, 'pypy') || contains(matrix.env.TOXENV, 'pinned') run: | sudo apt-get update sudo apt-get install libxml2-dev libxslt-dev diff --git a/.github/workflows/tests-windows.yml b/.github/workflows/tests-windows.yml index f60c48841..757d62285 100644 --- a/.github/workflows/tests-windows.yml +++ b/.github/workflows/tests-windows.yml @@ -1,6 +1,10 @@ name: Windows on: [push, pull_request] +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: runs-on: windows-latest @@ -8,31 +12,27 @@ jobs: fail-fast: false matrix: include: - - python-version: 3.7 - env: - TOXENV: windows-pinned - python-version: 3.8 env: - TOXENV: py + TOXENV: windows-pinned - python-version: 3.9 env: TOXENV: py - python-version: "3.10" env: TOXENV: py - - python-version: "3.10" + - python-version: "3.11" + env: + TOXENV: py + - python-version: "3.12" + env: + TOXENV: py + - python-version: "3.12" env: TOXENV: asyncio -# no binary package for lxml for 3.11 yet -# - python-version: "3.11" -# env: -# TOXENV: py -# - python-version: "3.11" -# env: -# TOXENV: asyncio steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 000000000..f238bf7ea --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +profile = black diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..0cff5cc73 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: +- repo: https://github.com/PyCQA/bandit + rev: 1.7.5 + hooks: + - id: bandit + args: [-r, -c, .bandit.yml] +- repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 +- repo: https://github.com/psf/black.git + rev: 23.9.1 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/adamchainz/blacken-docs + rev: 1.16.0 + hooks: + - id: blacken-docs + additional_dependencies: + - black==23.9.1 diff --git a/MANIFEST.in b/MANIFEST.in index ae7db51fa..4920dc0c3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ include NEWS include scrapy/VERSION include scrapy/mime.types +include scrapy/py.typed include codecov.yml include conftest.py diff --git a/README.rst b/README.rst index 970bf2c35..14adff648 100644 --- a/README.rst +++ b/README.rst @@ -17,9 +17,10 @@ Scrapy :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AUbuntu :alt: Ubuntu -.. image:: https://github.com/scrapy/scrapy/workflows/macOS/badge.svg - :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AmacOS - :alt: macOS +.. .. image:: https://github.com/scrapy/scrapy/workflows/macOS/badge.svg + .. :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AmacOS + .. :alt: macOS + .. image:: https://github.com/scrapy/scrapy/workflows/Windows/badge.svg :target: https://github.com/scrapy/scrapy/actions?query=workflow%3AWindows @@ -41,7 +42,7 @@ Scrapy Overview ======== -Scrapy is a fast high-level web crawling and web scraping framework, used to +Scrapy is a BSD-licensed fast high-level web crawling and web scraping framework, used to crawl websites and extract structured data from their pages. It can be used for a wide range of purposes, from data mining to monitoring and automated testing. @@ -58,7 +59,7 @@ including a list of features. Requirements ============ -* Python 3.7+ +* Python 3.8+ * Works on Linux, Windows, macOS, BSD Install @@ -110,4 +111,4 @@ See https://scrapy.org/companies/ for a list. Commercial Support ================== -See https://scrapy.org/support/ for details. +See https://scrapy.org/support/ for details. \ No newline at end of file diff --git a/artwork/README.rst b/artwork/README.rst index 8a1028cde..c1880ef6c 100644 --- a/artwork/README.rst +++ b/artwork/README.rst @@ -2,19 +2,19 @@ Scrapy artwork ============== -This folder contains Scrapy artwork resources such as logos and fonts. +This folder contains the Scrapy artwork resources such as logos and fonts. scrapy-logo.jpg --------------- -Main Scrapy logo, in JPEG format. +The main Scrapy logo, in JPEG format. qlassik.zip ----------- -Font used for Scrapy logo. Homepage: https://www.dafont.com/qlassik.font +The font used for the Scrapy logo. Homepage: https://www.dafont.com/qlassik.font scrapy-blog.logo.xcf -------------------- -The logo used in Scrapy blog, in Gimp format. +The logo used in the Scrapy blog, in Gimp format. diff --git a/conftest.py b/conftest.py index 2a5d55083..2bfa46f5a 100644 --- a/conftest.py +++ b/conftest.py @@ -1,36 +1,43 @@ +import platform +import sys from pathlib import Path import pytest +from twisted import version as twisted_version +from twisted.python.versions import Version from twisted.web.http import H2_ENABLED from scrapy.utils.reactor import install_reactor - from tests.keys import generate_keys def _py_files(folder): - return (str(p) for p in Path(folder).rglob('*.py')) + return (str(p) for p in Path(folder).rglob("*.py")) collect_ignore = [ # not a test, but looks like a test "scrapy/utils/testsite.py", + "tests/ftpserver.py", + "tests/mockserver.py", + "tests/pipelines.py", + "tests/spiders.py", # contains scripts to be run by tests/test_crawler.py::CrawlerProcessSubprocess *_py_files("tests/CrawlerProcess"), # contains scripts to be run by tests/test_crawler.py::CrawlerRunnerSubprocess *_py_files("tests/CrawlerRunner"), ] -with Path('tests/ignores.txt').open(encoding="utf-8") as reader: +with Path("tests/ignores.txt").open(encoding="utf-8") as reader: for line in reader: file_path = line.strip() - if file_path and file_path[0] != '#': + if file_path and file_path[0] != "#": collect_ignore.append(file_path) if not H2_ENABLED: collect_ignore.extend( ( - 'scrapy/core/downloader/handlers/http2.py', + "scrapy/core/downloader/handlers/http2.py", *_py_files("scrapy/core/http2"), ) ) @@ -50,7 +57,7 @@ def pytest_addoption(parser): ) -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def reactor_pytest(request): if not request.cls: # doctests @@ -61,14 +68,31 @@ def reactor_pytest(request): @pytest.fixture(autouse=True) def only_asyncio(request, reactor_pytest): - if request.node.get_closest_marker('only_asyncio') and reactor_pytest != 'asyncio': - pytest.skip('This test is only run with --reactor=asyncio') + if request.node.get_closest_marker("only_asyncio") and reactor_pytest != "asyncio": + pytest.skip("This test is only run with --reactor=asyncio") @pytest.fixture(autouse=True) def only_not_asyncio(request, reactor_pytest): - if request.node.get_closest_marker('only_not_asyncio') and reactor_pytest == 'asyncio': - pytest.skip('This test is only run without --reactor=asyncio') + if ( + request.node.get_closest_marker("only_not_asyncio") + and reactor_pytest == "asyncio" + ): + pytest.skip("This test is only run without --reactor=asyncio") + + +@pytest.fixture(autouse=True) +def requires_uvloop(request): + if not request.node.get_closest_marker("requires_uvloop"): + return + if sys.implementation.name == "pypy": + pytest.skip("uvloop does not support pypy properly") + if platform.system() == "Windows": + pytest.skip("uvloop does not support Windows") + if twisted_version == Version("twisted", 21, 2, 0): + pytest.skip("https://twistedmatrix.com/trac/ticket/10106") + if sys.version_info >= (3, 12): + pytest.skip("uvloop doesn't support Python 3.12 yet") def pytest_configure(config): diff --git a/docs/Makefile b/docs/Makefile index 596cb6cef..48401bac8 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -87,7 +87,7 @@ coverage: build htmlview: html $(PYTHON) -c "import webbrowser; from pathlib import Path; \ - webbrowser.open('file://' + Path('build/html/index.html').resolve())" + webbrowser.open(Path('build/html/index.html').resolve().as_uri())" clean: -rm -rf build/* diff --git a/docs/_ext/scrapydocs.py b/docs/_ext/scrapydocs.py index 337604cf1..c23a89089 100644 --- a/docs/_ext/scrapydocs.py +++ b/docs/_ext/scrapydocs.py @@ -1,7 +1,8 @@ from operator import itemgetter -from docutils.parsers.rst.roles import set_classes + from docutils import nodes from docutils.parsers.rst import Directive +from docutils.parsers.rst.roles import set_classes from sphinx.util.nodes import make_refnode @@ -11,15 +12,15 @@ class settingslist_node(nodes.General, nodes.Element): class SettingsListDirective(Directive): def run(self): - return [settingslist_node('')] + return [settingslist_node("")] def is_setting_index(node): - if node.tagname == 'index' and node['entries']: + if node.tagname == "index" and node["entries"]: # index entries for setting directives look like: # [('pair', 'SETTING_NAME; setting', 'std:setting-SETTING_NAME', '')] - entry_type, info, refid = node['entries'][0][:3] - return entry_type == 'pair' and info.endswith('; setting') + entry_type, info, refid = node["entries"][0][:3] + return entry_type == "pair" and info.endswith("; setting") return False @@ -30,14 +31,14 @@ def get_setting_target(node): def get_setting_name_and_refid(node): """Extract setting name from directive index node""" - entry_type, info, refid = node['entries'][0][:3] - return info.replace('; setting', ''), refid + entry_type, info, refid = node["entries"][0][:3] + return info.replace("; setting", ""), refid def collect_scrapy_settings_refs(app, doctree): env = app.builder.env - if not hasattr(env, 'scrapy_all_settings'): + if not hasattr(env, "scrapy_all_settings"): env.scrapy_all_settings = [] for node in doctree.traverse(is_setting_index): @@ -46,18 +47,23 @@ def collect_scrapy_settings_refs(app, doctree): setting_name, refid = get_setting_name_and_refid(node) - env.scrapy_all_settings.append({ - 'docname': env.docname, - 'setting_name': setting_name, - 'refid': refid, - }) + env.scrapy_all_settings.append( + { + "docname": env.docname, + "setting_name": setting_name, + "refid": refid, + } + ) def make_setting_element(setting_data, app, fromdocname): - refnode = make_refnode(app.builder, fromdocname, - todocname=setting_data['docname'], - targetid=setting_data['refid'], - child=nodes.Text(setting_data['setting_name'])) + refnode = make_refnode( + app.builder, + fromdocname, + todocname=setting_data["docname"], + targetid=setting_data["refid"], + child=nodes.Text(setting_data["setting_name"]), + ) p = nodes.paragraph() p += refnode @@ -71,10 +77,13 @@ def replace_settingslist_nodes(app, doctree, fromdocname): for node in doctree.traverse(settingslist_node): settings_list = nodes.bullet_list() - settings_list.extend([make_setting_element(d, app, fromdocname) - for d in sorted(env.scrapy_all_settings, - key=itemgetter('setting_name')) - if fromdocname != d['docname']]) + settings_list.extend( + [ + make_setting_element(d, app, fromdocname) + for d in sorted(env.scrapy_all_settings, key=itemgetter("setting_name")) + if fromdocname != d["docname"] + ] + ) node.replace_self(settings_list) @@ -99,41 +108,41 @@ def setup(app): rolename="reqmeta", indextemplate="pair: %s; reqmeta", ) - app.add_role('source', source_role) - app.add_role('commit', commit_role) - app.add_role('issue', issue_role) - app.add_role('rev', rev_role) + app.add_role("source", source_role) + app.add_role("commit", commit_role) + app.add_role("issue", issue_role) + app.add_role("rev", rev_role) app.add_node(settingslist_node) - app.add_directive('settingslist', SettingsListDirective) + app.add_directive("settingslist", SettingsListDirective) - app.connect('doctree-read', collect_scrapy_settings_refs) - app.connect('doctree-resolved', replace_settingslist_nodes) + app.connect("doctree-read", collect_scrapy_settings_refs) + app.connect("doctree-resolved", replace_settingslist_nodes) def source_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - ref = 'https://github.com/scrapy/scrapy/blob/master/' + text + ref = "https://github.com/scrapy/scrapy/blob/master/" + text set_classes(options) node = nodes.reference(rawtext, text, refuri=ref, **options) return [node], [] def issue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - ref = 'https://github.com/scrapy/scrapy/issues/' + text + ref = "https://github.com/scrapy/scrapy/issues/" + text set_classes(options) - node = nodes.reference(rawtext, 'issue ' + text, refuri=ref, **options) + node = nodes.reference(rawtext, "issue " + text, refuri=ref, **options) return [node], [] def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - ref = 'https://github.com/scrapy/scrapy/commit/' + text + ref = "https://github.com/scrapy/scrapy/commit/" + text set_classes(options) - node = nodes.reference(rawtext, 'commit ' + text, refuri=ref, **options) + node = nodes.reference(rawtext, "commit " + text, refuri=ref, **options) return [node], [] def rev_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - ref = 'http://hg.scrapy.org/scrapy/changeset/' + text + ref = "http://hg.scrapy.org/scrapy/changeset/" + text set_classes(options) - node = nodes.reference(rawtext, 'r' + text, refuri=ref, **options) + node = nodes.reference(rawtext, "r" + text, refuri=ref, **options) return [node], [] diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html deleted file mode 100644 index 18a5231ee..000000000 --- a/docs/_templates/layout.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "!layout.html" %} - -{% block footer %} -{{ super() }} - -{% endblock %} diff --git a/docs/_tests/quotes.html b/docs/_tests/quotes.html index 71aff8847..f4002ecd1 100644 --- a/docs/_tests/quotes.html +++ b/docs/_tests/quotes.html @@ -273,7 +273,7 @@ Quotes by: GoodReads.com

diff --git a/docs/_tests/quotes1.html b/docs/_tests/quotes1.html index 71aff8847..f4002ecd1 100644 --- a/docs/_tests/quotes1.html +++ b/docs/_tests/quotes1.html @@ -273,7 +273,7 @@ Quotes by: GoodReads.com

diff --git a/docs/conf.py b/docs/conf.py index d2a77003e..9ca0f817a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,30 +25,30 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'hoverxref.extension', - 'notfound.extension', - 'scrapydocs', - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', + "hoverxref.extension", + "notfound.extension", + "scrapydocs", + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Scrapy' -copyright = f'2008–{datetime.now().year}, Scrapy developers' +project = "Scrapy" +copyright = f"2008–{datetime.now().year}, Scrapy developers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -57,50 +57,51 @@ copyright = f'2008–{datetime.now().year}, Scrapy developers' # The short X.Y version. try: import scrapy - version = '.'.join(map(str, scrapy.version_info[:2])) + + version = ".".join(map(str, scrapy.version_info[:2])) release = scrapy.__version__ except ImportError: - version = '' - release = '' + version = "" + release = "" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -language = 'en' +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -#unused_docs = [] +# unused_docs = [] -exclude_patterns = ['build'] +exclude_patterns = ["build"] # List of directories, relative to source directory, that shouldn't be searched # for source files. -exclude_trees = ['.build'] +exclude_trees = [".build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # List of Sphinx warnings that will not be raised -suppress_warnings = ['epub.unknown_project_files'] +suppress_warnings = ["epub.unknown_project_files"] # Options for HTML output @@ -108,17 +109,18 @@ suppress_warnings = ['epub.unknown_project_files'] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # Add path to the RTD explicitly to robustify builds (otherwise might # fail in a clean Debian build env) import sphinx_rtd_theme + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The style sheet to use for HTML and HTML Help pages. A file of that name @@ -128,44 +130,44 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' +html_last_updated_fmt = "%b %d, %Y" # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. html_copy_source = True @@ -173,16 +175,16 @@ html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'Scrapydoc' +htmlhelp_basename = "Scrapydoc" html_css_files = [ - 'custom.css', + "custom.css", ] @@ -190,34 +192,33 @@ html_css_files = [ # ------------------------ # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ - ('index', 'Scrapy.tex', 'Scrapy Documentation', - 'Scrapy developers', 'manual'), + ("index", "Scrapy.tex", "Scrapy Documentation", "Scrapy developers", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True # Options for the linkcheck builder @@ -226,8 +227,9 @@ latex_documents = [ # A list of regular expressions that match URIs that should not be checked when # doing a linkcheck build. linkcheck_ignore = [ - 'http://localhost:\d+', 'http://hg.scrapy.org', - 'http://directory.google.com/' + "http://localhost:\d+", + "http://hg.scrapy.org", + "http://directory.google.com/", ] @@ -237,44 +239,35 @@ coverage_ignore_pyobjects = [ # Contract’s add_pre_hook and add_post_hook are not documented because # they should be transparent to contract developers, for whom pre_hook and # post_hook should be the actual concern. - r'\bContract\.add_(pre|post)_hook$', - + r"\bContract\.add_(pre|post)_hook$", # ContractsManager is an internal class, developers are not expected to # interact with it directly in any way. - r'\bContractsManager\b$', - + r"\bContractsManager\b$", # For default contracts we only want to document their general purpose in # their __init__ method, the methods they reimplement to achieve that purpose # should be irrelevant to developers using those contracts. - r'\w+Contract\.(adjust_request_args|(pre|post)_process)$', - + r"\w+Contract\.(adjust_request_args|(pre|post)_process)$", # Methods of downloader middlewares are not documented, only the classes # themselves, since downloader middlewares are controlled through Scrapy # settings. - r'^scrapy\.downloadermiddlewares\.\w*?\.(\w*?Middleware|DownloaderStats)\.', - + r"^scrapy\.downloadermiddlewares\.\w*?\.(\w*?Middleware|DownloaderStats)\.", # Base classes of downloader middlewares are implementation details that # are not meant for users. - r'^scrapy\.downloadermiddlewares\.\w*?\.Base\w*?Middleware', - + r"^scrapy\.downloadermiddlewares\.\w*?\.Base\w*?Middleware", # Private exception used by the command-line interface implementation. - r'^scrapy\.exceptions\.UsageError', - + r"^scrapy\.exceptions\.UsageError", # Methods of BaseItemExporter subclasses are only documented in # BaseItemExporter. - r'^scrapy\.exporters\.(?!BaseItemExporter\b)\w*?\.', - + r"^scrapy\.exporters\.(?!BaseItemExporter\b)\w*?\.", # Extension behavior is only modified through settings. Methods of # extension classes, as well as helper functions, are implementation # details that are not documented. - r'^scrapy\.extensions\.[a-z]\w*?\.[A-Z]\w*?\.', # methods - r'^scrapy\.extensions\.[a-z]\w*?\.[a-z]', # helper functions - + r"^scrapy\.extensions\.[a-z]\w*?\.[A-Z]\w*?\.", # methods + r"^scrapy\.extensions\.[a-z]\w*?\.[a-z]", # helper functions # Never documented before, and deprecated now. - r'^scrapy\.linkextractors\.FilteringLinkExtractor$', - + r"^scrapy\.linkextractors\.FilteringLinkExtractor$", # Implementation detail of LxmlLinkExtractor - r'^scrapy\.linkextractors\.lxmlhtml\.LxmlParserLinkExtractor', + r"^scrapy\.linkextractors\.lxmlhtml\.LxmlParserLinkExtractor", ] @@ -282,18 +275,18 @@ coverage_ignore_pyobjects = [ # ------------------------------------- intersphinx_mapping = { - 'attrs': ('https://www.attrs.org/en/stable/', None), - 'coverage': ('https://coverage.readthedocs.io/en/stable', None), - 'cryptography' : ('https://cryptography.io/en/latest/', None), - 'cssselect': ('https://cssselect.readthedocs.io/en/latest', None), - 'itemloaders': ('https://itemloaders.readthedocs.io/en/latest/', None), - 'pytest': ('https://docs.pytest.org/en/latest', None), - 'python': ('https://docs.python.org/3', None), - 'sphinx': ('https://www.sphinx-doc.org/en/master', None), - 'tox': ('https://tox.wiki/en/latest/', None), - 'twisted': ('https://docs.twisted.org/en/stable/', None), - 'twistedapi': ('https://docs.twisted.org/en/stable/api/', None), - 'w3lib': ('https://w3lib.readthedocs.io/en/latest', None), + "attrs": ("https://www.attrs.org/en/stable/", None), + "coverage": ("https://coverage.readthedocs.io/en/latest", None), + "cryptography": ("https://cryptography.io/en/latest/", None), + "cssselect": ("https://cssselect.readthedocs.io/en/latest", None), + "itemloaders": ("https://itemloaders.readthedocs.io/en/latest/", None), + "pytest": ("https://docs.pytest.org/en/latest", None), + "python": ("https://docs.python.org/3", None), + "sphinx": ("https://www.sphinx-doc.org/en/master", None), + "tox": ("https://tox.wiki/en/latest/", None), + "twisted": ("https://docs.twisted.org/en/stable/", None), + "twistedapi": ("https://docs.twisted.org/en/stable/api/", None), + "w3lib": ("https://w3lib.readthedocs.io/en/latest", None), } intersphinx_disabled_reftypes = [] @@ -313,16 +306,16 @@ hoverxref_role_types = { "setting": "tooltip", "signal": "tooltip", } -hoverxref_roles = ['command', 'reqmeta', 'setting', 'signal'] +hoverxref_roles = ["command", "reqmeta", "setting", "signal"] def setup(app): - app.connect('autodoc-skip-member', maybe_skip_member) + app.connect("autodoc-skip-member", maybe_skip_member) def maybe_skip_member(app, what, name, obj, skip, options): if not skip: # autodocs was generating a text "alias of" for the following members # https://github.com/sphinx-doc/sphinx/issues/4422 - return name in {'default_item_class', 'default_selector_class'} + return name in {"default_item_class", "default_selector_class"} return skip diff --git a/docs/conftest.py b/docs/conftest.py index a6dacd265..32f849a36 100644 --- a/docs/conftest.py +++ b/docs/conftest.py @@ -15,20 +15,20 @@ from scrapy.http.response.html import HtmlResponse def load_response(url: str, filename: str) -> HtmlResponse: - input_path = Path(__file__).parent / '_tests' / filename + input_path = Path(__file__).parent / "_tests" / filename return HtmlResponse(url, body=input_path.read_bytes()) def setup(namespace): - namespace['load_response'] = load_response + namespace["load_response"] = load_response pytest_collect_file = Sybil( parsers=[ DocTestParser(optionflags=ELLIPSIS | NORMALIZE_WHITESPACE), - PythonCodeBlockParser(future_imports=['print_function']), + PythonCodeBlockParser(future_imports=["print_function"]), skip, ], - pattern='*.rst', + pattern="*.rst", setup=setup, ).pytest() diff --git a/docs/contributing.rst b/docs/contributing.rst index 9cfe10012..2b3249601 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -11,10 +11,6 @@ Contributing to Scrapy There are many ways to contribute to Scrapy. Here are some of them: -* Blog about Scrapy. Tell the world how you're using Scrapy. This will help - newcomers with more examples and will help the Scrapy project to increase its - visibility. - * Report bugs and request features in the `issue tracker`_, trying to follow the guidelines detailed in `Reporting bugs`_ below. @@ -22,13 +18,16 @@ There are many ways to contribute to Scrapy. Here are some of them: :ref:`writing-patches` and `Submitting patches`_ below for details on how to write and submit a patch. +* Blog about Scrapy. Tell the world how you're using Scrapy. This will help + newcomers with more examples and will help the Scrapy project to increase its + visibility. + * Join the `Scrapy subreddit`_ and share your ideas on how to improve Scrapy. We're always open to suggestions. * Answer Scrapy questions at `Stack Overflow `__. - Reporting bugs ============== @@ -49,7 +48,7 @@ guidelines when you're going to report a new bug. (use "scrapy" tag). * check the `open issues`_ to see if the issue has already been reported. If it - has, don't dismiss the report, but check the ticket history and comments. If + has, don't dismiss the report, but check the ticket history and comments. If you have additional useful information, please leave a comment, or consider :ref:`sending a pull request ` with a fix. @@ -80,6 +79,13 @@ guidelines when you're going to report a new bug. Writing patches =============== +Scrapy has a list of `good first issues`_ and `help wanted issues`_ that you +can work on. These issues are a great way to get started with contributing to +Scrapy. If you're new to the codebase, you may want to focus on documentation +or testing-related issues, as they are always useful and can help you get +more familiar with the project. You can also check Scrapy's `test coverage`_ +to see which areas may benefit from more tests. + The better a patch is written, the higher the chances that it'll get accepted and the sooner it will be merged. Well-written patches should: @@ -169,16 +175,43 @@ Coding style Please follow these coding conventions when writing code for inclusion in Scrapy: -* Unless otherwise specified, follow :pep:`8`. - -* It's OK to use lines longer than 79 chars if it improves the code - readability. +* We use `black `_ for code formatting. + There is a hook in the pre-commit config + that will automatically format your code before every commit. You can also + run black manually with ``tox -e black``. * Don't put your name in the code you contribute; git provides enough metadata to identify author of the code. See https://help.github.com/en/github/using-git/setting-your-username-in-git for setup instructions. +.. _scrapy-pre-commit: + +Pre-commit +========== + +We use `pre-commit`_ to automatically address simple code issues before every +commit. + +.. _pre-commit: https://pre-commit.com/ + +After your create a local clone of your fork of the Scrapy repository: + +#. `Install pre-commit `_. + +#. On the root of your local clone of the Scrapy repository, run the following + command: + + .. code-block:: bash + + pre-commit install + +Now pre-commit will check your changes every time you create a Git commit. Upon +finding issues, pre-commit aborts your commit, and either fixes those issues +automatically, or only reports them to you. If it fixes those issues +automatically, creating your commit again should succeed. Otherwise, you may +need to address the corresponding issues manually first. + .. _documentation-policies: Documentation policies @@ -232,15 +265,15 @@ To run a specific test (say ``tests/test_loader.py``) use: To run the tests on a specific :doc:`tox ` environment, use ``-e `` with an environment name from ``tox.ini``. For example, to run -the tests with Python 3.7 use:: +the tests with Python 3.10 use:: - tox -e py37 + tox -e py310 You can also specify a comma-separated list of environments, and use :ref:`tox’s parallel mode ` to run the tests on multiple environments in parallel:: - tox -e py37,py38 -p auto + tox -e py39,py310 -p auto To pass command-line options to :doc:`pytest `, add them after ``--`` in your call to :doc:`tox `. Using ``--`` overrides the @@ -250,9 +283,9 @@ default positional arguments (``scrapy tests``) after ``--`` as well:: tox -- scrapy tests -x # stop after first failure You can also use the `pytest-xdist`_ plugin. For example, to run all tests on -the Python 3.7 :doc:`tox ` environment using all your CPU cores:: +the Python 3.10 :doc:`tox ` environment using all your CPU cores:: - tox -e py37 -- scrapy tests -n auto + tox -e py310 -- scrapy tests -n auto To see coverage report install :doc:`coverage ` (``pip install coverage``) and run: @@ -287,3 +320,6 @@ And their unit-tests are in:: .. _PEP 257: https://www.python.org/dev/peps/pep-0257/ .. _pull request: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request .. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist +.. _good first issues: https://github.com/scrapy/scrapy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 +.. _help wanted issues: https://github.com/scrapy/scrapy/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22 +.. _test coverage: https://app.codecov.io/gh/scrapy/scrapy diff --git a/docs/faq.rst b/docs/faq.rst index 8a9ba809b..20dd814df 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -35,8 +35,10 @@ for parsing HTML responses in Scrapy callbacks. You just have to feed the response's body into a ``BeautifulSoup`` object and extract whatever data you need from it. -Here's an example spider using BeautifulSoup API, with ``lxml`` as the HTML parser:: +Here's an example spider using BeautifulSoup API, with ``lxml`` as the HTML parser: +.. skip: next +.. code-block:: python from bs4 import BeautifulSoup import scrapy @@ -45,17 +47,12 @@ Here's an example spider using BeautifulSoup API, with ``lxml`` as the HTML pars class ExampleSpider(scrapy.Spider): name = "example" allowed_domains = ["example.com"] - start_urls = ( - 'http://www.example.com/', - ) + start_urls = ("http://www.example.com/",) def parse(self, response): # use lxml to get decent HTML parsing speed - soup = BeautifulSoup(response.text, 'lxml') - yield { - "url": response.url, - "title": soup.h1.string - } + soup = BeautifulSoup(response.text, "lxml") + yield {"url": response.url, "title": soup.h1.string} .. note:: @@ -109,11 +106,13 @@ basically means that it crawls in `DFO order`_. This order is more convenient in most cases. If you do want to crawl in true `BFO order`_, you can do it by -setting the following settings:: +setting the following settings: + +.. code-block:: python DEPTH_PRIORITY = 1 - SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue' - SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue' + SCHEDULER_DISK_QUEUE = "scrapy.squeues.PickleFifoDiskQueue" + SCHEDULER_MEMORY_QUEUE = "scrapy.squeues.FifoMemoryQueue" While pending requests are below the configured values of :setting:`CONCURRENT_REQUESTS`, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` or @@ -159,11 +158,13 @@ See also other suggestions at `StackOverflow`_. .. note:: Remember to disable :class:`scrapy.spidermiddlewares.offsite.OffsiteMiddleware` when you enable - your custom implementation:: + your custom implementation: + + .. code-block:: python SPIDER_MIDDLEWARES = { - 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': None, - 'myproject.middlewares.CustomOffsiteMiddleware': 500, + "scrapy.spidermiddlewares.offsite.OffsiteMiddleware": None, + "myproject.middlewares.CustomOffsiteMiddleware": 500, } .. _meet the installation requirements: https://github.com/andreasvc/pyre2#installation @@ -230,16 +231,20 @@ Can I return (Twisted) deferreds from signal handlers? Some signals support returning deferreds from their handlers, others don't. See the :ref:`topics-signals-ref` to know which ones. -What does the response status code 999 means? ---------------------------------------------- +What does the response status code 999 mean? +-------------------------------------------- 999 is a custom response status code used by Yahoo sites to throttle requests. Try slowing down the crawling speed by using a download delay of ``2`` (or -higher) in your spider:: +higher) in your spider: + +.. code-block:: python + + from scrapy.spiders import CrawlSpider + class MySpider(CrawlSpider): - - name = 'myspider' + name = "myspider" download_delay = 2 @@ -351,19 +356,21 @@ How to split an item into multiple items in an item pipeline? input item. :ref:`Create a spider middleware ` instead, and use its :meth:`~scrapy.spidermiddlewares.SpiderMiddleware.process_spider_output` -method for this purpose. For example:: +method for this purpose. For example: + +.. code-block:: python from copy import deepcopy from itemadapter import is_item, ItemAdapter - class MultiplyItemsMiddleware: + class MultiplyItemsMiddleware: def process_spider_output(self, response, result, spider): for item in result: if is_item(item): adapter = ItemAdapter(item) - for _ in range(adapter['multiply_by']): + for _ in range(adapter["multiply_by"]): yield deepcopy(item) Does Scrapy support IPv6 addresses? @@ -413,4 +420,4 @@ See :issue:`2680`. .. _user agents: https://en.wikipedia.org/wiki/User_agent .. _LIFO: https://en.wikipedia.org/wiki/Stack_(abstract_data_type) .. _DFO order: https://en.wikipedia.org/wiki/Depth-first_search -.. _BFO order: https://en.wikipedia.org/wiki/Breadth-first_search \ No newline at end of file +.. _BFO order: https://en.wikipedia.org/wiki/Breadth-first_search diff --git a/docs/index.rst b/docs/index.rst index 5404969e0..8798aebd1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -222,6 +222,7 @@ Extending Scrapy :hidden: topics/architecture + topics/addons topics/downloader-middleware topics/spider-middleware topics/extensions @@ -235,6 +236,9 @@ Extending Scrapy :doc:`topics/architecture` Understand the Scrapy architecture. +:doc:`topics/addons` + Enable and configure third-party extensions. + :doc:`topics/downloader-middleware` Customize how pages get requested and downloaded. diff --git a/docs/intro/install.rst b/docs/intro/install.rst index 2c2079f68..c90c1d2bf 100644 --- a/docs/intro/install.rst +++ b/docs/intro/install.rst @@ -9,7 +9,7 @@ Installation guide Supported Python versions ========================= -Scrapy requires Python 3.7+, either the CPython implementation (default) or +Scrapy requires Python 3.8+, either the CPython implementation (default) or the PyPy implementation (see :ref:`python:implementations`). .. _intro-install-scrapy: diff --git a/docs/intro/overview.rst b/docs/intro/overview.rst index cfa6bfa83..542760b4f 100644 --- a/docs/intro/overview.rst +++ b/docs/intro/overview.rst @@ -20,22 +20,24 @@ In order to show you what Scrapy brings to the table, we'll walk you through an example of a Scrapy Spider using the simplest way to run a spider. Here's the code for a spider that scrapes famous quotes from website -https://quotes.toscrape.com, following the pagination:: +https://quotes.toscrape.com, following the pagination: + +.. code-block:: python import scrapy class QuotesSpider(scrapy.Spider): - name = 'quotes' + name = "quotes" start_urls = [ - 'https://quotes.toscrape.com/tag/humor/', + "https://quotes.toscrape.com/tag/humor/", ] def parse(self, response): - for quote in response.css('div.quote'): + for quote in response.css("div.quote"): yield { - 'author': quote.xpath('span/small/text()').get(), - 'text': quote.css('span.text::text').get(), + "author": quote.xpath("span/small/text()").get(), + "text": quote.css("span.text::text").get(), } next_page = response.css('li.next a::attr("href")').get() diff --git a/docs/intro/tutorial.rst b/docs/intro/tutorial.rst index 901a170b4..8ea98f29b 100644 --- a/docs/intro/tutorial.rst +++ b/docs/intro/tutorial.rst @@ -83,7 +83,9 @@ optionally how to follow links in the pages, and how to parse the downloaded page content to extract data. This is the code for our first Spider. Save it in a file named -``quotes_spider.py`` under the ``tutorial/spiders`` directory in your project:: +``quotes_spider.py`` under the ``tutorial/spiders`` directory in your project: + +.. code-block:: python from pathlib import Path @@ -95,17 +97,17 @@ This is the code for our first Spider. Save it in a file named def start_requests(self): urls = [ - 'https://quotes.toscrape.com/page/1/', - 'https://quotes.toscrape.com/page/2/', + "https://quotes.toscrape.com/page/1/", + "https://quotes.toscrape.com/page/2/", ] for url in urls: yield scrapy.Request(url=url, callback=self.parse) def parse(self, response): page = response.url.split("/")[-2] - filename = f'quotes-{page}.html' + filename = f"quotes-{page}.html" Path(filename).write_bytes(response.body) - self.log(f'Saved file {filename}') + self.log(f"Saved file {filename}") As you can see, our Spider subclasses :class:`scrapy.Spider ` @@ -177,7 +179,9 @@ that generates :class:`scrapy.Request ` objects from URLs, you can just define a :attr:`~scrapy.Spider.start_urls` class attribute with a list of URLs. This list will then be used by the default implementation of :meth:`~scrapy.Spider.start_requests` to create the initial requests -for your spider:: +for your spider. + +.. code-block:: python from pathlib import Path @@ -187,13 +191,13 @@ for your spider:: class QuotesSpider(scrapy.Spider): name = "quotes" start_urls = [ - 'https://quotes.toscrape.com/page/1/', - 'https://quotes.toscrape.com/page/2/', + "https://quotes.toscrape.com/page/1/", + "https://quotes.toscrape.com/page/2/", ] def parse(self, response): page = response.url.split("/")[-2] - filename = f'quotes-{page}.html' + filename = f"quotes-{page}.html" Path(filename).write_bytes(response.body) The :meth:`~scrapy.Spider.parse` method will be called to handle each @@ -245,8 +249,10 @@ object: response = load_response('https://quotes.toscrape.com/page/1/', 'quotes1.html') ->>> response.css('title') -[] +.. code-block:: pycon + + >>> response.css("title") + [] The result of running ``response.css('title')`` is a list-like object called :class:`~scrapy.selector.SelectorList`, which represents a list of @@ -256,42 +262,54 @@ data. To extract the text from the title above, you can do: ->>> response.css('title::text').getall() -['Quotes to Scrape'] +.. code-block:: pycon + + >>> response.css("title::text").getall() + ['Quotes to Scrape'] There are two things to note here: one is that we've added ``::text`` to the CSS query, to mean we want to select only the text elements directly inside ```` element. If we don't specify ``::text``, we'd get the full title element, including its tags: ->>> response.css('title').getall() -['<title>Quotes to Scrape'] +.. code-block:: pycon + + >>> response.css("title").getall() + ['Quotes to Scrape'] The other thing is that the result of calling ``.getall()`` is a list: it is possible that a selector returns more than one result, so we extract them all. When you know you just want the first result, as in this case, you can do: ->>> response.css('title::text').get() -'Quotes to Scrape' +.. code-block:: pycon + + >>> response.css("title::text").get() + 'Quotes to Scrape' As an alternative, you could've written: ->>> response.css('title::text')[0].get() -'Quotes to Scrape' +.. code-block:: pycon + + >>> response.css("title::text")[0].get() + 'Quotes to Scrape' Accessing an index on a :class:`~scrapy.selector.SelectorList` instance will -raise an :exc:`IndexError` exception if there are no results:: +raise an :exc:`IndexError` exception if there are no results: - >>> response.css('noelement')[0].get() +.. code-block:: pycon + + >>> response.css("noelement")[0].get() Traceback (most recent call last): ... IndexError: list index out of range You might want to use ``.get()`` directly on the :class:`~scrapy.selector.SelectorList` instance instead, which returns ``None`` -if there are no results:: +if there are no results: ->>> response.css("noelement").get() +.. code-block:: pycon + + >>> response.css("noelement").get() There's a lesson here: for most scraping code, you want it to be resilient to errors due to things not being found on a page, so that even if some parts fail @@ -302,14 +320,16 @@ Besides the :meth:`~scrapy.selector.SelectorList.getall` and the :meth:`~scrapy.selector.SelectorList.re` method to extract using :doc:`regular expressions `: ->>> response.css('title::text').re(r'Quotes.*') -['Quotes to Scrape'] ->>> response.css('title::text').re(r'Q\w+') -['Quotes'] ->>> response.css('title::text').re(r'(\w+) to (\w+)') -['Quotes', 'Scrape'] +.. code-block:: pycon -In order to find the proper CSS selectors to use, you might find useful opening + >>> response.css("title::text").re(r"Quotes.*") + ['Quotes to Scrape'] + >>> response.css("title::text").re(r"Q\w+") + ['Quotes'] + >>> response.css("title::text").re(r"(\w+) to (\w+)") + ['Quotes', 'Scrape'] + +In order to find the proper CSS selectors to use, you might find it useful to open the response page from the shell in your web browser using ``view(response)``. You can use your browser's developer tools to inspect the HTML and come up with a selector (see :ref:`topics-developer-tools`). @@ -325,10 +345,12 @@ XPath: a brief intro Besides `CSS`_, Scrapy selectors also support using `XPath`_ expressions: ->>> response.xpath('//title') -[] ->>> response.xpath('//title/text()').get() -'Quotes to Scrape' +.. code-block:: pycon + + >>> response.xpath("//title") + [] + >>> response.xpath("//title/text()").get() + 'Quotes to Scrape' XPath expressions are very powerful, and are the foundation of Scrapy Selectors. In fact, CSS selectors are converted to XPath under-the-hood. You @@ -385,33 +407,41 @@ we want:: We get a list of selectors for the quote HTML elements with: ->>> response.css("div.quote") -[, - , - ...] +.. code-block:: pycon + + >>> response.css("div.quote") + [, + , + ...] Each of the selectors returned by the query above allows us to run further queries over their sub-elements. Let's assign the first selector to a variable, so that we can run our CSS selectors directly on a particular quote: ->>> quote = response.css("div.quote")[0] +.. code-block:: pycon + + >>> quote = response.css("div.quote")[0] Now, let's extract ``text``, ``author`` and the ``tags`` from that quote using the ``quote`` object we just created: ->>> text = quote.css("span.text::text").get() ->>> text -'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”' ->>> author = quote.css("small.author::text").get() ->>> author -'Albert Einstein' +.. code-block:: pycon + + >>> text = quote.css("span.text::text").get() + >>> text + '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”' + >>> author = quote.css("small.author::text").get() + >>> author + 'Albert Einstein' Given that the tags are a list of strings, we can use the ``.getall()`` method to get all of them: ->>> tags = quote.css("div.tags a.tag::text").getall() ->>> tags -['change', 'deep-thoughts', 'thinking', 'world'] +.. code-block:: pycon + + >>> tags = quote.css("div.tags a.tag::text").getall() + >>> tags + ['change', 'deep-thoughts', 'thinking', 'world'] .. invisible-code-block: python @@ -420,14 +450,17 @@ to get all of them: Having figured out how to extract each bit, we can now iterate over all the quotes elements and put them together into a Python dictionary: ->>> for quote in response.css("div.quote"): -... text = quote.css("span.text::text").get() -... author = quote.css("small.author::text").get() -... tags = quote.css("div.tags a.tag::text").getall() -... print(dict(text=text, author=author, tags=tags)) -{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']} -{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']} -... +.. code-block:: pycon + + >>> for quote in response.css("div.quote"): + ... text = quote.css("span.text::text").get() + ... author = quote.css("small.author::text").get() + ... tags = quote.css("div.tags a.tag::text").getall() + ... print(dict(text=text, author=author, tags=tags)) + ... + {'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']} + {'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']} + ... Extracting data in our spider ----------------------------- @@ -438,7 +471,9 @@ extraction logic above into our spider. A Scrapy spider typically generates many dictionaries containing the data extracted from the page. To do that, we use the ``yield`` Python keyword -in the callback, as you can see below:: +in the callback, as you can see below: + +.. code-block:: python import scrapy @@ -446,19 +481,27 @@ in the callback, as you can see below:: class QuotesSpider(scrapy.Spider): name = "quotes" start_urls = [ - 'https://quotes.toscrape.com/page/1/', - 'https://quotes.toscrape.com/page/2/', + "https://quotes.toscrape.com/page/1/", + "https://quotes.toscrape.com/page/2/", ] def parse(self, response): - for quote in response.css('div.quote'): + for quote in response.css("div.quote"): yield { - 'text': quote.css('span.text::text').get(), - 'author': quote.css('small.author::text').get(), - 'tags': quote.css('div.tags a.tag::text').getall(), + "text": quote.css("span.text::text").get(), + "author": quote.css("small.author::text").get(), + "tags": quote.css("div.tags a.tag::text").getall(), } -If you run this spider, it will output the extracted data with the log:: +To run this spider, exit the scrapy shell by entering:: + + quit() + +Then, run:: + + scrapy crawl quotes + +Now, it should output the extracted data with the log:: 2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/page/1/> {'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'} @@ -533,17 +576,23 @@ This gets the anchor element, but we want the attribute ``href``. For that, Scrapy supports a CSS extension that lets you select the attribute contents, like this: ->>> response.css('li.next a::attr(href)').get() -'/page/2/' +.. code-block:: pycon + + >>> response.css("li.next a::attr(href)").get() + '/page/2/' There is also an ``attrib`` property available (see :ref:`selecting-attributes` for more): ->>> response.css('li.next a').attrib['href'] -'/page/2/' +.. code-block:: pycon + + >>> response.css("li.next a").attrib["href"] + '/page/2/' Let's see now our spider modified to recursively follow the link to the next -page, extracting data from it:: +page, extracting data from it: + +.. code-block:: python import scrapy @@ -551,18 +600,18 @@ page, extracting data from it:: class QuotesSpider(scrapy.Spider): name = "quotes" start_urls = [ - 'https://quotes.toscrape.com/page/1/', + "https://quotes.toscrape.com/page/1/", ] def parse(self, response): - for quote in response.css('div.quote'): + for quote in response.css("div.quote"): yield { - 'text': quote.css('span.text::text').get(), - 'author': quote.css('small.author::text').get(), - 'tags': quote.css('div.tags a.tag::text').getall(), + "text": quote.css("span.text::text").get(), + "author": quote.css("small.author::text").get(), + "tags": quote.css("div.tags a.tag::text").getall(), } - next_page = response.css('li.next a::attr(href)').get() + next_page = response.css("li.next a::attr(href)").get() if next_page is not None: next_page = response.urljoin(next_page) yield scrapy.Request(next_page, callback=self.parse) @@ -594,7 +643,9 @@ A shortcut for creating Requests -------------------------------- As a shortcut for creating Request objects you can use -:meth:`response.follow `:: +:meth:`response.follow `: + +.. code-block:: python import scrapy @@ -602,18 +653,18 @@ As a shortcut for creating Request objects you can use class QuotesSpider(scrapy.Spider): name = "quotes" start_urls = [ - 'https://quotes.toscrape.com/page/1/', + "https://quotes.toscrape.com/page/1/", ] def parse(self, response): - for quote in response.css('div.quote'): + for quote in response.css("div.quote"): yield { - 'text': quote.css('span.text::text').get(), - 'author': quote.css('span small::text').get(), - 'tags': quote.css('div.tags a.tag::text').getall(), + "text": quote.css("span.text::text").get(), + "author": quote.css("span small::text").get(), + "tags": quote.css("div.tags a.tag::text").getall(), } - next_page = response.css('li.next a::attr(href)').get() + next_page = response.css("li.next a::attr(href)").get() if next_page is not None: yield response.follow(next_page, callback=self.parse) @@ -621,58 +672,72 @@ Unlike scrapy.Request, ``response.follow`` supports relative URLs directly - no need to call urljoin. Note that ``response.follow`` just returns a Request instance; you still have to yield this Request. -You can also pass a selector to ``response.follow`` instead of a string; -this selector should extract necessary attributes:: +.. skip: start - for href in response.css('ul.pager a::attr(href)'): +You can also pass a selector to ``response.follow`` instead of a string; +this selector should extract necessary attributes: + +.. code-block:: python + + for href in response.css("ul.pager a::attr(href)"): yield response.follow(href, callback=self.parse) For ```` elements there is a shortcut: ``response.follow`` uses their href -attribute automatically. So the code can be shortened further:: +attribute automatically. So the code can be shortened further: - for a in response.css('ul.pager a'): +.. code-block:: python + + for a in response.css("ul.pager a"): yield response.follow(a, callback=self.parse) To create multiple requests from an iterable, you can use -:meth:`response.follow_all ` instead:: +:meth:`response.follow_all ` instead: - anchors = response.css('ul.pager a') +.. code-block:: python + + anchors = response.css("ul.pager a") yield from response.follow_all(anchors, callback=self.parse) -or, shortening it further:: +or, shortening it further: - yield from response.follow_all(css='ul.pager a', callback=self.parse) +.. code-block:: python + + yield from response.follow_all(css="ul.pager a", callback=self.parse) + +.. skip: end More examples and patterns -------------------------- Here is another spider that illustrates callbacks and following links, -this time for scraping author information:: +this time for scraping author information: + +.. code-block:: python import scrapy class AuthorSpider(scrapy.Spider): - name = 'author' + name = "author" - start_urls = ['https://quotes.toscrape.com/'] + start_urls = ["https://quotes.toscrape.com/"] def parse(self, response): - author_page_links = response.css('.author + a') + author_page_links = response.css(".author + a") yield from response.follow_all(author_page_links, self.parse_author) - pagination_links = response.css('li.next a') + pagination_links = response.css("li.next a") yield from response.follow_all(pagination_links, self.parse) def parse_author(self, response): def extract_with_css(query): - return response.css(query).get(default='').strip() + return response.css(query).get(default="").strip() yield { - 'name': extract_with_css('h3.author-title::text'), - 'birthdate': extract_with_css('.author-born-date::text'), - 'bio': extract_with_css('.author-description::text'), + "name": extract_with_css("h3.author-title::text"), + "birthdate": extract_with_css(".author-born-date::text"), + "bio": extract_with_css(".author-description::text"), } This spider will start from the main page, it will follow all the links to the @@ -720,7 +785,9 @@ spider attributes by default. In this example, the value provided for the ``tag`` argument will be available via ``self.tag``. You can use this to make your spider fetch only quotes -with a specific tag, building the URL based on the argument:: +with a specific tag, building the URL based on the argument: + +.. code-block:: python import scrapy @@ -729,20 +796,20 @@ with a specific tag, building the URL based on the argument:: name = "quotes" def start_requests(self): - url = 'https://quotes.toscrape.com/' - tag = getattr(self, 'tag', None) + url = "https://quotes.toscrape.com/" + tag = getattr(self, "tag", None) if tag is not None: - url = url + 'tag/' + tag + url = url + "tag/" + tag yield scrapy.Request(url, self.parse) def parse(self, response): - for quote in response.css('div.quote'): + for quote in response.css("div.quote"): yield { - 'text': quote.css('span.text::text').get(), - 'author': quote.css('small.author::text').get(), + "text": quote.css("span.text::text").get(), + "author": quote.css("small.author::text").get(), } - next_page = response.css('li.next a::attr(href)').get() + next_page = response.css("li.next a::attr(href)").get() if next_page is not None: yield response.follow(next_page, self.parse) diff --git a/docs/news.rst b/docs/news.rst index c97de0ed8..65d9c5181 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -3,6 +3,639 @@ Release notes ============= +.. _release-2.11.0: + +Scrapy 2.11.0 (2023-09-18) +-------------------------- + +Highlights: + +- Spiders can now modify :ref:`settings ` in their + :meth:`~scrapy.Spider.from_crawler` methods, e.g. based on :ref:`spider + arguments `. + +- Periodic logging of stats. + + +Backward-incompatible changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Most of the initialization of :class:`scrapy.crawler.Crawler` instances is + now done in :meth:`~scrapy.crawler.Crawler.crawl`, so the state of + instances before that method is called is now different compared to older + Scrapy versions. We do not recommend using the + :class:`~scrapy.crawler.Crawler` instances before + :meth:`~scrapy.crawler.Crawler.crawl` is called. (:issue:`6038`) + +- :meth:`scrapy.Spider.from_crawler` is now called before the initialization + of various components previously initialized in + :meth:`scrapy.crawler.Crawler.__init__` and before the settings are + finalized and frozen. This change was needed to allow changing the settings + in :meth:`scrapy.Spider.from_crawler`. If you want to access the final + setting values and the initialized :class:`~scrapy.crawler.Crawler` + attributes in the spider code as early as possible you can do this in + :meth:`~scrapy.Spider.start_requests` or in a handler of the + :signal:`engine_started` signal. (:issue:`6038`) + +- The :meth:`TextResponse.json ` method now + requires the response to be in a valid JSON encoding (UTF-8, UTF-16, or + UTF-32). If you need to deal with JSON documents in an invalid encoding, + use ``json.loads(response.text)`` instead. (:issue:`6016`) + +- :class:`~scrapy.exporters.PythonItemExporter` used the binary output by + default but it no longer does. (:issue:`6006`, :issue:`6007`) + +Deprecation removals +~~~~~~~~~~~~~~~~~~~~ + +- Removed the binary export mode of + :class:`~scrapy.exporters.PythonItemExporter`, deprecated in Scrapy 1.1.0. + (:issue:`6006`, :issue:`6007`) + + .. note:: If you are using this Scrapy version on Scrapy Cloud with a stack + that includes an older Scrapy version and get a "TypeError: + Unexpected options: binary" error, you may need to add + ``scrapinghub-entrypoint-scrapy >= 0.14.1`` to your project + requirements or switch to a stack that includes Scrapy 2.11. + +- Removed the ``CrawlerRunner.spiders`` attribute, deprecated in Scrapy + 1.0.0, use :attr:`CrawlerRunner.spider_loader + ` instead. (:issue:`6010`) + +Deprecations +~~~~~~~~~~~~ + +- Running :meth:`~scrapy.crawler.Crawler.crawl` more than once on the same + :class:`scrapy.crawler.Crawler` instance is now deprecated. (:issue:`1587`, + :issue:`6040`) + +New features +~~~~~~~~~~~~ + +- Spiders can now modify settings in their + :meth:`~scrapy.Spider.from_crawler` method, e.g. based on :ref:`spider + arguments `. (:issue:`1305`, :issue:`1580`, :issue:`2392`, + :issue:`3663`, :issue:`6038`) + +- Added the :class:`~scrapy.extensions.periodic_log.PeriodicLog` extension + which can be enabled to log stats and/or their differences periodically. + (:issue:`5926`) + +- Optimized the memory usage in :meth:`TextResponse.json + ` by removing unnecessary body decoding. + (:issue:`5968`, :issue:`6016`) + +- Links to ``.webp`` files are now ignored by :ref:`link extractors + `. (:issue:`6021`) + +Bug fixes +~~~~~~~~~ + +- Fixed logging enabled add-ons. (:issue:`6036`) + +- Fixed :class:`~scrapy.mail.MailSender` producing invalid message bodies + when the ``charset`` argument is passed to + :meth:`~scrapy.mail.MailSender.send`. (:issue:`5096`, :issue:`5118`) + +- Fixed an exception when accessing ``self.EXCEPTIONS_TO_RETRY`` from a + subclass of :class:`~scrapy.downloadermiddlewares.retry.RetryMiddleware`. + (:issue:`6049`, :issue:`6050`) + +- :meth:`scrapy.settings.BaseSettings.getdictorlist`, used to parse + :setting:`FEED_EXPORT_FIELDS`, now handles tuple values. (:issue:`6011`, + :issue:`6013`) + +- Calls to ``datetime.utcnow()``, no longer recommended to be used, have been + replaced with calls to ``datetime.now()`` with a timezone. (:issue:`6014`) + +Documentation +~~~~~~~~~~~~~ + +- Updated a deprecated function call in a pipeline example. (:issue:`6008`, + :issue:`6009`) + +Quality assurance +~~~~~~~~~~~~~~~~~ + +- Extended typing hints. (:issue:`6003`, :issue:`6005`, :issue:`6031`, + :issue:`6034`) + +- Pinned brotli_ to 1.0.9 for the PyPy tests as 1.1.0 breaks them. + (:issue:`6044`, :issue:`6045`) + +- Other CI and pre-commit improvements. (:issue:`6002`, :issue:`6013`, + :issue:`6046`) + +.. _release-2.10.1: + +Scrapy 2.10.1 (2023-08-30) +-------------------------- + +Marked ``Twisted >= 23.8.0`` as unsupported. (:issue:`6024`, :issue:`6026`) + +.. _release-2.10.0: + +Scrapy 2.10.0 (2023-08-04) +-------------------------- + +Highlights: + +- Added Python 3.12 support, dropped Python 3.7 support. + +- The new add-ons framework simplifies configuring 3rd-party components that + support it. + +- Exceptions to retry can now be configured. + +- Many fixes and improvements for feed exports. + +Modified requirements +~~~~~~~~~~~~~~~~~~~~~ + +- Dropped support for Python 3.7. (:issue:`5953`) + +- Added support for the upcoming Python 3.12. (:issue:`5984`) + +- Minimum versions increased for these dependencies: + + - lxml_: 4.3.0 → 4.4.1 + + - cryptography_: 3.4.6 → 36.0.0 + +- ``pkg_resources`` is no longer used. (:issue:`5956`, :issue:`5958`) + +- boto3_ is now recommended instead of botocore_ for exporting to S3. + (:issue:`5833`). + +Backward-incompatible changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- The value of the :setting:`FEED_STORE_EMPTY` setting is now ``True`` + instead of ``False``. In earlier Scrapy versions empty files were created + even when this setting was ``False`` (which was a bug that is now fixed), + so the new default should keep the old behavior. (:issue:`872`, + :issue:`5847`) + +Deprecation removals +~~~~~~~~~~~~~~~~~~~~ + +- When a function is assigned to the :setting:`FEED_URI_PARAMS` setting, + returning ``None`` or modifying the ``params`` input parameter, deprecated + in Scrapy 2.6, is no longer supported. (:issue:`5994`, :issue:`5996`) + +- The ``scrapy.utils.reqser`` module, deprecated in Scrapy 2.6, is removed. + (:issue:`5994`, :issue:`5996`) + +- The ``scrapy.squeues`` classes ``PickleFifoDiskQueueNonRequest``, + ``PickleLifoDiskQueueNonRequest``, ``MarshalFifoDiskQueueNonRequest``, + and ``MarshalLifoDiskQueueNonRequest``, deprecated in + Scrapy 2.6, are removed. (:issue:`5994`, :issue:`5996`) + +- The property ``open_spiders`` and the methods ``has_capacity`` and + ``schedule`` of :class:`scrapy.core.engine.ExecutionEngine`, + deprecated in Scrapy 2.6, are removed. (:issue:`5994`, :issue:`5998`) + +- Passing a ``spider`` argument to the + :meth:`~scrapy.core.engine.ExecutionEngine.spider_is_idle`, + :meth:`~scrapy.core.engine.ExecutionEngine.crawl` and + :meth:`~scrapy.core.engine.ExecutionEngine.download` methods of + :class:`scrapy.core.engine.ExecutionEngine`, deprecated in Scrapy 2.6, is + no longer supported. (:issue:`5994`, :issue:`5998`) + +Deprecations +~~~~~~~~~~~~ + +- :class:`scrapy.utils.datatypes.CaselessDict` is deprecated, use + :class:`scrapy.utils.datatypes.CaseInsensitiveDict` instead. + (:issue:`5146`) + +- Passing the ``custom`` argument to + :func:`scrapy.utils.conf.build_component_list` is deprecated, it was used + in the past to merge ``FOO`` and ``FOO_BASE`` setting values but now Scrapy + uses :func:`scrapy.settings.BaseSettings.getwithbase` to do the same. + Code that uses this argument and cannot be switched to ``getwithbase()`` + can be switched to merging the values explicitly. (:issue:`5726`, + :issue:`5923`) + +New features +~~~~~~~~~~~~ + +- Added support for :ref:`Scrapy add-ons `. (:issue:`5950`) + +- Added the :setting:`RETRY_EXCEPTIONS` setting that configures which + exceptions will be retried by + :class:`~scrapy.downloadermiddlewares.retry.RetryMiddleware`. + (:issue:`2701`, :issue:`5929`) + +- Added the possiiblity to close the spider if no items were produced in the + specified time, configured by :setting:`CLOSESPIDER_TIMEOUT_NO_ITEM`. + (:issue:`5979`) + +- Added support for the :setting:`AWS_REGION_NAME` setting to feed exports. + (:issue:`5980`) + +- Added support for using :class:`pathlib.Path` objects that refer to + absolute Windows paths in the :setting:`FEEDS` setting. (:issue:`5939`) + +Bug fixes +~~~~~~~~~ + +- Fixed creating empty feeds even with ``FEED_STORE_EMPTY=False``. + (:issue:`872`, :issue:`5847`) + +- Fixed using absolute Windows paths when specifying output files. + (:issue:`5969`, :issue:`5971`) + +- Fixed problems with uploading large files to S3 by switching to multipart + uploads (requires boto3_). (:issue:`960`, :issue:`5735`, :issue:`5833`) + +- Fixed the JSON exporter writing extra commas when some exceptions occur. + (:issue:`3090`, :issue:`5952`) + +- Fixed the "read of closed file" error in the CSV exporter. (:issue:`5043`, + :issue:`5705`) + +- Fixed an error when a component added by the class object throws + :exc:`~scrapy.exceptions.NotConfigured` with a message. (:issue:`5950`, + :issue:`5992`) + +- Added the missing :meth:`scrapy.settings.BaseSettings.pop` method. + (:issue:`5959`, :issue:`5960`, :issue:`5963`) + +- Added :class:`~scrapy.utils.datatypes.CaseInsensitiveDict` as a replacement + for :class:`~scrapy.utils.datatypes.CaselessDict` that fixes some API + inconsistencies. (:issue:`5146`) + +Documentation +~~~~~~~~~~~~~ + +- Documented :meth:`scrapy.Spider.update_settings`. (:issue:`5745`, + :issue:`5846`) + +- Documented possible problems with early Twisted reactor installation and + their solutions. (:issue:`5981`, :issue:`6000`) + +- Added examples of making additional requests in callbacks. (:issue:`5927`) + +- Improved the feed export docs. (:issue:`5579`, :issue:`5931`) + +- Clarified the docs about request objects on redirection. (:issue:`5707`, + :issue:`5937`) + +Quality assurance +~~~~~~~~~~~~~~~~~ + +- Added support for running tests against the installed Scrapy version. + (:issue:`4914`, :issue:`5949`) + +- Extended typing hints. (:issue:`5925`, :issue:`5977`) + +- Fixed the ``test_utils_asyncio.AsyncioTest.test_set_asyncio_event_loop`` + test. (:issue:`5951`) + +- Fixed the ``test_feedexport.BatchDeliveriesTest.test_batch_path_differ`` + test on Windows. (:issue:`5847`) + +- Enabled CI runs for Python 3.11 on Windows. (:issue:`5999`) + +- Simplified skipping tests that depend on ``uvloop``. (:issue:`5984`) + +- Fixed the ``extra-deps-pinned`` tox env. (:issue:`5948`) + +- Implemented cleanups. (:issue:`5965`, :issue:`5986`) + +.. _release-2.9.0: + +Scrapy 2.9.0 (2023-05-08) +------------------------- + +Highlights: + +- Per-domain download settings. +- Compatibility with new cryptography_ and new parsel_. +- JMESPath selectors from the new parsel_. +- Bug fixes. + +Deprecations +~~~~~~~~~~~~ + +- :class:`scrapy.extensions.feedexport._FeedSlot` is renamed to + :class:`scrapy.extensions.feedexport.FeedSlot` and the old name is + deprecated. (:issue:`5876`) + +New features +~~~~~~~~~~~~ + +- Settings corresponding to :setting:`DOWNLOAD_DELAY`, + :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` and + :setting:`RANDOMIZE_DOWNLOAD_DELAY` can now be set on a per-domain basis + via the new :setting:`DOWNLOAD_SLOTS` setting. (:issue:`5328`) + +- Added :meth:`TextResponse.jmespath`, a shortcut for JMESPath selectors + available since parsel_ 1.8.1. (:issue:`5894`, :issue:`5915`) + +- Added :signal:`feed_slot_closed` and :signal:`feed_exporter_closed` + signals. (:issue:`5876`) + +- Added :func:`scrapy.utils.request.request_to_curl`, a function to produce a + curl command from a :class:`~scrapy.Request` object. (:issue:`5892`) + +- Values of :setting:`FILES_STORE` and :setting:`IMAGES_STORE` can now be + :class:`pathlib.Path` instances. (:issue:`5801`) + +Bug fixes +~~~~~~~~~ + +- Fixed a warning with Parsel 1.8.1+. (:issue:`5903`, :issue:`5918`) + +- Fixed an error when using feed postprocessing with S3 storage. + (:issue:`5500`, :issue:`5581`) + +- Added the missing :meth:`scrapy.settings.BaseSettings.setdefault` method. + (:issue:`5811`, :issue:`5821`) + +- Fixed an error when using cryptography_ 40.0.0+ and + :setting:`DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING` is enabled. + (:issue:`5857`, :issue:`5858`) + +- The checksums returned by :class:`~scrapy.pipelines.files.FilesPipeline` + for files on Google Cloud Storage are no longer Base64-encoded. + (:issue:`5874`, :issue:`5891`) + +- :func:`scrapy.utils.request.request_from_curl` now supports $-prefixed + string values for the curl ``--data-raw`` argument, which are produced by + browsers for data that includes certain symbols. (:issue:`5899`, + :issue:`5901`) + +- The :command:`parse` command now also works with async generator callbacks. + (:issue:`5819`, :issue:`5824`) + +- The :command:`genspider` command now properly works with HTTPS URLs. + (:issue:`3553`, :issue:`5808`) + +- Improved handling of asyncio loops. (:issue:`5831`, :issue:`5832`) + +- :class:`LinkExtractor ` + now skips certain malformed URLs instead of raising an exception. + (:issue:`5881`) + +- :func:`scrapy.utils.python.get_func_args` now supports more types of + callables. (:issue:`5872`, :issue:`5885`) + +- Fixed an error when processing non-UTF8 values of ``Content-Type`` headers. + (:issue:`5914`, :issue:`5917`) + +- Fixed an error breaking user handling of send failures in + :meth:`scrapy.mail.MailSender.send()`. (:issue:`1611`, :issue:`5880`) + +Documentation +~~~~~~~~~~~~~ + +- Expanded contributing docs. (:issue:`5109`, :issue:`5851`) + +- Added blacken-docs_ to pre-commit and reformatted the docs with it. + (:issue:`5813`, :issue:`5816`) + +- Fixed a JS issue. (:issue:`5875`, :issue:`5877`) + +- Fixed ``make htmlview``. (:issue:`5878`, :issue:`5879`) + +- Fixed typos and other small errors. (:issue:`5827`, :issue:`5839`, + :issue:`5883`, :issue:`5890`, :issue:`5895`, :issue:`5904`) + +Quality assurance +~~~~~~~~~~~~~~~~~ + +- Extended typing hints. (:issue:`5805`, :issue:`5889`, :issue:`5896`) + +- Tests for most of the examples in the docs are now run as a part of CI, + found problems were fixed. (:issue:`5816`, :issue:`5826`, :issue:`5919`) + +- Removed usage of deprecated Python classes. (:issue:`5849`) + +- Silenced ``include-ignored`` warnings from coverage. (:issue:`5820`) + +- Fixed a random failure of the ``test_feedexport.test_batch_path_differ`` + test. (:issue:`5855`, :issue:`5898`) + +- Updated docstrings to match output produced by parsel_ 1.8.1 so that they + don't cause test failures. (:issue:`5902`, :issue:`5919`) + +- Other CI and pre-commit improvements. (:issue:`5802`, :issue:`5823`, + :issue:`5908`) + +.. _blacken-docs: https://github.com/adamchainz/blacken-docs + +.. _release-2.8.0: + +Scrapy 2.8.0 (2023-02-02) +------------------------- + +This is a maintenance release, with minor features, bug fixes, and cleanups. + +Deprecation removals +~~~~~~~~~~~~~~~~~~~~ + +- The ``scrapy.utils.gz.read1`` function, deprecated in Scrapy 2.0, has now + been removed. Use the :meth:`~io.BufferedIOBase.read1` method of + :class:`~gzip.GzipFile` instead. + (:issue:`5719`) + +- The ``scrapy.utils.python.to_native_str`` function, deprecated in Scrapy + 2.0, has now been removed. Use :func:`scrapy.utils.python.to_unicode` + instead. + (:issue:`5719`) + +- The ``scrapy.utils.python.MutableChain.next`` method, deprecated in Scrapy + 2.0, has now been removed. Use + :meth:`~scrapy.utils.python.MutableChain.__next__` instead. + (:issue:`5719`) + +- The ``scrapy.linkextractors.FilteringLinkExtractor`` class, deprecated + in Scrapy 2.0, has now been removed. Use + :class:`LinkExtractor ` + instead. + (:issue:`5720`) + +- Support for using environment variables prefixed with ``SCRAPY_`` to + override settings, deprecated in Scrapy 2.0, has now been removed. + (:issue:`5724`) + +- Support for the ``noconnect`` query string argument in proxy URLs, + deprecated in Scrapy 2.0, has now been removed. We expect proxies that used + to need it to work fine without it. + (:issue:`5731`) + +- The ``scrapy.utils.python.retry_on_eintr`` function, deprecated in Scrapy + 2.3, has now been removed. + (:issue:`5719`) + +- The ``scrapy.utils.python.WeakKeyCache`` class, deprecated in Scrapy 2.4, + has now been removed. + (:issue:`5719`) + +- The ``scrapy.utils.boto.is_botocore()`` function, deprecated in Scrapy 2.4, + has now been removed. + (:issue:`5719`) + + +Deprecations +~~~~~~~~~~~~ + +- :exc:`scrapy.pipelines.images.NoimagesDrop` is now deprecated. + (:issue:`5368`, :issue:`5489`) + +- :meth:`ImagesPipeline.convert_image + ` must now accept a + ``response_body`` parameter. + (:issue:`3055`, :issue:`3689`, :issue:`4753`) + + +New features +~~~~~~~~~~~~ + +- Applied black_ coding style to files generated with the + :command:`genspider` and :command:`startproject` commands. + (:issue:`5809`, :issue:`5814`) + + .. _black: https://black.readthedocs.io/en/stable/ + +- :setting:`FEED_EXPORT_ENCODING` is now set to ``"utf-8"`` in the + ``settings.py`` file that the :command:`startproject` command generates. + With this value, JSON exports won’t force the use of escape sequences for + non-ASCII characters. + (:issue:`5797`, :issue:`5800`) + +- The :class:`~scrapy.extensions.memusage.MemoryUsage` extension now logs the + peak memory usage during checks, and the binary unit MiB is now used to + avoid confusion. + (:issue:`5717`, :issue:`5722`, :issue:`5727`) + +- The ``callback`` parameter of :class:`~scrapy.http.Request` can now be set + to :func:`scrapy.http.request.NO_CALLBACK`, to distinguish it from + ``None``, as the latter indicates that the default spider callback + (:meth:`~scrapy.Spider.parse`) is to be used. + (:issue:`5798`) + + +Bug fixes +~~~~~~~~~ + +- Enabled unsafe legacy SSL renegotiation to fix access to some outdated + websites. + (:issue:`5491`, :issue:`5790`) + +- Fixed STARTTLS-based email delivery not working with Twisted 21.2.0 and + better. + (:issue:`5386`, :issue:`5406`) + +- Fixed the :meth:`finish_exporting` method of :ref:`item exporters + ` not being called for empty files. + (:issue:`5537`, :issue:`5758`) + +- Fixed HTTP/2 responses getting only the last value for a header when + multiple headers with the same name are received. + (:issue:`5777`) + +- Fixed an exception raised by the :command:`shell` command on some cases + when :ref:`using asyncio `. + (:issue:`5740`, :issue:`5742`, :issue:`5748`, :issue:`5759`, :issue:`5760`, + :issue:`5771`) + +- When using :class:`~scrapy.spiders.CrawlSpider`, callback keyword arguments + (``cb_kwargs``) added to a request in the ``process_request`` callback of a + :class:`~scrapy.spiders.Rule` will no longer be ignored. + (:issue:`5699`) + +- The :ref:`images pipeline ` no longer re-encodes JPEG + files. + (:issue:`3055`, :issue:`3689`, :issue:`4753`) + +- Fixed the handling of transparent WebP images by the :ref:`images pipeline + `. + (:issue:`3072`, :issue:`5766`, :issue:`5767`) + +- :func:`scrapy.shell.inspect_response` no longer inhibits ``SIGINT`` + (Ctrl+C). + (:issue:`2918`) + +- :class:`LinkExtractor ` + with ``unique=False`` no longer filters out links that have identical URL + *and* text. + (:issue:`3798`, :issue:`3799`, :issue:`4695`, :issue:`5458`) + +- :class:`~scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware` now + ignores URL protocols that do not support ``robots.txt`` (``data://``, + ``file://``). + (:issue:`5807`) + +- Silenced the ``filelock`` debug log messages introduced in Scrapy 2.6. + (:issue:`5753`, :issue:`5754`) + +- Fixed the output of ``scrapy -h`` showing an unintended ``**commands**`` + line. + (:issue:`5709`, :issue:`5711`, :issue:`5712`) + +- Made the active project indication in the output of :ref:`commands + ` more clear. + (:issue:`5715`) + + +Documentation +~~~~~~~~~~~~~ + +- Documented how to :ref:`debug spiders from Visual Studio Code + `. + (:issue:`5721`) + +- Documented how :setting:`DOWNLOAD_DELAY` affects per-domain concurrency. + (:issue:`5083`, :issue:`5540`) + +- Improved consistency. + (:issue:`5761`) + +- Fixed typos. + (:issue:`5714`, :issue:`5744`, :issue:`5764`) + + +Quality assurance +~~~~~~~~~~~~~~~~~ + +- Applied :ref:`black coding style `, sorted import statements, + and introduced :ref:`pre-commit `. + (:issue:`4654`, :issue:`4658`, :issue:`5734`, :issue:`5737`, :issue:`5806`, + :issue:`5810`) + +- Switched from :mod:`os.path` to :mod:`pathlib`. + (:issue:`4916`, :issue:`4497`, :issue:`5682`) + +- Addressed many issues reported by Pylint. + (:issue:`5677`) + +- Improved code readability. + (:issue:`5736`) + +- Improved package metadata. + (:issue:`5768`) + +- Removed direct invocations of ``setup.py``. + (:issue:`5774`, :issue:`5776`) + +- Removed unnecessary :class:`~collections.OrderedDict` usages. + (:issue:`5795`) + +- Removed unnecessary ``__str__`` definitions. + (:issue:`5150`) + +- Removed obsolete code and comments. + (:issue:`5725`, :issue:`5729`, :issue:`5730`, :issue:`5732`) + +- Fixed test and CI issues. + (:issue:`5749`, :issue:`5750`, :issue:`5756`, :issue:`5762`, :issue:`5765`, + :issue:`5780`, :issue:`5781`, :issue:`5782`, :issue:`5783`, :issue:`5785`, + :issue:`5786`) + + .. _release-2.7.1: Scrapy 2.7.1 (2022-11-02) @@ -2417,11 +3050,13 @@ Backward-incompatible changes * :class:`~scrapy.loader.ItemLoader` now turns the values of its input item into lists: - >>> item = MyItem() - >>> item['field'] = 'value1' - >>> loader = ItemLoader(item=item) - >>> item['field'] - ['value1'] + .. code-block:: pycon + + >>> item = MyItem() + >>> item["field"] = "value1" + >>> loader = ItemLoader(item=item) + >>> item["field"] + ['value1'] This is needed to allow adding values to existing fields (``loader.add_value('field', 'value2')``). @@ -3999,8 +4634,6 @@ Relocations + Note: telnet is not enabled on Python 3 (https://github.com/scrapy/scrapy/pull/1524#issuecomment-146985595) -.. _parsel: https://github.com/scrapy/parsel - Bugfixes ~~~~~~~~ @@ -4700,7 +5333,7 @@ Scrapy 0.22.1 (released 2014-02-08) - BaseSgmlLinkExtractor: Added unit test of a link with an inner tag (:commit:`c1cb418`) - BaseSgmlLinkExtractor: Fixed unknown_endtag() so that it only set current_link=None when the end tag match the opening tag (:commit:`7e4d627`) - Fix tests for Travis-CI build (:commit:`76c7e20`) -- replace unencodable codepoints with html entities. fixes #562 and #285 (:commit:`5f87b17`) +- replace unencodeable codepoints with html entities. fixes #562 and #285 (:commit:`5f87b17`) - RegexLinkExtractor: encode URL unicode value when creating Links (:commit:`d0ee545`) - Updated the tutorial crawl output with latest output. (:commit:`8da65de`) - Updated shell docs with the crawler reference and fixed the actual shell output. (:commit:`875b9ab`) @@ -4725,7 +5358,7 @@ Enhancements - [**Backward incompatible**] Switched HTTPCacheMiddleware backend to filesystem (:issue:`541`) To restore old backend set ``HTTPCACHE_STORAGE`` to ``scrapy.contrib.httpcache.DbmCacheStorage`` - Proxy \https:// urls using CONNECT method (:issue:`392`, :issue:`397`) -- Add a middleware to crawl ajax crawleable pages as defined by google (:issue:`343`) +- Add a middleware to crawl ajax crawlable pages as defined by google (:issue:`343`) - Rename scrapy.spider.BaseSpider to scrapy.spider.Spider (:issue:`510`, :issue:`519`) - Selectors register EXSLT namespaces by default (:issue:`472`) - Unify item loaders similar to selectors renaming (:issue:`461`) @@ -4905,7 +5538,7 @@ Scrapy 0.18.0 (released 2013-08-09) ----------------------------------- - Lot of improvements to testsuite run using Tox, including a way to test on pypi -- Handle GET parameters for AJAX crawleable urls (:commit:`3fe2a32`) +- Handle GET parameters for AJAX crawlable urls (:commit:`3fe2a32`) - Use lxml recover option to parse sitemaps (:issue:`347`) - Bugfix cookie merging by hostname and not by netloc (:issue:`352`) - Support disabling ``HttpCompressionMiddleware`` using a flag setting (:issue:`359`) @@ -4939,8 +5572,8 @@ Scrapy 0.18.0 (released 2013-08-09) - Added ``--pdb`` option to ``scrapy`` command line tool - Added :meth:`XPathSelector.remove_namespaces ` which allows to remove all namespaces from XML documents for convenience (to work with namespace-less XPaths). Documented in :ref:`topics-selectors`. - Several improvements to spider contracts -- New default middleware named MetaRefreshMiddldeware that handles meta-refresh html tag redirections, -- MetaRefreshMiddldeware and RedirectMiddleware have different priorities to address #62 +- New default middleware named MetaRefreshMiddleware that handles meta-refresh html tag redirections, +- MetaRefreshMiddleware and RedirectMiddleware have different priorities to address #62 - added from_crawler method to spiders - added system tests with mock server - more improvements to macOS compatibility (thanks Alex Cepoi) @@ -5082,7 +5715,7 @@ Scrapy changes: - promoted :ref:`topics-djangoitem` to main contrib - LogFormatter method now return dicts(instead of strings) to support lazy formatting (:issue:`164`, :commit:`dcef7b0`) - downloader handlers (:setting:`DOWNLOAD_HANDLERS` setting) now receive settings as the first argument of the ``__init__`` method -- replaced memory usage acounting with (more portable) `resource`_ module, removed ``scrapy.utils.memory`` module +- replaced memory usage accounting with (more portable) `resource`_ module, removed ``scrapy.utils.memory`` module - removed signal: ``scrapy.mail.mail_sent`` - removed ``TRACK_REFS`` setting, now :ref:`trackrefs ` is always enabled - DBM is now the default storage backend for HTTP cache middleware @@ -5148,7 +5781,7 @@ Scrapy 0.14 New features and settings ~~~~~~~~~~~~~~~~~~~~~~~~~ -- Support for `AJAX crawleable urls`_ +- Support for `AJAX crawlable urls`_ - New persistent scheduler that stores requests on disk, allowing to suspend and resume crawls (:rev:`2737`) - added ``-o`` option to ``scrapy crawl``, a shortcut for dumping scraped items into a file (or standard output using ``-``) - Added support for passing custom settings to Scrapyd ``schedule.json`` api (:rev:`2779`, :rev:`2783`) @@ -5408,7 +6041,7 @@ Backward-incompatible changes - Renamed setting: ``REQUESTS_PER_DOMAIN`` to ``CONCURRENT_REQUESTS_PER_SPIDER`` (:rev:`1830`, :rev:`1844`) - Renamed setting: ``CONCURRENT_DOMAINS`` to ``CONCURRENT_SPIDERS`` (:rev:`1830`) - Refactored HTTP Cache middleware -- HTTP Cache middleware has been heavilty refactored, retaining the same functionality except for the domain sectorization which was removed. (:rev:`1843` ) +- HTTP Cache middleware has been heavily refactored, retaining the same functionality except for the domain sectorization which was removed. (:rev:`1843` ) - Renamed exception: ``DontCloseDomain`` to ``DontCloseSpider`` (:rev:`1859` | #120) - Renamed extension: ``DelayedCloseDomain`` to ``SpiderCloseDelay`` (:rev:`1861` | #121) - Removed obsolete ``scrapy.utils.markup.remove_escape_chars`` function - use ``scrapy.utils.markup.replace_escape_chars`` instead (:rev:`1865`) @@ -5419,7 +6052,8 @@ Scrapy 0.7 First release of Scrapy. -.. _AJAX crawleable urls: https://developers.google.com/search/docs/ajax-crawling/docs/getting-started?csw=1 +.. _AJAX crawlable urls: https://developers.google.com/search/docs/ajax-crawling/docs/getting-started?csw=1 +.. _boto3: https://github.com/boto/boto3 .. _botocore: https://github.com/boto/botocore .. _chunked transfer encoding: https://en.wikipedia.org/wiki/Chunked_transfer_encoding .. _ClientForm: http://wwwsearch.sourceforge.net/old/ClientForm/ @@ -5430,6 +6064,7 @@ First release of Scrapy. .. _LevelDB: https://github.com/google/leveldb .. _lxml: https://lxml.de/ .. _marshal: https://docs.python.org/2/library/marshal.html +.. _parsel: https://github.com/scrapy/parsel .. _parsel.csstranslator.GenericTranslator: https://parsel.readthedocs.io/en/latest/parsel.html#parsel.csstranslator.GenericTranslator .. _parsel.csstranslator.HTMLTranslator: https://parsel.readthedocs.io/en/latest/parsel.html#parsel.csstranslator.HTMLTranslator .. _parsel.csstranslator.XPathExpr: https://parsel.readthedocs.io/en/latest/parsel.html#parsel.csstranslator.XPathExpr diff --git a/docs/topics/addons.rst b/docs/topics/addons.rst new file mode 100644 index 000000000..1bf2172bd --- /dev/null +++ b/docs/topics/addons.rst @@ -0,0 +1,193 @@ +.. _topics-addons: + +======= +Add-ons +======= + +Scrapy's add-on system is a framework which unifies managing and configuring +components that extend Scrapy's core functionality, such as middlewares, +extensions, or pipelines. It provides users with a plug-and-play experience in +Scrapy extension management, and grants extensive configuration control to +developers. + + +Activating and configuring add-ons +================================== + +During :class:`~scrapy.crawler.Crawler` initialization, the list of enabled +add-ons is read from your ``ADDONS`` setting. + +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``:: + + ADDONS = { + 'path.to.someaddon': 0, + SomeAddonClass: 1, + } + + +Writing your own add-ons +======================== + +Add-ons are Python classes that include the following method: + +.. method:: update_settings(settings) + + This method is called during the initialization of the + :class:`~scrapy.crawler.Crawler`. Here, you should perform dependency checks + (e.g. for external Python libraries) and update the + :class:`~scrapy.settings.Settings` object as wished, e.g. enable components + for this add-on or set required configuration of other extensions. + + :param settings: The settings object storing Scrapy/component configuration + :type settings: :class:`~scrapy.settings.Settings` + +They can also have the following method: + +.. classmethod:: from_crawler(cls, crawler) + :noindex: + + If present, this class method is called to create an add-on instance + from a :class:`~scrapy.crawler.Crawler`. It must return a new instance + of the add-on. The crawler object provides access to all Scrapy core + components like settings and signals; it is a way for the add-on to access + them and hook its functionality into Scrapy. + + :param crawler: The crawler that uses this add-on + :type crawler: :class:`~scrapy.crawler.Crawler` + +The settings set by the add-on should use the ``addon`` priority (see +:ref:`populating-settings` and :func:`scrapy.settings.BaseSettings.set`):: + + class MyAddon: + def update_settings(self, settings): + settings.set("DNSCACHE_ENABLED", True, "addon") + +This allows users to override these settings in the project or spider +configuration. This is not possible with settings that are mutable objects, +such as the dict that is a value of :setting:`ITEM_PIPELINES`. In these cases +you can provide an add-on-specific setting that governs whether the add-on will +modify :setting:`ITEM_PIPELINES`:: + + class MyAddon: + def update_settings(self, settings): + if settings.getbool("MYADDON_ENABLE_PIPELINE"): + settings["ITEM_PIPELINES"]["path.to.mypipeline"] = 200 + +If the ``update_settings`` method raises +:exc:`scrapy.exceptions.NotConfigured`, the add-on will be skipped. This makes +it easy to enable an add-on only when some conditions are met. + +Fallbacks +--------- + +Some components provided by add-ons need to fall back to "default" +implementations, e.g. a custom download handler needs to send the request that +it doesn't handle via the default download handler, or a stats collector that +includes some additional processing but otherwise uses the default stats +collector. And it's possible that a project needs to use several custom +components of the same type, e.g. two custom download handlers that support +different kinds of custom requests and still need to use the default download +handler for other requests. To make such use cases easier to configure, we +recommend that such custom components should be written in the following way: + +1. The custom component (e.g. ``MyDownloadHandler``) shouldn't inherit from the + default Scrapy one (e.g. + ``scrapy.core.downloader.handlers.http.HTTPDownloadHandler``), but instead + be able to load the class of the fallback component from a special setting + (e.g. ``MY_FALLBACK_DOWNLOAD_HANDLER``), create an instance of it and use + it. +2. The add-ons that include these components should read the current value of + the default setting (e.g. ``DOWNLOAD_HANDLERS``) in their + ``update_settings()`` methods, save that value into the fallback setting + (``MY_FALLBACK_DOWNLOAD_HANDLER`` mentioned earlier) and set the default + setting to the component provided by the add-on (e.g. + ``MyDownloadHandler``). If the fallback setting is already set by the user, + they shouldn't change it. +3. This way, if there are several add-ons that want to modify the same setting, + all of them will fallback to the component from the previous one and then to + the Scrapy default. The order of that depends on the priority order in the + ``ADDONS`` setting. + + +Add-on examples +=============== + +Set some basic configuration: + +.. code-block:: python + + class MyAddon: + def update_settings(self, settings): + settings["ITEM_PIPELINES"]["path.to.mypipeline"] = 200 + settings.set("DNSCACHE_ENABLED", True, "addon") + +Check dependencies: + +.. code-block:: python + + class MyAddon: + def update_settings(self, settings): + try: + import boto + except ImportError: + raise NotConfigured("MyAddon requires the boto library") + ... + +Access the crawler instance: + +.. code-block:: python + + class MyAddon: + def __init__(self, crawler) -> None: + super().__init__() + self.crawler = crawler + + @classmethod + def from_crawler(cls, crawler): + return cls(crawler) + + def update_settings(self, settings): + ... + +Use a fallback component: + +.. code-block:: python + + from scrapy.core.downloader.handlers.http import HTTPDownloadHandler + + + FALLBACK_SETTING = "MY_FALLBACK_DOWNLOAD_HANDLER" + + + class MyHandler: + lazy = False + + def __init__(self, settings, crawler): + dhcls = load_object(settings.get(FALLBACK_SETTING)) + self._fallback_handler = create_instance( + dhcls, + settings=None, + crawler=crawler, + ) + + def download_request(self, request, spider): + if request.meta.get("my_params"): + # handle the request + ... + else: + return self._fallback_handler.download_request(request, spider) + + + class MyAddon: + def update_settings(self, settings): + if not settings.get(FALLBACK_SETTING): + settings.set( + FALLBACK_SETTING, + settings.getwithbase("DOWNLOAD_HANDLERS")["https"], + "addon", + ) + settings["DOWNLOAD_HANDLERS"]["https"] = MyHandler diff --git a/docs/topics/api.rst b/docs/topics/api.rst index 60b5acd10..175c877de 100644 --- a/docs/topics/api.rst +++ b/docs/topics/api.rst @@ -100,7 +100,7 @@ how you :ref:`configure the downloader middlewares Starts the crawler by instantiating its spider class with the given ``args`` and ``kwargs`` arguments, while setting the execution engine in - motion. + motion. Should be called only once. Returns a deferred that is fired when the crawl is finished. @@ -132,16 +132,15 @@ Settings API precedence over lesser ones when setting and retrieving values in the :class:`~scrapy.settings.Settings` class. - .. highlight:: python - - :: + .. code-block:: python SETTINGS_PRIORITIES = { - 'default': 0, - 'command': 10, - 'project': 20, - 'spider': 30, - 'cmdline': 40, + "default": 0, + "command": 10, + "addon": 15, + "project": 20, + "spider": 30, + "cmdline": 40, } For a detailed explanation on each settings sources, see: diff --git a/docs/topics/asyncio.rst b/docs/topics/asyncio.rst index dbee7146d..07baea071 100644 --- a/docs/topics/asyncio.rst +++ b/docs/topics/asyncio.rst @@ -27,54 +27,43 @@ reactor manually. You can do that using install_reactor('twisted.internet.asyncioreactor.AsyncioSelectorReactor') -.. _using-custom-loops: +.. _asyncio-preinstalled-reactor: -Using custom asyncio loops -========================== +Handling a pre-installed reactor +================================ -You can also use custom asyncio event loops with the asyncio reactor. Set the -:setting:`ASYNCIO_EVENT_LOOP` setting to the import path of the desired event loop class to -use it instead of the default asyncio event loop. +``twisted.internet.reactor`` and some other Twisted imports install the default +Twisted reactor as a side effect. Once a Twisted reactor is installed, it is +not possible to switch to a different reactor at run time. + +If you :ref:`configure the asyncio Twisted reactor ` and, at +run time, Scrapy complains that a different reactor is already installed, +chances are you have some such imports in your code. + +You can usually fix the issue by moving those offending module-level Twisted +imports to the method or function definitions where they are used. For example, +if you have something like: + +.. code-block:: python + + from twisted.internet import reactor -.. _asyncio-windows: + def my_function(): + reactor.callLater(...) -Windows-specific notes -====================== +Switch to something like: -The Windows implementation of :mod:`asyncio` can use two event loop -implementations: +.. code-block:: python -- :class:`~asyncio.SelectorEventLoop`, default before Python 3.8, required - when using Twisted. + def my_function(): + from twisted.internet import reactor -- :class:`~asyncio.ProactorEventLoop`, default since Python 3.8, cannot work - with Twisted. + reactor.callLater(...) -So on Python 3.8+ the event loop class needs to be changed. - -.. versionchanged:: 2.6.0 - The event loop class is changed automatically when you change the - :setting:`TWISTED_REACTOR` setting or call - :func:`~scrapy.utils.reactor.install_reactor`. - -To change the event loop class manually, call the following code before -installing the reactor:: - - import asyncio - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - -You can put this in the same function that installs the reactor, if you do that -yourself, or in some code that runs before the reactor is installed, e.g. -``settings.py``. - -.. note:: Other libraries you use may require - :class:`~asyncio.ProactorEventLoop`, e.g. because it supports - subprocesses (this is the case with `playwright`_), so you cannot use - them together with Scrapy on Windows (but you should be able to use - them on WSL or native Linux). - -.. _playwright: https://github.com/microsoft/playwright-python +Alternatively, you can try to :ref:`manually install the asyncio reactor +`, with :func:`~scrapy.utils.reactor.install_reactor`, before +those imports happen. .. _asyncio-await-dfd: @@ -106,12 +95,14 @@ Enforcing asyncio as a requirement If you are writing a :ref:`component ` that requires asyncio to work, use :func:`scrapy.utils.reactor.is_asyncio_reactor_installed` to :ref:`enforce it as a requirement `. For -example:: +example: + +.. code-block:: python from scrapy.utils.reactor import is_asyncio_reactor_installed - class MyComponent: + class MyComponent: def __init__(self): if not is_asyncio_reactor_installed(): raise ValueError( @@ -120,3 +111,36 @@ example:: f"TWISTED_REACTOR setting. See the asyncio documentation " f"of Scrapy for more information." ) + + +.. _asyncio-windows: + +Windows-specific notes +====================== + +The Windows implementation of :mod:`asyncio` can use two event loop +implementations, :class:`~asyncio.ProactorEventLoop` (default) and +:class:`~asyncio.SelectorEventLoop`. However, only +: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`. + +.. note:: Other libraries you use may require + :class:`~asyncio.ProactorEventLoop`, e.g. because it supports + subprocesses (this is the case with `playwright`_), so you cannot use + them together with Scrapy on Windows (but you should be able to use + them on WSL or native Linux). + +.. _playwright: https://github.com/microsoft/playwright-python + + +.. _using-custom-loops: + +Using custom asyncio loops +========================== + +You can also use custom asyncio event loops with the asyncio reactor. Set the +:setting:`ASYNCIO_EVENT_LOOP` setting to the import path of the desired event +loop class to use it instead of the default asyncio event loop. diff --git a/docs/topics/broad-crawls.rst b/docs/topics/broad-crawls.rst index 0927ac2d2..8be89feb2 100644 --- a/docs/topics/broad-crawls.rst +++ b/docs/topics/broad-crawls.rst @@ -48,9 +48,11 @@ Scrapy’s default scheduler priority queue is ``'scrapy.pqueues.ScrapyPriorityQ It works best during single-domain crawl. It does not work well with crawling many different domains in parallel -To apply the recommended priority queue use:: +To apply the recommended priority queue use: - SCHEDULER_PRIORITY_QUEUE = 'scrapy.pqueues.DownloaderAwarePriorityQueue' +.. code-block:: python + + SCHEDULER_PRIORITY_QUEUE = "scrapy.pqueues.DownloaderAwarePriorityQueue" .. _broad-crawls-concurrency: @@ -71,7 +73,9 @@ many different domains in parallel, so you will want to increase it. How much to increase it will depend on how much CPU and memory your crawler will have available. -A good starting point is ``100``:: +A good starting point is ``100``: + +.. code-block:: python CONCURRENT_REQUESTS = 100 @@ -92,7 +96,9 @@ hitting DNS resolver timeouts. Possible solution to increase the number of threads handling DNS queries. The DNS queue will be processed faster speeding up establishing of connection and crawling overall. -To increase maximum thread pool size use:: +To increase maximum thread pool size use: + +.. code-block:: python REACTOR_THREADPOOL_MAXSIZE = 20 @@ -114,9 +120,11 @@ should not use ``DEBUG`` log level when preforming large broad crawls in production. Using ``DEBUG`` level when developing your (broad) crawler may be fine though. -To set the log level use:: +To set the log level use: - LOG_LEVEL = 'INFO' +.. code-block:: python + + LOG_LEVEL = "INFO" Disable cookies =============== @@ -126,7 +134,9 @@ doing broad crawls (search engine crawlers ignore them), and they improve performance by saving some CPU cycles and reducing the memory footprint of your Scrapy crawler. -To disable cookies use:: +To disable cookies use: + +.. code-block:: python COOKIES_ENABLED = False @@ -138,7 +148,9 @@ when sites causes are very slow (or fail) to respond, thus causing a timeout error which gets retried many times, unnecessarily, preventing crawler capacity to be reused for other domains. -To disable retries use:: +To disable retries use: + +.. code-block:: python RETRY_ENABLED = False @@ -149,7 +161,9 @@ Unless you are crawling from a very slow connection (which shouldn't be the case for broad crawls) reduce the download timeout so that stuck requests are discarded quickly and free up capacity to process the next ones. -To reduce the download timeout use:: +To reduce the download timeout use: + +.. code-block:: python DOWNLOAD_TIMEOUT = 15 @@ -162,7 +176,9 @@ revisiting the site at a later crawl. This also help to keep the number of request constant per crawl batch, otherwise redirect loops may cause the crawler to dedicate too many resources on any specific domain. -To disable redirects use:: +To disable redirects use: + +.. code-block:: python REDIRECT_ENABLED = False @@ -179,7 +195,9 @@ Pages can indicate it in two ways: "main", "index" website pages. Scrapy handles (1) automatically; to handle (2) enable -:ref:`AjaxCrawlMiddleware `:: +:ref:`AjaxCrawlMiddleware `: + +.. code-block:: python AJAXCRAWL_ENABLED = True diff --git a/docs/topics/commands.rst b/docs/topics/commands.rst index 362190116..1d37895c2 100644 --- a/docs/topics/commands.rst +++ b/docs/topics/commands.rst @@ -238,9 +238,6 @@ genspider Create a new spider in the current folder or in the current project's ``spiders`` folder, if called from inside a project. The ```` parameter is set as the spider's ``name``, while ```` is used to generate the ``allowed_domains`` and ``start_urls`` spider's attributes. -.. note:: Even if an HTTPS URL is specified, the protocol used in - ``start_urls`` is always HTTP. This is a known issue: :issue:`3553`. - Usage example:: $ scrapy genspider -l @@ -288,13 +285,13 @@ Usage examples:: $ scrapy crawl myspider [ ... myspider starts crawling ... ] - $ scrapy -o myfile:csv myspider + $ scrapy crawl -o myfile:csv myspider [ ... myspider starts crawling and appends the result to the file myfile in csv format ... ] - $ scrapy -O myfile:json myspider + $ scrapy crawl -O myfile:json myspider [ ... myspider starts crawling and saves the result in myfile in json format overwriting the original content... ] - $ scrapy -o myfile -t csv myspider + $ scrapy crawl -o myfile -t csv myspider [ ... myspider starts crawling and appends the result to the file myfile in csv format ... ] .. command:: check @@ -617,7 +614,7 @@ Example: .. code-block:: python - COMMANDS_MODULE = 'mybot.commands' + COMMANDS_MODULE = "mybot.commands" .. _Deploying your project: https://scrapyd.readthedocs.io/en/latest/deploy.html @@ -636,10 +633,11 @@ The following example adds ``my_command`` command: from setuptools import setup, find_packages - setup(name='scrapy-mymodule', - entry_points={ - 'scrapy.commands': [ - 'my_command=my_scrapy_module.commands:MyCommand', - ], - }, - ) + setup( + name="scrapy-mymodule", + entry_points={ + "scrapy.commands": [ + "my_command=my_scrapy_module.commands:MyCommand", + ], + }, + ) diff --git a/docs/topics/components.rst b/docs/topics/components.rst index ca301b827..478dd9647 100644 --- a/docs/topics/components.rst +++ b/docs/topics/components.rst @@ -66,16 +66,18 @@ version mismatch, while :exc:`ValueError` may be better if the issue is the value of a setting. If your requirement is a minimum Scrapy version, you may use -:attr:`scrapy.__version__` to enforce your requirement. For example:: +:attr:`scrapy.__version__` to enforce your requirement. For example: - from pkg_resources import parse_version +.. code-block:: python + + from packaging.version import parse as parse_version import scrapy - class MyComponent: + class MyComponent: def __init__(self): - if parse_version(scrapy.__version__) < parse_version('2.7'): + if parse_version(scrapy.__version__) < parse_version("2.7"): raise RuntimeError( f"{MyComponent.__qualname__} requires Scrapy 2.7 or " f"later, which allow defining the process_spider_output " diff --git a/docs/topics/contracts.rst b/docs/topics/contracts.rst index c29a3a410..2d61026e9 100644 --- a/docs/topics/contracts.rst +++ b/docs/topics/contracts.rst @@ -11,10 +11,13 @@ integrated way of testing your spiders by the means of contracts. This allows you to test each callback of your spider by hardcoding a sample url and check various constraints for how the callback processes the response. Each contract is prefixed with an ``@`` and included in the docstring. See the -following example:: +following example: + +.. code-block:: python def parse(self, response): - """ This function parses a sample response. Some contracts are mingled + """ + This function parses a sample response. Some contracts are mingled with this docstring. @url http://www.amazon.com/s?field-keywords=selfish+gene @@ -64,11 +67,13 @@ Custom Contracts If you find you need more power than the built-in Scrapy contracts you can create and load your own contracts in the project by using the -:setting:`SPIDER_CONTRACTS` setting:: +:setting:`SPIDER_CONTRACTS` setting: + +.. code-block:: python SPIDER_CONTRACTS = { - 'myproject.contracts.ResponseCheck': 10, - 'myproject.contracts.ItemValidate': 10, + "myproject.contracts.ResponseCheck": 10, + "myproject.contracts.ItemValidate": 10, } Each contract must inherit from :class:`~scrapy.contracts.Contract` and can @@ -111,22 +116,27 @@ Raise :class:`~scrapy.exceptions.ContractFail` from .. autoclass:: scrapy.exceptions.ContractFail Here is a demo contract which checks the presence of a custom header in the -response received:: +response received: + +.. skip: next +.. code-block:: python from scrapy.contracts import Contract from scrapy.exceptions import ContractFail + class HasHeaderContract(Contract): - """ Demo contract which checks the presence of a custom header - @has_header X-CustomHeader + """ + Demo contract which checks the presence of a custom header + @has_header X-CustomHeader """ - name = 'has_header' + name = "has_header" def pre_process(self, response): for header in self.args: if header not in response.headers: - raise ContractFail('X-CustomHeader not present') + raise ContractFail("X-CustomHeader not present") .. _detecting-contract-check-runs: @@ -135,14 +145,17 @@ Detecting check runs When ``scrapy check`` is running, the ``SCRAPY_CHECK`` environment variable is set to the ``true`` string. You can use :data:`os.environ` to perform any change to -your spiders or your settings when ``scrapy check`` is used:: +your spiders or your settings when ``scrapy check`` is used: + +.. code-block:: python import os import scrapy + class ExampleSpider(scrapy.Spider): - name = 'example' + name = "example" def __init__(self): - if os.environ.get('SCRAPY_CHECK'): + if os.environ.get("SCRAPY_CHECK"): pass # Do some scraper adjustments when a check is running diff --git a/docs/topics/coroutines.rst b/docs/topics/coroutines.rst index a1ba4ba5c..a65bab3ca 100644 --- a/docs/topics/coroutines.rst +++ b/docs/topics/coroutines.rst @@ -58,49 +58,59 @@ There are several use cases for coroutines in Scrapy. Code that would return Deferreds when written for previous Scrapy versions, such as downloader middlewares and signal handlers, can be rewritten to be -shorter and cleaner:: +shorter and cleaner: + +.. code-block:: python from itemadapter import ItemAdapter + class DbPipeline: def _update_item(self, data, item): adapter = ItemAdapter(item) - adapter['field'] = data + adapter["field"] = data return item def process_item(self, item, spider): adapter = ItemAdapter(item) - dfd = db.get_some_data(adapter['id']) + dfd = db.get_some_data(adapter["id"]) dfd.addCallback(self._update_item, item) return dfd -becomes:: +becomes: + +.. code-block:: python from itemadapter import ItemAdapter + class DbPipeline: async def process_item(self, item, spider): adapter = ItemAdapter(item) - adapter['field'] = await db.get_some_data(adapter['id']) + adapter["field"] = await db.get_some_data(adapter["id"]) return item Coroutines may be used to call asynchronous code. This includes other coroutines, functions that return Deferreds and functions that return :term:`awaitable objects ` such as :class:`~asyncio.Future`. -This means you can use many useful Python libraries providing such code:: +This means you can use many useful Python libraries providing such code: + +.. skip: next +.. code-block:: python class MySpiderDeferred(Spider): # ... async def parse(self, response): - additional_response = await treq.get('https://additional.url') + additional_response = await treq.get("https://additional.url") additional_data = await treq.content(additional_response) # ... use response and additional_data to yield items and requests + class MySpiderAsyncio(Spider): # ... async def parse(self, response): async with aiohttp.ClientSession() as session: - async with session.get('https://additional.url') as additional_response: + async with session.get("https://additional.url") as additional_response: additional_data = await additional_response.text() # ... use response and additional_data to yield items and requests @@ -124,6 +134,63 @@ Common use cases for asynchronous code include: .. _aio-libs: https://github.com/aio-libs +.. _inline-requests: + +Inline requests +=============== + +The spider below shows how to send a request and await its response all from +within a spider callback: + +.. code-block:: python + + from scrapy import Spider, Request + from scrapy.utils.defer import maybe_deferred_to_future + + + class SingleRequestSpider(Spider): + name = "single" + start_urls = ["https://example.org/product"] + + async def parse(self, response, **kwargs): + additional_request = Request("https://example.org/price") + deferred = self.crawler.engine.download(additional_request) + additional_response = await maybe_deferred_to_future(deferred) + yield { + "h1": response.css("h1").get(), + "price": additional_response.css("#price").get(), + } + +You can also send multiple requests in parallel: + +.. code-block:: python + + from scrapy import Spider, Request + from scrapy.utils.defer import maybe_deferred_to_future + from twisted.internet.defer import DeferredList + + + class MultipleRequestsSpider(Spider): + name = "multiple" + start_urls = ["https://example.com/product"] + + async def parse(self, response, **kwargs): + additional_requests = [ + Request("https://example.com/price"), + Request("https://example.com/color"), + ] + deferreds = [] + for r in additional_requests: + deferred = self.crawler.engine.download(r) + deferreds.append(deferred) + responses = await maybe_deferred_to_future(DeferredList(deferreds)) + yield { + "h1": response.css("h1::text").get(), + "price": responses[0][1].css(".price::text").get(), + "price2": responses[1][1].css(".color::text").get(), + } + + .. _sync-async-spider-middleware: Mixing synchronous and asynchronous spider middlewares @@ -192,7 +259,9 @@ while maintaining support for older Scrapy versions, you may define :term:`asynchronous generator` version of that method with an alternative name: ``process_spider_output_async``. -For example:: +For example: + +.. code-block:: python class UniversalSpiderMiddleware: def process_spider_output(self, response, result, spider): diff --git a/docs/topics/debug.rst b/docs/topics/debug.rst index edbcaf432..49c5b0410 100644 --- a/docs/topics/debug.rst +++ b/docs/topics/debug.rst @@ -5,21 +5,25 @@ Debugging Spiders ================= This document explains the most common techniques for debugging spiders. -Consider the following Scrapy spider below:: +Consider the following Scrapy spider below: + +.. skip: next +.. code-block:: python import scrapy from myproject.items import MyItem + class MySpider(scrapy.Spider): - name = 'myspider' + name = "myspider" start_urls = ( - 'http://example.com/page1', - 'http://example.com/page2', - ) + "http://example.com/page1", + "http://example.com/page2", + ) def parse(self, response): # - # collect `item_urls` + # collect `item_urls` for item_url in item_urls: yield scrapy.Request(item_url, self.parse_item) @@ -28,7 +32,9 @@ Consider the following Scrapy spider below:: item = MyItem() # populate `item` fields # and extract item_details_url - yield scrapy.Request(item_details_url, self.parse_details, cb_kwargs={'item': item}) + yield scrapy.Request( + item_details_url, self.parse_details, cb_kwargs={"item": item} + ) def parse_details(self, response, item): # populate more `item` fields @@ -103,10 +109,13 @@ showing the response received and the output. How to debug the situation when .. highlight:: python Fortunately, the :command:`shell` is your bread and butter in this case (see -:ref:`topics-shell-inspect-response`):: +:ref:`topics-shell-inspect-response`): + +.. code-block:: python from scrapy.shell import inspect_response + def parse_details(self, response, item=None): if item: # populate more `item` fields @@ -121,10 +130,13 @@ Open in browser Sometimes you just want to see how a certain response looks in a browser, you can use the ``open_in_browser`` function for that. Here is an example of how -you would use it:: +you would use it: + +.. code-block:: python from scrapy.utils.response import open_in_browser + def parse_details(self, response): if "item name" not in response.body: open_in_browser(response) @@ -138,19 +150,23 @@ Logging Logging is another useful option for getting information about your spider run. Although not as convenient, it comes with the advantage that the logs will be -available in all future runs should they be necessary again:: +available in all future runs should they be necessary again: + +.. code-block:: python def parse_details(self, response, item=None): if item: # populate more `item` fields return item else: - self.logger.warning('No item received for %s', response.url) + self.logger.warning("No item received for %s", response.url) For more information, check the :ref:`topics-logging` section. .. _base tag: https://www.w3schools.com/tags/tag_base.asp +.. _debug-vscode: + Visual Studio Code ================== diff --git a/docs/topics/developer-tools.rst b/docs/topics/developer-tools.rst index 9bf97c628..a15ee1059 100644 --- a/docs/topics/developer-tools.rst +++ b/docs/topics/developer-tools.rst @@ -94,8 +94,10 @@ Then, back to your web browser, right-click on the ``span`` tag, select response = load_response('https://quotes.toscrape.com/', 'quotes.html') ->>> response.xpath('/html/body/div/div[2]/div[1]/div[1]/span[1]/text()').getall() -['“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'] +.. code-block:: pycon + + >>> response.xpath("/html/body/div/div[2]/div[1]/div[1]/span[1]/text()").getall() + ['“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'] Adding ``text()`` at the end we are able to extract the first quote with this basic selector. But this XPath is not really that clever. All it does is @@ -124,11 +126,13 @@ With this knowledge we can refine our XPath: Instead of a path to follow, we'll simply select all ``span`` tags with the ``class="text"`` by using the `has-class-extension`_: ->>> response.xpath('//span[has-class("text")]/text()').getall() -['“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', -'“It is our choices, Harry, that show what we truly are, far more than our abilities.”', -'“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', -...] +.. code-block:: pycon + + >>> response.xpath('//span[has-class("text")]/text()').getall() + ['“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', + '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', + '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', + ...] And with one simple, cleverer XPath we are able to extract all quotes from the page. We could have constructed a loop over our first XPath to increase @@ -237,17 +241,19 @@ on the request and open ``Open in new tab`` to get a better overview. :alt: JSON-object returned from the quotes.toscrape API With this response we can now easily parse the JSON-object and -also request each page to get every quote on the site:: +also request each page to get every quote on the site: + +.. code-block:: python import scrapy import json class QuoteSpider(scrapy.Spider): - name = 'quote' - allowed_domains = ['quotes.toscrape.com'] + name = "quote" + allowed_domains = ["quotes.toscrape.com"] page = 1 - start_urls = ['https://quotes.toscrape.com/api/quotes?page=1'] + start_urls = ["https://quotes.toscrape.com/api/quotes?page=1"] def parse(self, response): data = json.loads(response.text) @@ -275,7 +281,9 @@ requests, as we could need to add ``headers`` or ``cookies`` to make it work. In those cases you can export the requests in `cURL `_ format, by right-clicking on each of them in the network tool and using the :meth:`~scrapy.Request.from_curl()` method to generate an equivalent -request:: +request: + +.. code-block:: python from scrapy import Request @@ -286,7 +294,8 @@ request:: "-Requested-With: XMLHttpRequest' -H 'Proxy-Authorization: Basic QFRLLTAzM" "zEwZTAxLTk5MWUtNDFiNC1iZWRmLTJjNGI4M2ZiNDBmNDpAVEstMDMzMTBlMDEtOTkxZS00MW" "I0LWJlZGYtMmM0YjgzZmI0MGY0' -H 'Connection: keep-alive' -H 'Referer: http" - "://quotes.toscrape.com/scroll' -H 'Cache-Control: max-age=0'") + "://quotes.toscrape.com/scroll' -H 'Cache-Control: max-age=0'" + ) Alternatively, if you want to know the arguments needed to recreate that request you can use the :func:`~scrapy.utils.curl.curl_to_request_kwargs` diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 986da0476..1abbc4968 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -17,10 +17,12 @@ To activate a downloader middleware component, add it to the :setting:`DOWNLOADER_MIDDLEWARES` setting, which is a dict whose keys are the middleware class paths and their values are the middleware orders. -Here's an example:: +Here's an example: + +.. code-block:: python DOWNLOADER_MIDDLEWARES = { - 'myproject.middlewares.CustomDownloaderMiddleware': 543, + "myproject.middlewares.CustomDownloaderMiddleware": 543, } The :setting:`DOWNLOADER_MIDDLEWARES` setting is merged with the @@ -42,11 +44,13 @@ previous (or subsequent) middleware being applied. If you want to disable a built-in middleware (the ones defined in :setting:`DOWNLOADER_MIDDLEWARES_BASE` and enabled by default) you must define it in your project's :setting:`DOWNLOADER_MIDDLEWARES` setting and assign ``None`` -as its value. For example, if you want to disable the user-agent middleware:: +as its value. For example, if you want to disable the user-agent middleware: + +.. code-block:: python DOWNLOADER_MIDDLEWARES = { - 'myproject.middlewares.CustomDownloaderMiddleware': 543, - 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, + "myproject.middlewares.CustomDownloaderMiddleware": 543, + "scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": None, } Finally, keep in mind that some middlewares may need to be enabled through a @@ -226,20 +230,26 @@ There is support for keeping multiple cookie sessions per spider by using the :reqmeta:`cookiejar` Request meta key. By default it uses a single cookie jar (session), but you can pass an identifier to use different ones. -For example:: +For example: + +.. skip: next +.. code-block:: python for i, url in enumerate(urls): - yield scrapy.Request(url, meta={'cookiejar': i}, - callback=self.parse_page) + yield scrapy.Request(url, meta={"cookiejar": i}, callback=self.parse_page) Keep in mind that the :reqmeta:`cookiejar` meta key is not "sticky". You need to keep -passing it along on subsequent requests. For example:: +passing it along on subsequent requests. For example: + +.. code-block:: python def parse_page(self, response): # do some processing - return scrapy.Request("http://www.example.com/otherpage", - meta={'cookiejar': response.meta['cookiejar']}, - callback=self.parse_other_page) + return scrapy.Request( + "http://www.example.com/otherpage", + meta={"cookiejar": response.meta["cookiejar"]}, + callback=self.parse_other_page, + ) .. setting:: COOKIES_ENABLED @@ -339,16 +349,18 @@ HttpAuthMiddleware domain of the first request, which will work for some spiders but not for others. In the future the middleware will produce an error instead. - Example:: + Example: + + .. code-block:: python from scrapy.spiders import CrawlSpider - class SomeIntranetSiteSpider(CrawlSpider): - http_user = 'someuser' - http_pass = 'somepass' - http_auth_domain = 'intranet.example.com' - name = 'intranet.example.com' + class SomeIntranetSiteSpider(CrawlSpider): + http_user = "someuser" + http_pass = "somepass" + http_auth_domain = "intranet.example.com" + name = "intranet.example.com" # .. rest of the spider code omitted ... @@ -792,7 +804,9 @@ If you want to handle some redirect status codes in your spider, you can specify these in the ``handle_httpstatus_list`` spider attribute. For example, if you want the redirect middleware to ignore 301 and 302 -responses (and pass them through to your spider) you can do this:: +responses (and pass them through to your spider) you can do this: + +.. code-block:: python class MySpider(CrawlSpider): handle_httpstatus_list = [301, 302] @@ -901,6 +915,7 @@ settings (see the settings documentation for more info): * :setting:`RETRY_ENABLED` * :setting:`RETRY_TIMES` * :setting:`RETRY_HTTP_CODES` +* :setting:`RETRY_EXCEPTIONS` .. reqmeta:: dont_retry @@ -952,6 +967,37 @@ In some cases you may want to add 400 to :setting:`RETRY_HTTP_CODES` because it is a common code used to indicate server overload. It is not included by default because HTTP specs say so. +.. setting:: RETRY_EXCEPTIONS + +RETRY_EXCEPTIONS +^^^^^^^^^^^^^^^^ + +Default:: + + [ + 'twisted.internet.defer.TimeoutError', + 'twisted.internet.error.TimeoutError', + 'twisted.internet.error.DNSLookupError', + 'twisted.internet.error.ConnectionRefusedError', + 'twisted.internet.error.ConnectionDone', + 'twisted.internet.error.ConnectError', + 'twisted.internet.error.ConnectionLost', + 'twisted.internet.error.TCPTimedOutError', + 'twisted.web.client.ResponseFailed', + IOError, + 'scrapy.core.downloader.handlers.http11.TunnelError', + ] + +List of exceptions to retry. + +Each list entry may be an exception type or its import path as a string. + +An exception will not be caught when the exception type is not in +:setting:`RETRY_EXCEPTIONS` or when the maximum number of retries for a request +has been exceeded (see :setting:`RETRY_TIMES`). To learn about uncaught +exception propagation, see +:meth:`~scrapy.downloadermiddlewares.DownloaderMiddleware.process_exception`. + .. setting:: RETRY_PRIORITY_ADJUST RETRY_PRIORITY_ADJUST @@ -993,8 +1039,8 @@ RobotsTxtMiddleware * :ref:`Protego ` (default) * :ref:`RobotFileParser ` - * :ref:`Reppy ` * :ref:`Robotexclusionrulesparser ` + * :ref:`Reppy ` (deprecated) You can change the robots.txt_ parser with the :setting:`ROBOTSTXT_PARSER` setting. Or you can also :ref:`implement support for a new parser `. @@ -1087,6 +1133,7 @@ In order to use this parser: .. warning:: `Upstream issue #122 `_ prevents reppy usage in Python 3.9+. + Because of this the Reppy parser is deprecated. * Set :setting:`ROBOTSTXT_PARSER` setting to ``scrapy.robotstxt.ReppyRobotParser`` diff --git a/docs/topics/dynamic-content.rst b/docs/topics/dynamic-content.rst index ea5d06210..a0f4b4411 100644 --- a/docs/topics/dynamic-content.rst +++ b/docs/topics/dynamic-content.rst @@ -119,16 +119,20 @@ data from it depends on the type of response: ` as usual. - If the response is JSON, use :func:`json.loads` to load the desired data from - :attr:`response.text `:: + :attr:`response.text `: + + .. code-block:: python data = json.loads(response.text) If the desired data is inside HTML or XML code embedded within JSON data, you can load that HTML or XML code into a :class:`~scrapy.Selector` and then - :ref:`use it ` as usual:: + :ref:`use it ` as usual: - selector = Selector(data['html']) + .. code-block:: python + + selector = Selector(data["html"]) - If the response is JavaScript, or HTML with a ``