mirror of https://github.com/scrapy/scrapy.git
354 lines
13 KiB
ReStructuredText
354 lines
13 KiB
ReStructuredText
.. _optimize:
|
||
|
||
============
|
||
Optimization
|
||
============
|
||
|
||
A crawl goes as fast as its slowest part allows. :ref:`Find out which part that
|
||
is <optimize-bottleneck>` before changing any setting.
|
||
|
||
:ref:`Broad crawls <broad-crawls>` have their own set of recommended
|
||
adjustments.
|
||
|
||
.. _optimize-bottleneck:
|
||
|
||
Finding the bottleneck
|
||
======================
|
||
|
||
The bottleneck depends on the spider: on the same machine, one crawl can be
|
||
limited by its own parsing code and another by the target website. So measure
|
||
the crawl that you want to optimize.
|
||
|
||
:class:`~scrapy.extensions.logstats.LogStats` reports crawl speed every
|
||
:setting:`LOGSTATS_INTERVAL` seconds:
|
||
|
||
.. code-block:: text
|
||
|
||
[scrapy.extensions.logstats] INFO: Crawled 1200 pages (at 60 pages/min), scraped 1150 items (at 58 items/min)
|
||
|
||
A rate that stays flat as you raise :setting:`CONCURRENT_REQUESTS` means
|
||
something else is the limit.
|
||
|
||
|
||
Reading the engine status
|
||
-------------------------
|
||
|
||
The :ref:`telnet console <topics-telnetconsole>` reports, through ``est()``,
|
||
what every part of the engine is doing at a given moment:
|
||
|
||
.. code-block:: text
|
||
|
||
len(engine.downloader.active) : 16
|
||
len(engine._slot.scheduler.mqs) : 92
|
||
len(engine.scraper.slot.active) : 0
|
||
engine.scraper.slot.active_size : 0
|
||
engine.scraper.slot.needs_backout() : False
|
||
|
||
Take a few readings at different points of the crawl:
|
||
|
||
- ``len(engine.downloader.active)`` stays at :setting:`CONCURRENT_REQUESTS`:
|
||
the downloader is the limit. You are waiting on the network or on the
|
||
target website. See :ref:`optimize-concurrency`.
|
||
|
||
- ``len(engine.downloader.active)`` stays below
|
||
:setting:`CONCURRENT_REQUESTS` while the scheduler queues (``mqs``,
|
||
``dqs``) hold requests: something throttles those requests before they
|
||
reach the downloader, usually :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`,
|
||
:setting:`DOWNLOAD_DELAY` or :ref:`AutoThrottle <topics-autothrottle>`.
|
||
|
||
- Both the downloader and the scheduler queues stay near empty: your spider
|
||
is not producing requests fast enough. A crawl that walks pagination one
|
||
page at a time cannot use more concurrency than it creates. See
|
||
:ref:`optimize-requests`.
|
||
|
||
- ``needs_backout()`` is ``True``, or ``active_size`` approaches
|
||
:setting:`SCRAPER_SLOT_MAX_ACTIVE_SIZE`: responses arrive faster than your
|
||
callbacks and :ref:`item pipelines <topics-item-pipeline>` handle them. The
|
||
bottleneck is your own code.
|
||
|
||
- ``len(engine._slot.scheduler.mqs)`` grows without settling: the crawl
|
||
discovers requests faster than it downloads them. This is what makes long
|
||
crawls run out of memory.
|
||
|
||
|
||
Reading resource usage
|
||
----------------------
|
||
|
||
CPU
|
||
Scrapy runs in a single process, and everything except DNS resolution and
|
||
code you explicitly move to a thread runs in a single thread. One CPU core
|
||
is the ceiling; a process sitting at 100% of a core is CPU-bound no matter
|
||
how many cores the machine has.
|
||
|
||
Use a sampling profiler, such as py-spy_, to find out which code is
|
||
spending that CPU. :ref:`Selectors <topics-selectors>` and item pipelines
|
||
are the usual answer.
|
||
|
||
.. _py-spy: https://github.com/benfred/py-spy
|
||
|
||
Memory
|
||
The :ref:`memory usage extension <topics-extensions-ref-memusage>` records
|
||
:stat:`memusage/startup` and :stat:`memusage/max`. A :stat:`memusage/max`
|
||
far above :stat:`memusage/startup` is expected; what matters is whether it
|
||
keeps growing for as long as the crawl runs.
|
||
|
||
Growth that tracks ``len(engine._slot.scheduler.mqs)`` is a scheduling
|
||
problem, covered in :ref:`optimize-memory`. Growth that does not is a
|
||
:ref:`memory leak <topics-leaks>`.
|
||
|
||
Network
|
||
Compare :stat:`downloader/response_bytes` over the crawl time against your
|
||
available bandwidth. Saturated bandwidth caps concurrency regardless of any
|
||
setting.
|
||
|
||
DNS resolution is separate: it runs on a thread pool of
|
||
:setting:`REACTOR_THREADPOOL_MAXSIZE` threads, and results are cached
|
||
(:setting:`DNSCACHE_ENABLED`, :setting:`DNSCACHE_SIZE`). It only becomes a
|
||
limit of its own when there are many different domains to resolve, as in
|
||
:ref:`broad crawls <broad-crawls>`, where it shows up as slow starts and
|
||
DNS timeouts.
|
||
|
||
Disk
|
||
:ref:`Feed exports <topics-feed-exports>` write to disk on most crawls,
|
||
although item data is usually small enough for that not to matter. The ones
|
||
to suspect are
|
||
:class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware` and
|
||
the :ref:`media pipelines <topics-media-pipeline>`, which write whole
|
||
responses, and :setting:`JOBDIR`, which writes every scheduled request.
|
||
|
||
|
||
.. _optimize-concurrency:
|
||
|
||
Sending more requests at a time
|
||
===============================
|
||
|
||
:setting:`CONCURRENT_REQUESTS` caps how many requests are being downloaded at
|
||
any given moment, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` caps how many of
|
||
those may target the same domain, and :setting:`DOWNLOAD_DELAY` sets a minimum
|
||
wait between two consecutive requests to the same domain. A project generated by
|
||
:command:`startproject` gets one request per second per domain out of these.
|
||
|
||
Raise them to crawl a single website faster, and see
|
||
:ref:`broad-crawls-concurrency` to spread requests across many websites
|
||
instead.
|
||
|
||
The limit that matters, though, is the one the target website tolerates.
|
||
Exceeding it gets you throttled, served errors or banned, all of which make the
|
||
crawl slower than a lower concurrency would have been. To find that limit:
|
||
|
||
- Read the :ref:`robots.txt <topics-dlmw-robots>` file of the website. Scrapy
|
||
does not act on its ``Crawl-delay`` and ``Request-rate`` directives, so when
|
||
they are present, translate them into :setting:`DOWNLOAD_DELAY` and
|
||
concurrency settings yourself.
|
||
|
||
- Check the traffic that the website already gets, using a service like
|
||
`SimilarWeb`_ or `Cloudflare Radar`_. A rate that is a rounding error next
|
||
to what the website serves anyway is unlikely to be a problem for it.
|
||
|
||
.. _SimilarWeb: https://www.similarweb.com/
|
||
.. _Cloudflare Radar: https://radar.cloudflare.com/
|
||
|
||
- Look for a documented way in. An API, a bulk export or a search endpoint is
|
||
both faster for you and cheaper for the website than crawling its pages, and
|
||
the terms of service may state a rate.
|
||
|
||
- Crawl when the website is idle, in its own timezone, so that the capacity
|
||
you take is capacity nobody else wanted.
|
||
|
||
- Raise concurrency gradually and watch the website respond.
|
||
:stat:`downloader/response_status_count/{status_code}` counts for 429, 503
|
||
or the ban page of the website, growing :stat:`retry/count`, or a
|
||
:ref:`download latency <download-latency>` that climbs as you push harder,
|
||
all mean you have gone past the limit.
|
||
|
||
|
||
.. _optimize-requests:
|
||
|
||
Producing requests faster
|
||
=========================
|
||
|
||
A spider that discovers its requests one response at a time keeps the
|
||
downloader idle no matter how high you set :setting:`CONCURRENT_REQUESTS`. To
|
||
put more requests in the scheduler earlier:
|
||
|
||
- Request every page at once when you can work out how many there are, e.g.
|
||
from a page count or from a result count and a page size in the first
|
||
response, instead of following a link to the next page on every response.
|
||
|
||
- Get URLs from a source that lists many of them at once, such as a sitemap
|
||
or a search or export endpoint of the target website. For a crawl that
|
||
needs nothing else, :class:`~scrapy.spiders.SitemapSpider` reads sitemaps
|
||
for you.
|
||
|
||
- Raise the :attr:`~scrapy.Request.priority` of pagination requests, so that
|
||
they are downloaded before the requests that they compete with, and
|
||
discover the rest of the crawl sooner.
|
||
|
||
Each of these trades memory for speed: a request produced before the downloader
|
||
can take it waits in the scheduler, or on disk if you set :setting:`JOBDIR`.
|
||
Pushed far enough, they turn memory or disk into your new bottleneck, which is
|
||
why :ref:`optimize-memory` recommends the reverse of the last point.
|
||
|
||
|
||
.. _optimize-resources:
|
||
|
||
Lowering resource usage
|
||
=======================
|
||
|
||
.. _optimize-memory:
|
||
|
||
Lowering memory usage
|
||
---------------------
|
||
|
||
- Lower :setting:`SCRAPER_SLOT_MAX_ACTIVE_SIZE`.
|
||
|
||
- Lower :setting:`DOWNLOAD_MAXSIZE`, which allows a single response to take up
|
||
to 1 GiB of memory by default, multiplied by your concurrency. Set
|
||
:setting:`DOWNLOAD_WARNSIZE` first to find out whether the website actually
|
||
serves responses that big.
|
||
|
||
- Lower the number of :ref:`scheduled requests <topics-scheduler>` held in
|
||
memory:
|
||
|
||
- Increase the :attr:`~scrapy.Request.priority` of requests whose
|
||
:attr:`~scrapy.Request.callback` cannot yield additional requests.
|
||
|
||
For example, the following spider uses a higher priority (1) for book
|
||
requests than for pagination requests:
|
||
|
||
.. code-block:: python
|
||
|
||
from scrapy import Spider
|
||
|
||
|
||
class BooksToScrapeComSpider(Spider):
|
||
name = "books_toscrape_com"
|
||
start_urls = [
|
||
"http://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
|
||
]
|
||
|
||
def parse(self, response):
|
||
next_page_links = response.css(".next a")
|
||
yield from response.follow_all(next_page_links)
|
||
book_links = response.css("article a")
|
||
yield from response.follow_all(book_links, callback=self.parse_book, priority=1)
|
||
|
||
def parse_book(self, response):
|
||
yield {
|
||
"name": response.css("h1::text").get(),
|
||
"price": response.css(".price_color::text").re_first("£(.*)"),
|
||
"url": response.url,
|
||
}
|
||
|
||
.. note:: If the number of request-yielding, low-priority requests
|
||
scheduled at any given time is lower than concurrency settings
|
||
(:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` or
|
||
:setting:`CONCURRENT_REQUESTS`), as in the example above, this can
|
||
slow down your crawl by turning those requests into a bottleneck.
|
||
|
||
- If you have many :ref:`start requests <start-requests>`, consider
|
||
:ref:`delaying their iteration <start-requests-lazy>`.
|
||
|
||
- Set :setting:`JOBDIR` to offload all scheduled requests to disk.
|
||
|
||
- Be on the lookout for :ref:`memory leaks <topics-leaks>`.
|
||
|
||
|
||
Lowering network usage
|
||
----------------------
|
||
|
||
- Install brotli_ and zstandard_ to support brotli-compressed_ and
|
||
zstd-compressed_ responses.
|
||
|
||
.. _brotli-compressed: https://www.ietf.org/rfc/rfc7932.txt
|
||
.. _brotli: https://pypi.org/project/Brotli/
|
||
.. _zstd-compressed: https://www.ietf.org/rfc/rfc8478.txt
|
||
.. _zstandard: https://pypi.org/project/zstandard/
|
||
|
||
- Enable :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`
|
||
while developing your spider, so that re-runs do not download the same
|
||
responses again.
|
||
|
||
|
||
Lowering CPU usage
|
||
------------------
|
||
|
||
- Set :setting:`LOG_LEVEL` to ``"INFO"`` or higher.
|
||
|
||
- Restrict what you parse. A :ref:`selector <topics-selectors>` over a
|
||
smaller part of the response, or a single query whose result you reuse,
|
||
beats repeated queries over the whole document.
|
||
|
||
|
||
Other tips
|
||
----------
|
||
|
||
- Try :ref:`using the asyncio reactor <install-asyncio>` with uvloop_ as
|
||
:ref:`custom event loop <using-custom-loops>`, i.e. setting
|
||
:setting:`ASYNCIO_EVENT_LOOP` to ``"uvloop.Loop"``.
|
||
|
||
.. _uvloop: https://github.com/MagicStack/uvloop
|
||
|
||
Alternatively, try :ref:`switching to a non-asyncio reactor
|
||
<disable-asyncio>`.
|
||
|
||
- Disable unused :ref:`components <topics-components>`.
|
||
|
||
For example, set :setting:`COOKIES_ENABLED` to ``False`` unless you need
|
||
cookies.
|
||
|
||
- Split the crawl across separate processes to use more than one CPU core.
|
||
See :ref:`distributed-crawls`.
|
||
|
||
|
||
.. _broad-crawls:
|
||
.. _topics-broad-crawls:
|
||
|
||
Speeding up broad crawls
|
||
========================
|
||
|
||
While Scrapy is well suited for **broad crawls**, i.e. crawls that target many
|
||
websites, the default :ref:`settings <topics-settings>` are optimized for
|
||
crawls targeting a single website.
|
||
|
||
For broad crawls, consider these adjustments:
|
||
|
||
- .. _broad-crawls-concurrency:
|
||
|
||
Increase the global concurrency:
|
||
|
||
- Set :setting:`CONCURRENT_REQUESTS` as close to
|
||
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` × [number of target domains]
|
||
(e.g. 8 × 10 domains = 80 concurrent requests) as your CPU and memory
|
||
allow.
|
||
|
||
- Increase :setting:`SCRAPER_SLOT_MAX_ACTIVE_SIZE` when increasing
|
||
:setting:`CONCURRENT_REQUESTS` stops making a difference.
|
||
|
||
- .. _broad-crawls-bfo:
|
||
|
||
If memory is a bottleneck, see if :ref:`crawling in BFO order <bfo>` lowers
|
||
memory usage.
|
||
|
||
- Improve DNS resolution speed:
|
||
|
||
- Set up your own DNS server, with a local cache and upstream to a `large
|
||
DNS server`_, to avoid slowing down your network.
|
||
|
||
.. _large DNS server: https://en.wikipedia.org/wiki/Public_recursive_name_server#Notable_public_DNS_service_operators
|
||
|
||
- Increase :setting:`REACTOR_THREADPOOL_MAXSIZE` to the minimum value
|
||
that avoids DNS resolution timeouts and makes a noticeable positive
|
||
impact in crawl speed.
|
||
|
||
- Lower the negative impact of some responses:
|
||
|
||
- Set :setting:`RETRY_ENABLED` to ``False`` or, if you need retries,
|
||
consider lowering :setting:`RETRY_TIMES`.
|
||
|
||
- Lower :setting:`DOWNLOAD_TIMEOUT` to a more reasonable value, to
|
||
discard stuck requests more quickly.
|
||
|
||
- Set :setting:`REDIRECT_ENABLED` to ``False`` unless you want to follow
|
||
redirects.
|