ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Scrapy 性能优化实战:定位抓取瓶颈并调优并发、内存与网络资源

Scrapy 性能优化实战:定位抓取瓶颈并调优并发、内存与网络资源 Scrapy 性能优化实战定位抓取瓶颈并调优并发、内存与网络资源【免费下载链接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapyScrapy 的抓取速度取决于最慢的那个环节可能是下载器、目标网站、调度队列也可能是你自己编写的解析代码。本文基于 Scrapy 官方文档中的 Optimization 一章展开系统讲解先测量、再调参的优化方法论如何借助 LogStats 统计与 telnet 控制台逐一定位瓶颈如何调整CONCURRENT_REQUESTS、DOWNLOAD_DELAY等并发参数如何压低内存、网络与 CPU 开销以及面对多域名的广域抓取broad crawl时应做哪些专项调整。读完本文你应当能对任意一个 Scrapy 项目独立完成瓶颈诊断与针对性调优而不是盲目地加大并发。核心原则先找瓶颈再改设置一次抓取的速度由其最慢的部分决定A crawl goes as fast as its slowest part allows。瓶颈因蜘蛛而异同一台机器上一个抓取可能受限于自己的解析代码另一个却受限于目标网站。因此必须测量你真正想要优化的那个抓取而不是照搬别人项目的设置。诊断工具主要有三个LogStats扩展周期性报告抓取速率telnet 控制台的est()报告引擎各组件的瞬时状态资源占用监控CPU、内存、网络、磁盘。用 LogStats 观察抓取速率LogStats扩展scrapy/extensions/logstats.py每LOGSTATS_INTERVAL秒默认 60.0见 scrapy/settings/default_settings.py 第 474 行输出一次抓取速率[scrapy.extensions.logstats] INFO: Crawled 1200 pages (at 60 pages/min), scraped 1150 items (at 58 items/min)从源码看这条日志由LogStats.calculate_stats()生成它读取item_scraped_count已抓取 item 数与response_received_count已接收响应数乘以60.0 / interval折算为每分钟速率scrapy/extensions/logstats.py爬虫关闭时还会写入最终统计responses_per_minute与items_per_minutescrapy/extensions/logstats.py。判断技巧逐步调大CONCURRENT_REQUESTS如果速率保持平坦flat不再上升说明限制来自别处——网络、目标网站或你自己的代码此时应继续用下面的引擎状态做精确定位。阅读引擎状态telnet 控制台telnet 控制台 中的est()函数会打印引擎此刻各部分的运行状态。它的底层实现是 scrapy/utils/engine.py 中的get_engine_status()逐项 eval 一组探测表达式est变量由 telnet 扩展注入scrapy/extensions/telnet.py。典型输出len(engine.downloader.active) : 16 len(engine.scheduler.mqs) : 92 len(engine.scraper.slot.active) : 0 engine.scraper.slot.active_size : 0 engine.scraper.slot.needs_backout() : False建议在抓取的不同时间点多采集几组读数然后按以下规则对号入座len(engine.downloader.active)始终等于CONCURRENT_REQUESTS下载器是瓶颈。你在等待网络或目标网站。对策见下文提高并发一节。len(engine.downloader.active)明显低于CONCURRENT_REQUESTS而调度器队列mqs、dqs中积压着请求请求在进入下载器之前被某处节流了通常是CONCURRENT_REQUESTS_PER_DOMAIN、DOWNLOAD_DELAY或 AutoThrottle 在起作用。下载器活跃数与调度器队列都接近于零你的蜘蛛产生请求的速度不够快。逐页翻分页pagination的抓取无法利用比它产生的请求更多的并发。对策见加快请求产生一节。needs_backout()为True或active_size逼近SCRAPER_SLOT_MAX_ACTIVE_SIZE响应到达的速度超过了你的回调和 item pipeline 的处理速度瓶颈在你自己的代码。len(engine.scheduler.mqs)持续增长不收敛请求被发现的速度快于下载速度。这正是长时抓取内存耗尽的根源。其中needs_backout()的实现可以直接对照 scrapy/core/scraper.pydef needs_backout(self) - bool: return self.active_size self.max_active_sizemax_active_size即SCRAPER_SLOT_MAX_ACTIVE_SIZE设置默认 5000000 字节约 5 MB当在途响应体大小之和超过它scraper 会暂时停止从调度器取新请求转而等待当前回调与 pipeline 消化完毕。阅读资源占用CPUScrapy 运行在单进程内除 DNS 解析和你显式移到线程里的代码外其余逻辑都跑在单线程里。也就是说1 个 CPU 核就是天花板无论机器有多少核进程占满单核 100% 就意味着 CPU 受限。定位方法是用采样式 profiler如 py-spy看 CPU 花在了哪段代码上最常见的答案是 Selector 选择器 与 item pipeline。内存MemoryUsage 扩展 会记录memusage/startup启动时内存与memusage/max峰值内存两个统计项实现见 scrapy/extensions/memusage.pymemusage/startup在引擎启动时写入memusage/max每隔MEMUSAGE_CHECK_INTERVAL_SECONDS默认 60 秒取一次最大值。memusage/max远高于memusage/startup是正常现象真正要看的是它是否随抓取持续不断地增长。如果增长与len(engine.scheduler.mqs)同步是调度问题见下文降低内存占用如果增长与队列无关则是内存泄漏。网络对比downloader/response_bytes统计项与你的可用带宽。该统计由 scrapy/downloadermiddlewares/stats.py 中的DownloaderStats中间件在每次收到响应时累加self.stats.inc_value(downloader/response_bytes, reslen)。带宽打满时任何并发设置都无法再提速。DNS 解析是独立的一环它在REACTOR_THREADPOOL_MAXSIZE默认 10scrapy/settings/default_settings.py大小的线程池中执行结果会经DNSCACHE_ENABLED默认 True/DNSCACHE_SIZE默认 10000缓存。只有当要解析的域名数量很多时典型如广域抓取DNS 才会成为独立瓶颈表现为启动缓慢与 DNS 超时。磁盘Feed 导出 几乎在每次抓取中都会写盘但 item 数据通常小到可以忽略。真正要怀疑的是三者HttpCacheMiddlewarescrapy/extensions/httpcache.py与 media pipeline——它们写入的是完整响应体JOBDIR——它会把每一个已调度的请求都写到磁盘。提高并发一次发出更多请求三个关键设置的分工默认值均来自 scrapy/settings/default_settings.py设置默认值作用CONCURRENT_REQUESTS16同一时刻正在下载的请求总数上限CONCURRENT_REQUESTS_PER_DOMAIN8其中指向同一域名的请求上限DOWNLOAD_DELAY0同一域名的两个连续请求之间的最小等待配合RANDOMIZE_DOWNLOAD_DELAY默认在 0.5~1.5 倍之间随机由startproject生成的项目若设置了DOWNLOAD_DELAY 1等效于每个域名每秒 1 个请求。这些参数的执行位置在 scrapy/core/downloader/init.pyDownloader从设置中读取CONCURRENT_REQUESTS、CONCURRENT_REQUESTS_PER_DOMAIN与DOWNLOAD_DELAY构建下载槽slot槽内active集合记录在途请求当槽设置了 delay 时队列处理会被推迟到lastseen delay之后从而真正落实最小间隔。调大它们可以更快抓取单个网站若要同时抓取很多网站则见下文加速广域抓取。但真正决定上限的是目标网站能容忍的速率。超过容忍度会被限流、被返回错误或被封禁——这些惩罚都会让抓取比低并发时更慢。寻找这个上限的办法读该站的robots.txt。Scrapy 本身不会执行其中的Crawl-delay与Request-rate指令因此如果它们存在你需要自己把它们翻译成DOWNLOAD_DELAY与并发设置。查该站现有的流量规模如 SimilarWeb、Cloudflare Radar 一类服务。相对网站日常承载量只是零头的抓取速率通常不会构成问题。寻找官方通道。API、批量导出或搜索端点对你更快、对网站更省其服务条款中可能明确写出允许的速率。在网站的空闲时段按其所在时区抓取占用的正是别人不需要的那部分容量。渐进式加大并发并观察网站的反应。出现以下任何一种信号说明你已越过上限downloader/response_status_count/{status_code}中 429、503 或封禁页的数量上升retry/count持续增长或下载延迟随并发加大而爬升。注意 429 与 503 都在默认重试码列表RETRY_HTTP_CODES [500, 502, 503, 504, 522, 524, 408, 429]scrapy/settings/default_settings.py中意味着被限流还会额外拖慢请求。加快请求产生别让下载器空等一个每收到一个响应才产生一个新请求的蜘蛛无论CONCURRENT_REQUESTS设多高都会让下载器闲置。想更早把请求塞进调度器能算出总页数时一次性请求所有页面。例如从首页响应里的页码或结果数 ÷ 每页条数直接算出全部 URL而不是每页响应里再跟一个下一页链接。从一次性列出大量 URL 的来源获取sitemap、搜索或导出端点。若抓取别无他求可直接使用SitemapSpiderscrapy/spiders/sitemap.py它替你读取 sitemap。提高分页请求的priority。分页请求的Request.priority更高时会先于与之竞争的其他请求被下载从而更早发现后续整批请求。这三者本质上都是用内存换速度请求产生得比下载器消费得快就会积压在调度器里如果设置了JOBDIR则积压在磁盘上。推得太远内存或磁盘就会成为你的新瓶颈——这正是下文降低内存占用一节建议对提高分页优先级做反向处理的缘由。降低资源占用降低内存占用调低SCRAPER_SLOT_MAX_ACTIVE_SIZE默认 5000000限制在途响应体占用。调低DOWNLOAD_MAXSIZE。它默认允许单个响应占用高达 1 GiB 内存DOWNLOAD_MAXSIZE 1024 * 1024 * 1024再乘以并发数就是总量。建议先调低DOWNLOAD_WARNSIZE默认 32 MiB观察确认网站真的会返回这么大的响应再动手。减少驻留在内存中的已调度请求数量对其回调不会再产生新请求的请求调高Request.priority让它们尽快被下载、尽快释放。例如以下蜘蛛对书籍详情请求使用比分页更高的优先级1from 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, callbackself.parse_book, priority1) def parse_book(self, response): yield { name: response.css(h1::text).get(), price: response.css(.price_color::text).re_first(£(.*)), url: response.url, }注意原文档中的提醒如果某一时刻能产生请求的低优先级请求数量低于并发上限CONCURRENT_REQUESTS_PER_DOMAIN或CONCURRENT_REQUESTS这种优先级反转反而会把它们自身变成瓶颈拖慢整个抓取。上例中下一页请求就是典型的低优先级、且能产生后续请求的一类。如果你有很多 start requests考虑惰性迭代它们start-requests-lazy避免一次性全部进入调度器。设置JOBDIR把所有已调度请求卸载到磁盘。时刻警惕内存泄漏。降低网络占用开发调试阶段启用HttpCacheMiddlewarescrapy/extensions/httpcache.py重跑时不必重复下载相同响应。降低 CPU 占用把LOG_LEVEL设为INFO或更高项目默认是DEBUGscrapy/settings/default_settings.py。缩小解析范围。对响应的一部分使用 selector或复用单一查询的结果胜过对整篇文档反复查询。其他技巧尝试 asyncio reactor 配合 uvloop 作为自定义事件循环即把ASYNCIO_EVENT_LOOP设为uvloop.Loop也可以考虑切换到非 asyncio 的 reactor。禁用用不到的组件。例如确实不需要 cookie 时把COOKIES_ENABLED设为False默认是True。将抓取拆分到多个进程才能用上多个 CPU 核。参见 distributed-crawls。加速广域抓取broad crawlsScrapy 同样适合广域抓取——目标横跨许多网站但它的默认设置是为单网站抓取优化的。对广域抓取建议做以下调整提高全局并发在 CPU 与内存允许的范围内把CONCURRENT_REQUESTS设得尽量接近CONCURRENT_REQUESTS_PER_DOMAIN× 目标域名数例如 8 × 10 个域名 80 个并发请求当继续提高CONCURRENT_REQUESTS不再有效时再调大SCRAPER_SLOT_MAX_ACTIVE_SIZE。如果内存成为瓶颈可以试验改用 BFObreadth-first 顺序的调度变体抓取往往能降低内存占用参见 settings.rst 中相关说明与 faq-bfo-dfo。加快 DNS 解析自建带本地缓存、上游指向大型公共 DNS 的 DNS 服务器避免拖慢整个抓取网络把REACTOR_THREADPOOL_MAXSIZE增大到既不出现 DNS 超时、又对抓取速度有可感知提升的最小值。降低部分响应的负面拖累把RETRY_ENABLED设为False若必须重试考虑调低RETRY_TIMES默认 2把DOWNLOAD_TIMEOUT默认 180 秒降到更合理的值让卡死的请求更快被丢弃除非确实需要跟随跳转否则把REDIRECT_ENABLED设为False。总结Scrapy 优化的正确顺序可以浓缩为四步测量用LogStats的周期性速率日志 telnet 控制台est()的多点采样对照 scrapy/utils/engine.py 打印的引擎状态确认瓶颈落在下载器、调度器、scraper 还是你自己的代码对症调参瓶颈在网络就提高CONCURRENT_REQUESTS一族并发参数并以目标网站的容忍度为上限瓶颈在请求产生就批量生成 URL、利用 sitemap、调高分页priority压降资源内存上收紧SCRAPER_SLOT_MAX_ACTIVE_SIZE/DOWNLOAD_MAXSIZE并善用JOBDIR网络上用 HTTP 缓存CPU 上抬高LOG_LEVEL并缩小 selector 范围广域抓取专项全局并发 每域并发 × 域名数、自建 DNS、放宽重试与超时惩罚、关闭不必要的重定向跟随。所有关键设置的默认值都可直接在 scrapy/settings/default_settings.py 中查证调优前后用同一套统计项速率、memusage/*、downloader/response_bytes、retry/count做前后对比才能让每一次改动都有可验证的依据。【免费下载链接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表